Compare commits

..

31 Commits

Author SHA1 Message Date
Dennis Sheirer
db83d5121c Version 0.6.1 Final 2024-12-03 16:29:13 -05:00
ilyacodes
d61d9c07b0 Add Motorola OTA alias decoding (#2098) 2024-12-02 18:31:37 -05:00
Denny Sheirer
5c9c3fa503 #2089 Motorola P25P2 TDMA data channel support (#2090)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-12 02:22:22 -05:00
Denny Sheirer
552d3a6ce9 #2086 Audio Playback Manager can leak memory if audio queue processing thread is never started. (#2087)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-08 03:53:21 -05:00
Denny Sheirer
3bf425f94a #2084 Updates usb4java native library for windows aarch64 (#2085)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-06 03:59:14 -05:00
Denny Sheirer
660a5acd2f #2081 Windows aarch64 libusb support. (#2083)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-05 03:56:19 -05:00
Denny Sheirer
551277f399 #2078 Broadcastify feed verbose logging now includes logging of audio and metadata (#2080)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-03 03:20:17 -05:00
Dennis Sheirer
a12040be5c Revert "#2076 DMR CapMax Network and Site vales off by one. (#2077)"
This reverts commit e4be7dc5f4.
2024-11-02 06:58:23 -04:00
Denny Sheirer
e4be7dc5f4 #2076 DMR CapMax Network and Site vales off by one. (#2077)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-02 06:44:20 -04:00
Denny Sheirer
b615c1e16a #2074 DMR CapMax LRRP GPS positions sent to map now contain channel identifiers (alias list and system name). (#2075)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-02 06:12:38 -04:00
Denny Sheirer
912af4e3e6 #2071 Version 0.6.1 Beta 3 (#2072)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-01 05:46:42 -04:00
Denny Sheirer
df0d0468c8 #2055 MultiFrequencyTunerChannelSource Null Pointer Error On Channel Source Shutdown - different solution to resolve possible memory leak with previous solution. Also fixes P25 issue where a traffic channel that doesn't get started is returned to the traffic channel pool. (#2070)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-01 05:32:10 -04:00
Denny Sheirer
7509245751 #2068 P25 traffic channel manager ignores traffic channel requests if the frequency value is zero (#2069)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 05:53:42 -04:00
Denny Sheirer
02e236202c #2066 P25 Phase 1 frequency band link control messages are now checked for valid CRC before being used. (#2067)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 05:29:51 -04:00
Denny Sheirer
4b1d294211 #2064 Moto talker GPS event TO/FROM identifiers. (#2065)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 05:07:42 -04:00
Denny Sheirer
40840dbd5c #2062 Verbose logging option for Broadcastify Feed to troubleshoot streaming connection issues. (#2063)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 04:11:45 -04:00
Denny Sheirer
786ae9369b #1992 Traffic channels stuck in teardown. (#2061)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-30 03:57:42 -04:00
Denny Sheirer
b7bbf10415 #2055 Multi-Frequency channel source with channel rotation monitor startup can get ahead of the tuner channel source and cause an NPE. (#2056)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-28 03:30:37 -04:00
Denny Sheirer
486ac01416 #1867 Event logger ensures from identifier is the radio ID and not the system transmitted radio alias. (#2050)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 09:31:51 -04:00
Denny Sheirer
9e01cc48a5 #2034 Map plotted elements now include date in the location timestamp. (#2049)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 09:08:41 -04:00
Denny Sheirer
37996d0477 #2040 Reset AMBE/IMBE audio codec after each call to prevent carryover of last audio frame from previous call. (#2048)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 08:34:27 -04:00
Denny Sheirer
a15ff9040b #2037 JavaFX Windows Can't Be Shrunk. Reverts changes made under #1619. (#2047)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 08:22:45 -04:00
Denny Sheirer
b3cd3f431f #2045 P25 Phase 1 Data channels are now ignored if the channel config calls for it. (#2046)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 05:29:25 -04:00
Denny Sheirer
649d1f650a #2043 DMR Tier 3 Capacity Max talkgroups were parsing incorrectly. (#2044)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-26 16:35:59 -04:00
Denny Sheirer
215e4064fa #2039 P25P2 bogus talkgroup and radio values showing in calls. Corrects message length error that was causing mis-aligned mac message parsing that contained bad/errant talkgroup and radio values to be included in the call metadata. Adds support for a new Moto feature discovery that appears to be announcing talkgroup member radios that are active on/tuned to the traffic channel. (#2042)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-26 09:18:35 -04:00
Denny Sheirer
66a0b2543b #1060 Adds Alias List delete and rename operations to playlist editor (#2038)
Original code changes implemented by @whelgeson

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-21 05:34:06 -04:00
Denny Sheirer
378841b408 #2028 Adds support for Windows ARM/aarch64 Operating System (#2029)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-19 06:11:54 -04:00
Denny Sheirer
e28d726182 #2035 P25P1 Harris Patched Talkgroup Values Incorrect (#2036)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-19 04:43:15 -04:00
Denny Sheirer
71bc27e3d9 #2032 Audio output suppresses logging after 5 times when app can't get gain or mute controls for current audio output. (#2033)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-18 03:05:21 -04:00
Denny Sheirer
bcfb2700cb #2030 Playlist Editor double-click channel row to auto-start channel. (#2031)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-18 02:43:55 -04:00
Denny Sheirer
2d9491ac93 #2025 P25P1 audio module null codec error. (#2027)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-18 01:33:23 -04:00
52 changed files with 2346 additions and 225 deletions

View File

@@ -105,6 +105,7 @@ dependencies {
implementation 'commons-io:commons-io:2.11.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.1' //OSX & Windows aarch64 native libs
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
implementation 'org.apache.commons:commons-compress:1.26.0'
@@ -119,7 +120,6 @@ dependencies {
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
@@ -192,6 +192,7 @@ String targetLinuxX86_64 = 'linux-x86_64-v' + version
String targetOsxAarch64 = 'osx-aarch64-v' + version
String targetOsxX86_64 = 'osx-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
@@ -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_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_aarch64 = jdk_base + 'windows-arm64/jdk-22.0.2-full'
/**
* 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_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_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_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.
@@ -256,7 +259,12 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
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)
}
else if(os.isLinux())
@@ -292,6 +300,16 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
*/
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()) {
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
}

View File

@@ -28,4 +28,4 @@
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
projectVersion=0.6.1-beta-2
projectVersion=0.6.1

View File

@@ -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
* it under the terms of the GNU General Public License as published by
@@ -63,6 +63,69 @@ public class AliasModel
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
*/

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.broadcastify;
@@ -35,6 +32,7 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyFeedConfiguration.class);
private int mFeedID;
private boolean mVerboseLogging = false;
public BroadcastifyFeedConfiguration()
{
@@ -123,4 +121,15 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
{
mFeedID = feedID;
}
@JacksonXmlProperty(isAttribute = true, localName = "verbose_logging")
public boolean isVerboseLogging()
{
return mVerboseLogging;
}
public void setVerboseLogging(boolean verboseLogging)
{
mVerboseLogging = verboseLogging;
}
}

View File

@@ -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
* 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.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.convert.InputAudioFormat;
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.session.IoSession;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,6 +59,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
private IoSession mStreamingSession = null;
private long mLastConnectionAttempt = 0;
private boolean mVerboseLogging = false;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
@@ -73,6 +77,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
if(configuration instanceof BroadcastifyFeedConfiguration broadcastify)
{
mVerboseLogging = broadcastify.isVerboseLogging();
}
}
/**
@@ -85,7 +94,6 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
if(mInlineActive)
{
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
if (mInlineRemaining == -1)
{
mInlineRemaining = mInlineInterval;
@@ -96,16 +104,34 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
byte[] chunk = Arrays.copyOfRange(audio, audioOffset, Math.min(audioOffset + mInlineRemaining, audio.length));
mInlineRemaining -= chunk.length;
audioOffset += chunk.length;
if(mVerboseLogging)
{
mLog.info("Broadcasting audio data - sending [" + chunk.length + "/" + audio.length + "] bytes");
}
mStreamingSession.write(chunk);
if(mInlineRemaining == 0)
{
mInlineRemaining = mInlineInterval;
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
if(mVerboseLogging)
{
mLog.info("Sending inline metadata: " + new String(metadata));
}
mStreamingSession.write(metadata);
}
}
}
else
{
if(mVerboseLogging)
{
mLog.info("Broadcasting audio data [" + audio.length + "] bytes");
}
mStreamingSession.write(audio);
}
}
@@ -132,9 +158,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mSocketConnector = new NioSocketConnector();
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
if(mVerboseLogging)
{
LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
}
mSocketConnector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new IcecastCodecFactory()));
@@ -148,6 +177,10 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
public void run()
{
if(mVerboseLogging)
{
mLog.info("Attempting connection ...");
}
setBroadcastState(BroadcastState.CONNECTING);
try
@@ -156,19 +189,42 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
if(mVerboseLogging)
{
mLog.info("Socket created - asynchronous connect requested - entering wait period");
}
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
if(connected)
{
if(mVerboseLogging)
{
mLog.info("Connected.");
}
mStreamingSession = future.getSession();
mConnecting.set(false);
return;
}
else
{
if(mVerboseLogging)
{
mLog.info("Not Connected. Connection attempt timeout [" + CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS + "ms] exceeded");
}
}
}
catch(RuntimeIoException rioe)
{
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);
mConnecting.set(false);
return;
@@ -176,12 +232,22 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
}
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);
mConnecting.set(false);
return;
}
catch(Exception e)
{
if(mVerboseLogging)
{
mLog.info("Unknown error. An error occurred while attempting to connect to the server.", e);
}
mLog.error("Error", e);
//Disregard ... we'll disconnect and try again
}
@@ -190,6 +256,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mLog.error("Throwable error caught", t);
}
if(mVerboseLogging)
{
mLog.info("Starting disconnect sequence since an error occurred while trying to connect.");
}
disconnect();
mConnecting.set(false);
}
@@ -209,6 +280,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
if(connected() && mStreamingSession != null)
{
if(mVerboseLogging)
{
mLog.info("Routine disconnect requested from a connected state with a non-null streaming session");
}
mStreamingSession.closeNow();
}
else
@@ -217,6 +293,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
//want to preserve the error state that got us here, so the user can see it.
if(!getBroadcastState().isErrorState())
{
if(mVerboseLogging)
{
mLog.info("Disconnect requested - previous non-error state was [" + getBroadcastState() +
"] - changing state to DISCONNECTED");
}
setBroadcastState(BroadcastState.DISCONNECTED);
}
@@ -282,12 +364,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mInlineActive = false;
}
if(mVerboseLogging)
{
mLog.info("Session opened. Sending: " + sb);
}
session.write(sb.toString());
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
if(mVerboseLogging)
{
mLog.info("Session closed. Setting connecting flag to false.");
}
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected
@@ -311,15 +404,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
}
if(mVerboseLogging)
{
mLog.info("Session error caught.", cause);
}
disconnect();
}
@Override
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())
{

View File

@@ -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
* 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.LoggerFactory;
/**
* Base audio module for protocols that use the JMBE audio codec.
*/
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
ISquelchStateListener
{
@@ -59,12 +62,23 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
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
public void dispose()
{
super.dispose();
MyEventBus.getGlobalEventBus().unregister(this);
mAudioCodec = null;
}
public IAudioCodec getAudioCodec()

View File

@@ -140,8 +140,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
try
@@ -151,8 +152,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
"MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
//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)
{
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;
}
@@ -321,8 +323,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
"MASTER GAIN control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
try
@@ -332,23 +335,24 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
"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");
}
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");
return;
}
}
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);
return;
}
@@ -391,7 +395,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
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 [" +
mMixer.getMixerInfo().getName() + "]");
return;

