mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-14 09:37:36 -05:00
Compare commits
21 Commits
v0.6.1-bet
...
v0.6.1-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
912af4e3e6 | ||
|
|
df0d0468c8 | ||
|
|
7509245751 | ||
|
|
02e236202c | ||
|
|
4b1d294211 | ||
|
|
40840dbd5c | ||
|
|
786ae9369b | ||
|
|
b7bbf10415 | ||
|
|
486ac01416 | ||
|
|
9e01cc48a5 | ||
|
|
37996d0477 | ||
|
|
a15ff9040b | ||
|
|
b3cd3f431f | ||
|
|
649d1f650a | ||
|
|
215e4064fa | ||
|
|
66a0b2543b | ||
|
|
378841b408 | ||
|
|
e28d726182 | ||
|
|
71bc27e3d9 | ||
|
|
bcfb2700cb | ||
|
|
2d9491ac93 |
22
build.gradle
22
build.gradle
@@ -192,6 +192,7 @@ String targetLinuxX86_64 = 'linux-x86_64-v' + version
|
|||||||
String targetOsxAarch64 = 'osx-aarch64-v' + version
|
String targetOsxAarch64 = 'osx-aarch64-v' + version
|
||||||
String targetOsxX86_64 = 'osx-x86_64-v' + version
|
String targetOsxX86_64 = 'osx-x86_64-v' + version
|
||||||
String targetWindowsX86_64 = 'windows-x86_64-v' + version
|
String targetWindowsX86_64 = 'windows-x86_64-v' + version
|
||||||
|
String targetWindowsAarch64 = 'windows-aarch64-v' + version
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional pre-downloaded Java Development Kit (JDK) for each target OS and CPU. These locations are hard-coded to the
|
* Optional pre-downloaded Java Development Kit (JDK) for each target OS and CPU. These locations are hard-coded to the
|
||||||
@@ -203,17 +204,19 @@ String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-22.0.2-full'
|
|||||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-22.0.2-full.jdk'
|
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-22.0.2-full.jdk'
|
||||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-22.0.2-full.jdk'
|
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-22.0.2-full.jdk'
|
||||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-22.0.2-full'
|
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-22.0.2-full'
|
||||||
|
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-22.0.2-full'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download URLs to download the JDK as part of the gradle build packaging process
|
* Download URLs to download the JDK as part of the gradle build packaging process
|
||||||
*/
|
*/
|
||||||
def jdk_download_base = "https://download.bell-sw.com/java/23+38/bellsoft-jdk23+38-"
|
def jdk_download_base = "https://download.bell-sw.com/java/23.0.1+13/bellsoft-jdk23.0.1+13-"
|
||||||
def jdk_download_suffix = "-full.tar.gz"
|
def jdk_download_suffix = "-full.tar.gz"
|
||||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
||||||
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
||||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
def jdk_download_osx_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
def jdk_download_osx_aarch64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||||
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
||||||
|
def jdk_download_windows_aarch64 = jdk_download_base + "windows-aarch64-full.zip"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures the runtime zip task with additional options/settings.
|
* Configures the runtime zip task with additional options/settings.
|
||||||
@@ -256,7 +259,12 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
|||||||
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
|
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
|
||||||
|
|
||||||
if(os.isWindows()) {
|
if(os.isWindows()) {
|
||||||
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
|
if(arch.equals("amd64")) {
|
||||||
|
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
|
||||||
|
}
|
||||||
|
else if(arch.equals("aarch64")) {
|
||||||
|
rt.extension.targetPlatform(targetWindowsAarch64, javaHome)
|
||||||
|
}
|
||||||
configure(rt, jvmArgsWindows)
|
configure(rt, jvmArgsWindows)
|
||||||
}
|
}
|
||||||
else if(os.isLinux())
|
else if(os.isLinux())
|
||||||
@@ -292,6 +300,16 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
|||||||
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
|
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
|
||||||
*/
|
*/
|
||||||
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
|
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||||
|
//Windows aarch64
|
||||||
|
if(file(jdk_windows_aarch64).exists()) {
|
||||||
|
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, [])
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rt.extension.targetPlatform(targetWindowsAarch64) {
|
||||||
|
jdkHome = jdkDownload(jdk_download_windows_aarch64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Windows x86-64
|
||||||
if(file(jdk_windows_x86_64).exists()) {
|
if(file(jdk_windows_x86_64).exists()) {
|
||||||
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
|
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
||||||
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
||||||
|
|
||||||
projectVersion=0.6.1-beta-2
|
projectVersion=0.6.1-beta-3
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This 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
|
||||||
@@ -63,6 +63,69 @@ public class AliasModel
|
|||||||
return mAliasListNames;
|
return mAliasListNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears and reloads the list of alias list names from the current set of aliases.
|
||||||
|
*/
|
||||||
|
public void refreshAliasListNames()
|
||||||
|
{
|
||||||
|
mAliasListNames.clear();
|
||||||
|
|
||||||
|
for(Alias alias : mAliases)
|
||||||
|
{
|
||||||
|
String aliasListName = alias.getAliasListName();
|
||||||
|
|
||||||
|
if(aliasListName != null && !aliasListName.isEmpty() && !mAliasListNames.contains(aliasListName))
|
||||||
|
{
|
||||||
|
mAliasListNames.add(aliasListName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames the alias list across the set of aliases.
|
||||||
|
* @param oldName currently used by the alias
|
||||||
|
* @param newName to apply to the alias
|
||||||
|
*/
|
||||||
|
public void renameAliasList(String oldName, String newName)
|
||||||
|
{
|
||||||
|
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAliases.stream().filter(alias -> alias.getAliasListName().equals(oldName)).forEach(alias -> alias.setAliasListName(newName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes any aliases that have the alias list name
|
||||||
|
* @param aliasListName to delete
|
||||||
|
*/
|
||||||
|
public void deleteAliasList(String aliasListName)
|
||||||
|
{
|
||||||
|
if(aliasListName == null || aliasListName.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAliases.removeIf(alias -> alias.getAliasListName().equals(aliasListName));
|
||||||
|
mAliasListMap.remove(aliasListName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the list of alias list names to the currently managed set of alias list names.
|
||||||
|
* @param aliasListNames to add
|
||||||
|
*/
|
||||||
|
public void addAliasListNames(List<String> aliasListNames)
|
||||||
|
{
|
||||||
|
for(String aliasListName : aliasListNames)
|
||||||
|
{
|
||||||
|
if(!mAliasListNames.contains(aliasListName))
|
||||||
|
{
|
||||||
|
mAliasListNames.add(aliasListName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unmodifiable list of all aliases currently in the model
|
* Unmodifiable list of all aliases currently in the model
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 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.audio.broadcast.broadcastify;
|
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||||
|
|
||||||
@@ -35,6 +32,7 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
|
|||||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyFeedConfiguration.class);
|
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyFeedConfiguration.class);
|
||||||
|
|
||||||
private int mFeedID;
|
private int mFeedID;
|
||||||
|
private boolean mVerboseLogging = false;
|
||||||
|
|
||||||
public BroadcastifyFeedConfiguration()
|
public BroadcastifyFeedConfiguration()
|
||||||
{
|
{
|
||||||
@@ -123,4 +121,15 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
|
|||||||
{
|
{
|
||||||
mFeedID = feedID;
|
mFeedID = feedID;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JacksonXmlProperty(isAttribute = true, localName = "verbose_logging")
|
||||||
|
public boolean isVerboseLogging()
|
||||||
|
{
|
||||||
|
return mVerboseLogging;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVerboseLogging(boolean verboseLogging)
|
||||||
|
{
|
||||||
|
mVerboseLogging = verboseLogging;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
* Copyright (C) 2014-2024 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.audio.broadcast.icecast;
|
|||||||
|
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||||
|
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||||
import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
|
import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
|
||||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||||
@@ -39,6 +40,8 @@ import org.apache.mina.core.future.ConnectFuture;
|
|||||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||||
import org.apache.mina.core.session.IoSession;
|
import org.apache.mina.core.session.IoSession;
|
||||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||||
|
import org.apache.mina.filter.logging.LogLevel;
|
||||||
|
import org.apache.mina.filter.logging.LoggingFilter;
|
||||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -56,6 +59,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
private IoSession mStreamingSession = null;
|
private IoSession mStreamingSession = null;
|
||||||
|
|
||||||
private long mLastConnectionAttempt = 0;
|
private long mLastConnectionAttempt = 0;
|
||||||
|
private boolean mVerboseLogging = false;
|
||||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,6 +77,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||||
{
|
{
|
||||||
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
|
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
|
||||||
|
|
||||||
|
if(configuration instanceof BroadcastifyFeedConfiguration broadcastify)
|
||||||
|
{
|
||||||
|
mVerboseLogging = broadcastify.isVerboseLogging();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,9 +141,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
mSocketConnector = new NioSocketConnector();
|
mSocketConnector = new NioSocketConnector();
|
||||||
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
|
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
|
||||||
|
|
||||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
if(mVerboseLogging)
|
||||||
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
{
|
||||||
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||||
|
loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||||
|
mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||||
|
}
|
||||||
|
|
||||||
mSocketConnector.getFilterChain().addLast("codec",
|
mSocketConnector.getFilterChain().addLast("codec",
|
||||||
new ProtocolCodecFilter(new IcecastCodecFactory()));
|
new ProtocolCodecFilter(new IcecastCodecFactory()));
|
||||||
@@ -148,6 +160,10 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
@Override
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Attempting connection ...");
|
||||||
|
}
|
||||||
setBroadcastState(BroadcastState.CONNECTING);
|
setBroadcastState(BroadcastState.CONNECTING);
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -156,19 +172,42 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||||
getBroadcastConfiguration().getPort()));
|
getBroadcastConfiguration().getPort()));
|
||||||
|
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Socket created - asynchronous connect requested - entering wait period");
|
||||||
|
}
|
||||||
|
|
||||||
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
if(connected)
|
if(connected)
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Connected.");
|
||||||
|
}
|
||||||
|
|
||||||
mStreamingSession = future.getSession();
|
mStreamingSession = future.getSession();
|
||||||
mConnecting.set(false);
|
mConnecting.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Not Connected. Connection attempt timeout [" + CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS + "ms] exceeded");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch(RuntimeIoException rioe)
|
catch(RuntimeIoException rioe)
|
||||||
{
|
{
|
||||||
if(rioe.getCause() instanceof SocketException)
|
if(rioe.getCause() instanceof SocketException)
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Socket error. This usually indicates sdrtrunk can't reach the server " +
|
||||||
|
"address over the current network connection. Setting state to " +
|
||||||
|
"NETWORK UNAVAILABLE", rioe);
|
||||||
|
}
|
||||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||||
mConnecting.set(false);
|
mConnecting.set(false);
|
||||||
return;
|
return;
|
||||||
@@ -176,12 +215,22 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
}
|
}
|
||||||
catch(UnresolvedAddressException uae)
|
catch(UnresolvedAddressException uae)
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Unresolved Address error. This means the domain name services can't resolve " +
|
||||||
|
"the server URL to an IP address. Setting state to NETWORK UNAVAILABLE", uae);
|
||||||
|
}
|
||||||
|
|
||||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||||
mConnecting.set(false);
|
mConnecting.set(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Unknown error. An error occurred while attempting to connect to the server.", e);
|
||||||
|
}
|
||||||
mLog.error("Error", e);
|
mLog.error("Error", e);
|
||||||
//Disregard ... we'll disconnect and try again
|
//Disregard ... we'll disconnect and try again
|
||||||
}
|
}
|
||||||
@@ -190,6 +239,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
mLog.error("Throwable error caught", t);
|
mLog.error("Throwable error caught", t);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Starting disconnect sequence since an error occurred while trying to connect.");
|
||||||
|
}
|
||||||
|
|
||||||
disconnect();
|
disconnect();
|
||||||
mConnecting.set(false);
|
mConnecting.set(false);
|
||||||
}
|
}
|
||||||
@@ -209,6 +263,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
{
|
{
|
||||||
if(connected() && mStreamingSession != null)
|
if(connected() && mStreamingSession != null)
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Routine disconnect requested from a connected state with a non-null streaming session");
|
||||||
|
}
|
||||||
|
|
||||||
mStreamingSession.closeNow();
|
mStreamingSession.closeNow();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -217,6 +276,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
//want to preserve the error state that got us here, so the user can see it.
|
//want to preserve the error state that got us here, so the user can see it.
|
||||||
if(!getBroadcastState().isErrorState())
|
if(!getBroadcastState().isErrorState())
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Disconnect requested - previous non-error state was [" + getBroadcastState() +
|
||||||
|
"] - changing state to DISCONNECTED");
|
||||||
|
}
|
||||||
|
|
||||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,12 +347,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
mInlineActive = false;
|
mInlineActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Session opened. Sending: " + sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
session.write(sb.toString());
|
session.write(sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sessionClosed(IoSession session) throws Exception
|
public void sessionClosed(IoSession session) throws Exception
|
||||||
{
|
{
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Session closed. Setting connecting flag to false.");
|
||||||
|
}
|
||||||
|
|
||||||
mLastConnectionAttempt = System.currentTimeMillis();
|
mLastConnectionAttempt = System.currentTimeMillis();
|
||||||
|
|
||||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||||
@@ -311,15 +387,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
|||||||
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Session error caught.", cause);
|
||||||
|
}
|
||||||
|
|
||||||
disconnect();
|
disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void messageReceived(IoSession session, Object object) throws Exception
|
public void messageReceived(IoSession session, Object object) throws Exception
|
||||||
{
|
{
|
||||||
if(object instanceof String)
|
if(object instanceof String message)
|
||||||
{
|
{
|
||||||
String message = (String) object;
|
if(mVerboseLogging)
|
||||||
|
{
|
||||||
|
mLog.info("Message Received [" + message + "]");
|
||||||
|
}
|
||||||
|
|
||||||
if(message != null && !message.trim().isEmpty())
|
if(message != null && !message.trim().isEmpty())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This 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
|
||||||
@@ -41,6 +41,9 @@ import jmbe.iface.IAudioCodecLibrary;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base audio module for protocols that use the JMBE audio codec.
|
||||||
|
*/
|
||||||
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
|
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
|
||||||
ISquelchStateListener
|
ISquelchStateListener
|
||||||
{
|
{
|
||||||
@@ -59,12 +62,23 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
|||||||
loadConverter();
|
loadConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void closeAudioSegment()
|
||||||
|
{
|
||||||
|
super.closeAudioSegment();
|
||||||
|
|
||||||
|
//Reset the audio codec to clear any leftover frame data from the previous call.
|
||||||
|
if(mAudioCodec != null)
|
||||||
|
{
|
||||||
|
mAudioCodec.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
super.dispose();
|
super.dispose();
|
||||||
MyEventBus.getGlobalEventBus().unregister(this);
|
MyEventBus.getGlobalEventBus().unregister(this);
|
||||||
mAudioCodec = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IAudioCodec getAudioCodec()
|
public IAudioCodec getAudioCodec()
|
||||||
|
|||||||
@@ -140,8 +140,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
}
|
}
|
||||||
catch(IllegalArgumentException iae)
|
catch(IllegalArgumentException iae)
|
||||||
{
|
{
|
||||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
|
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
|
||||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
|
||||||
|
getChannelName() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -151,8 +152,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
}
|
}
|
||||||
catch(IllegalArgumentException iae)
|
catch(IllegalArgumentException iae)
|
||||||
{
|
{
|
||||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
|
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
|
||||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
"MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
|
||||||
|
getChannelName() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
//Run the queue processor task every 100 milliseconds or 10 times a second
|
//Run the queue processor task every 100 milliseconds or 10 times a second
|
||||||
@@ -276,7 +278,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
{
|
{
|
||||||
if(mOutput == null)
|
if(mOutput == null)
|
||||||
{
|
{
|
||||||
LOGGING_SUPPRESSOR.error("null output", 5, "Audio Output is null - ignoring audio playback request");
|
LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,8 +323,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
}
|
}
|
||||||
catch(IllegalArgumentException iae)
|
catch(IllegalArgumentException iae)
|
||||||
{
|
{
|
||||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
|
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
|
||||||
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
"MASTER GAIN control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
|
||||||
|
getChannelName() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -332,23 +335,24 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
}
|
}
|
||||||
catch(IllegalArgumentException iae)
|
catch(IllegalArgumentException iae)
|
||||||
{
|
{
|
||||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
|
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
|
||||||
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
"MASTER MUTE control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
|
||||||
|
getChannelName() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGING_SUPPRESSOR.info("reopen audio output success", 5,
|
LOGGING_SUPPRESSOR.info("reopen audio output success", 2,
|
||||||
"Closed and reopened audio output - success - mOutput is not null");
|
"Closed and reopened audio output - success - mOutput is not null");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
LOGGING_SUPPRESSOR.info("reopen audio output fail", 5,
|
LOGGING_SUPPRESSOR.info("reopen audio output fail", 2,
|
||||||
"Closed and reopened audio output - fail - mOutput is null");
|
"Closed and reopened audio output - fail - mOutput is null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(LineUnavailableException lue)
|
catch(LineUnavailableException lue)
|
||||||
{
|
{
|
||||||
LOGGING_SUPPRESSOR.error("reopen fail for lua", 3, "Attempt to reopen " +
|
LOGGING_SUPPRESSOR.error("reopen fail for lua", 2, "Attempt to reopen " +
|
||||||
"audio source data line failed", lue);
|
"audio source data line failed", lue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -391,7 +395,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
|||||||
}
|
}
|
||||||
catch(LineUnavailableException e)
|
catch(LineUnavailableException e)
|
||||||
{
|
{
|
||||||
LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 3,
|
LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 2,
|
||||||
"Failed to (re)open the source data line for audio output - mixer [" +
|
"Failed to (re)open the source data line for audio output - mixer [" +
|
||||||
mMixer.getMixerInfo().getName() + "]");
|
mMixer.getMixerInfo().getName() + "]");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
* Copyright (C) 2014-2024 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,11 +35,10 @@ import io.github.dsheirer.source.ISourceEventProvider;
|
|||||||
import io.github.dsheirer.source.SourceEvent;
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
||||||
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
||||||
|
import java.util.List;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider,
|
public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider,
|
||||||
IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider,
|
IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider,
|
||||||
IdentifierUpdateProvider, IOverflowListener
|
IdentifierUpdateProvider, IOverflowListener
|
||||||
@@ -53,7 +52,8 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
|
|||||||
private Channel mChannel;
|
private Channel mChannel;
|
||||||
protected boolean mSourceOverflow = false;
|
protected boolean mSourceOverflow = false;
|
||||||
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
|
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
|
||||||
|
protected boolean mTeardownSequenceStarted = false;
|
||||||
|
protected boolean mTeardownSequenceCompleted = false;
|
||||||
|
|
||||||
//TODO: remove the IOverflowListener code from this class
|
//TODO: remove the IOverflowListener code from this class
|
||||||
|
|
||||||
@@ -66,6 +66,22 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
|
|||||||
mChannel = channel;
|
mChannel = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the teardown sequence was started.
|
||||||
|
*/
|
||||||
|
public boolean isTeardownSequenceCompleted()
|
||||||
|
{
|
||||||
|
return mTeardownSequenceCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the teardown sequence was completed, meaning that the request disable channel event was dispatched.
|
||||||
|
*/
|
||||||
|
public boolean isTeardownSequenceStarted()
|
||||||
|
{
|
||||||
|
return mTeardownSequenceStarted;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates/replaces the current channel configuration with the argument.
|
* Updates/replaces the current channel configuration with the argument.
|
||||||
*/
|
*/
|
||||||
@@ -89,6 +105,11 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
|
|||||||
*/
|
*/
|
||||||
protected abstract void checkState();
|
protected abstract void checkState();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if any timeslot is currently in a TEARDOWN state.
|
||||||
|
*/
|
||||||
|
public abstract boolean isTeardownState();
|
||||||
|
|
||||||
public abstract List<ChannelMetadata> getChannelMetadata();
|
public abstract List<ChannelMetadata> getChannelMetadata();
|
||||||
|
|
||||||
public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification);
|
public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
* Copyright (C) 2014-2024 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
|
||||||
@@ -44,13 +44,12 @@ import io.github.dsheirer.source.SourceEvent;
|
|||||||
import io.github.dsheirer.source.SourceType;
|
import io.github.dsheirer.source.SourceType;
|
||||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
|
* Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
|
||||||
@@ -166,6 +165,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
mStateMachineMap.get(timeslot).setState(State.IDLE);
|
mStateMachineMap.get(timeslot).setState(State.IDLE);
|
||||||
break;
|
break;
|
||||||
case TEARDOWN:
|
case TEARDOWN:
|
||||||
|
mTeardownSequenceStarted = true;
|
||||||
if(getChannel().isTrafficChannel())
|
if(getChannel().isTrafficChannel())
|
||||||
{
|
{
|
||||||
checkTeardown();
|
checkTeardown();
|
||||||
@@ -206,7 +206,16 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
{
|
{
|
||||||
if(getChannel().isTrafficChannel())
|
if(getChannel().isTrafficChannel())
|
||||||
{
|
{
|
||||||
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
|
try
|
||||||
|
{
|
||||||
|
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
|
||||||
|
}
|
||||||
|
catch(Throwable t)
|
||||||
|
{
|
||||||
|
mLog.error("Error broadcasting shutdown channel event", t);
|
||||||
|
}
|
||||||
|
|
||||||
|
mTeardownSequenceStarted = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -238,6 +247,20 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTeardownState()
|
||||||
|
{
|
||||||
|
for(StateMachine stateMachine: mStateMachineMap.values())
|
||||||
|
{
|
||||||
|
if(stateMachine.getState() == State.TEARDOWN)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates configuration identifiers for the channel name, system, site and alias list name.
|
* Creates configuration identifiers for the channel name, system, site and alias list name.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
* Copyright (C) 2014-2024 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
|
||||||
@@ -46,11 +46,10 @@ import io.github.dsheirer.source.SourceEvent;
|
|||||||
import io.github.dsheirer.source.SourceType;
|
import io.github.dsheirer.source.SourceType;
|
||||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel state tracks the overall state of all processing modules and decoders configured for the channel and
|
* Channel state tracks the overall state of all processing modules and decoders configured for the channel and
|
||||||
@@ -163,9 +162,11 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
|||||||
mStateMachine.setState(State.IDLE);
|
mStateMachine.setState(State.IDLE);
|
||||||
break;
|
break;
|
||||||
case TEARDOWN:
|
case TEARDOWN:
|
||||||
|
mTeardownSequenceStarted = true;
|
||||||
if(getChannel().isTrafficChannel())
|
if(getChannel().isTrafficChannel())
|
||||||
{
|
{
|
||||||
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
|
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
|
||||||
|
mTeardownSequenceCompleted = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -175,6 +176,12 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTeardownState()
|
||||||
|
{
|
||||||
|
return mStateMachine.getState() == State.TEARDOWN;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void checkState()
|
protected void checkState()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 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.controller.channel;
|
package io.github.dsheirer.controller.channel;
|
||||||
|
|
||||||
@@ -26,15 +23,13 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
|||||||
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
|
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel Model
|
* Channel Model
|
||||||
*/
|
*/
|
||||||
@@ -55,6 +50,58 @@ public class ChannelModel implements Listener<ChannelEvent>
|
|||||||
mTrafficChannels.addListener(changeListener);
|
mTrafficChannels.addListener(changeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a deduplicated list of alias list names from across the channel configurations.
|
||||||
|
* @return list of alias list names.
|
||||||
|
*/
|
||||||
|
public List<String> getAliasListNames()
|
||||||
|
{
|
||||||
|
List<String> aliasListNames = new ArrayList<>();
|
||||||
|
|
||||||
|
for(Channel channel : mChannels)
|
||||||
|
{
|
||||||
|
String aliasListName = channel.getAliasListName();
|
||||||
|
|
||||||
|
if(aliasListName != null && !aliasListName.isEmpty() && !aliasListNames.contains(aliasListName))
|
||||||
|
{
|
||||||
|
aliasListNames.add(aliasListName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliasListNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames the alias list across the set of aliases.
|
||||||
|
* @param oldName currently used by the alias
|
||||||
|
* @param newName to apply to the alias
|
||||||
|
*/
|
||||||
|
public void renameAliasList(String oldName, String newName)
|
||||||
|
{
|
||||||
|
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mChannels.stream().filter(channel -> channel.getAliasListName().equals(oldName))
|
||||||
|
.forEach(channel -> channel.setAliasListName(newName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes any aliases that have the alias list name
|
||||||
|
* @param aliasListName to delete
|
||||||
|
*/
|
||||||
|
public void deleteAliasList(String aliasListName)
|
||||||
|
{
|
||||||
|
if(aliasListName == null || aliasListName.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mChannels.stream().filter(channel -> channel.getAliasListName().equals(aliasListName))
|
||||||
|
.forEach(channel -> channel.setAliasListName(null));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable list of channel configurations managed by this model
|
* Observable list of channel configurations managed by this model
|
||||||
*/
|
*/
|
||||||
@@ -109,6 +156,45 @@ public class ChannelModel implements Listener<ChannelEvent>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames the alias list across all channels.
|
||||||
|
* @param existing alias list name
|
||||||
|
* @param renamed alias list name.
|
||||||
|
*/
|
||||||
|
public void renameAliaslistForChannels(String existing, String renamed)
|
||||||
|
{
|
||||||
|
if(existing == null || existing.isEmpty() || renamed == null || renamed.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mChannels.forEach(channel -> {
|
||||||
|
if(channel.getAliasListName() != null && channel.getAliasListName().equals(existing))
|
||||||
|
{
|
||||||
|
channel.setAliasListName(renamed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the alias list name from any channels.
|
||||||
|
* @param name to delete
|
||||||
|
*/
|
||||||
|
public void deleteAliasListName(String name)
|
||||||
|
{
|
||||||
|
if(name == null || name.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mChannels.forEach(channel -> {
|
||||||
|
if(channel.getAliasListName() != null && channel.getAliasListName().equals(name))
|
||||||
|
{
|
||||||
|
channel.setAliasListName(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of unique system values from across the channel set
|
* Returns a list of unique system values from across the channel set
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -279,15 +279,27 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
break;
|
break;
|
||||||
case REQUEST_DISABLE:
|
case REQUEST_DISABLE:
|
||||||
case NOTIFICATION_DELETE:
|
case NOTIFICATION_DELETE:
|
||||||
if(channel != null && channel.isProcessing())
|
if(channel != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
stopProcessing(channel);
|
stopProcessing(channel);
|
||||||
}
|
}
|
||||||
catch(ChannelException ce)
|
catch(Throwable t)
|
||||||
{
|
{
|
||||||
mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage());
|
mLog.error("Error stopping channel [" + channel + "]", t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Request to disable channel must have non-null channel");
|
||||||
|
}
|
||||||
|
catch(IllegalArgumentException iae)
|
||||||
|
{
|
||||||
|
mLog.error("Caught a [" + event.getEvent() + "] non-standard channel event - logging stack trace. " +
|
||||||
|
"This should not happen, please send this error to the developer.", iae);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -521,7 +533,14 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
|
|
||||||
if(addProcessingChain(channel, processingChain))
|
if(addProcessingChain(channel, processingChain))
|
||||||
{
|
{
|
||||||
processingChain.start();
|
try
|
||||||
|
{
|
||||||
|
processingChain.start();
|
||||||
|
}
|
||||||
|
catch(Throwable t)
|
||||||
|
{
|
||||||
|
mLog.error("Error caught during processing chain startup - continuing", t);
|
||||||
|
}
|
||||||
|
|
||||||
if(GraphicsEnvironment.isHeadless())
|
if(GraphicsEnvironment.isHeadless())
|
||||||
{
|
{
|
||||||
@@ -627,8 +646,28 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
//This has to be done on the FX event thread when the playlist editor is constructed
|
//When we're in non-headless mode we have to change the processing property on the JavaFX
|
||||||
Platform.runLater(() -> channel.setProcessing(false));
|
//event thread. However, if it hasn't yet been initialized (ie an FX window opened), we'll
|
||||||
|
//get an ISE. In that case, just set the property to false because there won't be any
|
||||||
|
//property listeners being triggered.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
channel.setProcessing(false);
|
||||||
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
mLog.error("Error during channel stop while setting processing to false [" + channel +
|
||||||
|
"] - continuing channel stop process", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(IllegalStateException e)
|
||||||
|
{
|
||||||
|
channel.setProcessing(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -847,6 +886,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
mLog.error("Error stopping channel [" + (toShutdown != null ? toShutdown.getName() : "unknown") +
|
mLog.error("Error stopping channel [" + (toShutdown != null ? toShutdown.getName() : "unknown") +
|
||||||
"] with source error - " + ce.getMessage());
|
"] with source error - " + ce.getMessage());
|
||||||
}
|
}
|
||||||
|
catch(Throwable t)
|
||||||
|
{
|
||||||
|
mLog.error("Error stopping channel [" + toShutdown + "]", t);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -875,6 +918,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
sb.append("\tProcessing Chain - Processing: ").append(chain.isProcessing()).append("\n");
|
sb.append("\tProcessing Chain - Processing: ").append(chain.isProcessing()).append("\n");
|
||||||
AbstractChannelState state = chain.getChannelState();
|
AbstractChannelState state = chain.getChannelState();
|
||||||
sb.append("Channel State: ").append(state.getClass()).append("\n");
|
sb.append("Channel State: ").append(state.getClass()).append("\n");
|
||||||
|
sb.append(" Teardown Started:").append(state.isTeardownSequenceStarted()).append("\n");
|
||||||
|
sb.append(" Teardown Completed:").append(state.isTeardownSequenceCompleted()).append("\n");
|
||||||
for(ChannelMetadata metadata: state.getChannelMetadata())
|
for(ChannelMetadata metadata: state.getChannelMetadata())
|
||||||
{
|
{
|
||||||
sb.append(metadata.getDescription()).append("\n");
|
sb.append(metadata.getDescription()).append("\n");
|
||||||
|
|||||||
@@ -252,11 +252,16 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
|||||||
synchronized(mBufferDispatcher)
|
synchronized(mBufferDispatcher)
|
||||||
{
|
{
|
||||||
mChannelSources.remove(channelSource);
|
mChannelSources.remove(channelSource);
|
||||||
mPolyphaseChannelizer.removeChannel(channelSource);
|
|
||||||
|
if(mPolyphaseChannelizer != null)
|
||||||
|
{
|
||||||
|
mPolyphaseChannelizer.removeChannel(channelSource);
|
||||||
|
}
|
||||||
|
|
||||||
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
|
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
|
||||||
|
|
||||||
//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.getRegisteredChannelCount() == 0)
|
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
||||||
{
|
{
|
||||||
mNativeBufferProvider.removeBufferListener(mBufferDispatcher);
|
mNativeBufferProvider.removeBufferListener(mBufferDispatcher);
|
||||||
mBufferDispatcher.stop();
|
mBufferDispatcher.stop();
|
||||||
|
|||||||
@@ -460,9 +460,6 @@ public class JavaFxWindowManager extends Application
|
|||||||
stage.show();
|
stage.show();
|
||||||
stage.requestFocus();
|
stage.requestFocus();
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
stage.setMinHeight(stage.getHeight());
|
|
||||||
stage.setMinWidth(stage.getWidth());
|
|
||||||
// System.out.println("Stage [" + stage.getTitle() + "] restored - width[" + stage.getWidth() + "] height [" + stage.getHeight() + "]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.gui.playlist;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for editors that use alias list controls that can trigger when the playlist manager updates the observable
|
||||||
|
* set of alias list names and cause the editor to prompt the user to save changes.
|
||||||
|
*
|
||||||
|
* Implementors should clear any currently selected/editing element that could be impacted by a behind-the-scenes update
|
||||||
|
* to the list of alias list names such as delete or rename operations.
|
||||||
|
*/
|
||||||
|
public interface IAliasListRefreshListener
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Preparate foran alias list refresh. For alias editors, this can be a clear the currently selected
|
||||||
|
* alias so that if the value is changed, the editor doesn't think that the user has modified the alias and
|
||||||
|
* prompt the user to save the changes.
|
||||||
|
*/
|
||||||
|
void prepareForAliasListRefresh();
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This 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,7 @@ import io.github.dsheirer.alias.AliasList;
|
|||||||
import io.github.dsheirer.alias.AliasModel;
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
|
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
|
||||||
import io.github.dsheirer.gui.playlist.Editor;
|
import io.github.dsheirer.gui.playlist.Editor;
|
||||||
|
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
|
||||||
import io.github.dsheirer.icon.Icon;
|
import io.github.dsheirer.icon.Icon;
|
||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
@@ -41,7 +42,6 @@ import javafx.collections.transformation.SortedList;
|
|||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Orientation;
|
import javafx.geometry.Orientation;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Node;
|
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
@@ -76,7 +76,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
/**
|
/**
|
||||||
* Editor for aliases
|
* Editor for aliases
|
||||||
*/
|
*/
|
||||||
public class AliasConfigurationEditor extends SplitPane
|
public class AliasConfigurationEditor extends SplitPane implements IAliasListRefreshListener
|
||||||
{
|
{
|
||||||
private static final Logger mLog = LoggerFactory.getLogger(AliasConfigurationEditor.class);
|
private static final Logger mLog = LoggerFactory.getLogger(AliasConfigurationEditor.class);
|
||||||
|
|
||||||
@@ -89,6 +89,7 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
private Label mPlaceholderLabel;
|
private Label mPlaceholderLabel;
|
||||||
private Button mNewAliasButton;
|
private Button mNewAliasButton;
|
||||||
private Button mDeleteAliasButton;
|
private Button mDeleteAliasButton;
|
||||||
|
private Button mRenameAliasButton;
|
||||||
private Button mCloneAliasButton;
|
private Button mCloneAliasButton;
|
||||||
private MenuButton mMoveToAliasButton;
|
private MenuButton mMoveToAliasButton;
|
||||||
private VBox mButtonBox;
|
private VBox mButtonBox;
|
||||||
@@ -96,13 +97,20 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
private TextField mSearchField;
|
private TextField mSearchField;
|
||||||
private ComboBox<String> mAliasListNameComboBox;
|
private ComboBox<String> mAliasListNameComboBox;
|
||||||
private Button mNewAliasListButton;
|
private Button mNewAliasListButton;
|
||||||
|
private Button mDeleteAliasListButton;
|
||||||
private FilteredList<Alias> mAliasFilteredList;
|
private FilteredList<Alias> mAliasFilteredList;
|
||||||
private SortedList<Alias> mAliasSortedList;
|
private SortedList<Alias> mAliasSortedList;
|
||||||
private AliasPredicate mAliasPredicate;
|
private AliasPredicate mAliasPredicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an instance
|
||||||
|
* @param playlistManager for playlist operations
|
||||||
|
* @param userPreferences for user preferences
|
||||||
|
*/
|
||||||
public AliasConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
public AliasConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||||
{
|
{
|
||||||
mPlaylistManager = playlistManager;
|
mPlaylistManager = playlistManager;
|
||||||
|
mPlaylistManager.addAliasListRefreshListener(this);
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
|
|
||||||
VBox leftBox = new VBox();
|
VBox leftBox = new VBox();
|
||||||
@@ -118,6 +126,15 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
getItems().addAll(topBox, getAliasItemEditor());
|
getItems().addAll(topBox, getAliasItemEditor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares for an alias list refresh by clearing the currently selected alias item from the editor.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void prepareForAliasListRefresh()
|
||||||
|
{
|
||||||
|
getAliasTableView().getSelectionModel().select(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request to show the specified alias in the editor.
|
* Request to show the specified alias in the editor.
|
||||||
* <p>
|
* <p>
|
||||||
@@ -167,18 +184,17 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
alert.setTitle("Save Changes");
|
alert.setTitle("Save Changes");
|
||||||
alert.setHeaderText("Alias configuration has been modified");
|
alert.setHeaderText("Alias configuration has been modified");
|
||||||
alert.setContentText("Do you want to save these changes?");
|
alert.setContentText("Do you want to save these changes?");
|
||||||
alert.initOwner(((Node) getButtonBox()).getScene().getWindow());
|
alert.initOwner((getButtonBox()).getScene().getWindow());
|
||||||
|
|
||||||
//Workaround for JavaFX KDE on Linux bug in FX 10/11: https://bugs.openjdk.java.net/browse/JDK-8179073
|
//Workaround for JavaFX KDE on Linux bug in FX 10/11: https://bugs.openjdk.java.net/browse/JDK-8179073
|
||||||
alert.setResizable(true);
|
alert.setResizable(true);
|
||||||
alert.onShownProperty().addListener(e ->
|
alert.onShownProperty().addListener(e -> {
|
||||||
{
|
|
||||||
Platform.runLater(() -> alert.setResizable(false));
|
Platform.runLater(() -> alert.setResizable(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
Optional<ButtonType> result = alert.showAndWait();
|
Optional<ButtonType> result = alert.showAndWait();
|
||||||
|
|
||||||
if(result.get() == ButtonType.YES)
|
if(result.isPresent() && result.get() == ButtonType.YES)
|
||||||
{
|
{
|
||||||
getAliasItemEditor().save();
|
getAliasItemEditor().save();
|
||||||
}
|
}
|
||||||
@@ -202,7 +218,7 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
getAliasBulkEditor().setItem(aliases);
|
getAliasBulkEditor().setItem(aliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCloneAliasButton().setDisable(aliases.isEmpty() || aliases.size() > 1);
|
getCloneAliasButton().setDisable(aliases.size() != 1);
|
||||||
getDeleteAliasButton().setDisable(aliases.isEmpty());
|
getDeleteAliasButton().setDisable(aliases.isEmpty());
|
||||||
getMoveToAliasButton().setDisable(aliases.isEmpty());
|
getMoveToAliasButton().setDisable(aliases.isEmpty());
|
||||||
}
|
}
|
||||||
@@ -248,7 +264,7 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
searchBox.setAlignment(Pos.BASELINE_RIGHT);
|
searchBox.setAlignment(Pos.BASELINE_RIGHT);
|
||||||
|
|
||||||
mSearchAndListSelectionBox.getChildren().addAll(listLabel, getAliasListNameComboBox(),
|
mSearchAndListSelectionBox.getChildren().addAll(listLabel, getAliasListNameComboBox(),
|
||||||
getNewAliasListButton(), searchBox);
|
getNewAliasListButton(), getRenameAliasListButton(), getDeleteAliasListButton(), searchBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mSearchAndListSelectionBox;
|
return mSearchAndListSelectionBox;
|
||||||
@@ -347,6 +363,70 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
return mNewAliasListButton;
|
return mNewAliasListButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Button getRenameAliasListButton() {
|
||||||
|
|
||||||
|
if (mRenameAliasButton == null) {
|
||||||
|
mRenameAliasButton = new Button("Rename Alias List");
|
||||||
|
mRenameAliasButton.setOnAction(event -> {
|
||||||
|
String aliasListName = getAliasListNameComboBox().getSelectionModel().getSelectedItem();
|
||||||
|
|
||||||
|
if (aliasListName.equals(AliasModel.NO_ALIAS_LIST)) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR, "", ButtonType.OK);
|
||||||
|
alert.setTitle("Rename Alias List");
|
||||||
|
alert.setHeaderText("You cannot rename " + aliasListName + ".");
|
||||||
|
alert.initOwner((getRenameAliasListButton()).getScene().getWindow());
|
||||||
|
alert.showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInputDialog dialog = new TextInputDialog();
|
||||||
|
dialog.setTitle("Renaming Alias List: " + aliasListName);
|
||||||
|
dialog.setHeaderText("Please enter the new alias list name (max 25 chars).");
|
||||||
|
dialog.setContentText("Name:");
|
||||||
|
dialog.getEditor().setTextFormatter(new TextFormatter<String>(new MaxLengthUnaryOperator(25)));
|
||||||
|
Optional<String> result = dialog.showAndWait();
|
||||||
|
result.ifPresent(newAliasListName -> {
|
||||||
|
mPlaylistManager.renameAliasList(aliasListName, newAliasListName);
|
||||||
|
getAliasListNameComboBox().getSelectionModel().select(newAliasListName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
return mRenameAliasButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button getDeleteAliasListButton() {
|
||||||
|
|
||||||
|
if (mDeleteAliasListButton == null) {
|
||||||
|
mDeleteAliasListButton = new Button("Delete Alias List");
|
||||||
|
mDeleteAliasListButton.setOnAction(event -> {
|
||||||
|
String aliasListName = getAliasListNameComboBox().getSelectionModel().getSelectedItem();
|
||||||
|
if (aliasListName.equals(AliasModel.NO_ALIAS_LIST)) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR, "", ButtonType.OK);
|
||||||
|
alert.setTitle("Delete Alias List");
|
||||||
|
alert.setHeaderText("You cannot delete " + aliasListName + ".");
|
||||||
|
alert.initOwner((getDeleteAliasListButton()).getScene().getWindow());
|
||||||
|
alert.showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "", ButtonType.NO, ButtonType.YES);
|
||||||
|
alert.setTitle("Delete Alias List");
|
||||||
|
alert.setHeaderText("Are you sure you want to delete the alias list " + aliasListName + " and all associated aliases?");
|
||||||
|
alert.initOwner((getDeleteAliasListButton()).getScene().getWindow());
|
||||||
|
|
||||||
|
Optional<ButtonType> result = alert.showAndWait();
|
||||||
|
if (result.isPresent() && result.get() == ButtonType.YES)
|
||||||
|
{
|
||||||
|
mPlaylistManager.deleteAliasList(aliasListName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mDeleteAliasListButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private TableView<Alias> getAliasTableView()
|
private TableView<Alias> getAliasTableView()
|
||||||
{
|
{
|
||||||
if(mAliasTableView == null)
|
if(mAliasTableView == null)
|
||||||
@@ -423,8 +503,7 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
mAliasTableView.setPlaceholder(getPlaceholderLabel());
|
mAliasTableView.setPlaceholder(getPlaceholderLabel());
|
||||||
mAliasTableView.setItems(getAliasSortedList());
|
mAliasTableView.setItems(getAliasSortedList());
|
||||||
mAliasTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
mAliasTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Alias>) c ->
|
mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Alias>)c -> {
|
||||||
{
|
|
||||||
Platform.runLater(() -> setAliases(mAliasTableView.getSelectionModel().getSelectedItems()));
|
Platform.runLater(() -> setAliases(mAliasTableView.getSelectionModel().getSelectedItems()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -537,11 +616,11 @@ public class AliasConfigurationEditor extends SplitPane
|
|||||||
ButtonType.NO, ButtonType.YES);
|
ButtonType.NO, ButtonType.YES);
|
||||||
alert.setTitle("Delete Alias");
|
alert.setTitle("Delete Alias");
|
||||||
alert.setHeaderText("Are you sure?");
|
alert.setHeaderText("Are you sure?");
|
||||||
alert.initOwner(((Node) getDeleteAliasButton()).getScene().getWindow());
|
alert.initOwner((getDeleteAliasButton()).getScene().getWindow());
|
||||||
|
|
||||||
Optional<ButtonType> result = alert.showAndWait();
|
Optional<ButtonType> result = alert.showAndWait();
|
||||||
|
|
||||||
if(result.get() == ButtonType.YES)
|
if(result.isPresent() && result.get() == ButtonType.YES)
|
||||||
{
|
{
|
||||||
List<Alias> selectedAliases = new ArrayList<>(getAliasTableView().getSelectionModel().getSelectedItems());
|
List<Alias> selectedAliases = new ArrayList<>(getAliasTableView().getSelectionModel().getSelectedItems());
|
||||||
mPlaylistManager.getAliasModel().removeAliases(selectedAliases);
|
mPlaylistManager.getAliasModel().removeAliases(selectedAliases);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This 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
|
||||||
@@ -142,6 +142,17 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the current channel when the play button is not disabled.
|
||||||
|
*/
|
||||||
|
public void startChannel()
|
||||||
|
{
|
||||||
|
if(!getPlayButton().disabledProperty().get())
|
||||||
|
{
|
||||||
|
getPlayButton().fire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public abstract DecoderType getDecoderType();
|
public abstract DecoderType getDecoderType();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ package io.github.dsheirer.gui.playlist.channel;
|
|||||||
|
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import io.github.dsheirer.controller.channel.Channel;
|
import io.github.dsheirer.controller.channel.Channel;
|
||||||
|
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
|
||||||
import io.github.dsheirer.module.decode.DecoderFactory;
|
import io.github.dsheirer.module.decode.DecoderFactory;
|
||||||
import io.github.dsheirer.module.decode.DecoderType;
|
import io.github.dsheirer.module.decode.DecoderType;
|
||||||
import io.github.dsheirer.playlist.PlaylistManager;
|
import io.github.dsheirer.playlist.PlaylistManager;
|
||||||
@@ -57,6 +58,7 @@ import javafx.scene.control.TableView;
|
|||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.control.ToggleButton;
|
import javafx.scene.control.ToggleButton;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
@@ -72,7 +74,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
/**
|
/**
|
||||||
* JavaFX editor for managing channel configurations.
|
* JavaFX editor for managing channel configurations.
|
||||||
*/
|
*/
|
||||||
public class ChannelEditor extends SplitPane implements IFilterProcessor
|
public class ChannelEditor extends SplitPane implements IFilterProcessor, IAliasListRefreshListener
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
|
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
|
||||||
private PlaylistManager mPlaylistManager;
|
private PlaylistManager mPlaylistManager;
|
||||||
@@ -103,6 +105,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
|||||||
public ChannelEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
|
public ChannelEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
|
||||||
{
|
{
|
||||||
mPlaylistManager = playlistManager;
|
mPlaylistManager = playlistManager;
|
||||||
|
mPlaylistManager.addAliasListRefreshListener(this);
|
||||||
mTunerManager = tunerManager;
|
mTunerManager = tunerManager;
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
|
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
|
||||||
@@ -123,6 +126,15 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
|||||||
getItems().addAll(topBox, getChannelConfigurationEditor());
|
getItems().addAll(topBox, getChannelConfigurationEditor());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares for an alias list refresh by clearing the currently selected channel from the channel table.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void prepareForAliasListRefresh()
|
||||||
|
{
|
||||||
|
getChannelTableView().getSelectionModel().select(null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes the channel view request.
|
* Processes the channel view request.
|
||||||
*
|
*
|
||||||
@@ -504,6 +516,12 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
|||||||
mChannelTableView.setItems(sortedList);
|
mChannelTableView.setItems(sortedList);
|
||||||
mChannelTableView.getSelectionModel().selectedItemProperty()
|
mChannelTableView.getSelectionModel().selectedItemProperty()
|
||||||
.addListener((observable, oldValue, newValue) -> setChannel(newValue));
|
.addListener((observable, oldValue, newValue) -> setChannel(newValue));
|
||||||
|
mChannelTableView.setOnMouseClicked(event -> {
|
||||||
|
if(event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2)
|
||||||
|
{
|
||||||
|
getChannelConfigurationEditor().startChannel();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return mChannelTableView;
|
return mChannelTableView;
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 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.gui.playlist.streaming;
|
package io.github.dsheirer.gui.playlist.streaming;
|
||||||
@@ -31,7 +28,9 @@ import javafx.geometry.HPos;
|
|||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
|
import org.controlsfx.control.ToggleSwitch;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcastify streaming configuration editor
|
* Broadcastify streaming configuration editor
|
||||||
@@ -39,6 +38,7 @@ import javafx.scene.layout.GridPane;
|
|||||||
public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyFeedConfiguration>
|
public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyFeedConfiguration>
|
||||||
{
|
{
|
||||||
private GridPane mEditorPane;
|
private GridPane mEditorPane;
|
||||||
|
private ToggleSwitch mVerboseLoggingToggle;
|
||||||
private TextField mMountPointTextField;
|
private TextField mMountPointTextField;
|
||||||
private IntegerTextField mFeedIdTextField;
|
private IntegerTextField mFeedIdTextField;
|
||||||
|
|
||||||
@@ -54,16 +54,19 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyF
|
|||||||
|
|
||||||
getFeedIdTextField().setDisable(item == null);
|
getFeedIdTextField().setDisable(item == null);
|
||||||
getMountPointTextField().setDisable(item == null);
|
getMountPointTextField().setDisable(item == null);
|
||||||
|
getVerboseLoggingToggle().setDisable(item == null);
|
||||||
|
|
||||||
if(item != null)
|
if(item != null)
|
||||||
{
|
{
|
||||||
getFeedIdTextField().set(item.getFeedID());
|
getFeedIdTextField().set(item.getFeedID());
|
||||||
getMountPointTextField().setText(item.getMountPoint());
|
getMountPointTextField().setText(item.getMountPoint());
|
||||||
|
getVerboseLoggingToggle().setSelected(item.isVerboseLogging());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
getFeedIdTextField().set(0);
|
getFeedIdTextField().set(0);
|
||||||
getMountPointTextField().setText(null);
|
getMountPointTextField().setText(null);
|
||||||
|
getVerboseLoggingToggle().setSelected(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
modifiedProperty().set(false);
|
modifiedProperty().set(false);
|
||||||
@@ -76,6 +79,7 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyF
|
|||||||
{
|
{
|
||||||
getItem().setFeedID(getFeedIdTextField().get());
|
getItem().setFeedID(getFeedIdTextField().get());
|
||||||
getItem().setMountPoint(getMountPointTextField().getText());
|
getItem().setMountPoint(getMountPointTextField().getText());
|
||||||
|
getItem().setVerboseLogging(getVerboseLoggingToggle().isSelected());
|
||||||
}
|
}
|
||||||
|
|
||||||
super.save();
|
super.save();
|
||||||
@@ -178,11 +182,32 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyF
|
|||||||
|
|
||||||
GridPane.setConstraints(getFeedIdTextField(), 1, 5);
|
GridPane.setConstraints(getFeedIdTextField(), 1, 5);
|
||||||
getEditorPane().getChildren().add(getFeedIdTextField());
|
getEditorPane().getChildren().add(getFeedIdTextField());
|
||||||
|
|
||||||
|
Label loggingLabel = new Label("Verbose Logging");
|
||||||
|
GridPane.setHalignment(loggingLabel, HPos.RIGHT);
|
||||||
|
GridPane.setConstraints(loggingLabel, 2, 5);
|
||||||
|
getEditorPane().getChildren().add(loggingLabel);
|
||||||
|
|
||||||
|
GridPane.setConstraints(getVerboseLoggingToggle(), 3, 5);
|
||||||
|
getEditorPane().getChildren().add(getVerboseLoggingToggle());
|
||||||
}
|
}
|
||||||
|
|
||||||
return mEditorPane;
|
return mEditorPane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ToggleSwitch getVerboseLoggingToggle()
|
||||||
|
{
|
||||||
|
if(mVerboseLoggingToggle == null)
|
||||||
|
{
|
||||||
|
mVerboseLoggingToggle = new ToggleSwitch();
|
||||||
|
mVerboseLoggingToggle.setTooltip(new Tooltip("Turn on additional logging for connection troubleshooting"));
|
||||||
|
mVerboseLoggingToggle.setDisable(true);
|
||||||
|
mVerboseLoggingToggle.selectedProperty().addListener((ob, ol, ne) -> modifiedProperty().set(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mVerboseLoggingToggle;
|
||||||
|
}
|
||||||
|
|
||||||
private TextField getMountPointTextField()
|
private TextField getMountPointTextField()
|
||||||
{
|
{
|
||||||
if(mMountPointTextField == null)
|
if(mMountPointTextField == null)
|
||||||
|
|||||||
@@ -1,13 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.map;
|
package io.github.dsheirer.map;
|
||||||
|
|
||||||
import javax.swing.table.AbstractTableModel;
|
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.ToLongFunction;
|
|
||||||
|
import javax.swing.table.AbstractTableModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table model for track history.
|
* Table model for track history.
|
||||||
@@ -15,7 +33,7 @@ import java.util.function.ToLongFunction;
|
|||||||
public class TrackHistoryModel extends AbstractTableModel
|
public class TrackHistoryModel extends AbstractTableModel
|
||||||
{
|
{
|
||||||
private static final String[] COLUMNS = new String[]{"Time", "Latitude", "Longitude"};
|
private static final String[] COLUMNS = new String[]{"Time", "Latitude", "Longitude"};
|
||||||
private SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("HH:mm:ss");
|
private SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
private DecimalFormat mDegreeFormat = new DecimalFormat("0.00000");
|
private DecimalFormat mDegreeFormat = new DecimalFormat("0.00000");
|
||||||
private List<TimestampedGeoPosition> mTimestampedGeoPositions = new ArrayList<>();
|
private List<TimestampedGeoPosition> mTimestampedGeoPositions = new ArrayList<>();
|
||||||
private PlottableEntityHistory mPlottableEntityHistory;
|
private PlottableEntityHistory mPlottableEntityHistory;
|
||||||
|
|||||||
@@ -1,3 +1,22 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
|
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
|
||||||
|
|
||||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
@@ -6,7 +25,6 @@ import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
|||||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -15,7 +33,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public class CapacityMaxVoiceChannelUser extends CapacityPlusVoiceChannelUser
|
public class CapacityMaxVoiceChannelUser extends CapacityPlusVoiceChannelUser
|
||||||
{
|
{
|
||||||
private static final int[] TALKGROUP = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
private static final int[] TALKGROUP = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||||
|
41, 42, 43, 44, 45, 46, 47};
|
||||||
private static final int[] RADIO = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
|
private static final int[] RADIO = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
|
||||||
66, 67, 68, 69, 70, 71};
|
66, 67, 68, 69, 70, 71};
|
||||||
|
|
||||||
|
|||||||
@@ -1062,7 +1062,7 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
private void requestTrafficChannelStart(Channel trafficChannel, APCO25Channel apco25Channel,
|
private void requestTrafficChannelStart(Channel trafficChannel, APCO25Channel apco25Channel,
|
||||||
IdentifierCollection identifierCollection, long timestamp)
|
IdentifierCollection identifierCollection, long timestamp)
|
||||||
{
|
{
|
||||||
if(getInterModuleEventBus() != null)
|
if(apco25Channel != null && apco25Channel.getDownlinkFrequency() > 0 && getInterModuleEventBus() != null)
|
||||||
{
|
{
|
||||||
SourceConfigTuner sourceConfig = new SourceConfigTuner();
|
SourceConfigTuner sourceConfig = new SourceConfigTuner();
|
||||||
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
|
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
|
||||||
@@ -1086,6 +1086,18 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values()));
|
startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values()));
|
||||||
getInterModuleEventBus().post(startChannelRequest);
|
getInterModuleEventBus().post(startChannelRequest);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//Return the channel to the traffic channel pool since we didn't start it.
|
||||||
|
if(mManagedPhase1TrafficChannels.contains(trafficChannel))
|
||||||
|
{
|
||||||
|
mAvailablePhase1TrafficChannelQueue.add(trafficChannel);
|
||||||
|
}
|
||||||
|
else if(mManagedPhase2TrafficChannels.contains(trafficChannel))
|
||||||
|
{
|
||||||
|
mAvailablePhase2TrafficChannelQueue.add(trafficChannel);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1163,16 +1175,20 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mIgnoreDataCalls && isDataChannelGrant && tracker == null)
|
if(mIgnoreDataCalls && isDataChannelGrant)
|
||||||
{
|
{
|
||||||
P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions)
|
if(tracker == null)
|
||||||
.channelDescriptor(apco25Channel)
|
{
|
||||||
.details("IGNORED: PHASE 1 DATA CALL " + (serviceOptions != null ? serviceOptions : ""))
|
P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions)
|
||||||
.identifiers(ic)
|
.channelDescriptor(apco25Channel)
|
||||||
.build();
|
.details("IGNORED: PHASE 1 DATA CALL " + (serviceOptions != null ? serviceOptions : ""))
|
||||||
tracker = new P25TrafficChannelEventTracker(event);
|
.identifiers(ic)
|
||||||
addTracker(tracker, frequency, P25P1Message.TIMESLOT_1);
|
.build();
|
||||||
broadcast(tracker);
|
tracker = new P25TrafficChannelEventTracker(event);
|
||||||
|
addTracker(tracker, frequency, P25P1Message.TIMESLOT_1);
|
||||||
|
broadcast(tracker);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
private final PatchGroupManager mPatchGroupManager = new PatchGroupManager();
|
private final PatchGroupManager mPatchGroupManager = new PatchGroupManager();
|
||||||
private final P25P1NetworkConfigurationMonitor mNetworkConfigurationMonitor;
|
private final P25P1NetworkConfigurationMonitor mNetworkConfigurationMonitor;
|
||||||
private final Listener<ChannelEvent> mChannelEventListener;
|
private final Listener<ChannelEvent> mChannelEventListener;
|
||||||
private P25TrafficChannelManager mTrafficChannelManager;
|
private final P25TrafficChannelManager mTrafficChannelManager;
|
||||||
private ServiceOptions mCurrentServiceOptions;
|
private ServiceOptions mCurrentServiceOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2055,7 +2055,10 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
|||||||
if(lcw instanceof LCMotorolaUnitGPS gps)
|
if(lcw instanceof LCMotorolaUnitGPS gps)
|
||||||
{
|
{
|
||||||
mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), gps.getLocation(), timestamp, lcw.toString());
|
mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), gps.getLocation(), timestamp, lcw.toString());
|
||||||
MutableIdentifierCollection mic = getMutableIdentifierCollection(gps.getIdentifiers(), timestamp);
|
|
||||||
|
//We want to preserve the current TO/FROM and any other identifiers for the GPS event and add the GPS location
|
||||||
|
MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||||
|
mic.update(gps.getIdentifiers());
|
||||||
|
|
||||||
PlottableDecodeEvent event = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, timestamp)
|
PlottableDecodeEvent event = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, timestamp)
|
||||||
.location(gps.getGeoPosition())
|
.location(gps.getGeoPosition())
|
||||||
|
|||||||
@@ -236,13 +236,13 @@ public class P25P1MessageProcessor implements Listener<IMessage>
|
|||||||
processForFrequencyBands(message);
|
processForFrequencyBands(message);
|
||||||
|
|
||||||
//Also process the link control messages for frequency bands.
|
//Also process the link control messages for frequency bands.
|
||||||
if(message instanceof LDU1Message ldu1)
|
if(message instanceof LDU1Message ldu1 && ldu1.getLinkControlWord() instanceof LinkControlWord lcw && lcw.isValid())
|
||||||
{
|
{
|
||||||
processForFrequencyBands(ldu1.getLinkControlWord());
|
processForFrequencyBands(lcw);
|
||||||
}
|
}
|
||||||
else if(message instanceof TDULCMessage tdulc)
|
else if(message instanceof TDULCMessage tdulc && tdulc.getLinkControlWord() instanceof LinkControlWord lcw && lcw.isValid())
|
||||||
{
|
{
|
||||||
processForFrequencyBands(tdulc.getLinkControlWord());
|
processForFrequencyBands(lcw);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mMessageListener != null)
|
if(mMessageListener != null)
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ public class L3HarrisGroupRegroupExplicitEncryptionCommand extends VendorOSPMess
|
|||||||
private static final IntField REGROUP_OPTIONS = IntField.length8(OCTET_2_BIT_16);
|
private static final IntField REGROUP_OPTIONS = IntField.length8(OCTET_2_BIT_16);
|
||||||
private static final IntField SUPERGROUP_ADDRESS = IntField.length16(OCTET_3_BIT_24);
|
private static final IntField SUPERGROUP_ADDRESS = IntField.length16(OCTET_3_BIT_24);
|
||||||
private static final IntField ENCRYPTION_KEY_ID = IntField.length16(OCTET_5_BIT_40);
|
private static final IntField ENCRYPTION_KEY_ID = IntField.length16(OCTET_5_BIT_40);
|
||||||
private static final IntField TARGET_ADDRESS = IntField.length24(OCTET_7_BIT_56);
|
private static final IntField TARGET_RADIO_ADDRESS = IntField.length24(OCTET_7_BIT_56);
|
||||||
|
private static final IntField TARGET_TALKGROUP_ADDRESS = IntField.length16(OCTET_8_BIT_64);
|
||||||
|
|
||||||
private TalkgroupIdentifier mSuperGroupIdentifier;
|
private TalkgroupIdentifier mSuperGroupIdentifier;
|
||||||
private APCO25PatchGroup mPatchGroup;
|
private APCO25PatchGroup mPatchGroup;
|
||||||
@@ -128,11 +129,11 @@ public class L3HarrisGroupRegroupExplicitEncryptionCommand extends VendorOSPMess
|
|||||||
{
|
{
|
||||||
if(getRegroupOptions().isTalkgroupAddress())
|
if(getRegroupOptions().isTalkgroupAddress())
|
||||||
{
|
{
|
||||||
mPatchedIdentifier = APCO25Talkgroup.create(getInt(TARGET_ADDRESS));
|
mPatchedIdentifier = APCO25Talkgroup.create(getInt(TARGET_TALKGROUP_ADDRESS));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mPatchedIdentifier = APCO25RadioIdentifier.createTo(getInt(TARGET_ADDRESS));
|
mPatchedIdentifier = APCO25RadioIdentifier.createTo(getInt(TARGET_RADIO_ADDRESS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -465,9 +465,16 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
|||||||
case PHASE1_90_GROUP_REGROUP_VOICE_CHANNEL_USER_ABBREVIATED:
|
case PHASE1_90_GROUP_REGROUP_VOICE_CHANNEL_USER_ABBREVIATED:
|
||||||
if(mac instanceof GroupRegroupVoiceChannelUserAbbreviated gr)
|
if(mac instanceof GroupRegroupVoiceChannelUserAbbreviated gr)
|
||||||
{
|
{
|
||||||
mPatchGroupManager.addPatchGroup(gr.getPatchgroup(), message.getTimestamp());
|
if(gr.hasPatchgroup())
|
||||||
|
{
|
||||||
|
mPatchGroupManager.addPatchGroup(gr.getPatchgroup(), message.getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(gr.hasPatchgroup() || gr.hasRadio())
|
||||||
|
{
|
||||||
|
processChannelUser(message, mac);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
processChannelUser(message, mac);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harri
|
|||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisUnknownOpcode143;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisUnknownOpcode143;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.UnknownOpcode136;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.UnknownOpcode136;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaAcknowledgeResponse;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaAcknowledgeResponse;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaActiveGroupRadiosOpcode130_x82;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaActiveGroupRadiosOpcode143_x8F;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaActiveGroupRadiosOpcode191_xBF;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaDenyResponse;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaDenyResponse;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupAddCommand;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupAddCommand;
|
||||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupChannelGrantExplicit;
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupChannelGrantExplicit;
|
||||||
@@ -578,12 +581,18 @@ public class MacMessageFactory
|
|||||||
return new MotorolaGroupRegroupVoiceChannelUserAbbreviated(message, offset);
|
return new MotorolaGroupRegroupVoiceChannelUserAbbreviated(message, offset);
|
||||||
case MOTOROLA_81_GROUP_REGROUP_ADD:
|
case MOTOROLA_81_GROUP_REGROUP_ADD:
|
||||||
return new MotorolaGroupRegroupAddCommand(message, offset);
|
return new MotorolaGroupRegroupAddCommand(message, offset);
|
||||||
|
case MOTOROLA_82_ACTIVE_GROUP_RADIOS_130:
|
||||||
|
return new MotorolaActiveGroupRadiosOpcode130_x82(message, offset);
|
||||||
case MOTOROLA_83_GROUP_REGROUP_VOICE_CHANNEL_UPDATE:
|
case MOTOROLA_83_GROUP_REGROUP_VOICE_CHANNEL_UPDATE:
|
||||||
return new MotorolaGroupRegroupVoiceChannelUpdate(message, offset);
|
return new MotorolaGroupRegroupVoiceChannelUpdate(message, offset);
|
||||||
case MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND:
|
case MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND:
|
||||||
return new MotorolaGroupRegroupExtendedFunctionCommand(message, offset);
|
return new MotorolaGroupRegroupExtendedFunctionCommand(message, offset);
|
||||||
case MOTOROLA_89_GROUP_REGROUP_DELETE:
|
case MOTOROLA_89_GROUP_REGROUP_DELETE:
|
||||||
return new MotorolaGroupRegroupDeleteCommand(message, offset);
|
return new MotorolaGroupRegroupDeleteCommand(message, offset);
|
||||||
|
case MOTOROLA_8F_ACTIVE_GROUP_RADIOS_143:
|
||||||
|
return new MotorolaActiveGroupRadiosOpcode143_x8F(message, offset);
|
||||||
|
case MOTOROLA_BF_ACTIVE_GROUP_RADIOS_191:
|
||||||
|
return new MotorolaActiveGroupRadiosOpcode191_xBF(message, offset);
|
||||||
case MOTOROLA_91_TALKER_ALIAS_HEADER:
|
case MOTOROLA_91_TALKER_ALIAS_HEADER:
|
||||||
return new MotorolaTalkerAliasHeader(message, offset);
|
return new MotorolaTalkerAliasHeader(message, offset);
|
||||||
case MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK:
|
case MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK:
|
||||||
|
|||||||
@@ -115,12 +115,15 @@ public enum MacOpcode
|
|||||||
|
|
||||||
MOTOROLA_80_GROUP_REGROUP_VOICE_CHANNEL_USER_ABBREVIATED(Vendor.MOTOROLA, 128, "MOTOROLA GROUP REGROUP VOICE CHANNEL USER ABBREVIATED", 8),
|
MOTOROLA_80_GROUP_REGROUP_VOICE_CHANNEL_USER_ABBREVIATED(Vendor.MOTOROLA, 128, "MOTOROLA GROUP REGROUP VOICE CHANNEL USER ABBREVIATED", 8),
|
||||||
MOTOROLA_81_GROUP_REGROUP_ADD(Vendor.MOTOROLA, 129, "MOTOROLA GROUP REGROUP ADD", 17),
|
MOTOROLA_81_GROUP_REGROUP_ADD(Vendor.MOTOROLA, 129, "MOTOROLA GROUP REGROUP ADD", 17),
|
||||||
MOTOROLA_83_GROUP_REGROUP_VOICE_CHANNEL_UPDATE(Vendor.MOTOROLA, 131, "MOTOROLA INDIVIDUAL REGROUP ACTIVE", 8),
|
MOTOROLA_82_ACTIVE_GROUP_RADIOS_130(Vendor.MOTOROLA, 130, "MOTOROLA ACTIVE GROUP RADIOS 130", Integer.MIN_VALUE),
|
||||||
|
MOTOROLA_83_GROUP_REGROUP_VOICE_CHANNEL_UPDATE(Vendor.MOTOROLA, 131, "MOTOROLA INDIVIDUAL REGROUP ACTIVE", 7),
|
||||||
MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND(Vendor.MOTOROLA, 133, "MOTOROLA GROUP REGROUP", 11),
|
MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND(Vendor.MOTOROLA, 133, "MOTOROLA GROUP REGROUP", 11),
|
||||||
MOTOROLA_89_GROUP_REGROUP_DELETE(Vendor.MOTOROLA, 137, "MOTOROLA GROUP REGROUP DELETE", 17),
|
MOTOROLA_89_GROUP_REGROUP_DELETE(Vendor.MOTOROLA, 137, "MOTOROLA GROUP REGROUP DELETE", 17),
|
||||||
|
MOTOROLA_8F_ACTIVE_GROUP_RADIOS_143(Vendor.MOTOROLA, 143, "MOTOROLA ACTIVE GROUP RADIOS 143", Integer.MIN_VALUE),
|
||||||
|
MOTOROLA_BF_ACTIVE_GROUP_RADIOS_191(Vendor.MOTOROLA, 191, "MOTOROLA ACTIVE GROUP RADIOS 191", Integer.MIN_VALUE),
|
||||||
//Opcode 144 uses STANDARD vendor ID for some reason.
|
//Opcode 144 uses STANDARD vendor ID for some reason.
|
||||||
MOTOROLA_91_TALKER_ALIAS_HEADER(Vendor.MOTOROLA, 145, "MOTOROLA GROUP REGROUP UNKNOWN", 17),
|
MOTOROLA_91_TALKER_ALIAS_HEADER(Vendor.MOTOROLA, 145, "MOTOROLA TALKER ALIAS HEADER", 17),
|
||||||
MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK(Vendor.MOTOROLA, 149, "MOTOROLA UNKNOWN 149", 17),
|
MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK(Vendor.MOTOROLA, 149, "MOTOROLA TALKER ALIAS DATA BLOCK", 17),
|
||||||
MOTOROLA_A0_GROUP_REGROUP_VOICE_CHANNEL_USER_EXTENDED(Vendor.MOTOROLA, 160, "MOTOROLA GROUP REGROUP VOICE CHANNEL USER EXTENDED", 16),
|
MOTOROLA_A0_GROUP_REGROUP_VOICE_CHANNEL_USER_EXTENDED(Vendor.MOTOROLA, 160, "MOTOROLA GROUP REGROUP VOICE CHANNEL USER EXTENDED", 16),
|
||||||
MOTOROLA_A3_GROUP_REGROUP_CHANNEL_GRANT_IMPLICIT(Vendor.MOTOROLA, 163, "MOTOROLA GROUP REGROUP CHANNEL GRANT IMPLICIT", 11),
|
MOTOROLA_A3_GROUP_REGROUP_CHANNEL_GRANT_IMPLICIT(Vendor.MOTOROLA, 163, "MOTOROLA GROUP REGROUP CHANNEL GRANT IMPLICIT", 11),
|
||||||
MOTOROLA_A4_GROUP_REGROUP_CHANNEL_GRANT_EXPLICIT(Vendor.MOTOROLA, 164, "MOTOROLA GROUP REGROUP CHANNEL GRANT EXPLICIT", 13),
|
MOTOROLA_A4_GROUP_REGROUP_CHANNEL_GRANT_EXPLICIT(Vendor.MOTOROLA, 164, "MOTOROLA GROUP REGROUP CHANNEL GRANT EXPLICIT", 13),
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import io.github.dsheirer.module.decode.p25.identifier.patch.APCO25PatchGroup;
|
|||||||
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.reference.ServiceOptions;
|
import io.github.dsheirer.module.decode.p25.reference.ServiceOptions;
|
||||||
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
|
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -64,11 +63,7 @@ public class GroupRegroupVoiceChannelUserAbbreviated extends MacStructure implem
|
|||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("GROUP REGROUP VOICE CHANNEL USER ABBREVIATED");
|
sb.append("GROUP REGROUP VOICE CHANNEL USER ABBREVIATED");
|
||||||
sb.append(" TALKGROUP:").append(getPatchgroup());
|
sb.append(" TALKGROUP:").append(getPatchgroup());
|
||||||
if(hasRadio())
|
sb.append(" TALKER RADIO:").append(getRadio());
|
||||||
{
|
|
||||||
sb.append(" TALKER RADIO:").append(getRadio());
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +86,14 @@ public class GroupRegroupVoiceChannelUserAbbreviated extends MacStructure implem
|
|||||||
return mPatchgroup;
|
return mPatchgroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message has a non-zero patch group identifier.
|
||||||
|
*/
|
||||||
|
public boolean hasPatchgroup()
|
||||||
|
{
|
||||||
|
return getInt(SUPERGROUP) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Talker radio identifier.
|
* Talker radio identifier.
|
||||||
*/
|
*/
|
||||||
@@ -118,7 +121,11 @@ public class GroupRegroupVoiceChannelUserAbbreviated extends MacStructure implem
|
|||||||
if(mIdentifiers == null)
|
if(mIdentifiers == null)
|
||||||
{
|
{
|
||||||
mIdentifiers = new ArrayList<>();
|
mIdentifiers = new ArrayList<>();
|
||||||
mIdentifiers.add(getPatchgroup());
|
|
||||||
|
if(hasPatchgroup())
|
||||||
|
{
|
||||||
|
mIdentifiers.add(getPatchgroup());
|
||||||
|
}
|
||||||
|
|
||||||
if(hasRadio())
|
if(hasRadio())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import io.github.dsheirer.bits.IntField;
|
||||||
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
|
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||||
|
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motorola Group Radios Active Feature. Observed on NM-DTRS Phase 2 network. My theory is that this lists the
|
||||||
|
* subset of talkgroup member radios that are currently active on the traffic channel.
|
||||||
|
*
|
||||||
|
* Opcodes 130 and 143 are almost the same, except 143 is mostly transmitted during an ACTIVE call state and 130 is
|
||||||
|
* mostly transmitted during the HANGTIME state. I say mostly, because there are examples where both opcodes are seen
|
||||||
|
* in both channel states.
|
||||||
|
*
|
||||||
|
* Curiously: opcode 130 is in the middle of the message number sequence where GROUP REGROUP messages are allocated.
|
||||||
|
*
|
||||||
|
* Opcode 143 includes an additional 1-byte status field that seems to contain a 2-bit sequence counter that mostly
|
||||||
|
* cycles through all values, but sometimes it skips values or just toggles between two of the observed values.
|
||||||
|
* Examples: 0x00, 0x40, 0x80, 0xC0
|
||||||
|
* There doesn't seem to be any correlation between the value of this nibble and the radio IDs being transmitted.
|
||||||
|
*
|
||||||
|
* The lower nibble of this 1-byte field seems to have an additional flag bit that is sometimes transmitted:
|
||||||
|
* Examples: 0x08, 0x88
|
||||||
|
*
|
||||||
|
* Both opcode 130 and 143 employ up to 2x 1-byte fields that always transmit a 0x09 value. The second field is only
|
||||||
|
* present when the message is sending 3 or 4 radio IDs.
|
||||||
|
*
|
||||||
|
* It seems that the complete list of active member radios is transmitted in a fixed sequence with zero to four of those
|
||||||
|
* radios transmitted per message. The messages are transmitted when the talkgroup is first allocated to the channel
|
||||||
|
* and eventually the 130/143 messages stop listing radios and the empty 143 and the 191 messages are intermittently
|
||||||
|
* sent. Occasionally there are radios listed in the message with all zeros values.
|
||||||
|
*
|
||||||
|
* Opcode 143 is transmitted with just the status byte and no radio IDs after the full sequence of radios has been
|
||||||
|
* sent and are no longer being sent.
|
||||||
|
*
|
||||||
|
* I'm not sure what Opcode 191 indicates. It's always the same (3-bytes) value: BF9003. It may be some kind of
|
||||||
|
* feature marker to let the listening radios/devices know that the feature is active on that traffic channel.
|
||||||
|
*
|
||||||
|
* These three opcodes seem to be used for this feature.
|
||||||
|
* Opcode 130/x82 - talkgroup member radios currently active - mostly transmitted during HANGTIME
|
||||||
|
* Opcode 143/x8F - talkgroup member radios currently active - mostly transmitted during ACTIVE call state
|
||||||
|
* Opcode 191/xBF - transmitted continuously while channel is allocated to a talkgroup - fixed value: 0xBF9003
|
||||||
|
*/
|
||||||
|
public class MotorolaActiveGroupRadiosOpcode130_x82 extends MacStructureVendor
|
||||||
|
{
|
||||||
|
private static final IntField SEPARATOR_1 = IntField.length8(OCTET_4_BIT_24); //Value is always 0x09
|
||||||
|
private static final IntField RADIO_1 = IntField.length24(OCTET_5_BIT_32);
|
||||||
|
private static final IntField RADIO_2 = IntField.length24(OCTET_8_BIT_56);
|
||||||
|
private static final IntField SEPARATOR_2 = IntField.length8(OCTET_11_BIT_80); //Value is always 0x09
|
||||||
|
private static final IntField RADIO_3 = IntField.length24(OCTET_12_BIT_88);
|
||||||
|
private static final IntField RADIO_4 = IntField.length24(OCTET_15_BIT_112);
|
||||||
|
private RadioIdentifier mRadio1;
|
||||||
|
private RadioIdentifier mRadio2;
|
||||||
|
private RadioIdentifier mRadio3;
|
||||||
|
private RadioIdentifier mRadio4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the message
|
||||||
|
*
|
||||||
|
* @param message containing the message bits
|
||||||
|
* @param offset into the message for this structure
|
||||||
|
*/
|
||||||
|
public MotorolaActiveGroupRadiosOpcode130_x82(CorrectedBinaryMessage message, int offset)
|
||||||
|
{
|
||||||
|
super(message, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textual representation of this message
|
||||||
|
*/
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("MOTOROLA 130 ACTIVE GROUP RADIOS: ");
|
||||||
|
sb.append(getRadio1());
|
||||||
|
|
||||||
|
if(hasRadio2())
|
||||||
|
{
|
||||||
|
sb.append(", ").append(getRadio2());
|
||||||
|
|
||||||
|
if(hasRadio3())
|
||||||
|
{
|
||||||
|
sb.append(", ").append(getRadio3());
|
||||||
|
|
||||||
|
if(hasRadio4())
|
||||||
|
{
|
||||||
|
sb.append(", ").append(getRadio4());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CorrectedBinaryMessage cbm = getMessage().getSubMessage(getOffset(), getOffset() + (getLength() * 8));
|
||||||
|
sb.append(" MSG:").append(cbm.toHexString());
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio1()
|
||||||
|
{
|
||||||
|
if(mRadio1 == null && hasRadio1())
|
||||||
|
{
|
||||||
|
mRadio1 = APCO25RadioIdentifier.createAny(getInt(RADIO_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio2()
|
||||||
|
{
|
||||||
|
if(mRadio2 == null && hasRadio2())
|
||||||
|
{
|
||||||
|
mRadio2 = APCO25RadioIdentifier.createAny(getInt(RADIO_2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio3()
|
||||||
|
{
|
||||||
|
if(mRadio3 == null && hasRadio3())
|
||||||
|
{
|
||||||
|
mRadio3 = APCO25RadioIdentifier.createAny(getInt(RADIO_3));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio4()
|
||||||
|
{
|
||||||
|
if(mRadio4 == null && hasRadio4())
|
||||||
|
{
|
||||||
|
mRadio4 = APCO25RadioIdentifier.createAny(getInt(RADIO_4));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio1()
|
||||||
|
{
|
||||||
|
return getLength() > 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio2()
|
||||||
|
{
|
||||||
|
return getLength() > 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio3()
|
||||||
|
{
|
||||||
|
return getLength() > 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio4()
|
||||||
|
{
|
||||||
|
return getLength() > 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Identifier> getIdentifiers()
|
||||||
|
{
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import io.github.dsheirer.bits.IntField;
|
||||||
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
|
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||||
|
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motorola Group Radios Active Feature. Observed on NM-DTRS Phase 2 network. My theory is that this lists the
|
||||||
|
* subset of talkgroup member radios that are currently active on the traffic channel.
|
||||||
|
*
|
||||||
|
* Opcodes 130 and 143 are almost the same, except 143 is mostly transmitted during an ACTIVE call state and 130 is
|
||||||
|
* mostly transmitted during the HANGTIME state. I say mostly, because there are examples where both opcodes are seen
|
||||||
|
* in both channel states.
|
||||||
|
*
|
||||||
|
* Curiously: opcode 130 is in the middle of the message number sequence where GROUP REGROUP messages are allocated.
|
||||||
|
*
|
||||||
|
* Opcode 143 includes an additional 1-byte status field that seems to contain a 2-bit sequence counter that mostly
|
||||||
|
* cycles through all values, but sometimes it skips values or just toggles between two of the observed values.
|
||||||
|
* Examples: 0x00, 0x40, 0x80, 0xC0
|
||||||
|
* There doesn't seem to be any correlation between the value of this nibble and the radio IDs being transmitted.
|
||||||
|
*
|
||||||
|
* The lower nibble of this 1-byte field seems to have an additional flag bit that is sometimes transmitted:
|
||||||
|
* Examples: 0x08, 0x88
|
||||||
|
*
|
||||||
|
* Both opcode 130 and 143 employ up to 2x 1-byte fields that always transmit a 0x09 value. The second field is only
|
||||||
|
* present when the message is sending 3 or 4 radio IDs.
|
||||||
|
*
|
||||||
|
* It seems that the complete list of active member radios is transmitted in a fixed sequence with zero to four of those
|
||||||
|
* radios transmitted per message. The messages are transmitted when the talkgroup is first allocated to the channel
|
||||||
|
* and eventually the 130/143 messages stop listing radios and the empty 143 and the 191 messages are intermittently
|
||||||
|
* sent. Occasionally there are radios listed in the message with all zeros values.
|
||||||
|
*
|
||||||
|
* Opcode 143 is transmitted with just the status byte and no radio IDs after the full sequence of radios has been
|
||||||
|
* sent and are no longer being sent.
|
||||||
|
*
|
||||||
|
* I'm not sure what Opcode 191 indicates. It's always the same (3-bytes) value: BF9003. It may be some kind of
|
||||||
|
* feature marker to let the listening radios/devices know that the feature is active on that traffic channel.
|
||||||
|
*
|
||||||
|
* These three opcodes seem to be used for this feature.
|
||||||
|
* Opcode 130/x82 - talkgroup member radios currently active - mostly transmitted during HANGTIME
|
||||||
|
* Opcode 143/x8F - talkgroup member radios currently active - mostly transmitted during ACTIVE call state
|
||||||
|
* Opcode 191/xBF - transmitted continuously while channel is allocated to a talkgroup - fixed value: 0xBF9003
|
||||||
|
*/
|
||||||
|
public class MotorolaActiveGroupRadiosOpcode143_x8F extends MacStructureVendor
|
||||||
|
{
|
||||||
|
private static final IntField STATUS = IntField.length8(OCTET_4_BIT_24);
|
||||||
|
private static final IntField SEPARATOR_1 = IntField.length8(OCTET_5_BIT_32);
|
||||||
|
private static final IntField RADIO_1 = IntField.length24(OCTET_6_BIT_40);
|
||||||
|
private static final IntField RADIO_2 = IntField.length24(OCTET_9_BIT_64);
|
||||||
|
private static final IntField SEPARATOR_2 = IntField.length8(OCTET_12_BIT_88);
|
||||||
|
private static final IntField RADIO_3 = IntField.length24(OCTET_13_BIT_96);
|
||||||
|
private static final IntField RADIO_4 = IntField.length24(OCTET_16_BIT_120);
|
||||||
|
private RadioIdentifier mRadio1;
|
||||||
|
private RadioIdentifier mRadio2;
|
||||||
|
private RadioIdentifier mRadio3;
|
||||||
|
private RadioIdentifier mRadio4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the message
|
||||||
|
*
|
||||||
|
* @param message containing the message bits
|
||||||
|
* @param offset into the message for this structure
|
||||||
|
*/
|
||||||
|
public MotorolaActiveGroupRadiosOpcode143_x8F(CorrectedBinaryMessage message, int offset)
|
||||||
|
{
|
||||||
|
super(message, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textual representation of this message
|
||||||
|
*/
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("MOTOROLA 143 ACTIVE GROUP RADIOS: ");
|
||||||
|
|
||||||
|
if(hasRadio1())
|
||||||
|
{
|
||||||
|
sb.append(getRadio1());
|
||||||
|
|
||||||
|
if(hasRadio2())
|
||||||
|
{
|
||||||
|
sb.append(", ").append(getRadio2());
|
||||||
|
|
||||||
|
if(hasRadio3())
|
||||||
|
{
|
||||||
|
sb.append(", ").append(getRadio3());
|
||||||
|
|
||||||
|
if(hasRadio4())
|
||||||
|
{
|
||||||
|
sb.append(", ").append(getRadio4());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.append("NONE");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(" STATUS:").append(getStatusCode());
|
||||||
|
|
||||||
|
CorrectedBinaryMessage cbm = getMessage().getSubMessage(getOffset(), getOffset() + (getLength() * 8));
|
||||||
|
sb.append(" MSG:").append(cbm.toHexString());
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unknown status byte. First two bits seem to be a cycle counter 0x00, 0x40, 0x80, 0xC0. Second nibble, high
|
||||||
|
* bit observed to be set sometimes: 0x08, 0x88
|
||||||
|
* @return code value in hex.
|
||||||
|
*/
|
||||||
|
public String getStatusCode()
|
||||||
|
{
|
||||||
|
return Integer.toHexString(getInt(STATUS)).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio1()
|
||||||
|
{
|
||||||
|
if(mRadio1 == null && hasRadio1())
|
||||||
|
{
|
||||||
|
mRadio1 = APCO25RadioIdentifier.createAny(getInt(RADIO_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio2()
|
||||||
|
{
|
||||||
|
if(mRadio2 == null && hasRadio2())
|
||||||
|
{
|
||||||
|
mRadio2 = APCO25RadioIdentifier.createAny(getInt(RADIO_2));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio3()
|
||||||
|
{
|
||||||
|
if(mRadio3 == null && hasRadio3())
|
||||||
|
{
|
||||||
|
mRadio3 = APCO25RadioIdentifier.createAny(getInt(RADIO_3));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Radio identifier or null if the message doesn't contain the value.
|
||||||
|
*/
|
||||||
|
public RadioIdentifier getRadio4()
|
||||||
|
{
|
||||||
|
if(mRadio4 == null && hasRadio4())
|
||||||
|
{
|
||||||
|
mRadio4 = APCO25RadioIdentifier.createAny(getInt(RADIO_4));
|
||||||
|
}
|
||||||
|
|
||||||
|
return mRadio4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio1()
|
||||||
|
{
|
||||||
|
return getLength() > 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio2()
|
||||||
|
{
|
||||||
|
return getLength() > 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio3()
|
||||||
|
{
|
||||||
|
return getLength() > 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this message contains the radio value.
|
||||||
|
*/
|
||||||
|
public boolean hasRadio4()
|
||||||
|
{
|
||||||
|
return getLength() > 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Identifier> getIdentifiers()
|
||||||
|
{
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
* ****************************************************************************
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola;
|
||||||
|
|
||||||
|
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||||
|
import io.github.dsheirer.identifier.Identifier;
|
||||||
|
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motorola Group Radios Active Feature. Observed on NM-DTRS Phase 2 network. My theory is that this lists the
|
||||||
|
* subset of talkgroup member radios that are currently active on the traffic channel.
|
||||||
|
*
|
||||||
|
* Opcodes 130 and 143 are almost the same, except 143 is mostly transmitted during an ACTIVE call state and 130 is
|
||||||
|
* mostly transmitted during the HANGTIME state. I say mostly, because there are examples where both opcodes are seen
|
||||||
|
* in both channel states.
|
||||||
|
*
|
||||||
|
* Curiously: opcode 130 is in the middle of the message number sequence where GROUP REGROUP messages are allocated.
|
||||||
|
*
|
||||||
|
* Opcode 143 includes an additional 1-byte status field that seems to contain a 2-bit sequence counter that mostly
|
||||||
|
* cycles through all values, but sometimes it skips values or just toggles between two of the observed values.
|
||||||
|
* Examples: 0x00, 0x40, 0x80, 0xC0
|
||||||
|
* There doesn't seem to be any correlation between the value of this nibble and the radio IDs being transmitted.
|
||||||
|
*
|
||||||
|
* The lower nibble of this 1-byte field seems to have an additional flag bit that is sometimes transmitted:
|
||||||
|
* Examples: 0x08, 0x48, 0x88
|
||||||
|
*
|
||||||
|
* Both opcode 130 and 143 employ up to 2x 1-byte fields that always transmit a 0x09 value. The second field is only
|
||||||
|
* present when the message is sending 3 or 4 radio IDs.
|
||||||
|
*
|
||||||
|
* It seems that the complete list of active member radios is transmitted in a fixed sequence with zero to four of those
|
||||||
|
* radios transmitted per message. The messages are transmitted when the talkgroup is first allocated to the channel
|
||||||
|
* and eventually the 130/143 messages stop listing radios and the empty 143 and the 191 messages are intermittently
|
||||||
|
* sent. Occasionally there are radios listed in the message with all zeros values.
|
||||||
|
*
|
||||||
|
* Opcode 143 is transmitted with just the status byte and no radio IDs after the full sequence of radios has been
|
||||||
|
* sent and are no longer being sent.
|
||||||
|
*
|
||||||
|
* I'm not sure what Opcode 191 indicates. It's always the same (3-bytes) value: BF9003. It may be some kind of
|
||||||
|
* feature marker to let the listening radios/devices know that the feature is active on that traffic channel.
|
||||||
|
*
|
||||||
|
* These three opcodes seem to be used for this feature.
|
||||||
|
* Opcode 130/x82 - talkgroup member radios currently active - mostly transmitted during HANGTIME
|
||||||
|
* Opcode 143/x8F - talkgroup member radios currently active - mostly transmitted during ACTIVE call state
|
||||||
|
* Opcode 191/xBF - transmitted continuously while channel is allocated to a talkgroup - fixed value: 0xBF9003
|
||||||
|
*/
|
||||||
|
public class MotorolaActiveGroupRadiosOpcode191_xBF extends MacStructureVendor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Constructs the message
|
||||||
|
*
|
||||||
|
* @param message containing the message bits
|
||||||
|
* @param offset into the message for this structure
|
||||||
|
*/
|
||||||
|
public MotorolaActiveGroupRadiosOpcode191_xBF(CorrectedBinaryMessage message, int offset)
|
||||||
|
{
|
||||||
|
super(message, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Textual representation of this message
|
||||||
|
*/
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
CorrectedBinaryMessage cbm = getMessage().getSubMessage(getOffset(), getOffset() + (getLength() * 8));
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("MOTOROLA 191 ACTIVE GROUP RADIOS-FEATURE ACTIVE MSG:").append(cbm.toHexString());
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Identifier> getIdentifiers()
|
||||||
|
{
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -100,19 +100,19 @@ public class DecodeEventLogger extends EventLogger implements IDecodeEventListen
|
|||||||
cells.add(event.getProtocol());
|
cells.add(event.getProtocol());
|
||||||
cells.add(event.getEventType());
|
cells.add(event.getEventType());
|
||||||
|
|
||||||
List<Identifier> fromIdentifiers = event.getIdentifierCollection().getIdentifiers(Role.FROM);
|
Identifier fromIdentifier = event.getIdentifierCollection().getFromIdentifier();
|
||||||
if(fromIdentifiers != null && !fromIdentifiers.isEmpty())
|
if(fromIdentifier != null)
|
||||||
{
|
{
|
||||||
cells.add(fromIdentifiers.get(0));
|
cells.add(fromIdentifier);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cells.add("");
|
cells.add("");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Identifier> toIdentifiers = event.getIdentifierCollection().getIdentifiers(Role.TO);
|
Identifier toIdentifier = event.getIdentifierCollection().getToIdentifier();
|
||||||
|
|
||||||
if(toIdentifiers != null && !toIdentifiers.isEmpty())
|
if(toIdentifier != null)
|
||||||
{
|
{
|
||||||
Identifier identifier = event.getIdentifierCollection()
|
Identifier identifier = event.getIdentifierCollection()
|
||||||
.getIdentifier(IdentifierClass.CONFIGURATION,Form.ALIAS_LIST,Role.ANY);
|
.getIdentifier(IdentifierClass.CONFIGURATION,Form.ALIAS_LIST,Role.ANY);
|
||||||
@@ -120,9 +120,9 @@ public class DecodeEventLogger extends EventLogger implements IDecodeEventListen
|
|||||||
|
|
||||||
if(mAliasList != null)
|
if(mAliasList != null)
|
||||||
{
|
{
|
||||||
String mystring = (!mAliasList.getAliases(toIdentifiers.get(0)).isEmpty()) ?
|
String mystring = (!mAliasList.getAliases(toIdentifier).isEmpty()) ?
|
||||||
mAliasList.getAliases(toIdentifiers.get(0)).get(0).toString() : "";
|
mAliasList.getAliases(toIdentifier).toString() : "";
|
||||||
cells.add(mystring + " (" + toIdentifiers.get(0) + ")");
|
cells.add(mystring + " (" + toIdentifier + ")");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* *****************************************************************************
|
* *****************************************************************************
|
||||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
* Copyright (C) 2014-2024 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.controller.channel.ChannelProcessingManager;
|
|||||||
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
||||||
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
|
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
|
||||||
import io.github.dsheirer.icon.IconModel;
|
import io.github.dsheirer.icon.IconModel;
|
||||||
import io.github.dsheirer.module.log.EventLogManager;
|
import io.github.dsheirer.module.log.EventLogManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
@@ -48,6 +49,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@@ -77,6 +79,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
|
|||||||
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
|
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
|
||||||
private ScheduledFuture<?> mPlaylistSaveFuture;
|
private ScheduledFuture<?> mPlaylistSaveFuture;
|
||||||
private boolean mPlaylistLoading = false;
|
private boolean mPlaylistLoading = false;
|
||||||
|
private List<IAliasListRefreshListener> mAliasListRefreshListeners = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Playlist manager - manages all channel configurations, channel maps, and alias lists and handles loading or
|
* Playlist manager - manages all channel configurations, channel maps, and alias lists and handles loading or
|
||||||
@@ -135,6 +138,68 @@ public class PlaylistManager implements Listener<ChannelEvent>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the listener to be notified when an alias list refresh operation is about to take place. The listener
|
||||||
|
* should clear any selected or editing item to prepare for the list of alias list names to be updated so that
|
||||||
|
* alias list combo boxes won't trigger an editor modified event when the list contents changes.
|
||||||
|
* @param listener for alias list refresh event.
|
||||||
|
*/
|
||||||
|
public void addAliasListRefreshListener(IAliasListRefreshListener listener)
|
||||||
|
{
|
||||||
|
mAliasListRefreshListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies listeners that the alias list will be refreshed
|
||||||
|
*/
|
||||||
|
private void prepareForAliasListRefresh()
|
||||||
|
{
|
||||||
|
for(IAliasListRefreshListener editor : mAliasListRefreshListeners)
|
||||||
|
{
|
||||||
|
editor.prepareForAliasListRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the alias list names after a rename or delete operation.
|
||||||
|
*/
|
||||||
|
private void refreshAliasListNames()
|
||||||
|
{
|
||||||
|
//Do a refresh from the aliases
|
||||||
|
getAliasModel().refreshAliasListNames();
|
||||||
|
//Add in the alias list names referred to by the channels.
|
||||||
|
getAliasModel().addAliasListNames(mChannelModel.getAliasListNames());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames alias list references across aliases and channels from the old name to the new name.
|
||||||
|
*
|
||||||
|
* Note: this method should be invoked on the JavaFX thread since it will touch observable alias and channel lists.
|
||||||
|
* @param oldName that is currently used by channels and aliases
|
||||||
|
* @param newName to apply to channels and aliases
|
||||||
|
*/
|
||||||
|
public void renameAliasList(String oldName, String newName)
|
||||||
|
{
|
||||||
|
prepareForAliasListRefresh();
|
||||||
|
getAliasModel().renameAliasList(oldName, newName);
|
||||||
|
getChannelModel().renameAliasList(oldName, newName);
|
||||||
|
refreshAliasListNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes all aliases that have the alias list name and removes the alias list name from all channels.
|
||||||
|
*
|
||||||
|
* Note: this method should be invoked on the JavaFX thread since it will touch observable alias and channel lists.
|
||||||
|
* @param aliasListName to delete
|
||||||
|
*/
|
||||||
|
public void deleteAliasList(String aliasListName)
|
||||||
|
{
|
||||||
|
prepareForAliasListRefresh();
|
||||||
|
getAliasModel().deleteAliasList(aliasListName);
|
||||||
|
getChannelModel().deleteAliasList(aliasListName);
|
||||||
|
refreshAliasListNames();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel model managed by this playlist manager
|
* Channel model managed by this playlist manager
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -68,9 +68,13 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid
|
|||||||
mRotationDelay = rotationDelay;
|
mRotationDelay = rotationDelay;
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
|
|
||||||
if(mRotationDelay == 0)
|
if(mRotationDelay < CHANNEL_ROTATION_DELAY_MINIMUM)
|
||||||
{
|
{
|
||||||
mRotationDelay = 200;
|
mRotationDelay = CHANNEL_ROTATION_DELAY_MINIMUM;
|
||||||
|
}
|
||||||
|
else if(mRotationDelay > CHANNEL_ROTATION_DELAY_MAXIMUM)
|
||||||
|
{
|
||||||
|
mRotationDelay = CHANNEL_ROTATION_DELAY_MAXIMUM;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +188,7 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(runnable, mRotationDelay,
|
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(runnable, mRotationDelay * 2,
|
||||||
mRotationDelay / 2, TimeUnit.MILLISECONDS);
|
mRotationDelay / 2, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user