View File

@@ -41,6 +41,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -65,6 +66,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
private ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
private AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
private ReentrantLock mAudioOutputLock = new ReentrantLock();
/**
* Constructs an instance.
@@ -95,6 +98,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
mLog.warn("No audio output devices available");
}
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
}
/**
@@ -189,36 +195,53 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
else if(audioSegment.isLinked())
{
for(AudioOutput audioOutput: mAudioOutputs)
mAudioOutputLock.lock();
try
{
if(audioOutput.isLinkedTo(audioSegment))
for(AudioOutput audioOutput: mAudioOutputs)
{
it.remove();
audioOutput.play(audioSegment);
if(audioOutput.isLinkedTo(audioSegment))
{
it.remove();
audioOutput.play(audioSegment);
}
}
}
finally
{
mAudioOutputLock.unlock();
}
}
}
//Sort audio segments by playback priority and assign to empty audio outputs
if(!mAudioSegments.isEmpty())
{
//TODO: change this to take audio segment start time into account also
mAudioSegments.sort(Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
mAudioSegments.sort(mAudioSegmentPrioritySorter);
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
mAudioOutputLock.lock();
try
{
if(audioOutput.isEmpty())
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
{
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
if(audioOutput.isEmpty())
{
return;
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
{
return;
}
}
}
}
finally
{
mAudioOutputLock.unlock();
}
}
//Remove any audio segments marked as complete that didn't get assigned to an output
@@ -288,37 +311,39 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
if(mProcessingTask != null)
mAudioOutputLock.lock();
try
{
mProcessingTask.cancel(true);
for(AudioOutput audioOutput: mAudioOutputs)
{
audioOutput.dispose();
}
mAudioOutputs.clear();
switch(entry.getMixerChannel())
{
case MONO:
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
mAudioOutputs.add(mono);
break;
case STEREO:
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
mAudioOutputs.add(left);
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
mAudioOutputs.add(right);
break;
default:
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
}
}
finally
{
mAudioOutputLock.unlock();
}
for(AudioOutput audioOutput: mAudioOutputs)
{
audioOutput.dispose();
}
mAudioOutputs.clear();
switch(entry.getMixerChannel())
{
case MONO:
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
mAudioOutputs.add(mono);
break;
case STEREO:
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
mAudioOutputs.add(left);
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
mAudioOutputs.add(right);
break;
default:
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
}
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
mMixerChannelConfiguration = entry;
}
@@ -389,4 +414,30 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
}
}
/**
* Audio segment comparator for sorting audio segments by: 1)Playback priority and 2)Segment start time
*/
public class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
{
@Override
public int compare(AudioSegment segment1, AudioSegment segment2)
{
if(segment1 == null || segment2 == null)
{
return -1;
}
//If priority is the same, sort by start time
if(segment1.monitorPriorityProperty().get() == segment2.monitorPriorityProperty().get())
{
return Long.compare(segment1.getStartTimestamp(), segment2.getStartTimestamp());
}
//Otherwise, sort by priority
else
{
return Integer.compare(segment1.monitorPriorityProperty().get(), segment2.monitorPriorityProperty().get());
}
}
}
}

View File

@@ -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
* 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.heartbeat.Heartbeat;
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider,
IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider,
IdentifierUpdateProvider, IOverflowListener
@@ -53,7 +52,8 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
private Channel mChannel;
protected boolean mSourceOverflow = false;
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
protected boolean mTeardownSequenceStarted = false;
protected boolean mTeardownSequenceCompleted = false;
//TODO: remove the IOverflowListener code from this class
@@ -66,6 +66,22 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
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.
*/
@@ -89,6 +105,11 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
*/
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 void updateChannelStateIdentifiers(IdentifierUpdateNotification notification);

View File

@@ -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
* 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.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
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
@@ -166,6 +165,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
mStateMachineMap.get(timeslot).setState(State.IDLE);
break;
case TEARDOWN:
mTeardownSequenceStarted = true;
if(getChannel().isTrafficChannel())
{
checkTeardown();
@@ -206,7 +206,16 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
{
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
{
@@ -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.
*/

View File

@@ -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
* 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.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
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
@@ -163,9 +162,11 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mStateMachine.setState(State.IDLE);
break;
case TEARDOWN:
mTeardownSequenceStarted = true;
if(getChannel().isTrafficChannel())
{
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
mTeardownSequenceCompleted = true;
}
else
{
@@ -175,6 +176,12 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
}
}
@Override
public boolean isTeardownState()
{
return mStateMachine.getState() == State.TEARDOWN;
}
@Override
protected void checkState()
{

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.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.sample.Broadcaster;
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.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Channel Model
*/
@@ -55,6 +50,58 @@ public class ChannelModel implements Listener<ChannelEvent>
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
*/
@@ -109,6 +156,45 @@ public class ChannelModel implements Listener<ChannelEvent>
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
*/

View File

@@ -279,15 +279,27 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
break;
case REQUEST_DISABLE:
case NOTIFICATION_DELETE:
if(channel != null && channel.isProcessing())
if(channel != null)
{
try
{
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;
@@ -521,7 +533,14 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
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())
{
@@ -627,8 +646,28 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
else
{
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(false));
//When we're in non-headless mode we have to change the processing property on the JavaFX
//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
@@ -847,6 +886,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
mLog.error("Error stopping channel [" + (toShutdown != null ? toShutdown.getName() : "unknown") +
"] 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");
AbstractChannelState state = chain.getChannelState();
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())
{
sb.append(metadata.getDescription()).append("\n");

View File

@@ -252,11 +252,16 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
synchronized(mBufferDispatcher)
{
mChannelSources.remove(channelSource);
mPolyphaseChannelizer.removeChannel(channelSource);
if(mPolyphaseChannelizer != null)
{
mPolyphaseChannelizer.removeChannel(channelSource);
}
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
//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);
mBufferDispatcher.stop();

View File

@@ -460,9 +460,6 @@ public class JavaFxWindowManager extends Application
stage.show();
stage.requestFocus();
stage.toFront();
stage.setMinHeight(stage.getHeight());
stage.setMinWidth(stage.getWidth());
// System.out.println("Stage [" + stage.getTitle() + "] restored - width[" + stage.getWidth() + "] height [" + stage.getHeight() + "]");
}
@Override

View File

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

View File

@@ -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
* 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.gui.control.MaxLengthUnaryOperator;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
@@ -41,7 +42,6 @@ import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
@@ -76,7 +76,7 @@ import org.slf4j.LoggerFactory;
/**
* Editor for aliases
*/
public class AliasConfigurationEditor extends SplitPane
public class AliasConfigurationEditor extends SplitPane implements IAliasListRefreshListener
{
private static final Logger mLog = LoggerFactory.getLogger(AliasConfigurationEditor.class);
@@ -89,6 +89,7 @@ public class AliasConfigurationEditor extends SplitPane
private Label mPlaceholderLabel;
private Button mNewAliasButton;
private Button mDeleteAliasButton;
private Button mRenameAliasButton;
private Button mCloneAliasButton;
private MenuButton mMoveToAliasButton;
private VBox mButtonBox;
@@ -96,13 +97,20 @@ public class AliasConfigurationEditor extends SplitPane
private TextField mSearchField;
private ComboBox<String> mAliasListNameComboBox;
private Button mNewAliasListButton;
private Button mDeleteAliasListButton;
private FilteredList<Alias> mAliasFilteredList;
private SortedList<Alias> mAliasSortedList;
private AliasPredicate mAliasPredicate;
/**
* Constructs an instance
* @param playlistManager for playlist operations
* @param userPreferences for user preferences
*/
public AliasConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mPlaylistManager.addAliasListRefreshListener(this);
mUserPreferences = userPreferences;
VBox leftBox = new VBox();
@@ -118,6 +126,15 @@ public class AliasConfigurationEditor extends SplitPane
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.
* <p>
@@ -167,18 +184,17 @@ public class AliasConfigurationEditor extends SplitPane
alert.setTitle("Save Changes");
alert.setHeaderText("Alias configuration has been modified");
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
alert.setResizable(true);
alert.onShownProperty().addListener(e ->
{
alert.onShownProperty().addListener(e -> {
Platform.runLater(() -> alert.setResizable(false));
});
Optional<ButtonType> result = alert.showAndWait();
if(result.get() == ButtonType.YES)
if(result.isPresent() && result.get() == ButtonType.YES)
{
getAliasItemEditor().save();
}
@@ -202,7 +218,7 @@ public class AliasConfigurationEditor extends SplitPane
getAliasBulkEditor().setItem(aliases);
}
getCloneAliasButton().setDisable(aliases.isEmpty() || aliases.size() > 1);
getCloneAliasButton().setDisable(aliases.size() != 1);
getDeleteAliasButton().setDisable(aliases.isEmpty());
getMoveToAliasButton().setDisable(aliases.isEmpty());
}
@@ -248,7 +264,7 @@ public class AliasConfigurationEditor extends SplitPane
searchBox.setAlignment(Pos.BASELINE_RIGHT);
mSearchAndListSelectionBox.getChildren().addAll(listLabel, getAliasListNameComboBox(),
getNewAliasListButton(), searchBox);
getNewAliasListButton(), getRenameAliasListButton(), getDeleteAliasListButton(), searchBox);
}
return mSearchAndListSelectionBox;
@@ -347,6 +363,70 @@ public class AliasConfigurationEditor extends SplitPane
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()
{
if(mAliasTableView == null)
@@ -423,8 +503,7 @@ public class AliasConfigurationEditor extends SplitPane
mAliasTableView.setPlaceholder(getPlaceholderLabel());
mAliasTableView.setItems(getAliasSortedList());
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()));
});
}
@@ -537,11 +616,11 @@ public class AliasConfigurationEditor extends SplitPane
ButtonType.NO, ButtonType.YES);
alert.setTitle("Delete Alias");
alert.setHeaderText("Are you sure?");
alert.initOwner(((Node) getDeleteAliasButton()).getScene().getWindow());
alert.initOwner((getDeleteAliasButton()).getScene().getWindow());
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());
mPlaylistManager.getAliasModel().removeAliases(selectedAliases);

View File

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

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.gui.playlist.channel;
import com.google.common.base.Joiner;
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.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
@@ -57,6 +58,7 @@ import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -72,7 +74,7 @@ import org.slf4j.LoggerFactory;
/**
* 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 PlaylistManager mPlaylistManager;
@@ -103,6 +105,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
public ChannelEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mPlaylistManager.addAliasListRefreshListener(this);
mTunerManager = tunerManager;
mUserPreferences = userPreferences;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
@@ -123,6 +126,15 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
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.
*
@@ -504,6 +516,12 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
mChannelTableView.setItems(sortedList);
mChannelTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> setChannel(newValue));
mChannelTableView.setOnMouseClicked(event -> {
if(event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2)
{
getChannelConfigurationEditor().startChannel();
}
});
}
return mChannelTableView;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.streaming;
@@ -31,7 +28,9 @@ import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import org.controlsfx.control.ToggleSwitch;
/**
* Broadcastify streaming configuration editor
@@ -39,6 +38,7 @@ import javafx.scene.layout.GridPane;
public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyFeedConfiguration>
{
private GridPane mEditorPane;
private ToggleSwitch mVerboseLoggingToggle;
private TextField mMountPointTextField;
private IntegerTextField mFeedIdTextField;
@@ -54,16 +54,19 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyF
getFeedIdTextField().setDisable(item == null);
getMountPointTextField().setDisable(item == null);
getVerboseLoggingToggle().setDisable(item == null);
if(item != null)
{
getFeedIdTextField().set(item.getFeedID());
getMountPointTextField().setText(item.getMountPoint());
getVerboseLoggingToggle().setSelected(item.isVerboseLogging());
}
else
{
getFeedIdTextField().set(0);
getMountPointTextField().setText(null);
getVerboseLoggingToggle().setSelected(false);
}
modifiedProperty().set(false);
@@ -76,6 +79,7 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyF
{
getItem().setFeedID(getFeedIdTextField().get());
getItem().setMountPoint(getMountPointTextField().getText());
getItem().setVerboseLogging(getVerboseLoggingToggle().isSelected());
}
super.save();
@@ -178,11 +182,32 @@ public class BroadcastifyStreamEditor extends AbstractStreamEditor<BroadcastifyF
GridPane.setConstraints(getFeedIdTextField(), 1, 5);
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;
}
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()
{
if(mMountPointTextField == null)

View File

@@ -90,6 +90,9 @@ public class P25P2Viewer extends VBox
private static final Logger mLog = LoggerFactory.getLogger(P25P2Viewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.p25p2";
private static final String LAST_WACN_VALUE = "last.wacn.value.p25p2";
private static final String LAST_SYSTEM_VALUE = "last.system.value.p25p2";
private static final String LAST_NAC_VALUE = "last.nac.value.p25p2";
private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
private Preferences mPreferences = Preferences.userNodeForPackage(P25P2Viewer.class);
private Button mSelectFileButton;
@@ -415,6 +418,12 @@ public class P25P2Viewer extends VBox
if(mWACNTextField == null)
{
mWACNTextField = new IntegerTextField();
mWACNTextField.textProperty().addListener((ob, ol, ne) -> mPreferences.putInt(LAST_WACN_VALUE, getWACNTextField().get()));
int previous = mPreferences.getInt(LAST_WACN_VALUE, 0);
if(previous > 0)
{
getWACNTextField().set(previous);
}
}
return mWACNTextField;
@@ -425,6 +434,12 @@ public class P25P2Viewer extends VBox
if(mSystemTextField == null)
{
mSystemTextField = new IntegerTextField();
mSystemTextField.textProperty().addListener((ob, ol, ne) -> mPreferences.putInt(LAST_SYSTEM_VALUE, getSystemTextField().get()));
int previous = mPreferences.getInt(LAST_SYSTEM_VALUE, 0);
if(previous > 0)
{
getSystemTextField().set(previous);
}
}
return mSystemTextField;
@@ -435,6 +450,12 @@ public class P25P2Viewer extends VBox
if(mNACTextField == null)
{
mNACTextField = new IntegerTextField();
mNACTextField.textProperty().addListener((ob, ol, ne) -> mPreferences.putInt(LAST_NAC_VALUE, getNACTextField().get()));
int previous = mPreferences.getInt(LAST_NAC_VALUE, 0);
if(previous > 0)
{
getNACTextField().set(previous);
}
}
return mNACTextField;

View File

@@ -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;
import javax.swing.table.AbstractTableModel;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.ToLongFunction;
import javax.swing.table.AbstractTableModel;
/**
* Table model for track history.
@@ -15,7 +33,7 @@ import java.util.function.ToLongFunction;
public class TrackHistoryModel extends AbstractTableModel
{
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 List<TimestampedGeoPosition> mTimestampedGeoPositions = new ArrayList<>();
private PlottableEntityHistory mPlottableEntityHistory;

View File

@@ -526,10 +526,11 @@ public class DMRDecoderState extends TimeslotDecoderState
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
if (geoPosition != null) {
if (geoPosition != null)
{
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
.channel(getCurrentChannel())
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
.protocol(Protocol.LRRP)
.location(geoPosition)
.build();

View File

@@ -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;
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.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
@@ -15,7 +33,8 @@ import java.util.List;
*/
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,
66, 67, 68, 69, 70, 71};

View File

@@ -36,6 +36,7 @@ public class P25TrafficChannelEventTracker
{
private static final Logger LOGGER = LoggerFactory.getLogger(P25TrafficChannelEventTracker.class);
private static final long STALE_EVENT_THRESHOLD_MS = 2000;
private static final long MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS = 15000;
private P25ChannelGrantEvent mEvent;
private boolean mStarted = false;
private boolean mComplete = false;
@@ -74,6 +75,14 @@ public class P25TrafficChannelEventTracker
return timestamp - getEvent().getTimeStart() > STALE_EVENT_THRESHOLD_MS;
}
/**
* Indicates if the TDMA data channel duration exceeds the threshold (15 seconds)
*/
public boolean exceedsMaxTDMADataDuration()
{
return getEvent().getDuration() > MAX_TDMA_DATA_CHANNEL_EVENT_DURATION_MS;
}
/**
* Adds the identifier to the tracked event if the event's identifier collection does not already have it.
* @param identifier to add

View File

@@ -58,6 +58,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.Net
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacOpcode;
import io.github.dsheirer.module.decode.p25.reference.DataServiceOptions;
import io.github.dsheirer.module.decode.p25.reference.ServiceOptions;
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
@@ -528,6 +529,99 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
}
}
/**
* Process a TDMA data channel grant.
* @param channel for data.
* @param timestamp of the event
*/
public void processP2DataChannel(APCO25Channel channel, long timestamp)
{
long frequency = channel != null ? channel.getDownlinkFrequency() : 0;
if(frequency > 0)
{
mLock.lock();
try
{
P25TrafficChannelEventTracker trackerTS1 = getTrackerRemoveIfStale(channel.getDownlinkFrequency(),
P25P1Message.TIMESLOT_1, timestamp);
if(trackerTS1 != null && trackerTS1.exceedsMaxTDMADataDuration())
{
removeTracker(frequency, P25P1Message.TIMESLOT_1);
trackerTS1 = null;
}
if(trackerTS1 == null)
{
P25ChannelGrantEvent continuationGrantEvent = P25ChannelGrantEvent.builder(DecodeEventType.DATA_CALL,
timestamp, new DataServiceOptions(0))
.channelDescriptor(channel)
.details("TDMA PHASE 2 DATA CHANNEL ACTIVE")
.identifiers(new IdentifierCollection())
.timeslot(P25P1Message.TIMESLOT_1)
.build();
trackerTS1 = new P25TrafficChannelEventTracker(continuationGrantEvent);
addTracker(trackerTS1, frequency, P25P1Message.TIMESLOT_1);
}
//update the ending timestamp so that the duration value is correctly calculated
trackerTS1.updateDurationTraffic(timestamp);
broadcast(trackerTS1);
//Even though we have a tracked event, the initial channel grant may have been rejected. Check to
// see if there is a traffic channel allocated. If not, allocate one and update the event description.
if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !mIgnoreDataCalls &&
(getCurrentControlFrequency() != frequency))
{
Channel trafficChannel = mAvailablePhase2TrafficChannelQueue.poll();
if(trafficChannel != null)
{
requestTrafficChannelStart(trafficChannel, channel, new IdentifierCollection(), timestamp);
}
else
{
trackerTS1.setDetails(MAX_TRAFFIC_CHANNELS_EXCEEDED);
}
}
P25TrafficChannelEventTracker trackerTS2 = getTrackerRemoveIfStale(channel.getDownlinkFrequency(),
P25P1Message.TIMESLOT_2, timestamp);
if(trackerTS2 != null && trackerTS2.exceedsMaxTDMADataDuration())
{
removeTracker(frequency, P25P1Message.TIMESLOT_2);
trackerTS2 = null;
}
if(trackerTS2 == null)
{
P25ChannelGrantEvent continuationGrantEvent = P25ChannelGrantEvent.builder(DecodeEventType.DATA_CALL,
timestamp, new DataServiceOptions(0))
.channelDescriptor(channel)
.details("TDMA PHASE 2 DATA CHANNEL ACTIVE")
.identifiers(new IdentifierCollection())
.timeslot(P25P1Message.TIMESLOT_2)
.build();
trackerTS2 = new P25TrafficChannelEventTracker(continuationGrantEvent);
addTracker(trackerTS2, frequency, P25P1Message.TIMESLOT_2);
}
//update the ending timestamp so that the duration value is correctly calculated
trackerTS2.updateDurationTraffic(timestamp);
broadcast(trackerTS1);
}
finally
{
mLock.unlock();
}
}
}
/**
* Starts a tracked event and updates the duration for a tracked event.
*
@@ -1062,7 +1156,7 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
private void requestTrafficChannelStart(Channel trafficChannel, APCO25Channel apco25Channel,
IdentifierCollection identifierCollection, long timestamp)
{
if(getInterModuleEventBus() != null)
if(apco25Channel != null && apco25Channel.getDownlinkFrequency() > 0 && getInterModuleEventBus() != null)
{
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
@@ -1086,6 +1180,18 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
startChannelRequest.addPreloadDataContent(new P25FrequencyBandPreloadDataContent(mFrequencyBandMap.values()));
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 +1269,20 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
return;
}
if(mIgnoreDataCalls && isDataChannelGrant && tracker == null)
if(mIgnoreDataCalls && isDataChannelGrant)
{
P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions)
.channelDescriptor(apco25Channel)
.details("IGNORED: PHASE 1 DATA CALL " + (serviceOptions != null ? serviceOptions : ""))
.identifiers(ic)
.build();
tracker = new P25TrafficChannelEventTracker(event);
addTracker(tracker, frequency, P25P1Message.TIMESLOT_1);
broadcast(tracker);
if(tracker == null)
{
P25ChannelGrantEvent event = P25ChannelGrantEvent.builder(decodeEventType, timestamp, serviceOptions)
.channelDescriptor(apco25Channel)
.details("IGNORED: PHASE 1 DATA CALL " + (serviceOptions != null ? serviceOptions : ""))
.identifiers(ic)
.build();
tracker = new P25TrafficChannelEventTracker(event);
addTracker(tracker, frequency, P25P1Message.TIMESLOT_1);
broadcast(tracker);
}
return;
}

View File

@@ -118,6 +118,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.L3HarrisGroupRegroupExplicitEncryptionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaAcknowledgeResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaDenyResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExplicitTDMADataChannelAnnouncement;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExtendedFunctionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupChannelUpdate;
@@ -188,7 +189,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
private final PatchGroupManager mPatchGroupManager = new PatchGroupManager();
private final P25P1NetworkConfigurationMonitor mNetworkConfigurationMonitor;
private final Listener<ChannelEvent> mChannelEventListener;
private P25TrafficChannelManager mTrafficChannelManager;
private final P25TrafficChannelManager mTrafficChannelManager;
private ServiceOptions mCurrentServiceOptions;
/**
@@ -1518,6 +1519,10 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
break;
case MOTOROLA_OSP_QUEUED_RESPONSE:
processTSBKQueuedResponse(tsbk);
break;
case MOTOROLA_OSP_TDMA_DATA_CHANNEL:
processTSBKActiveTDMADataChannel(tsbk);
break;
default:
// if(!tsbk.getOpcode().name().startsWith("ISP"))
// {
@@ -1531,6 +1536,19 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
}
}
/**
* TSBK Motorola TDMA data channel is active.
* @param tsbk with channel
*/
private void processTSBKActiveTDMADataChannel(TSBKMessage tsbk)
{
if(tsbk instanceof MotorolaExplicitTDMADataChannelAnnouncement tdma && tdma.hasChannel())
{
mTrafficChannelManager.processP2DataChannel(tdma.getChannel(), tsbk.getTimestamp());
mNetworkConfigurationMonitor.process(tsbk);
}
}
/**
* TSBK Status messaging
*/
@@ -2055,7 +2073,10 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
if(lcw instanceof LCMotorolaUnitGPS gps)
{
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)
.location(gps.getGeoPosition())

View File

@@ -236,13 +236,13 @@ public class P25P1MessageProcessor implements Listener<IMessage>
processForFrequencyBands(message);
//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)

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.module.decode.p25.phase1;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcast;
@@ -38,6 +39,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCNe
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCRFSSStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaBaseStationId;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExplicitTDMADataChannelAnnouncement;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.AdjacentStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.NetworkStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.standard.osp.RFSSStatusBroadcast;
@@ -79,8 +81,9 @@ public class P25P1NetworkConfigurationMonitor
//Current Site Secondary Control Channels
private Map<String,IChannelDescriptor> mSecondaryControlChannels = new TreeMap<>();
//Current Site Data Channel
//Current Site Data Channel(s)
private SNDCPDataChannelAnnouncementExplicit mSNDCPDataChannel;
private Map<APCO25Channel, MotorolaExplicitTDMADataChannelAnnouncement> mTDMADataChannelMap = new HashMap<>();
//Current Site Services
private SystemServiceBroadcast mTSBKSystemServiceBroadcast;
@@ -178,6 +181,12 @@ public class P25P1NetworkConfigurationMonitor
mMotorolaBaseStationId = (MotorolaBaseStationId)tsbk;
}
break;
case MOTOROLA_OSP_TDMA_DATA_CHANNEL:
if(tsbk instanceof MotorolaExplicitTDMADataChannelAnnouncement tdma && tdma.hasChannel())
{
mTDMADataChannelMap.put(tdma.getChannel(), tdma);
}
break;
}
}
@@ -442,11 +451,21 @@ public class P25P1NetworkConfigurationMonitor
if(mSNDCPDataChannel != null)
{
sb.append(" CURRENT DATA CHANNEL:").append(mSNDCPDataChannel.getChannel());
sb.append(" CURRENT FDMA DATA CHANNEL:").append(mSNDCPDataChannel.getChannel());
sb.append(" DOWNLINK:").append(mSNDCPDataChannel.getChannel().getDownlinkFrequency());
sb.append(" UPLINK:").append(mSNDCPDataChannel.getChannel().getUplinkFrequency()).append("\n");
}
if(!mTDMADataChannelMap.isEmpty())
{
for(Map.Entry<APCO25Channel, MotorolaExplicitTDMADataChannelAnnouncement> entry: mTDMADataChannelMap.entrySet())
{
sb.append(" ACTIVE TDMA DATA CHANNEL:").append(entry.getKey());
sb.append(" DOWNLINK:").append(entry.getKey().getDownlinkFrequency());
sb.append(" UPLINK:").append(entry.getKey().getUplinkFrequency()).append("\n");
}
}
if(mMotorolaBaseStationId != null)
{
sb.append(" STATION ID/LICENSE: ").append(mMotorolaBaseStationId.getCWID()).append("\n");

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer, 2024 Ilya Smirnov
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,6 +38,26 @@ import java.util.List;
*/
public class MotorolaTalkerAliasComplete extends TimeslotMessage implements IMessage
{
private static final byte[] LOOKUP_TABLE =
{
-14, 46, 102, -112, 116, -118, 111, 120, -69, 83, 3, 17, 104, -51, 68, 23,
40, 95, 30, -124, 117, 121, 110, -101, 44, -66, 98, 45, -15, 124, -72, -125,
-39, 78, 109, 2, 97, 61, -88, 6, -71, -8, -100, 55, 58, 35, -63, 80,
-19, -97, -81, 59, -67, -126, -70, -96, -33, -62, 71, 34, -16, -18, -95, -2,
-94, 16, 91, 72, 87, -93, 5, 96, 123, 13, -7, 108, -77, 86, 76, -68,
41, -92, 15, -20, -74, -91, -90, 60, 127, 107, -76, 33, -83, -82, -60, -56,
-59, 93, -34, -32, 29, 25, 75, -58, 12, 63, 90, -57, -31, 89, 85, 84,
74, 67, 66, -30, -29, -6, 0, -28, -27, 24, 65, 11, 10, -26, -4, -3,
-46, -10, -44, 43, 99, 73, -108, 94, -89, 92, 112, 105, -9, 8, -79, 125,
56, -49, -52, -40, 81, -113, -43, -109, 106, -13, -17, 126, -5, 100, -12, 53,
39, 7, 49, 20, -121, -104, 118, 52, -54, -110, 51, 27, 79, -116, 9, 64,
50, 54, 119, 18, -45, -61, 1, -85, 114, -127, -107, -55, -64, -23, 101, 82,
36, 48, 28, -37, -120, -24, -105, -99, 88, 38, 4, 57, -84, 42, -98, -86,
37, -41, -50, -21, -106, -11, 14, -115, -36, -87, 47, -35, 31, -22, -111, -73,
-42, -119, -117, -47, -80, -103, 19, 122, -25, -102, -75, -122, -1, 70, -123, -78,
115, -38, -65, -48, 113, -53, 77, -128, 21, 103, 22, 26, 32, -114, 69, 62
};
private static final IntField SUID_WACN = IntField.length20(OCTET_0_BIT_0);
private static final IntField SUID_SYSTEM = IntField.length12(OCTET_2_BIT_16 + 4);
private static final IntField SUID_ID = IntField.length24(OCTET_4_BIT_32);
@@ -113,9 +133,64 @@ public class MotorolaTalkerAliasComplete extends TimeslotMessage implements IMes
{
if(mAlias == null)
{
//BinaryMessage encoded = getEncodedAlias();
//TODO: implement decoding of the encoded alias.
String alias = "*ALIAS " + getRadio().getValue() + "*"; //Temporary value until we implement decoding
byte[] encoded = getEncodedAlias().toByteArray();
// TODO: check CRC in last two bytes to validate alias has no errors
//
// Data: wwwww sss iiiiii aaaa...aaaa cccc
//
// - w = WACN
// - s = system
// - i = id
// - a = encoded alias
// - c = CRC-16/GSM of the previous bytes
// Get number of bytes and characters excluding checksum
char bytes = (char)(encoded.length - 2);
char chars = (char)(bytes / 2);
// Create array for decoded computed bytes (big-endian)
byte[] decoded = new byte[encoded.length];
// Set up the long-running accumulator
char accumulator = bytes;
char byteIndex = 0;
// Calculate each byte sequentially
do
{
// Multiplication step 1
char accumMult = (char)(((accumulator + 65536) % 65536) * 293 + 0x72E9);
// Lookup table step
byte lut = LOOKUP_TABLE[encoded[byteIndex] + 128];
byte mult1 = (byte)(lut - (byte)(accumMult >> 8));
// Incrementing step
byte mult2 = 1;
byte shortstop = (byte)(accumMult | 0x1);
byte increment = (byte)(shortstop << 1);
while(mult2 != -1 && shortstop != 1) {
shortstop = (byte)(shortstop + increment);
mult2 += 2;
}
// Multiplication step 2
decoded[byteIndex] = (byte)(mult1 * mult2);
// Update the accumulator
accumulator += (char)(((encoded[byteIndex] + 256) % 256) + 1);
byteIndex += 1;
}
while(byteIndex < bytes);
// Copy decoded bytes (as chars) to our alias string
String alias = "";
for (char i = 0; i < chars; i++) {
alias += (char)((decoded[i * 2] << 8) | decoded[i * 2 + 1]);
}
mAlias = P25TalkerAliasIdentifier.create(alias);
}
@@ -192,8 +267,7 @@ public class MotorolaTalkerAliasComplete extends TimeslotMessage implements IMes
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
mIdentifiers.add(getRadio());
//TODO: uncomment once alias decoding is implemented.
// mIdentifiers.add(getAlias());
mIdentifiers.add(getAlias());
}
return mIdentifiers;

View File

@@ -181,7 +181,7 @@ public enum Opcode
MOTOROLA_OSP_BASE_STATION_ID(11, "CCH BASE STAT ID", "CONTROL CHANNEL BASE STATION ID"),
MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN(14, "CCH PLND SHUTDWN", "CONTROL CHANNEL PLANNED SHUTDOWN"),
MOTOROLA_OSP_OPCODE_15(15, "MOTOROLA OPCODE 15", "MOTOROLA OPCODE 15"),
//Opcode 22 - observed on PA-STARNET VHF Phase 1 CC site: 1690423FFFFFFFFF0000D458 & 9690423FFFFFFFFF0000306C
MOTOROLA_OSP_TDMA_DATA_CHANNEL(22, "MOTOROLA TDMA DATA CHANNEL", "MOTOROLA TDMA DATA CHANNEL"),
MOTOROLA_OSP_UNKNOWN(-1, "MOTOROLA OSP UNKNOWN OPCODE", "MOTOROLA OSP UNKNOWN OPCODE"),
//Vendor: L3Harris, Inbound Service Packet (ISP)
@@ -299,7 +299,7 @@ public enum Opcode
MOTOROLA_OSP_GROUP_REGROUP_DELETE, MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_GRANT,
MOTOROLA_OSP_GROUP_REGROUP_CHANNEL_UPDATE, MOTOROLA_OSP_TRAFFIC_CHANNEL_ID,
MOTOROLA_OSP_DENY_RESPONSE, MOTOROLA_OSP_SYSTEM_LOADING, MOTOROLA_OSP_BASE_STATION_ID,
MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN, MOTOROLA_OSP_UNKNOWN);
MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN, MOTOROLA_OSP_TDMA_DATA_CHANNEL, MOTOROLA_OSP_UNKNOWN);
/**
* Harris opcodes
@@ -479,6 +479,8 @@ public enum Opcode
return MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN;
case 0x0F:
return MOTOROLA_OSP_OPCODE_15;
case 0x16:
return MOTOROLA_OSP_TDMA_DATA_CHANNEL;
default:
return MOTOROLA_OSP_UNKNOWN;
}

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.Mot
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaBaseStationId;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaDenyResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaEmergencyAlarmActivation;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExplicitTDMADataChannelAnnouncement;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaExtendedFunctionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupAddCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.MotorolaGroupRegroupChannelGrant;
@@ -441,6 +442,9 @@ public class TSBKMessageFactory
case MOTOROLA_OSP_OPCODE_15:
tsbk = new MotorolaOpcode15(dataUnitID, message, nac, timestamp);
break;
case MOTOROLA_OSP_TDMA_DATA_CHANNEL:
tsbk = new MotorolaExplicitTDMADataChannelAnnouncement(dataUnitID, message, nac, timestamp);
break;
case MOTOROLA_OSP_UNKNOWN:
tsbk = new UnknownMotorolaOSPMessage(dataUnitID, message, nac, timestamp);
break;

View File

@@ -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 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 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 APCO25PatchGroup mPatchGroup;
@@ -128,11 +129,11 @@ public class L3HarrisGroupRegroupExplicitEncryptionCommand extends VendorOSPMess
{
if(getRegroupOptions().isTalkgroupAddress())
{
mPatchedIdentifier = APCO25Talkgroup.create(getInt(TARGET_ADDRESS));
mPatchedIdentifier = APCO25Talkgroup.create(getInt(TARGET_TALKGROUP_ADDRESS));
}
else
{
mPatchedIdentifier = APCO25RadioIdentifier.createTo(getInt(TARGET_ADDRESS));
mPatchedIdentifier = APCO25RadioIdentifier.createTo(getInt(TARGET_RADIO_ADDRESS));
}
}

View File

@@ -0,0 +1,143 @@
/*
* *****************************************************************************
* 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.phase1.message.tsbk.motorola.osp;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.bits.IntField;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25ExplicitChannel;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.VendorOSPMessage;
import java.util.Collections;
import java.util.List;
/**
* Motorola TSBK Opcode 22 (0x16). This seems to be a TDMA data channel announcement message. A curious thing is the
* frequency band for the channel number is an FDMA band. I only have two examples of this, but in both examples
* the frequency band and channel number align with known frequencies for the system and site and in one of those cases
* it was an actual TDMA data channel, so this may or may not be correct.
*
* Example: PA-STARNET VHF control channel - not announcing a dedicated TDMA channel (15-4095).
* 1690423FFFFFFFFF0000D458
* 9690423FFFFFFFFF0000306C
*
* Example: Victoria Radio Network (VRN) Metro Mobile Radio - UHF control channel with a dedicated TDMA data channel.
* https://www.radioreference.com/db/sid/7197 Site 1
* 1690C23F3060FFFF00001199 0x3060 = 3-96 = 420.6125 MHz
* 9690C23F3060FFFF0000F5AD
* 1690C23F803EFFFF000030AF 0x803E = 8-62 = 467.9000 MHz.
* 9690C23F803EFFFF0000D49B
*
* This messages correlates with Motorola TDMA MAC OPCODE 139 which contained both sequences x3060 and x803E that were
* carried in separate Opcode 22 messages in the VRN control channel examples, but transmitted just this single message
* repeatedly on the dedicated TDMA data channel. TDMA Data Channel was active on 420.6125 MHz.
* 8B900FFF803EFF3060FF3060FF3060
* .... .... .... ....
*/
public class MotorolaExplicitTDMADataChannelAnnouncement extends VendorOSPMessage implements IFrequencyBandReceiver
{
private static final IntField DOWNLINK_FREQUENCY_BAND = IntField.length4(OCTET_4_BIT_32);
private static final IntField DOWNLINK_CHANNEL_NUMBER = IntField.length12(OCTET_4_BIT_32 + 4);
private static final IntField UPLINK_FREQUENCY_BAND = IntField.length4(OCTET_6_BIT_48);
private static final IntField UPLINK_CHANNEL_NUMBER = IntField.length12(OCTET_6_BIT_48 + 4);
private APCO25Channel mChannel;
/**
* Constructs a TSBK from the binary message sequence.
*/
public MotorolaExplicitTDMADataChannelAnnouncement(P25P1DataUnitID dataUnitId, CorrectedBinaryMessage message, int nac, long timestamp)
{
super(dataUnitId, message, nac, timestamp);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
if(hasChannel())
{
sb.append(" ACTIVE CHAN:").append(getChannel());
sb.append(" FREQ:").append(getChannel().getDownlinkFrequency());
}
else
{
sb.append(" NOT ACTIVE");
}
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* TDMA data channel.
*/
public APCO25Channel getChannel()
{
if(mChannel == null && hasChannel())
{
if(hasUplink())
{
mChannel = APCO25ExplicitChannel.create(getInt(DOWNLINK_FREQUENCY_BAND), getInt(DOWNLINK_CHANNEL_NUMBER),
getInt(UPLINK_FREQUENCY_BAND), getInt(UPLINK_CHANNEL_NUMBER));
}
else
{
mChannel = APCO25Channel.create(getInt(DOWNLINK_FREQUENCY_BAND), getInt(DOWNLINK_CHANNEL_NUMBER));
}
}
return mChannel;
}
/**
* Indicates if the message has a data channel
*/
public boolean hasChannel()
{
return getMessage().getInt(DOWNLINK_FREQUENCY_BAND) != 0xF && getMessage().getInt(DOWNLINK_CHANNEL_NUMBER) != 0xFFF;
}
/**
* Indicates if the message has an uplink frequency band and channel number.
*/
private boolean hasUplink()
{
return getMessage().getInt(UPLINK_FREQUENCY_BAND) != 0xF && getMessage().getInt(UPLINK_CHANNEL_NUMBER) != 0xFFF;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
@Override
public List<IChannelDescriptor> getChannels()
{
if(hasChannel())
{
return List.of(getChannel());
}
return List.of();
}
}

View File

@@ -126,6 +126,7 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorol
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaQueuedResponse;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.DatchTimeslot;
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
import io.github.dsheirer.protocol.Protocol;
import java.util.Collections;
@@ -240,6 +241,11 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
//If we're tracking the call event, update the duration on it
mTrafficChannelManager.processP2TrafficVoice(getCurrentFrequency(), getTimeslot(), message.getTimestamp());
}
//Motorola TDMA data channel.
else if(message instanceof DatchTimeslot)
{
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot()));
}
else if(message instanceof EncryptionSynchronizationSequence ess)
{
//We don't send any state events for this message since it can only occur in conjunction with
@@ -465,9 +471,16 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case PHASE1_90_GROUP_REGROUP_VOICE_CHANNEL_USER_ABBREVIATED:
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;
/**
@@ -516,6 +529,8 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case MOTOROLA_89_GROUP_REGROUP_DELETE:
processDynamicRegrouping(message, mac);
break;
case MOTOROLA_8B_TDMA_DATA_CHANNEL:
break;
case MOTOROLA_91_TALKER_ALIAS_HEADER:
//Unknown
break;

View File

@@ -36,6 +36,7 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorol
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasHeader;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractSignalingTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.DatchTimeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Timeslot;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot;
import io.github.dsheirer.sample.Listener;
@@ -98,7 +99,11 @@ public class P25P2MessageProcessor implements Listener<IMessage>
for(Timeslot timeslot: sff.getTimeslots())
{
if(timeslot instanceof AbstractSignalingTimeslot)
if(timeslot instanceof DatchTimeslot)
{
mMessageListener.receive(timeslot);
}
else if(timeslot instanceof AbstractSignalingTimeslot)
{
AbstractSignalingTimeslot ast = (AbstractSignalingTimeslot)timeslot;

View File

@@ -25,7 +25,6 @@ import java.util.EnumSet;
*/
public enum DataUnitID
{
VOICE_4(0, 0x00,"VOICE-4"),
RESERVED_1(1, 0x17, "RESERVED 1"),
RESERVED_2(2, 0x2E, "RESERVED 2"),
@@ -36,7 +35,7 @@ public enum DataUnitID
RESERVED_7(7, 0x72, "RESERVED 7"),
RESERVED_8(8, 0x8D, "RESERVED 8"),
SCRAMBLED_FACCH(9, 0x9A, "FACCH-S"),
RESERVED_A(10, 0xA3, "RESERVED 10"),
SCRAMBLED_DATCH(10, 0xA3, "DATCH-S"), //Motorola APX-Next TDMA Data Channel
RESERVED_B(11, 0xB4, "RESERVED 11"),
UNSCRAMBLED_SACCH(12, 0xC6, "SACCH-U"),
UNSCRAMBLED_LCCH(13, 0xD1, "LOCCH-U"),
@@ -133,7 +132,7 @@ public enum DataUnitID
case 0x9A:
return SCRAMBLED_FACCH;
case 0xA3:
return RESERVED_A;
return SCRAMBLED_DATCH;
case 0xB4:
return RESERVED_B;
case 0xC6:

View File

@@ -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.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.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.MotorolaGroupRegroupAddCommand;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupChannelGrantExplicit;
@@ -140,8 +143,10 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorol
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserAbbreviated;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaQueuedResponse;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTDMADataChannel;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasDataBlock;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasHeader;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaUnknownOpcode135;
import io.github.dsheirer.module.decode.p25.reference.Vendor;
import java.util.ArrayList;
import java.util.List;
@@ -578,12 +583,22 @@ public class MacMessageFactory
return new MotorolaGroupRegroupVoiceChannelUserAbbreviated(message, offset);
case MOTOROLA_81_GROUP_REGROUP_ADD:
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:
return new MotorolaGroupRegroupVoiceChannelUpdate(message, offset);
case MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND:
return new MotorolaGroupRegroupExtendedFunctionCommand(message, offset);
case MOTOROLA_87_UNKNOWN_OPCODE_135:
return new MotorolaUnknownOpcode135(message, offset);
case MOTOROLA_89_GROUP_REGROUP_DELETE:
return new MotorolaGroupRegroupDeleteCommand(message, offset);
case MOTOROLA_8B_TDMA_DATA_CHANNEL:
return new MotorolaTDMADataChannel(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:
return new MotorolaTalkerAliasHeader(message, offset);
case MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK:

View File

@@ -115,12 +115,17 @@ public enum MacOpcode
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_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_87_UNKNOWN_OPCODE_135(Vendor.MOTOROLA, 135, "MOTOROLA UNKNOWN OPCODE 135", Integer.MIN_VALUE),
MOTOROLA_89_GROUP_REGROUP_DELETE(Vendor.MOTOROLA, 137, "MOTOROLA GROUP REGROUP DELETE", 17),
MOTOROLA_8B_TDMA_DATA_CHANNEL(Vendor.MOTOROLA, 139, "MOTOROLA TDMA DATA CHANNEL", Integer.MIN_VALUE),
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.
MOTOROLA_91_TALKER_ALIAS_HEADER(Vendor.MOTOROLA, 145, "MOTOROLA GROUP REGROUP UNKNOWN", 17),
MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK(Vendor.MOTOROLA, 149, "MOTOROLA UNKNOWN 149", 17),
MOTOROLA_91_TALKER_ALIAS_HEADER(Vendor.MOTOROLA, 145, "MOTOROLA TALKER ALIAS HEADER", 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_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),

View File

@@ -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.reference.ServiceOptions;
import io.github.dsheirer.module.decode.p25.reference.VoiceServiceOptions;
import java.util.ArrayList;
import java.util.List;
@@ -64,11 +63,7 @@ public class GroupRegroupVoiceChannelUserAbbreviated extends MacStructure implem
StringBuilder sb = new StringBuilder();
sb.append("GROUP REGROUP VOICE CHANNEL USER ABBREVIATED");
sb.append(" TALKGROUP:").append(getPatchgroup());
if(hasRadio())
{
sb.append(" TALKER RADIO:").append(getRadio());
}
sb.append(" TALKER RADIO:").append(getRadio());
return sb.toString();
}
@@ -91,6 +86,14 @@ public class GroupRegroupVoiceChannelUserAbbreviated extends MacStructure implem
return mPatchgroup;
}
/**
* Indicates if this message has a non-zero patch group identifier.
*/
public boolean hasPatchgroup()
{
return getInt(SUPERGROUP) > 0;
}
/**
* Talker radio identifier.
*/
@@ -118,7 +121,11 @@ public class GroupRegroupVoiceChannelUserAbbreviated extends MacStructure implem
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getPatchgroup());
if(hasPatchgroup())
{
mIdentifiers.add(getPatchgroup());
}
if(hasRadio())
{

View File

@@ -70,4 +70,12 @@ public abstract class MacStructureVariableLength extends MacStructure
{
return getLength(getMessage(), getOffset());
}
/**
* Message fragment for this variable length message.
*/
public CorrectedBinaryMessage getSubMessage()
{
return getMessage().getSubMessage(getOffset(), getOffset() + (getLength() * 8));
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,269 @@
/*
* *****************************************************************************
* 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.channel.IChannelDescriptor;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola TDMA data channel active announcement. Opcode 139.
*
* This messages correlates with Motorola FDMA TSBK OPCODE 22 which contained the same sequences x3060 and x803E and
* was transmitted on the control channel for this TDMA data channel and seems to be frequency band and channel number
* references.
* 8B900FFF803EFF3060FF3060FF3060
*
* Example: Victoria Radio Network (VRN) - UHF control channel with a dedicated TDMA data channel.
* It toggled through all four examples.
* 1690C23F3060FFFF00001199
* 9690C23F3060FFFF0000F5AD
* 1690C23F803EFFFF000030AF
* 9690C23F803EFFFF0000D49B
*/
public class MotorolaTDMADataChannel extends MacStructureVendor implements IFrequencyBandReceiver
{
private static final IntField UNKNOWN_1 = IntField.length8(OCTET_4_BIT_24);
private static final IntField FREQUENCY_BAND_1 = IntField.length4(OCTET_5_BIT_32);
private static final IntField CHANNEL_NUMBER_1 = IntField.length12(OCTET_5_BIT_32 + 4);
private static final IntField UNKNOWN_2 = IntField.length8(OCTET_7_BIT_48);
private static final IntField FREQUENCY_BAND_2 = IntField.length4(OCTET_8_BIT_56);
private static final IntField CHANNEL_NUMBER_2 = IntField.length12(OCTET_8_BIT_56 + 4);
private static final IntField UNKNOWN_3 = IntField.length8(OCTET_10_BIT_72);
private static final IntField FREQUENCY_BAND_3 = IntField.length4(OCTET_11_BIT_80);
private static final IntField CHANNEL_NUMBER_3 = IntField.length12(OCTET_11_BIT_80 + 4);
private static final IntField UNKNOWN_4 = IntField.length8(OCTET_13_BIT_96);
private static final IntField FREQUENCY_BAND_4 = IntField.length4(OCTET_14_BIT_104);
private static final IntField CHANNEL_NUMBER_4 = IntField.length12(OCTET_14_BIT_104 + 4);
private APCO25Channel mChannel1;
private APCO25Channel mChannel2;
private APCO25Channel mChannel3;
private APCO25Channel mChannel4;
/**
* Constructs the message
*
* @param message containing the message bits
* @param offset into the message for this structure
*/
public MotorolaTDMADataChannel(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
/**
* Textual representation of this message
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getOpcode());
if(hasChannel1())
{
sb.append(" ACTIVE CHAN1:").append(getChannel1());
if(hasChannel2())
{
sb.append(" CHAN2:").append(getChannel2());
if(hasChannel3())
{
sb.append(" CHAN3:").append(getChannel3());
if(hasChannel1())
{
sb.append(" CHAN4:").append(getChannel4());
}
}
}
}
else
{
sb.append(" NOT ACTIVE");
}
sb.append(" MSG:").append(getSubMessage().toHexString());
return sb.toString();
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel1()
{
return getInt(CHANNEL_NUMBER_1) != 0xF && getInt(FREQUENCY_BAND_1) != 0xFFF;
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel1()
{
if(mChannel1 == null)
{
mChannel1 = APCO25Channel.create(getInt(FREQUENCY_BAND_1), getInt(CHANNEL_NUMBER_1));
}
return mChannel1;
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel2()
{
return getInt(CHANNEL_NUMBER_2) != 0xF && getInt(FREQUENCY_BAND_2) != 0xFFF &&
getInt(FREQUENCY_BAND_2) != getInt(FREQUENCY_BAND_1) &&
getInt(CHANNEL_NUMBER_2) != getInt(CHANNEL_NUMBER_1);
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel2()
{
if(mChannel2 == null)
{
mChannel2 = APCO25Channel.create(getInt(FREQUENCY_BAND_2), getInt(CHANNEL_NUMBER_2));
}
return mChannel2;
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel3()
{
return getInt(CHANNEL_NUMBER_3) != 0xF && getInt(FREQUENCY_BAND_3) != 0xFFF &&
getInt(FREQUENCY_BAND_3) != getInt(FREQUENCY_BAND_2) &&
getInt(CHANNEL_NUMBER_3) != getInt(CHANNEL_NUMBER_2);
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel3()
{
if(mChannel3 == null)
{
mChannel3 = APCO25Channel.create(getInt(FREQUENCY_BAND_3), getInt(CHANNEL_NUMBER_3));
}
return mChannel3;
}
/**
* Indicates if message has channel info.
*/
private boolean hasChannel4()
{
return getInt(CHANNEL_NUMBER_4) != 0xF && getInt(FREQUENCY_BAND_4) != 0xFFF &&
getInt(FREQUENCY_BAND_4) != getInt(FREQUENCY_BAND_3) &&
getInt(CHANNEL_NUMBER_4) != getInt(CHANNEL_NUMBER_3);
}
/**
* Optional channel information.
*/
public APCO25Channel getChannel4()
{
if(mChannel4 == null)
{
mChannel4 = APCO25Channel.create(getInt(FREQUENCY_BAND_4), getInt(CHANNEL_NUMBER_4));
}
return mChannel4;
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
/**
* List of active TDMA data channels.
*/
public List<APCO25Channel> getActiveChannels()
{
List<APCO25Channel> channels = new ArrayList<>();
if(hasChannel1())
{
channels.add(getChannel1());
if(hasChannel2())
{
channels.add(getChannel2());
if(hasChannel3())
{
channels.add(getChannel3());
if(hasChannel4())
{
channels.add(getChannel4());
}
}
}
return channels;
}
return List.of();
}
@Override
public List<IChannelDescriptor> getChannels()
{
List<IChannelDescriptor> channels = new ArrayList<>();
if(hasChannel1())
{
channels.add(getChannel1());
if(hasChannel2())
{
channels.add(getChannel2());
if(hasChannel3())
{
channels.add(getChannel3());
if(hasChannel4())
{
channels.add(getChannel4());
}
}
}
return channels;
}
return List.of();
}
}

View File

@@ -0,0 +1,62 @@
/*
* *****************************************************************************
* 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.List;
/**
* Motorola Unknown Opcode 135
*
* This message was observed on a TDMA dedicated data channel for the Victoria Radio Network (VRN). It may be an
* indicator that this is a data channel.
*/
public class MotorolaUnknownOpcode135 extends MacStructureVendor
{
/**
* Constructs the message
*
* @param message containing the message bits
* @param offset into the message for this structure
*/
public MotorolaUnknownOpcode135(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
/**
* Textual representation of this message
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getOpcode());
sb.append(" MSG:").append(getSubMessage().toHexString());
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -0,0 +1,71 @@
/*
* *****************************************************************************
* 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.timeslot;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.DataUnitID;
import java.util.List;
/**
* Motorola TDMA Data Channel (DATCH) timeslot. Is probably used with APX-NEXT and MDT equipment.
*
* Analysis:
* - It seems to use the standard P25 Phase 2 scrambling sequence.
* - The overall signalling doesn't appear to be encrypted because the data does not appear randomized. There's a lot
* of repeated messaging with just a few bits changing.
* - It doesn't seem to be RS encoded data, or the first nibble is not protected in the RS encoding because the overall
* message doesn't change with variations in the first nibble, when there's repeating messages.
* - First 4 bits follow a pattern in repeating messages across both timeslots where the first 2 bits follows the pattern
* and the 3rd and 4th bits are set across sequences of 3 or 4 messages spanning both timeslots.
* - Not sure if the timeslots are independent of each other, or if they are strapped.
*/
public class DatchTimeslot extends Timeslot
{
/**
* Constructs a scrambled DATCH timeslot
*
* @param message containing 320 scrambled bits for the timeslot
* @param scramblingSequence to descramble the message
* @param timeslot of the message
* @param timestamp of the message
*/
public DatchTimeslot(CorrectedBinaryMessage message, BinaryMessage scramblingSequence, int timeslot,
long timestamp)
{
super(message, DataUnitID.SCRAMBLED_DATCH, scramblingSequence, timeslot, timestamp);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("TS").append(getTimeslot());
sb.append(" DATCH-SC TDMA DATA TIMESLOT MSG:").append(getMessage().toHexString());
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return List.of();
}
}

View File

@@ -52,6 +52,8 @@ public class TimeslotFactory
return new FacchTimeslot(message, scramblingSequence, timeslot, timestamp);
case SCRAMBLED_SACCH:
return new SacchTimeslot(message, scramblingSequence, timeslot, timestamp);
case SCRAMBLED_DATCH:
return new DatchTimeslot(message, scramblingSequence, timeslot, timestamp);
case UNSCRAMBLED_FACCH:
return new FacchTimeslot(message, timeslot, timestamp);
case UNSCRAMBLED_SACCH:

View File

@@ -41,6 +41,7 @@ public class UnknownTimeslot extends Timeslot
StringBuilder sb = new StringBuilder();
sb.append("TS").append(getTimeslot());
sb.append(" UNKNOWN TIMESLOT - DATA UNIT ID: ").append(getDataUnitID());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}

View File

@@ -100,19 +100,19 @@ public class DecodeEventLogger extends EventLogger implements IDecodeEventListen
cells.add(event.getProtocol());
cells.add(event.getEventType());
List<Identifier> fromIdentifiers = event.getIdentifierCollection().getIdentifiers(Role.FROM);
if(fromIdentifiers != null && !fromIdentifiers.isEmpty())
Identifier fromIdentifier = event.getIdentifierCollection().getFromIdentifier();
if(fromIdentifier != null)
{
cells.add(fromIdentifiers.get(0));
cells.add(fromIdentifier);
}
else
{
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()
.getIdentifier(IdentifierClass.CONFIGURATION,Form.ALIAS_LIST,Role.ANY);
@@ -120,9 +120,9 @@ public class DecodeEventLogger extends EventLogger implements IDecodeEventListen
if(mAliasList != null)
{
String mystring = (!mAliasList.getAliases(toIdentifiers.get(0)).isEmpty()) ?
mAliasList.getAliases(toIdentifiers.get(0)).get(0).toString() : "";
cells.add(mystring + " (" + toIdentifiers.get(0) + ")");
String mystring = (!mAliasList.getAliases(toIdentifier).isEmpty()) ?
mAliasList.getAliases(toIdentifier).toString() : "";
cells.add(mystring + " (" + toIdentifier + ")");
}
else
{

View File

@@ -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
* 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.ChannelMapModel;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.playlist.IAliasListRefreshListener;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.preference.UserPreferences;
@@ -48,6 +49,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -77,6 +79,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
private AtomicBoolean mPlaylistSavePending = new AtomicBoolean();
private ScheduledFuture<?> mPlaylistSaveFuture;
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
@@ -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
*/

View File

@@ -68,9 +68,13 @@ public class ChannelRotationMonitor extends Module implements ISourceEventProvid
mRotationDelay = rotationDelay;
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);
}
}