Compare commits

...

20 Commits

Author SHA1 Message Date
Denny Sheirer
02363c3113 Merge pull request #1690 from DSheirer/1689-harris-patch-group-prefixes
#1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris Patch Groups
2023-11-04 07:13:10 -04:00
Dennis Sheirer
cfaac0fcbc #1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris patch groups. 2023-11-04 07:12:29 -04:00
Denny Sheirer
bee0405e00 Merge pull request #1688 from DSheirer/1687-rtl2832-fc0013
Support For RTL2832 With Embedded FC0013 Tuner
2023-11-04 06:54:55 -04:00
Dennis Sheirer
50bdf0ee81 #1687 Adds support for RTL2832 with embedded FC0013 tuner. 2023-11-04 06:52:37 -04:00
Denny Sheirer
a27ddbe0d0 Merge pull request #1679 from DSheirer/1643-r828d-v4-tuner
#1643 RTL2832/R828D aka RTL-SDR V4 Dongle & Bias-T Support
2023-10-22 08:40:33 -04:00
Dennis Sheirer
0d652b9153 #1643 Adds support for RTL2832 with embedded R828D tuner and RTL-SDR.com V4 dongles. Introduces Bias-T support for all RTL-2832 dongles. 2023-10-22 08:38:16 -04:00
Denny Sheirer
9468fd189b Merge pull request #1677 from DSheirer/1607-harris-p25-patch-groups
#1607 P25 P1/P2 L3Harris Patch Group Support
2023-10-21 07:00:41 -04:00
sheirerd
215737ad9b #1607 Add support for L3Harris dynamic regroup/patch messages and events. Includes parsing for L3Harris unknown opcodes 10, 42 and 43. 2023-10-21 06:58:11 -04:00
Denny Sheirer
123c25b142 Merge pull request #1672 from DSheirer/1650-fm-decoder-7_5-bandwidth
#1650 Adds 7.5 kHz channel bandwidth option for FM channels.
2023-10-15 09:22:48 -04:00
Dennis Sheirer
af56198f0e #1650 Adds 7.5 kHz channel bandwidth option for FM channels. 2023-10-15 09:21:38 -04:00
Denny Sheirer
3c73fdfd6e Merge pull request #1671 from DSheirer/1670-dmr-cap-plus-missing-call-audio
#1670 Missing Audio Segments - DMR Cap+
2023-10-15 08:56:48 -04:00
Dennis Sheirer
b391ebbb5f #1670 Resolves missing audio segments for some DMR Cap+ call events. 2023-10-15 08:55:35 -04:00
Denny Sheirer
bad5a1f9f0 Merge pull request #1669 from DSheirer/1668-message-viewer-p25-support
#1668 Message Recording Viewer - P25P1 Support
2023-10-15 07:17:06 -04:00
Dennis Sheirer
a7470b463f #1668 Updates message recording (.bits) viewer to support P25 Phase 1. 2023-10-15 07:14:53 -04:00
Denny Sheirer
d6c706cb35 Merge pull request #1667 from DSheirer/1666-p25-moto-unit-gps
#1666 P25 Motorola Unit GPS Parsing & Plot to Map
2023-10-14 08:16:48 -04:00
Dennis Sheirer
a0d6950c75 #1666 P25 Motorola unit self-reported GPS location parsing and plotting to map. 2023-10-14 08:15:35 -04:00
Denny Sheirer
2495200b36 Merge pull request #1663 from DSheirer/1648-dmr-lrrp-not-plotting-map
#1648 DMR Embedded GPS & LRRP Plotting
2023-10-05 03:32:08 -04:00
Dennis Sheirer
bfe7ae0b1b #1648 Resolves issue with plotting DMR embedded GPS and LRRP positions. 2023-10-05 03:31:17 -04:00
Denny Sheirer
4912abbf07 Merge pull request #1662 from DSheirer/1661-dmr-gps-location-parsing-error
#1661 DMR Talker GPS Lat/Long Values Incorrect
2023-10-05 03:21:24 -04:00
Dennis Sheirer
2f2998d02e #1661 Resolves issue with parsing of DMR embedded GPS latitude and longitude values. 2023-10-05 03:20:17 -04:00
59 changed files with 4394 additions and 894 deletions

View File

@@ -53,7 +53,8 @@ repositories {
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
} }
version = '0.6.0-beta2' //Note: this should match the GitLab release tag.
version = '0.6.0-beta-3'
//Java 20 is required for this version of the Project Panama preview/incubator feature //Java 20 is required for this version of the Project Panama preview/incubator feature
java { java {

View File

@@ -388,6 +388,8 @@ public class AliasList
} }
break; break;
case PATCH_GROUP: case PATCH_GROUP:
List<Alias> aliases = new ArrayList<>();
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier; PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
PatchGroup patchGroup = patchGroupIdentifier.getValue(); PatchGroup patchGroup = patchGroupIdentifier.getValue();
@@ -395,8 +397,6 @@ public class AliasList
if(patchGroupAliasList != null) if(patchGroupAliasList != null)
{ {
List<Alias> aliases = new ArrayList<>();
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup()); Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
if(alias != null) if(alias != null)
@@ -404,19 +404,36 @@ public class AliasList
aliases.add(alias); aliases.add(alias);
} }
for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedGroupIdentifiers()) for(TalkgroupIdentifier patchedTalkgroup: patchGroup.getPatchedTalkgroupIdentifiers())
{ {
Alias patchedAlias = patchGroupAliasList.getAlias(patchedGroup); Alias patchedTalkgroupAlias = patchGroupAliasList.getAlias(patchedTalkgroup);
if(patchedAlias != null && !aliases.contains(patchedAlias)) if(patchedTalkgroupAlias != null && !aliases.contains(patchedTalkgroupAlias))
{ {
aliases.add(patchedAlias); aliases.add(patchedTalkgroupAlias);
} }
} }
return aliases;
} }
break;
if(patchGroup.hasPatchedRadios())
{
RadioAliasList radioAliasList = mRadioProtocolMap.get(patchGroupIdentifier.getProtocol());
if(radioAliasList != null)
{
for(RadioIdentifier patchedRadio: patchGroup.getPatchedRadioIdentifiers())
{
Alias patchedRadioAlias = radioAliasList.getAlias(patchedRadio);
if(patchedRadioAlias != null && !aliases.contains(patchedRadioAlias))
{
aliases.add(patchedRadioAlias);
}
}
}
}
return aliases;
case RADIO: case RADIO:
RadioIdentifier radio = (RadioIdentifier)identifier; RadioIdentifier radio = (RadioIdentifier)identifier;
@@ -483,9 +500,7 @@ public class AliasList
{ {
if(alias != null) if(alias != null)
{ {
List<Alias> aliases = new ArrayList<>(); return Collections.singletonList(alias);
aliases.add(alias);
return aliases;
} }
return Collections.emptyList(); return Collections.emptyList();

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -38,9 +38,6 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@@ -48,12 +45,13 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.time.Duration; import java.time.Duration;
import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API. * Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
@@ -475,7 +473,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(patchGroup.getPatchGroup().getValue().toString()); sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers()) for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{ {
sb.append(",").append(patched.getValue()); sb.append(",").append(patched.getValue());
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
package io.github.dsheirer.audio.broadcast.rdioscanner; package io.github.dsheirer.audio.broadcast.rdioscanner;
import com.google.common.net.HttpHeaders; import com.google.common.net.HttpHeaders;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias; import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel; import io.github.dsheirer.alias.AliasModel;
@@ -34,7 +33,6 @@ import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier; import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup; import io.github.dsheirer.identifier.patch.PatchGroup;
@@ -42,24 +40,12 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool; import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI; import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Queue; import java.util.Queue;
@@ -67,6 +53,8 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
@@ -525,7 +513,12 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
sb.append(patchGroup.getPatchGroup().getValue().toString()); sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers()) for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{ {
sb.append(",").append(patched.getValue()); sb.append(",").append(patched.getValue());
} }

View File

@@ -844,13 +844,18 @@ public class BinaryMessage extends BitSet
*/ */
public int getTwosComplement(int start, int end) public int getTwosComplement(int start, int end)
{ {
BinaryMessage sub = getSubMessage(start, end); if(get(start))
boolean negative = sub.get(start); {
sub.flip(0, sub.size()); //Negative value - flip and add one
BinaryMessage fragment = getSubMessage(start, end);
int value = sub.getInt(0, sub.size()) + 1; fragment.flip(0, fragment.size());
value *= (negative ? -1 : 1); return fragment.getInt(1, fragment.size()) + 1;
return value; }
else
{
//Positive value - return the contents.
return getInt(start + 1, end);
}
} }

View File

@@ -24,8 +24,6 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.map.ChannelMap; import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelRange; import io.github.dsheirer.controller.channel.map.ChannelRange;
import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.DMRRecordingViewer;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.IconManager; import io.github.dsheirer.gui.icon.IconManager;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest; import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.PlaylistEditor; import io.github.dsheirer.gui.playlist.PlaylistEditor;
@@ -37,6 +35,8 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.UserPreferencesEditor; import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest; import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog; import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.RecordingViewer;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor; import io.github.dsheirer.jmbe.JmbeEditor;
import io.github.dsheirer.jmbe.JmbeEditorRequest; import io.github.dsheirer.jmbe.JmbeEditorRequest;
@@ -67,7 +67,7 @@ public class JavaFxWindowManager extends Application
public static final String USER_PREFERENCES_EDITOR = "preferences"; public static final String USER_PREFERENCES_EDITOR = "preferences";
public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog"; public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog";
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map"; public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
public static final String STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER = "dmr.message.viewer"; public static final String STAGE_MONITOR_KEY_RECORDING_VIEWER = "recording.viewer";
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager"; public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor"; public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist"; public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
@@ -82,14 +82,14 @@ public class JavaFxWindowManager extends Application
private TunerManager mTunerManager; private TunerManager mTunerManager;
private UserPreferences mUserPreferences; private UserPreferences mUserPreferences;
private UserPreferencesEditor mUserPreferencesEditor; private UserPreferencesEditor mUserPreferencesEditor;
private DMRRecordingViewer mDmrRecordingViewer; private RecordingViewer mRecordingViewer;
private Stage mChannelMapStage; private Stage mChannelMapStage;
private Stage mIconManagerStage; private Stage mIconManagerStage;
private Stage mJmbeEditorStage; private Stage mJmbeEditorStage;
private Stage mPlaylistStage; private Stage mPlaylistStage;
private Stage mUserPreferencesStage; private Stage mUserPreferencesStage;
private Stage mDmrRecordingViewerStage; private Stage mRecordingViewerStage;
/** /**
* Constructs an instance. Note: this constructor is used for Swing applications. * Constructs an instance. Note: this constructor is used for Swing applications.
@@ -195,31 +195,31 @@ public class JavaFxWindowManager extends Application
} }
/** /**
* Stage for the DMR Message Viewer * Stage for the recording viewer
*/ */
public Stage getDmrRecordingViewerStage() public Stage getRecordingViewerStage()
{ {
if(mDmrRecordingViewerStage == null) if(mRecordingViewerStage == null)
{ {
createJFXPanel(); createJFXPanel();
Scene scene = new Scene(getDmrRecordingViewer(), 1100, 800); Scene scene = new Scene(getRecordingViewer(), 1100, 800);
mDmrRecordingViewerStage = new Stage(); mRecordingViewerStage = new Stage();
mDmrRecordingViewerStage.setTitle("sdrtrunk - DMR Recording Viewer"); mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
mDmrRecordingViewerStage.setScene(scene); mRecordingViewerStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER); mUserPreferences.getJavaFxPreferences().monitor(mRecordingViewerStage, STAGE_MONITOR_KEY_RECORDING_VIEWER);
} }
return mDmrRecordingViewerStage; return mRecordingViewerStage;
} }
public DMRRecordingViewer getDmrRecordingViewer() public RecordingViewer getRecordingViewer()
{ {
if(mDmrRecordingViewer == null) if(mRecordingViewer == null)
{ {
mDmrRecordingViewer = new DMRRecordingViewer(); mRecordingViewer = new RecordingViewer();
} }
return mDmrRecordingViewer; return mRecordingViewer;
} }
public Stage getIconManagerStage() public Stage getIconManagerStage()
@@ -453,12 +453,12 @@ public class JavaFxWindowManager extends Application
* Process a channel map editor request * Process a channel map editor request
*/ */
@Subscribe @Subscribe
public void process(final ViewDmrRecordingViewerRequest request) public void process(final ViewRecordingViewerRequest request)
{ {
execute(() -> { execute(() -> {
getDmrRecordingViewerStage().show(); getRecordingViewerStage().show();
getDmrRecordingViewerStage().requestFocus(); getRecordingViewerStage().requestFocus();
getDmrRecordingViewerStage().toFront(); getRecordingViewerStage().toFront();
}); });
} }

View File

@@ -32,13 +32,13 @@ import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelException; import io.github.dsheirer.controller.channel.ChannelException;
import io.github.dsheirer.controller.channel.ChannelSelectionManager; import io.github.dsheirer.controller.channel.ChannelSelectionManager;
import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.dmr.ViewDmrRecordingViewerRequest;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest; import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest; import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
import io.github.dsheirer.gui.preference.CalibrateRequest; import io.github.dsheirer.gui.preference.CalibrateRequest;
import io.github.dsheirer.gui.preference.PreferenceEditorType; import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest; import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog; import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.log.ApplicationLog; import io.github.dsheirer.log.ApplicationLog;
import io.github.dsheirer.map.MapService; import io.github.dsheirer.map.MapService;
@@ -420,9 +420,9 @@ public class SDRTrunk implements Listener<TunerEvent>
viewMenu.add(new JSeparator()); viewMenu.add(new JSeparator());
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer"); JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest())); recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
viewMenu.add(dmrMessageViewerMenu); viewMenu.add(recordingViewerMenu);
JMenuItem settingsMenu = new JMenuItem("Icon Manager"); JMenuItem settingsMenu = new JMenuItem("Icon Manager");
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest())); settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));

View File

@@ -17,20 +17,22 @@
* **************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.gui.dmr; package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.DMRMessageFramer; import io.github.dsheirer.module.decode.dmr.DMRMessageFramer;
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor; import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR; import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
import io.github.dsheirer.record.binary.BinaryReader; import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.util.ThreadPool;
import java.io.File; import java.io.File;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.prefs.Preferences; import java.util.prefs.Preferences;
import javafx.application.Application;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@@ -40,10 +42,10 @@ import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition; import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
@@ -58,21 +60,19 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.Callback; import javafx.util.Callback;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Utility application to load and view a DMR .bits recording file with the messages fully parsed. * DMR Viewer panel
*/ */
public class DMRRecordingViewer extends VBox public class DmrViewer extends VBox
{ {
private static final Logger mLog = LoggerFactory.getLogger(DMRRecordingViewer.class); private static final Logger mLog = LoggerFactory.getLogger(DmrViewer.class);
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY); private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory"; private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.dmr";
private Preferences mPreferences = Preferences.userNodeForPackage(DMRRecordingViewer.class); private Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class);
private Scene mScene;
private Button mSelectFileButton; private Button mSelectFileButton;
private Label mSelectedFileLabel; private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView; private TableView<IMessage> mMessageTableView;
@@ -86,8 +86,9 @@ public class DMRRecordingViewer extends VBox
private TextField mFindText; private TextField mFindText;
private Button mFindButton; private Button mFindButton;
private Button mFindNextButton; private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
public DMRRecordingViewer() public DmrViewer()
{ {
setPadding(new Insets(5)); setPadding(new Insets(5));
setSpacing(5); setSpacing(5);
@@ -132,6 +133,21 @@ public class DMRRecordingViewer extends VBox
getChildren().addAll(fileBox, filterBox, getMessageTableView()); getChildren().addAll(fileBox, filterBox, getMessageTableView());
} }
/**
* Spinny loading icon to show over the message table view
*/
private ProgressIndicator getLoadingIndicator()
{
if(mLoadingIndicator == null)
{
mLoadingIndicator = new ProgressIndicator();
mLoadingIndicator.setProgress(-1);
mLoadingIndicator.setVisible(false);
}
return mLoadingIndicator;
}
/** /**
* Processes the recording file and loads the content into the viewer * Processes the recording file and loads the content into the viewer
* @param file containing a .bits recording of decoded DMR data. * @param file containing a .bits recording of decoded DMR data.
@@ -140,33 +156,45 @@ public class DMRRecordingViewer extends VBox
{ {
if(file != null && file.exists()) if(file != null && file.exists())
{ {
getSelectedFileLabel().setText(file.getName());
mMessages.clear(); mMessages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
final boolean useCompressed = getUseCompressedTalkgroups().isSelected();
DMRMessageFramer messageFramer = new DMRMessageFramer(null); ThreadPool.CACHED.submit(new Runnable()
DecodeConfigDMR config = new DecodeConfigDMR();
if(getUseCompressedTalkgroups().isSelected())
{ {
config.setUseCompressedTalkgroups(true); @Override
} public void run()
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> mMessages.add(message));
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{ {
ByteBuffer buffer = reader.next(); List<IMessage> messages = new ArrayList<>();
messageFramer.receive(buffer); DMRMessageFramer messageFramer = new DMRMessageFramer(null);
} DecodeConfigDMR config = new DecodeConfigDMR();
} config.setUseCompressedTalkgroups(useCompressed);
catch(Exception ioe) DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
{ messageFramer.setListener(messageProcessor);
ioe.printStackTrace(); messageProcessor.setMessageListener(message -> messages.add(message));
}
getMessageTableView().scrollTo(0); try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
});
}
});
} }
} }
@@ -176,9 +204,9 @@ public class DMRRecordingViewer extends VBox
private void updateFilters() private void updateFilters()
{ {
Predicate<IMessage> timeslotPredicate = message -> Predicate<IMessage> timeslotPredicate = message ->
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) || (getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) || (getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
(getShowTS2().isSelected() && (message.getTimeslot() == 2)); (getShowTS2().isSelected() && (message.getTimeslot() == 2));
String filterText = getSearchText().getText(); String filterText = getSearchText().getText();
@@ -256,6 +284,7 @@ public class DMRRecordingViewer extends VBox
if(mMessageTableView == null) if(mMessageTableView == null)
{ {
mMessageTableView = new TableView<>(); mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages); SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty()); sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList); mMessageTableView.setItems(sortedList);
@@ -509,37 +538,9 @@ public class DMRRecordingViewer extends VBox
{ {
if(mUseCompressedTalkgroups == null) if(mUseCompressedTalkgroups == null)
{ {
mUseCompressedTalkgroups = new CheckBox("Use Compressed Talkgroups"); mUseCompressedTalkgroups = new CheckBox("Use Hytera Tier III Compressed Talkgroups");
} }
return mUseCompressedTalkgroups; return mUseCompressedTalkgroups;
} }
public static void main(String[] args)
{
Application viewer = new Application()
{
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new DMRRecordingViewer(), 1100, 800);
primaryStage.setTitle("DMR Recording Viewer");
primaryStage.setScene(scene);
primaryStage.show();
}
};
Runnable r = () -> {
try
{
viewer.start(new Stage());
}
catch(Exception e)
{
mLog.error("Error starting DMR recording viewer application", e);
}
};
Platform.startup(r);
}
} }

View File

@@ -0,0 +1,462 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.StuffBitsMessage;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer;
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor;
import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.util.ThreadPool;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* APCO25 Phase 1 viewer panel
*/
public class P25P1Viewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(P25P1Viewer.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.p25p1";
private Preferences mPreferences = Preferences.userNodeForPackage(P25P1Viewer.class);
private Button mSelectFileButton;
private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView;
private ObservableList<IMessage> mMessages = FXCollections.observableArrayList();
private FilteredList<IMessage> mFilteredMessages = new FilteredList<>(mMessages);
private TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
private ProgressIndicator mLoadingIndicator;
public P25P1Viewer()
{
setPadding(new Insets(5));
setSpacing(5);
HBox fileBox = new HBox();
fileBox.setMaxWidth(Double.MAX_VALUE);
fileBox.setAlignment(Pos.CENTER_LEFT);
fileBox.setSpacing(5);
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
HBox filterBox = new HBox();
filterBox.setMaxWidth(Double.MAX_VALUE);
filterBox.setAlignment(Pos.BASELINE_CENTER);
filterBox.setSpacing(5);
Label searchLabel = new Label("Message Filter:");
HBox.setMargin(searchLabel, new Insets(0,0,0,15));
Label findLabel = new Label("Find:");
HBox.setHgrow(getFindText(), Priority.ALWAYS);
HBox.setHgrow(getSearchText(), Priority.ALWAYS);
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
getSearchText());
VBox.setVgrow(fileBox, Priority.NEVER);
VBox.setVgrow(filterBox, Priority.NEVER);
VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
getChildren().addAll(fileBox, filterBox, getMessageTableView());
}
/**
* Processes the recording file and loads the content into the viewer
*
* Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the
* message table back on the JavaFX UI thread.
*
* @param file containing a .bits recording of decoded DMR data.
*/
private void load(File file)
{
if(file != null && file.exists())
{
mMessages.clear();
getLoadingIndicator().setVisible(true);
getSelectedFileLabel().setText("Loading ...");
ThreadPool.CACHED.submit(() -> {
List<IMessage> messages = new ArrayList<>();
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
messageFramer.setListener(messageProcessor);
messageProcessor.setMessageListener(message -> {
if(!(message instanceof StuffBitsMessage))
{
messages.add(message);
}
});
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
{
while(reader.hasNext())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
Platform.runLater(() -> {
getLoadingIndicator().setVisible(false);
getSelectedFileLabel().setText(file.getName());
mMessages.addAll(messages);
getMessageTableView().scrollTo(0);
});
});
}
}
/**
* Updates the filter(s) applies to the list of messages
*/
private void updateFilters()
{
String filterText = getSearchText().getText();
if(filterText != null && !filterText.isEmpty())
{
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessages.setPredicate(textPredicate);
}
else
{
mFilteredMessages.setPredicate(null);
}
}
/**
* Finds and selects the first row containing the text argument.
* @param text to search for.
*/
private void find(String text)
{
if(text != null && !text.isEmpty())
{
for(IMessage message: mFilteredMessages)
{
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
return;
}
}
}
}
/**
* Finds and selects the first row containing the text argument, after the currently selected row.
* @param text to search for.
*/
private void findNext(String text)
{
if(text != null && !text.isEmpty())
{
IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
if(selected == null)
{
find(text);
return;
}
int row = mFilteredMessages.indexOf(selected);
for(int x = row + 1; x < mFilteredMessages.size(); x++)
{
if(x < mFilteredMessages.size())
{
IMessage message = mFilteredMessages.get(x);
if(message.toString().toLowerCase().contains(text.toLowerCase()))
{
getMessageTableView().getSelectionModel().select(message);
getMessageTableView().scrollTo(message);
return;
}
}
}
}
}
/**
* List view control with DMR messages
*/
private TableView<IMessage> getMessageTableView()
{
if(mMessageTableView == null)
{
mMessageTableView = new TableView<>();
mMessageTableView.setPlaceholder(getLoadingIndicator());
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
mMessageTableView.setItems(sortedList);
mMessageTableView.setOnKeyPressed(event ->
{
if(KEY_CODE_COPY.match(event))
{
final Set<Integer> rows = new TreeSet<>();
for (final TablePosition tablePosition : mMessageTableView.getSelectionModel().getSelectedCells())
{
rows.add(tablePosition.getRow());
}
final StringBuilder sb = new StringBuilder();
boolean firstRow = true;
for (final Integer row : rows)
{
if(firstRow)
{
firstRow = false;
}
else
{
sb.append('\n');
}
boolean firstCol = true;
for (final TableColumn<?, ?> column : mMessageTableView.getColumns())
{
if(firstCol)
{
firstCol = false;
}
else
{
sb.append('\t');
}
final Object cellData = column.getCellData(row);
sb.append(cellData == null ? "" : cellData.toString());
}
}
final ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.putString(sb.toString());
Clipboard.getSystemClipboard().setContent(clipboardContent);
}
});
TableColumn timestampColumn = new TableColumn();
timestampColumn.setPrefWidth(110);
timestampColumn.setText("Time");
timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
TableColumn validColumn = new TableColumn();
validColumn.setPrefWidth(50);
validColumn.setText("Valid");
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
TableColumn messageColumn = new TableColumn();
messageColumn.setPrefWidth(1000);
messageColumn.setText("Message");
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
SimpleStringProperty property = new SimpleStringProperty();
if(param.getValue() instanceof IMessage message)
{
property.set(message.toString());
}
return property;
});
mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
}
return mMessageTableView;
}
/**
* File selection button
* @return button
*/
private Button getSelectFileButton()
{
if(mSelectFileButton == null)
{
mSelectFileButton = new Button("Select ...");
mSelectFileButton.onActionProperty().set(event -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select DMR .bits Recording");
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
if(lastDirectory != null)
{
File file = new File(lastDirectory);
if(file.exists() && file.isDirectory())
{
fileChooser.setInitialDirectory(file);
}
}
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != null)
{
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
load(selected);
}
});
}
return mSelectFileButton;
}
/**
* Spinny loading icon to show over the message table view
*/
private ProgressIndicator getLoadingIndicator()
{
if(mLoadingIndicator == null)
{
mLoadingIndicator = new ProgressIndicator();
mLoadingIndicator.setProgress(-1);
mLoadingIndicator.setVisible(false);
}
return mLoadingIndicator;
}
/**
* Selected file path label.
*/
private Label getSelectedFileLabel()
{
if(mSelectedFileLabel == null)
{
mSelectedFileLabel = new Label(" ");
}
return mSelectedFileLabel;
}
/**
* Search text filter box
* @return text control for entering search text
*/
private TextField getSearchText()
{
if(mSearchText == null)
{
mSearchText = new TextField();
mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mSearchText;
}
/**
* Text box for find text
*/
private TextField getFindText()
{
if(mFindText == null)
{
mFindText = new TextField();
mFindText.setOnKeyPressed(event -> {
if(event.getCode().equals(KeyCode.ENTER))
{
getFindButton().fire();
}
});
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
}
return mFindText;
}
/**
* Find button to search for the text in the find text box.
* @return button
*/
private Button getFindButton()
{
if(mFindButton == null)
{
mFindButton = new Button("Find");
mFindButton.setOnAction(event -> find(getFindText().getText()));
}
return mFindButton;
}
/**
* Find next button to search for the text in the find text box.
* @return button
*/
private Button getFindNextButton()
{
if(mFindNextButton == null)
{
mFindNextButton = new Button("Next");
mFindNextButton.setOnAction(event -> findNext(getFindText().getText()));
}
return mFindNextButton;
}
}

View File

@@ -0,0 +1,180 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.viewer;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Utility application to load and view .bits recording file with the messages fully parsed.
*
* Supported Protocols: DMR and APCO25 Phase 1.
*/
public class RecordingViewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
private MenuBar mMenuBar;
private TabPane mTabPane;
private int mTabCounterDmr = 1;
private int mTabCounterP25P1 = 1;
/**
* Constructs an instance
*/
public RecordingViewer()
{
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
getChildren().addAll(getMenuBar(), getTabPane());
}
public MenuBar getMenuBar()
{
if(mMenuBar == null)
{
mMenuBar = new MenuBar();
Menu fileMenu = new Menu("File");
Menu createNewViewerMenu = new Menu("New Viewer ...");
MenuItem dmrMenuItem = new MenuItem("DMR");
dmrMenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer())));
MenuItem p25p1MenuItem = new MenuItem("P25P1");
p25p1MenuItem.onActionProperty().set(event -> getTabPane().getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer())));
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
MenuItem exitMenu = new MenuItem("Exit");
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
fileMenu.getItems().addAll(createNewViewerMenu, new SeparatorMenuItem(), exitMenu);
mMenuBar.getMenus().add(fileMenu);
}
return mMenuBar;
}
/**
* Tab pane for each viewer instance
*/
public TabPane getTabPane()
{
if(mTabPane == null)
{
mTabPane = new TabPane();
mTabPane.setMaxHeight(Double.MAX_VALUE);
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()));
}
return mTabPane;
}
/**
* Decorates the tab with label renaming feature using double-click or right-click to change text
*/
public class LabeledTab extends Tab
{
private Label mLabel = new Label();
private TextField mTextField = new TextField();
/**
* Constructs an instance
* @param label to use initially
* @param node for content
*/
public LabeledTab(String label, Node node)
{
super(null, node);
mLabel.setText(label);
setGraphic(mLabel);
mLabel.setOnMouseClicked(event -> {
if((event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() > 1) ||
event.getButton().equals(MouseButton.SECONDARY))
{
setGraphic(mTextField);
mTextField.setText(mLabel.getText());
mTextField.selectAll();
mTextField.requestFocus();
}
});
mTextField.setOnAction(event -> {
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
});
mTextField.focusedProperty().addListener((observable, oldValue, newValue) -> {
if(!newValue)
{
mLabel.setText(mTextField.getText());
LabeledTab.this.setGraphic(mLabel);
}
});
}
public LabeledTab(String label)
{
this(label, null);
}
}
public static void main(String[] args)
{
Application viewer = new Application()
{
@Override
public void start(Stage primaryStage) throws Exception
{
Scene scene = new Scene(new RecordingViewer(), 1100, 800);
primaryStage.setTitle("Message Recording Viewer (.bits)");
primaryStage.setScene(scene);
primaryStage.show();
}
};
Runnable r = () -> {
try
{
viewer.start(new Stage());
}
catch(Exception e)
{
mLog.error("Error starting message recording viewer application", e);
}
};
Platform.startup(r);
}
}

View File

@@ -17,17 +17,17 @@
* **************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.gui.dmr; package io.github.dsheirer.gui.viewer;
import io.github.dsheirer.gui.JavaFxWindowRequest; import io.github.dsheirer.gui.JavaFxWindowRequest;
/** /**
* Request to show the DMR recording viewer * Request to show the recording viewer
*/ */
public class ViewDmrRecordingViewerRequest extends JavaFxWindowRequest public class ViewRecordingViewerRequest extends JavaFxWindowRequest
{ {
/** /**
* Constructs an instance * Constructs an instance
*/ */
public ViewDmrRecordingViewerRequest() {} public ViewRecordingViewerRequest() {}
} }

View File

@@ -1,69 +1,161 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/ */
package io.github.dsheirer.identifier.patch; package io.github.dsheirer.identifier.patch;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
/**
* Temporary grouping or regrouping of talkgroups or individual radios.
*/
public class PatchGroup public class PatchGroup
{ {
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroup.class);
private TalkgroupIdentifier mPatchGroupIdentifier; private TalkgroupIdentifier mPatchGroupIdentifier;
private List<TalkgroupIdentifier> mPatchedGroupIdentifiers = new ArrayList<>(); private List<TalkgroupIdentifier> mPatchedTalkgroupIdentifiers = new ArrayList<>();
private List<RadioIdentifier> mPatchedRadioIdentifiers = new ArrayList<>();
private int mVersion;
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier) /**
* Constructs an instance
* @param patchGroupIdentifier for the patch group
* @param version for this patch group identifier. Note: if the patch group identifier is frequently reused, a
* new version number will indicate a new instance of the patch group.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier, int version)
{ {
mPatchGroupIdentifier = patchGroupIdentifier; mPatchGroupIdentifier = patchGroupIdentifier;
mVersion = version;
} }
/**
* Constructs an instance
* @param patchGroupIdentifier that is the overall identifier for this temporary grouping.
*/
public PatchGroup(TalkgroupIdentifier patchGroupIdentifier)
{
this(patchGroupIdentifier, 0);
}
/**
* Version number for this patch group used to differentiate frequently reused patch group identifiers.
* @return version number (aka super group sequence number).
*/
public int getVersion()
{
return mVersion;
}
/**
* Patch group identifier.
* @return identifier
*/
public TalkgroupIdentifier getPatchGroup() public TalkgroupIdentifier getPatchGroup()
{ {
return mPatchGroupIdentifier; return mPatchGroupIdentifier;
} }
public void addPatchedGroup(TalkgroupIdentifier patchedGroupIdentifier) /**
* Indicates if this patch group contains any patched talkgroups.
* @return true if there are patched talkgroups.
*/
public boolean hasPatchedTalkgroups()
{ {
if(!mPatchedGroupIdentifiers.contains(patchedGroupIdentifier)) return !getPatchedTalkgroupIdentifiers().isEmpty();
}
/**
* Indicates if this patch group contains any patched radio identifiers.
* @return true if there are patched radios.
*/
public boolean hasPatchedRadios()
{
return !getPatchedRadioIdentifiers().isEmpty();
}
/**
* Adds the talkgroup identifier to this patched group.
* @param patchedGroupIdentifier to add
*/
public void addPatchedTalkgroup(TalkgroupIdentifier patchedGroupIdentifier)
{
if(!mPatchedTalkgroupIdentifiers.contains(patchedGroupIdentifier))
{ {
mPatchedGroupIdentifiers.add(patchedGroupIdentifier); mPatchedTalkgroupIdentifiers.add(patchedGroupIdentifier);
} }
} }
public void addPatchedGroups(List<TalkgroupIdentifier> patchedGroupIdentifiers) /**
* Adds a list of talkgroup identifiers to this patched group.
* @param patchedGroupIdentifiers to add
*/
public void addPatchedTalkgroups(List<TalkgroupIdentifier> patchedGroupIdentifiers)
{ {
for(TalkgroupIdentifier identifier : patchedGroupIdentifiers) for(TalkgroupIdentifier identifier : patchedGroupIdentifiers)
{ {
addPatchedGroup(identifier); addPatchedTalkgroup(identifier);
} }
} }
public List<TalkgroupIdentifier> getPatchedGroupIdentifiers() /**
* List of patched group identifiers that are part of this patch group.
* @return patched talkgroups.
*/
public List<TalkgroupIdentifier> getPatchedTalkgroupIdentifiers()
{ {
return mPatchedGroupIdentifiers; return mPatchedTalkgroupIdentifiers;
}
/**
* Adds the radio identifier to this patched group.
* @param patchedRadioIdentifier to add
*/
public void addPatchedRadio(RadioIdentifier patchedRadioIdentifier)
{
if(!mPatchedRadioIdentifiers.contains(patchedRadioIdentifier))
{
mPatchedRadioIdentifiers.add(patchedRadioIdentifier);
}
}
/**
* Adds a list of radio identifiers to this patched group.
* @param patchedRadioIdentifiers to add
*/
public void addPatchedRadios(List<RadioIdentifier> patchedRadioIdentifiers)
{
for(RadioIdentifier identifier : patchedRadioIdentifiers)
{
addPatchedRadio(identifier);
}
}
/**
* List of patched radio identifiers that are part of this patch group.
* @return patched radios.
*/
public List<RadioIdentifier> getPatchedRadioIdentifiers()
{
return mPatchedRadioIdentifiers;
} }
@Override @Override
@@ -72,9 +164,14 @@ public class PatchGroup
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("P:").append(getPatchGroup()); sb.append("P:").append(getPatchGroup());
if(!getPatchedGroupIdentifiers().isEmpty()) if(hasPatchedTalkgroups())
{ {
sb.append(" ").append(getPatchedGroupIdentifiers()); sb.append(getPatchedTalkgroupIdentifiers());
}
if(hasPatchedRadios())
{
sb.append(getPatchedRadioIdentifiers());
} }
return sb.toString(); return sb.toString();

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -22,27 +22,23 @@ package io.github.dsheirer.identifier.patch;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role; import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* Manager for (temporary) patch groups and related identifiers. * Manager for (temporary) patch groups aka super groups. This manager monitors patch group additions and deletions and
* maintains a map of the current state of each patch group active in the system.
* *
* Patch groups are primarily used on Motorola P25 systems. This manager monitors patch group additions and deletions * Traffic channel calls will reference a patch group by simply using the patch group identifier. This
* and maintains a map of the current patch groups. * manager will replace that reference with the current state of the full patch group so that the call event has the
* * full patch group including all patched talkgroups or individual radios, which may not have been included in the
* Traffic channel calls will sometimes reference a patch group by simply using the patch group identifier. This * patch group reference on the control or traffic channel.
* manager will replace that reference with the full patch group so that the call event has the full patch group
* including all of the patched groups, which may not have been included in the patch group reference on the
* traffic channel.
*/ */
public class PatchGroupManager public class PatchGroupManager
{ {
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroupManager.class);
private Map<Integer,Long> mPatchGroupLastUpdatedMap = new HashMap<>();
private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>(); private Map<Integer,PatchGroupIdentifier> mPatchGroupMap = new HashMap<>();
/** /**
@@ -58,13 +54,13 @@ public class PatchGroupManager
public void clear() public void clear()
{ {
mPatchGroupMap.clear(); mPatchGroupMap.clear();
mPatchGroupLastUpdatedMap.clear();
} }
/** /**
* Adds the patch group identifier to the set of managed patch groups. If the patch group is already being * Adds the patch group identifier to the set of managed patch groups. If the patch group is already being
* managed, the new identifier is checked for additional patched talkgroups and they are added to the existing * managed, the new patch group argument is checked for additional patched talkgroups and they are added to the
* managed patch group. * existing managed patch group. Where the patch group version (super group sequence number) is different, the
* existing patch group is discarded and replaced with the updated version of the patch group.
* *
* @param patchGroupIdentifier to add or update * @param patchGroupIdentifier to add or update
*/ */
@@ -76,8 +72,19 @@ public class PatchGroupManager
{ {
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue())) if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
{ {
PatchGroup current = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue(); PatchGroup existingPatchGroup = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
current.addPatchedGroups(update.getPatchedGroupIdentifiers());
//If the patch group version number is the same, this is an update
if(existingPatchGroup.getVersion() == update.getVersion())
{
existingPatchGroup.addPatchedTalkgroups(update.getPatchedTalkgroupIdentifiers());
existingPatchGroup.addPatchedRadios(update.getPatchedRadioIdentifiers());
}
//Otherwise, this is a replace operation.
else
{
mPatchGroupMap.put(update.getPatchGroup().getValue(), patchGroupIdentifier);
}
} }
else else
{ {
@@ -109,7 +116,6 @@ public class PatchGroupManager
{ {
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue(); int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
mPatchGroupMap.remove(id); mPatchGroupMap.remove(id);
mPatchGroupLastUpdatedMap.remove(id);
} }
/** /**
@@ -153,23 +159,28 @@ public class PatchGroupManager
*/ */
public Identifier update(Identifier identifier) public Identifier update(Identifier identifier)
{ {
if(identifier == null) if(identifier != null && identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
{
return identifier;
}
if(identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
{ {
switch(identifier.getForm()) switch(identifier.getForm())
{ {
case PATCH_GROUP: case TALKGROUP:
if(identifier instanceof PatchGroupIdentifier) if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
Identifier mapValue = mPatchGroupMap.get(talkgroupIdentifier.getValue());
if (mapValue != null)
{
return mapValue;
}
}
break;
case PATCH_GROUP:
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{ {
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue(); int patchGroupId = patchGroupIdentifier.getValue().getPatchGroup().getValue();
Identifier mapValue = mPatchGroupMap.get(patchGroupId); Identifier mapValue = mPatchGroupMap.get(patchGroupId);
if (mapValue != null) { if (mapValue != null)
{
return mapValue; return mapValue;
} }
} }

View File

@@ -145,6 +145,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
{ {
BW_3_0("3.0 kHz", 3000.0), BW_3_0("3.0 kHz", 3000.0),
BW_5_0("5.0 kHz", 5000.0), BW_5_0("5.0 kHz", 5000.0),
BW_7_5("7.5 kHz", 7500.0),
BW_8_33("8.33 kHz", 8333.0), BW_8_33("8.33 kHz", 8333.0),
BW_12_5("12.5 kHz", 12500.0), BW_12_5("12.5 kHz", 12500.0),
BW_15_0("15.0 kHz", 15000.0), BW_15_0("15.0 kHz", 15000.0),
@@ -168,7 +169,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
public static EnumSet<Bandwidth> AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0); public static EnumSet<Bandwidth> AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0);
//FM demodulator channel bandwidth options //FM demodulator channel bandwidth options
public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_12_5, BW_25_0); public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_7_5, BW_12_5, BW_25_0);
/** /**
* Indicates if this entry is valid for the AM decoder. * Indicates if this entry is valid for the AM decoder.

View File

@@ -424,18 +424,19 @@ public class DMRDecoderState extends TimeslotDecoderState
broadcast(packetEvent); broadcast(packetEvent);
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket()); }
if (geoPosition != null) { GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
.channel(getCurrentChannel())
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
.protocol(Protocol.LRRP)
.location(geoPosition)
.build();
broadcast(plottableDecodeEvent); if (geoPosition != null) {
} PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
.channel(getCurrentChannel())
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
.protocol(Protocol.LRRP)
.location(geoPosition)
.build();
broadcast(plottableDecodeEvent);
} }
} }
@@ -1209,19 +1210,17 @@ public class DMRDecoderState extends TimeslotDecoderState
} }
break; break;
case FULL_STANDARD_GPS_INFO: case FULL_STANDARD_GPS_INFO:
if(message instanceof GPSInformation) if(message instanceof GPSInformation gpsInformation)
{ {
GPSInformation gpsInformation = (GPSInformation)message; PlottableDecodeEvent plottableGPS = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, message.getTimestamp())
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); .channel(getCurrentChannel())
ic.update(gpsInformation.getGPSLocation()); .details("LOCATION:" + gpsInformation.getGPSLocation())
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
.protocol(Protocol.DMR)
.location(gpsInformation.getPosition())
.build();
DecodeEvent gpsEvent = DMRDecodeEvent.builder(DecodeEventType.GPS, message.getTimestamp()) broadcast(plottableGPS);
.identifiers(ic)
.timeslot(getTimeslot())
.details("LOCATION:" + gpsInformation.getGPSLocation())
.build();
broadcast(gpsEvent);
} }
break; break;
case FULL_STANDARD_TALKER_ALIAS_COMPLETE: case FULL_STANDARD_TALKER_ALIAS_COMPLETE:

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -38,7 +38,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator; import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage; import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.UserPreferences;
@@ -105,63 +104,54 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
{ {
if(hasAudioCodec() && message.getTimeslot() == getTimeslot()) if(hasAudioCodec() && message.getTimeslot() == getTimeslot())
{ {
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted. //Attempt to set the audio encryption state from certain types of messages
if(!mEncryptedCallStateEstablished && message instanceof VoiceMessage vm && if(!mEncryptedCallStateEstablished)
vm.getSyncPattern().isDirectMode())
{ {
mEncryptedCallStateEstablished = true; //DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
mEncryptedCall = false; if(message instanceof VoiceMessage vm && vm.getSyncPattern().isDirectMode())
List<byte[]> frames = vm.getAMBEFrames();
for(byte[] frame: frames)
{ {
processAudio(frame, message.getTimestamp()); mEncryptedCallStateEstablished = true;
mEncryptedCall = false;
} }
} //Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames //in the EMB field.
//in the EMB field. else if(message instanceof VoiceEMBMessage voiceMessage && voiceMessage.getEMB().isValid())
else if(!mEncryptedCallStateEstablished && message instanceof VoiceEMBMessage voiceMessage)
{
if(voiceMessage.getEMB().isValid())
{ {
mEncryptedCallStateEstablished = true; mEncryptedCallStateEstablished = true;
mEncryptedCall = voiceMessage.getEMB().isEncrypted(); mEncryptedCall = voiceMessage.getEMB().isEncrypted();
} }
else if(message instanceof VoiceHeader voiceHeader)
{
LCMessage lc = voiceHeader.getLCMessage();
List<byte[]> frames = voiceMessage.getAMBEFrames(); if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
for(byte[] frame: frames)
{
processAudio(frame, message.getTimestamp());
}
}
else if(message instanceof VoiceMessage voiceMessage)
{
List<byte[]> frames = voiceMessage.getAMBEFrames();
for(byte[] frame: frames)
{
processAudio(frame, message.getTimestamp());
}
}
else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader voiceHeader)
{
LCMessage lc = voiceHeader.getLCMessage();
if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
{
if(flc.isValid())
{ {
mEncryptedCallStateEstablished = true; if(flc.isValid())
mEncryptedCall = flc.isEncrypted(); {
mEncryptedCallStateEstablished = true;
mEncryptedCall = flc.isEncrypted();
}
} }
} }
//Note: the DMRMessageProcessor extracts Full Link Control messages from Voice Frames B-C and sends them
// independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user.
else if(message instanceof AbstractVoiceChannelUser avcu)
{
mEncryptedCallStateEstablished = true;
mEncryptedCall = avcu.getServiceOptions().isEncrypted();
}
} }
//Note: the DMRMessageProcess extracts Full Link Control messages from Voice Frames B-C and sends them
// independent of any DMR Burst messaging. When encountered, it can be assumed that they are part of //Queue or process audio frames. Note: audio frames are held/queued until encrypted state is established
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user. //before any audio is generated for the audio segment.
else if(!mEncryptedCallStateEstablished && message instanceof AbstractVoiceChannelUser avcu) if(message instanceof VoiceMessage voiceMessage)
{ {
ServiceOptions serviceOptions = avcu.getServiceOptions(); List<byte[]> frames = voiceMessage.getAMBEFrames();
mEncryptedCallStateEstablished = true; for(byte[] frame: frames)
mEncryptedCall = serviceOptions.isEncrypted(); {
processAudio(frame, message.getTimestamp());
}
} }
else if(message instanceof Terminator) else if(message instanceof Terminator)
{ {

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,10 +24,10 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.location.LocationIdentifier; import io.github.dsheirer.identifier.location.LocationIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRLocation; import io.github.dsheirer.module.decode.dmr.identifier.DMRLocation;
import io.github.dsheirer.module.decode.dmr.message.type.PositionError; import io.github.dsheirer.module.decode.dmr.message.type.PositionError;
import org.apache.commons.math3.util.FastMath;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.jdesktop.swingx.mapviewer.GeoPosition;
/** /**
* GPS Information * GPS Information
@@ -81,16 +81,30 @@ public class GPSInformation extends FullLCMessage
return PositionError.fromValue(getMessage().getInt(POSITION_ERROR)); return PositionError.fromValue(getMessage().getInt(POSITION_ERROR));
} }
/**
* Latitude in decimal degrees
*/
public double getLatitude() public double getLatitude()
{ {
return getMessage().getTwosComplement(LATITUDE_START, LATITUDE_END) * LATITUDE_UNITS; return getMessage().getTwosComplement(LATITUDE_START, LATITUDE_END) * LATITUDE_UNITS;
} }
/**
* Longitude in decimal degrees
*/
public double getLongitude() public double getLongitude()
{ {
return getMessage().getTwosComplement(LONGITUDE_START, LONGITUDE_END) * LONGITUDE_UNITS; return getMessage().getTwosComplement(LONGITUDE_START, LONGITUDE_END) * LONGITUDE_UNITS;
} }
/**
* Geo-position for the lat/lon
*/
public GeoPosition getPosition()
{
return new GeoPosition(getLatitude(), getLongitude());
}
/** /**
* GPS location as an identifier * GPS location as an identifier
*/ */

View File

@@ -21,13 +21,13 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider; import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.type.CapacityPlusServiceOptions; import io.github.dsheirer.module.decode.dmr.message.type.CapacityPlusServiceOptions;
/** /**
* Any Capacity Plus Voice Channel User link control message that contains vendor-specific service options. * Any Capacity Plus Voice Channel User link control message that contains vendor-specific service options.
*/ */
public abstract class CapacityPlusVoiceChannelUser extends FullLCMessage implements IServiceOptionsProvider public abstract class CapacityPlusVoiceChannelUser extends AbstractVoiceChannelUser implements IServiceOptionsProvider
{ {
private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23}; private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
//Reed Solomon FEC: 72-95 //Reed Solomon FEC: 72-95

View File

@@ -59,6 +59,8 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
{ {
switch(getBandwidth()) switch(getBandwidth())
{ {
case BW_7_5:
return new ChannelSpecification(25000.0, 7500, 3500.0, 3750.0);
case BW_12_5: case BW_12_5:
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0); return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
case BW_25_0: case BW_25_0:

View File

@@ -57,6 +57,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage; import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HDUMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData; import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCCallTermination; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCCallTermination;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommand; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCMessageUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCMessageUpdate;
@@ -94,6 +95,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.umbtc.isp.UMBTCTe
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.Opcode; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.Opcode;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.HarrisGroupRegroupExplicitEncryptionCommand;
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.MotorolaDenyResponse;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.PatchGroupVoiceChannelGrant; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.PatchGroupVoiceChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.PatchGroupVoiceChannelGrantUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.PatchGroupVoiceChannelGrantUpdate;
@@ -1221,6 +1223,21 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
mPatchGroupManager.removePatchGroups(tsbk.getIdentifiers()); mPatchGroupManager.removePatchGroups(tsbk.getIdentifiers());
break; break;
//L3HARRIS PATCH GROUP OPCODES
case HARRIS_OSP_GRG_EXENC_CMD:
if(tsbk instanceof HarrisGroupRegroupExplicitEncryptionCommand regroup)
{
if(regroup.getRegroupOptions().isActivate())
{
mPatchGroupManager.addPatchGroup(regroup.getPatchGroup());
}
else
{
mPatchGroupManager.removePatchGroup(regroup.getPatchGroup());
}
}
break;
//STANDARD - INBOUND OPCODES //STANDARD - INBOUND OPCODES
case ISP_GROUP_VOICE_SERVICE_REQUEST: case ISP_GROUP_VOICE_SERVICE_REQUEST:
if(tsbk instanceof GroupVoiceServiceRequest) if(tsbk instanceof GroupVoiceServiceRequest)
@@ -1885,6 +1902,22 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
case UNIT_TO_UNIT_ANSWER_REQUEST: case UNIT_TO_UNIT_ANSWER_REQUEST:
processBroadcast(lcw.getIdentifiers(), timestamp, DecodeEventType.PAGE, "Unit-to-Unit Answer Request"); processBroadcast(lcw.getIdentifiers(), timestamp, DecodeEventType.PAGE, "Unit-to-Unit Answer Request");
break; break;
case MOTOROLA_UNIT_GPS:
if(lcw instanceof LCMotorolaUnitGPS moto)
{
String gpsDetails = "LOCATION: " + moto.getLatitude() + " " + moto.getLongitude();
PlottableDecodeEvent event = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, timestamp)
.location(new GeoPosition(moto.getLatitude(), moto.getLongitude()))
.channel(getCurrentChannel())
.details(gpsDetails)
.end(timestamp)
.protocol(Protocol.APCO25)
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
.build();
broadcast(event);
}
break;
default: default:
// mLog.debug("Unrecognized LCW Opcode: " + lcw.getOpcode().name() + " VENDOR:" + lcw.getVendor() + // mLog.debug("Unrecognized LCW Opcode: " + lcw.getOpcode().name() + " VENDOR:" + lcw.getVendor() +
// " OPCODE:" + lcw.getOpcodeNumber()); // " OPCODE:" + lcw.getOpcodeNumber());

View File

@@ -42,6 +42,7 @@ public class TerminatorMessageFilterSet extends FilterSet<IMessage>
addFilter(new TerminatorMessageFilter("Command/Status", LinkControlOpcode.COMMAND_STATUS_OPCODES)); addFilter(new TerminatorMessageFilter("Command/Status", LinkControlOpcode.COMMAND_STATUS_OPCODES));
addFilter(new TerminatorMessageFilter("Network/Channel", LinkControlOpcode.NETWORK_OPCODES)); addFilter(new TerminatorMessageFilter("Network/Channel", LinkControlOpcode.NETWORK_OPCODES));
addFilter(new TerminatorMessageFilter("Voice", LinkControlOpcode.VOICE_OPCODES)); addFilter(new TerminatorMessageFilter("Voice", LinkControlOpcode.VOICE_OPCODES));
addFilter(new TerminatorMessageFilter("L3Harris", LinkControlOpcode.L3HARRIS_OPCODES));
addFilter(new TerminatorMessageFilter("Motorola", LinkControlOpcode.MOTOROLA_OPCODES)); addFilter(new TerminatorMessageFilter("Motorola", LinkControlOpcode.MOTOROLA_OPCODES));
List<LinkControlOpcode> others = new ArrayList<>(); List<LinkControlOpcode> others = new ArrayList<>();

View File

@@ -96,8 +96,14 @@ public enum LinkControlOpcode
MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE("PATCH GROUP VOICE CHANNEL UPDATE", 1), MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE("PATCH GROUP VOICE CHANNEL UPDATE", 1),
MOTOROLA_PATCH_GROUP_ADD("PATCH GROUP ADD", 3), MOTOROLA_PATCH_GROUP_ADD("PATCH GROUP ADD", 3),
MOTOROLA_PATCH_GROUP_DELETE("PATCH GROUP DELETE", 4), MOTOROLA_PATCH_GROUP_DELETE("PATCH GROUP DELETE", 4),
MOTOROLA_UNIT_GPS("UNIT GPS", 6),
MOTOROLA_TALK_COMPLETE("TALK_COMPLETE", 15), MOTOROLA_TALK_COMPLETE("TALK_COMPLETE", 15),
MOTOROLA_UNKNOWN("UNKNOWN", -1), MOTOROLA_UNKNOWN("MOTOROLA UNKNOWN", -1),
L3HARRIS_UNKNOWN_0A("UNKNOWN OPCODE 10", 10),
L3HARRIS_UNKNOWN_2A("UNKNOWN OPCODE 42", 42),
L3HARRIS_UNKNOWN_2B("UNKNOWN OPCODE 43", 43),
L3HARRIS_UNKNOWN("L3HARRIS UNKNOWN", -1),
UNKNOWN("UNKNOWN", -1); UNKNOWN("UNKNOWN", -1);
@@ -129,6 +135,11 @@ public enum LinkControlOpcode
public static final EnumSet<LinkControlOpcode> MOTOROLA_OPCODES = public static final EnumSet<LinkControlOpcode> MOTOROLA_OPCODES =
EnumSet.range(MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER, MOTOROLA_UNKNOWN); EnumSet.range(MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_USER, MOTOROLA_UNKNOWN);
/**
* L3Harris Opcodes
*/
public static final EnumSet<LinkControlOpcode> L3HARRIS_OPCODES = EnumSet.of(L3HARRIS_UNKNOWN_0A,
L3HARRIS_UNKNOWN_2A, L3HARRIS_UNKNOWN_2B, L3HARRIS_UNKNOWN);
/** /**
* Network/channel related opcodes * Network/channel related opcodes
@@ -190,6 +201,18 @@ public enum LinkControlOpcode
return values()[value]; return values()[value];
} }
break; break;
case HARRIS:
switch(value)
{
case 10:
return L3HARRIS_UNKNOWN_0A;
case 42:
return L3HARRIS_UNKNOWN_2A;
case 43:
return L3HARRIS_UNKNOWN_2B;
default:
return L3HARRIS_UNKNOWN;
}
case MOTOROLA: case MOTOROLA:
switch(value) switch(value)
{ {
@@ -201,16 +224,18 @@ public enum LinkControlOpcode
return MOTOROLA_PATCH_GROUP_ADD; return MOTOROLA_PATCH_GROUP_ADD;
case 4: case 4:
return MOTOROLA_PATCH_GROUP_DELETE; return MOTOROLA_PATCH_GROUP_DELETE;
case 6:
return MOTOROLA_UNIT_GPS;
case 15: case 15:
return MOTOROLA_TALK_COMPLETE; return MOTOROLA_TALK_COMPLETE;
default: default:
return MOTOROLA_UNKNOWN; return MOTOROLA_UNKNOWN;
} }
default: }
if(0 <= value && value <= 63)
{ if(0 <= value && value <= 63)
return values()[value]; {
} return values()[value];
} }
return UNKNOWN; return UNKNOWN;

View File

@@ -1,7 +1,6 @@
/* /*
* ****************************************************************************** * *****************************************************************************
* sdrtrunk * Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2019 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -15,17 +14,21 @@
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
* ***************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.module.decode.p25.phase1.message.lc; package io.github.dsheirer.module.decode.p25.phase1.message.lc;
import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode10;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode42;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode43;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupAdd; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupAdd;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupDelete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupDelete;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUser; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaPatchGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnknownOpcode; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnknownOpcode;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcast; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcast;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcastExplicit; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcastExplicit;
@@ -128,6 +131,15 @@ public class LinkControlWordFactory
case UNIT_TO_UNIT_VOICE_CHANNEL_USER: case UNIT_TO_UNIT_VOICE_CHANNEL_USER:
return new LCUnitToUnitVoiceChannelUser(binaryMessage); return new LCUnitToUnitVoiceChannelUser(binaryMessage);
case L3HARRIS_UNKNOWN_0A:
return new LCHarrisUnknownOpcode10(binaryMessage);
case L3HARRIS_UNKNOWN_2A:
return new LCHarrisUnknownOpcode42(binaryMessage);
case L3HARRIS_UNKNOWN_2B:
return new LCHarrisUnknownOpcode43(binaryMessage);
case L3HARRIS_UNKNOWN:
return new UnknownLinkControlWord(binaryMessage);
case MOTOROLA_PATCH_GROUP_ADD: case MOTOROLA_PATCH_GROUP_ADD:
return new LCMotorolaPatchGroupAdd(binaryMessage); return new LCMotorolaPatchGroupAdd(binaryMessage);
case MOTOROLA_PATCH_GROUP_DELETE: case MOTOROLA_PATCH_GROUP_DELETE:
@@ -138,6 +150,8 @@ public class LinkControlWordFactory
return new LCMotorolaTalkComplete(binaryMessage); return new LCMotorolaTalkComplete(binaryMessage);
case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE: case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE:
return new LCMotorolaPatchGroupVoiceChannelUpdate(binaryMessage); return new LCMotorolaPatchGroupVoiceChannelUpdate(binaryMessage);
case MOTOROLA_UNIT_GPS:
return new LCMotorolaUnitGPS(binaryMessage);
case MOTOROLA_UNKNOWN: case MOTOROLA_UNKNOWN:
return new LCMotorolaUnknownOpcode(binaryMessage); return new LCMotorolaUnknownOpcode(binaryMessage);

View File

@@ -0,0 +1,128 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.lc.l3harris;
import io.github.dsheirer.bits.BinaryMessage;
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.phase1.message.lc.LinkControlWord;
import java.util.ArrayList;
import java.util.List;
/**
* L3Harris Unknown Opcode 10 (0x0A)
* <p>
* Observed in a traffic channel carried by a TDU during an SNDCP packet data session. The controller send an 'All
* blocks received' PDU to the radio in the middle of a stream of these TDU/LC messages and the traffic channel ended
* with Call Termination TDULC messages. So, this may be some form of current data session user information update.
*/
public class LCHarrisUnknownOpcode10 extends LinkControlWord
{
private static final int[] UNKNOWN_1 = {16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] TARGET_RADIO = {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[] SOURCE_RADIO = {48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65,
66, 67, 68, 69, 70, 71};
private RadioIdentifier mSourceRadio;
private RadioIdentifier mTargetRadio;
private List<Identifier> mIdentifiers;
/**
* Constructs a Link Control Word from the binary message sequence.
*
* @param message
*/
public LCHarrisUnknownOpcode10(BinaryMessage message)
{
super(message);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("**CRC-FAILED** ");
}
if(isEncrypted())
{
sb.append(" ENCRYPTED");
}
else
{
sb.append("L3HARRIS UNKNOWN OPCODE 10 ");
sb.append(" FM:").append(getSourceRadio());
sb.append(" TO:").append(getTargetRadio());
sb.append(" UNK:").append(getUnknown());
sb.append(" MSG:").append(getMessage().toHexString());
}
return sb.toString();
}
public String getUnknown()
{
return getMessage().getHex(UNKNOWN_1, 2);
}
/**
* Source radio identifier
*/
public RadioIdentifier getSourceRadio()
{
if(mSourceRadio == null)
{
mSourceRadio = APCO25RadioIdentifier.createFrom(getMessage().getInt(SOURCE_RADIO));
}
return mSourceRadio;
}
/**
* Target radio identifier
*/
public RadioIdentifier getTargetRadio()
{
if(mTargetRadio == null)
{
mTargetRadio = APCO25RadioIdentifier.createTo(getMessage().getInt(TARGET_RADIO));
}
return mTargetRadio;
}
/**
* List of identifiers contained in this message
*/
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getSourceRadio());
}
return mIdentifiers;
}
}

View File

@@ -0,0 +1,74 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.lc.l3harris;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import java.util.Collections;
import java.util.List;
/**
* L3Harris Unknown Opcode 42 (0x2A)
* <p>
* Observed in a voice traffic channel carried by an LDU1. This was transmitted once followed by an Opcode 43 (0x2B)
* which is also unknown.
*/
public class LCHarrisUnknownOpcode42 extends LinkControlWord
{
/**
* Constructs a Link Control Word from the binary message sequence.
*
* @param message
*/
public LCHarrisUnknownOpcode42(BinaryMessage message)
{
super(message);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("**CRC-FAILED** ");
}
if(isEncrypted())
{
sb.append(" ENCRYPTED");
}
else
{
sb.append("L3HARRIS UNKNOWN OPCODE 42 MSG:").append(getMessage().toHexString());
}
return sb.toString();
}
/**
* List of identifiers contained in this message
*/
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,74 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.lc.l3harris;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import java.util.Collections;
import java.util.List;
/**
* L3Harris Unknown Opcode 43 (0x2B)
* <p>
* Observed in a voice traffic channel carried by an LDU1. This was transmitted once following an Opcode 42 (0x2A)
* which is also unknown.
*/
public class LCHarrisUnknownOpcode43 extends LinkControlWord
{
/**
* Constructs a Link Control Word from the binary message sequence.
*
* @param message
*/
public LCHarrisUnknownOpcode43(BinaryMessage message)
{
super(message);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("**CRC-FAILED** ");
}
if(isEncrypted())
{
sb.append(" ENCRYPTED");
}
else
{
sb.append("L3HARRIS UNKNOWN OPCODE 43 MSG:").append(getMessage().toHexString());
}
return sb.toString();
}
/**
* List of identifiers contained in this message
*/
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -66,7 +66,7 @@ public class LCMotorolaPatchGroupAdd extends MotorolaLinkControlWord
if(mPatchGroup == null) if(mPatchGroup == null)
{ {
PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getMessage().getInt(PATCH_GROUP))); PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getMessage().getInt(PATCH_GROUP)));
patchGroup.addPatchedGroups(getPatchedGroups()); patchGroup.addPatchedTalkgroups(getPatchedGroups());
mPatchGroup = APCO25PatchGroup.create(patchGroup); mPatchGroup = APCO25PatchGroup.create(patchGroup);
} }

View File

@@ -66,7 +66,7 @@ public class LCMotorolaPatchGroupDelete extends MotorolaLinkControlWord
if(mPatchGroup == null) if(mPatchGroup == null)
{ {
PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getMessage().getInt(PATCH_GROUP))); PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getMessage().getInt(PATCH_GROUP)));
patchGroup.addPatchedGroups(getPatchedGroups()); patchGroup.addPatchedTalkgroups(getPatchedGroups());
mPatchGroup = APCO25PatchGroup.create(patchGroup); mPatchGroup = APCO25PatchGroup.create(patchGroup);
} }

View File

@@ -0,0 +1,84 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.lc.motorola;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import java.util.Collections;
import java.util.List;
/**
* Motorola Unit Self-Reported GPS Location
*/
public class LCMotorolaUnitGPS extends MotorolaLinkControlWord
{
private static final double LATITUDE_MULTIPLIER = 90.0 / 0x7FFFFF;
private static final double LONGITUDE_MULTIPLIER = 180.0 / 0x7FFFFF;
private static final int[] UNKNOWN = {16, 17, 18, 19, 20, 21, 22, 23};
private static final int LATITUDE_SIGN = 24;
private static final int[] LATITUDE = {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 LONGITUDE_SIGN = 48;
private static final int[] LONGITUDE = {49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
/**
* Constructs an instance
* @param message binary
*/
public LCMotorolaUnitGPS(BinaryMessage message)
{
super(message);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("MOTOROLA UNIT GPS LOCATION: ").append(getLatitude()).append(" ").append(getLongitude());
return sb.toString();
}
/**
* GPS Latitude value.
* @return value in decimal degrees
*/
public double getLatitude()
{
return getMessage().getInt(LATITUDE) * LATITUDE_MULTIPLIER * (getMessage().get(LATITUDE_SIGN) ? -1 : 1);
}
/**
* GPS Longitude value.
* @return value in decimal degrees
*/
public double getLongitude()
{
return getMessage().getInt(LONGITUDE) * LONGITUDE_MULTIPLIER * (getMessage().get(LONGITUDE_SIGN) ? -1 : 1);
}
/**
* List of identifiers contained in this message
*/
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -1,7 +1,6 @@
/* /*
* ****************************************************************************** * *****************************************************************************
* sdrtrunk * Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2019 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
* ***************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp; package io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp;
@@ -29,7 +28,6 @@ import io.github.dsheirer.module.decode.p25.reference.NetworkAddressType;
import io.github.dsheirer.module.decode.p25.reference.PDUPriorityMaximumType; import io.github.dsheirer.module.decode.p25.reference.PDUPriorityMaximumType;
import io.github.dsheirer.module.decode.p25.reference.ReadyTimer; import io.github.dsheirer.module.decode.p25.reference.ReadyTimer;
import io.github.dsheirer.module.decode.p25.reference.StandbyTimer; import io.github.dsheirer.module.decode.p25.reference.StandbyTimer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -81,7 +79,7 @@ public class ActivateTdsContextAccept extends SNDCPMessage
} }
sb.append(" READY TIMER:").append(getReadyTimer().getSeconds()).append("s"); sb.append(" READY TIMER:").append(getReadyTimer().getSeconds()).append("s");
sb.append(" STANDBY TIMER:").append(getStandbyTimer().getSeconds()).append("s"); sb.append(" STANDBY TIMER:").append(getStandbyTimer().getSeconds()).append("s");
sb.append(" MTU:").append(getMaximumTransmitUnit().getByteCount()).append("B"); sb.append(" MTU:").append(getMaximumTransmitUnit().getByteCount());
sb.append(" AUTHORIZED GROUPS:").append(getDataAccessGroups()); sb.append(" AUTHORIZED GROUPS:").append(getDataAccessGroups());
return sb.toString(); return sb.toString();
} }

View File

@@ -176,11 +176,11 @@ public enum Opcode
MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN(14, "CCH PLND SHUTDWN", "CONTROL CHANNEL PLANNED SHUTDOWN"), MOTOROLA_OSP_CONTROL_CHANNEL_PLANNED_SHUTDOWN(14, "CCH PLND SHUTDWN", "CONTROL CHANNEL PLANNED SHUTDOWN"),
MOTOROLA_OSP_UNKNOWN(-1, "MOTOROLA OSP UNKNOWN OPCODE", "MOTOROLA OSP UNKNOWN OPCODE"), MOTOROLA_OSP_UNKNOWN(-1, "MOTOROLA OSP UNKNOWN OPCODE", "MOTOROLA OSP UNKNOWN OPCODE"),
//Vendor: motorola, Inbound Service Packet (ISP) //Vendor: L3Harris, Inbound Service Packet (ISP)
HARRIS_ISP_UNKNOWN(-1, "HARRIS ISP UNKNOWN OPCODE", "HARRIS ISP UNKNOWN OPCODE"), HARRIS_ISP_UNKNOWN(-1, "HARRIS ISP UNKNOWN OPCODE", "HARRIS ISP UNKNOWN OPCODE"),
//Vendor: harris, Outbound Service Packet (OSP) //Vendor: L3Harris, Outbound Service Packet (OSP)
HARRIS_OSP_TDMA_SYNC(48, "HARRIS TDMA SYNC", "HARRIS TDMA SYNC BROADCAST"), HARRIS_OSP_GRG_EXENC_CMD(48, "HARRIS REGROUP", "HARRIS GROUP REGROUP COMMAND WITH EXPLICIT ENCRYPTION"),
HARRIS_OSP_UNKNOWN(-1, "HARRIS OSP UNKNOWN OPCODE", "HARRIS OSP UNKNOWN OPCODE"), HARRIS_OSP_UNKNOWN(-1, "HARRIS OSP UNKNOWN OPCODE", "HARRIS OSP UNKNOWN OPCODE"),
//Vendor: unknown, Inbound Service Packet (ISP) //Vendor: unknown, Inbound Service Packet (ISP)
@@ -294,7 +294,7 @@ public enum Opcode
/** /**
* Harris opcodes * Harris opcodes
*/ */
public static final EnumSet<Opcode> HARRIS = EnumSet.of(HARRIS_ISP_UNKNOWN, HARRIS_OSP_TDMA_SYNC, public static final EnumSet<Opcode> HARRIS = EnumSet.of(HARRIS_ISP_UNKNOWN, HARRIS_OSP_GRG_EXENC_CMD,
HARRIS_OSP_UNKNOWN); HARRIS_OSP_UNKNOWN);
/** /**
@@ -425,7 +425,7 @@ public enum Opcode
switch(value) switch(value)
{ {
case 48: case 48:
return HARRIS_OSP_TDMA_SYNC; return HARRIS_OSP_GRG_EXENC_CMD;
default: default:
return HARRIS_OSP_UNKNOWN; return HARRIS_OSP_UNKNOWN;
} }

View File

@@ -27,7 +27,7 @@ import io.github.dsheirer.edac.trellis.ViterbiDecoder_1_2_P25;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID; import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.P25P1Interleave; import io.github.dsheirer.module.decode.p25.phase1.P25P1Interleave;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.isp.UnknownHarrisISPMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.isp.UnknownHarrisISPMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.HarrisTDMASyncBroadcast; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.HarrisGroupRegroupExplicitEncryptionCommand;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.UnknownHarrisOSPMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp.UnknownHarrisOSPMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.isp.UnknownMotorolaISPMessage; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.isp.UnknownMotorolaISPMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.ChannelLoading; import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.ChannelLoading;
@@ -298,8 +298,8 @@ public class TSBKMessageFactory
case HARRIS_ISP_UNKNOWN: case HARRIS_ISP_UNKNOWN:
return new UnknownHarrisISPMessage(dataUnitID, message, nac, timestamp); return new UnknownHarrisISPMessage(dataUnitID, message, nac, timestamp);
case HARRIS_OSP_TDMA_SYNC: case HARRIS_OSP_GRG_EXENC_CMD:
return new HarrisTDMASyncBroadcast(dataUnitID, message, nac, timestamp); return new HarrisGroupRegroupExplicitEncryptionCommand(dataUnitID, message, nac, timestamp);
case HARRIS_OSP_UNKNOWN: case HARRIS_OSP_UNKNOWN:
return new UnknownHarrisOSPMessage(dataUnitID, message, nac, timestamp); return new UnknownHarrisOSPMessage(dataUnitID, message, nac, timestamp);

View File

@@ -0,0 +1,200 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.harris.osp;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.patch.APCO25PatchGroup;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.OSPMessage;
import io.github.dsheirer.module.decode.p25.reference.RegroupOptions;
import java.util.ArrayList;
import java.util.List;
/**
* Harris Group Regroup Action and Explicit Encryption Command.
* <p>
* This OSP is used to create, remove or update a Supergroup address associated with one member WGID or WUID with
* explicit encryption parameters.
*
* See: https://forums.radioreference.com/threads/duke-energy-p25-system.411183/page-28#post-3908078
*/
public class HarrisGroupRegroupExplicitEncryptionCommand extends OSPMessage
{
private static final int[] REGROUP_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] SUPERGROUP_ADDRESS = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] ENCRYPTION_KEY_ID = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] TARGET_ADDRESS = 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};
private TalkgroupIdentifier mSuperGroupIdentifier;
private APCO25PatchGroup mPatchGroup;
private Identifier mPatchedIdentifier;
private List<Identifier> mIdentifiers;
private RegroupOptions mRegroupOptions;
/**
* Constructs a TSBK from the binary message sequence.
*/
public HarrisGroupRegroupExplicitEncryptionCommand(P25P1DataUnitID dataUnitId, CorrectedBinaryMessage message, int nac, long timestamp)
{
super(dataUnitId, message, nac, timestamp);
}
/**
* Options for this regrouping that indicate the regrouping actions and targets.
* @return regroup options
*/
public RegroupOptions getRegroupOptions()
{
if(mRegroupOptions == null)
{
mRegroupOptions = new RegroupOptions(getMessage().getInt(REGROUP_OPTIONS));
}
return mRegroupOptions;
}
/**
* Encryption key ID to use for the patch group. Note: this only applies for patched talkgroups and the value is
* zero if the target address is a radio identifier.
* @return encryption key identifier
*/
public int getEncryptionKeyId()
{
return getMessage().getInt(ENCRYPTION_KEY_ID);
}
/**
* Indicates if there is a specified encryption key to use for the patch or simulselect communications.
* @return true if key is specified.
*/
public boolean hasEncryptionKeyId()
{
return getEncryptionKeyId() > 0;
}
/**
* APCO25 Patch Group or Simul-Select (e.g. Harris) loaded with the target address specified in this message.
* @return patch group
*/
public APCO25PatchGroup getPatchGroup()
{
if(mPatchGroup == null)
{
PatchGroup patchGroup = new PatchGroup(getSuperGroup(), getRegroupOptions().getSupergroupSequenceNumber());
if(getTargetAddress() instanceof APCO25Talkgroup talkgroup)
{
patchGroup.addPatchedTalkgroup(talkgroup);
}
else if(getTargetAddress() instanceof APCO25RadioIdentifier radio)
{
patchGroup.addPatchedRadio(radio);
}
mPatchGroup = APCO25PatchGroup.create(patchGroup);
}
return mPatchGroup;
}
/**
* Target address for the regrouping action.
* @return target talkgroup or radio address.
*/
public Identifier getTargetAddress()
{
if(mPatchedIdentifier == null)
{
if(getRegroupOptions().isTalkgroupAddress())
{
mPatchedIdentifier = APCO25Talkgroup.create(getMessage().getInt(TARGET_ADDRESS));
}
else
{
mPatchedIdentifier = APCO25RadioIdentifier.createTo(getMessage().getInt(TARGET_ADDRESS));
}
}
return mPatchedIdentifier;
}
/**
* Talkgroup identifier for the super group aka patch group
*
* @return patch group identifier
*/
public TalkgroupIdentifier getSuperGroup()
{
if(mSuperGroupIdentifier == null)
{
mSuperGroupIdentifier = APCO25Talkgroup.create(getMessage().getInt(SUPERGROUP_ADDRESS));
}
return mSuperGroupIdentifier;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
if(getRegroupOptions().isActivate())
{
sb.append(" ACTIVATE");
sb.append(getRegroupOptions().isPatch() ? " TWO-WAY PATCH" : " ONE-WAY SIMUL-SELECT");
sb.append(" SUPERGROUP:").append(getSuperGroup());
sb.append(" V").append(getRegroupOptions().getSupergroupSequenceNumber());
sb.append(" INCLUDE");
sb.append(getRegroupOptions().isTalkgroupAddress() ? " TALKGROUP:" : " RADIO:");
sb.append(getTargetAddress());
if(hasEncryptionKeyId())
{
sb.append(" USE ENCRYPTION KEY:").append(getEncryptionKeyId());
}
}
else
{
sb.append(" DEACTIVATE SUPERGROUP:").append(getSuperGroup());
sb.append(" V").append(getRegroupOptions().getSupergroupSequenceNumber());
}
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getPatchGroup());
}
return mIdentifiers;
}
}

View File

@@ -1,58 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2019 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.module.decode.p25.phase1.message.tsbk.harris.osp;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.P25P1DataUnitID;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.OSPMessage;
import java.util.Collections;
import java.util.List;
/**
* Harris TDMA Sync Broadcast message.
*
* Note: Standard Opcode 48 is a TDMA Sync message, so this likely has something
* to do with TDMA Phase II
*/
public class HarrisTDMASyncBroadcast extends OSPMessage
{
/**
* Constructs a TSBK from the binary message sequence.
*/
public HarrisTDMASyncBroadcast(P25P1DataUnitID dataUnitId, CorrectedBinaryMessage message, int nac, long timestamp)
{
super(dataUnitId, message, nac, timestamp);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getMessageStub());
sb.append(" HARRIS TDMA SYNC BROADCAST");
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.EMPTY_LIST;
}
}

View File

@@ -66,7 +66,7 @@ public class PatchGroupAdd extends OSPMessage
if(mPatchGroup == null) if(mPatchGroup == null)
{ {
PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getPatchAddress())); PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getPatchAddress()));
patchGroup.addPatchedGroups(getPatchedTalkgroups()); patchGroup.addPatchedTalkgroups(getPatchedTalkgroups());
mPatchGroup = APCO25PatchGroup.create(patchGroup); mPatchGroup = APCO25PatchGroup.create(patchGroup);
} }

View File

@@ -66,7 +66,7 @@ public class PatchGroupDelete extends OSPMessage
if(mPatchGroup == null) if(mPatchGroup == null)
{ {
PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getPatchAddress())); PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getPatchAddress()));
patchGroup.addPatchedGroups(getPatchedTalkgroups()); patchGroup.addPatchedTalkgroups(getPatchedTalkgroups());
mPatchGroup = APCO25PatchGroup.create(patchGroup); mPatchGroup = APCO25PatchGroup.create(patchGroup);
} }

View File

@@ -87,6 +87,7 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToU
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelGrantUpdateExtended; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelGrantUpdateExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserAbbreviated; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserAbbreviated;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -391,6 +392,19 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case PHASE1_109_UNIT_REGISTRATION_COMMAND_ABBREVIATED: case PHASE1_109_UNIT_REGISTRATION_COMMAND_ABBREVIATED:
broadcast(message, mac, getCurrentChannel(), DecodeEventType.COMMAND, "UNIT REGISTRATION"); broadcast(message, mac, getCurrentChannel(), DecodeEventType.COMMAND, "UNIT REGISTRATION");
break; break;
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
if(mac instanceof L3HarrisRegroupCommand regroup)
{
if(regroup.getRegroupOptions().isActivate())
{
mPatchGroupManager.addPatchGroup(regroup.getPatchGroup());
}
else
{
mPatchGroupManager.removePatchGroup(regroup.getPatchGroup());
}
}
break;
case PHASE1_197_UNIT_TO_UNIT_ANSWER_REQUEST_EXTENDED: case PHASE1_197_UNIT_TO_UNIT_ANSWER_REQUEST_EXTENDED:
if(mac instanceof UnitToUnitAnswerRequestExtended uuare) if(mac instanceof UnitToUnitAnswerRequestExtended uuare)
{ {

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/ */
package io.github.dsheirer.module.decode.p25.phase2.message.mac; package io.github.dsheirer.module.decode.p25.phase2.message.mac;
@@ -87,11 +84,11 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToU
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnitToUnitVoiceChannelUserExtended;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownStructure; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownStructure;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownVendorMessage; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.UnknownVendorMessage;
import org.slf4j.Logger; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris.L3HarrisRegroupCommand;
import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Message factory for creating Encoded MAC Information (EMI) message parser instances. * Message factory for creating Encoded MAC Information (EMI) message parser instances.
@@ -293,6 +290,8 @@ public class MacMessageFactory
return new AdjacentStatusBroadcastAbbreviated(message, offset); return new AdjacentStatusBroadcastAbbreviated(message, offset);
case PHASE1_125_IDENTIFIER_UPDATE: case PHASE1_125_IDENTIFIER_UPDATE:
return new FrequencyBandUpdate(message, offset); return new FrequencyBandUpdate(message, offset);
case PHASE1_176_L3HARRIS_GROUP_REGROUP:
return new L3HarrisRegroupCommand(message, offset);
case PHASE1_192_GROUP_VOICE_CHANNEL_GRANT_EXTENDED: case PHASE1_192_GROUP_VOICE_CHANNEL_GRANT_EXTENDED:
return new GroupVoiceChannelGrantExtended(message, offset); return new GroupVoiceChannelGrantExtended(message, offset);
case PHASE1_195_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT: case PHASE1_195_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT:

View File

@@ -76,6 +76,7 @@ public enum MacOpcode
PHASE1_123_NETWORK_STATUS_BROADCAST_ABBREVIATED(123, "NETWORK STATUS BROADCAST ABBREVIATED", 11), PHASE1_123_NETWORK_STATUS_BROADCAST_ABBREVIATED(123, "NETWORK STATUS BROADCAST ABBREVIATED", 11),
PHASE1_124_ADJACENT_STATUS_BROADCAST_ABBREVIATED(124, "ADJACENT STATUS BROADCAST ABBREVIATED", 9), PHASE1_124_ADJACENT_STATUS_BROADCAST_ABBREVIATED(124, "ADJACENT STATUS BROADCAST ABBREVIATED", 9),
PHASE1_125_IDENTIFIER_UPDATE(125, "IDENTIFIER UPDATE", 9), PHASE1_125_IDENTIFIER_UPDATE(125, "IDENTIFIER UPDATE", 9),
PHASE1_176_L3HARRIS_GROUP_REGROUP(176, "REGROUP COMMAND", 17),
PHASE1_PARTITION_1_UNKNOWN_OPCODE(-1, "UNKNOWN PHASE 1 OPCODE", -1), PHASE1_PARTITION_1_UNKNOWN_OPCODE(-1, "UNKNOWN PHASE 1 OPCODE", -1),
VENDOR_PARTITION_2_UNKNOWN_OPCODE(-1, "UNKNOWN VENDOR OPCODE", -1), VENDOR_PARTITION_2_UNKNOWN_OPCODE(-1, "UNKNOWN VENDOR OPCODE", -1),
@@ -138,7 +139,7 @@ public enum MacOpcode
TDMA_1_GROUP_VOICE_CHANNEL_USER_ABBREVIATED, TDMA_2_UNIT_TO_UNIT_VOICE_CHANNEL_USER, TDMA_1_GROUP_VOICE_CHANNEL_USER_ABBREVIATED, TDMA_2_UNIT_TO_UNIT_VOICE_CHANNEL_USER,
TDMA_3_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER, TDMA_5_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE, TDMA_3_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER, TDMA_5_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE,
TDMA_33_GROUP_VOICE_CHANNEL_USER_EXTENDED, TDMA_34_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED, TDMA_33_GROUP_VOICE_CHANNEL_USER_EXTENDED, TDMA_34_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED,
TDMA_37_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE_EXPLICIT); TDMA_37_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE_EXPLICIT, PHASE1_176_L3HARRIS_GROUP_REGROUP);
/** /**
* Data channel grants * Data channel grants

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/ */
package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure; package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure;
@@ -26,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -72,7 +68,7 @@ public class FrequencyBandUpdate extends MacStructure implements IFrequencyBand
@Override @Override
public int getIdentifier() public int getIdentifier()
{ {
return getMessage().getInt(FREQUENCY_BAND_IDENTIFIER); return getMessage().getInt(FREQUENCY_BAND_IDENTIFIER, getOffset());
} }
@Override @Override

View File

@@ -0,0 +1,272 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.l3harris;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.patch.APCO25PatchGroup;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacStructure;
import io.github.dsheirer.module.decode.p25.reference.Encryption;
import io.github.dsheirer.module.decode.p25.reference.RegroupOptions;
import io.github.dsheirer.module.decode.p25.reference.Vendor;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* L3Harris Group Regroup Explicit Encryption Command. Provides super group activate/deactivate for dynamic regrouping
* of talkgroups or groups of individual radios and includes optional encryption algorithm and key parameters when the
* regrouping targets talkgroups (not radios).
*
* See: https://forums.radioreference.com/threads/duke-energy-p25-system.411183/page-28#post-3908078
*/
public class L3HarrisRegroupCommand extends MacStructure
{
private static final int[] VENDOR = {8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] LENGTH = {18, 19, 20, 21, 22, 23};
private static final int[] REGROUP_OPTIONS = {24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] SUPERGROUP = {32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] KEY_ID = {48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63};
//Variation 1: Radio identifiers only.
private static final int[] RADIO_1 = {64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82,
83, 84, 85, 86, 87};
private static final int[] RADIO_2 = {88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105,
106, 107, 108, 109, 110, 111};
private static final int[] RADIO_3 = {112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
127, 128, 129, 130, 131, 132, 133, 134, 135};
//Variation 2: Talkgroups and Algorithm ID
private static final int[] ALGORITHM_ID = {64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] TALKGROUP_1 = {72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87};
private static final int[] TALKGROUP_2 = {88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103};
private static final int[] TALKGROUP_3 = {104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119};
private static final int[] TALKGROUP_4 = {120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135};
private RegroupOptions mRegroupOptions;
private PatchGroupIdentifier mPatchGroupIdentifier;
private List<Identifier> mIdentifiers;
/**
* Constructs the message
*
* @param message containing the message bits
* @param offset into the message for this structure
*/
public L3HarrisRegroupCommand(CorrectedBinaryMessage message, int offset)
{
super(message, offset);
}
/**
* Textual representation of this message
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
if(getVendor() == Vendor.HARRIS)
{
sb.append("L3HARRIS ");
}
else
{
sb.append("WARNING: UNKNOWN VENDOR:").append(getVendor());
}
if(getRegroupOptions().isActivate())
{
sb.append(" ACTIVATE");
sb.append(getRegroupOptions().isPatch() ? " TWO-WAY PATCH" : " ONE-WAY SIMUL-SELECT");
sb.append(" SUPERGROUP:").append(getPatchGroup().getValue().getPatchGroup().getValue());
sb.append(" V").append(getRegroupOptions().getSupergroupSequenceNumber());
sb.append(" V").append(getRegroupOptions().getSupergroupSequenceNumber());
if(getRegroupOptions().isActivate())
{
if(getRegroupOptions().isTalkgroupAddress())
{
sb.append(" INCLUDE TALKGROUPS:");
sb.append(getPatchGroup().getValue().getPatchedTalkgroupIdentifiers().stream()
.map(tg -> tg.getValue().toString()).collect(Collectors.joining(",")));
Encryption encryption = getEncryptionAlgorithm();
if(encryption != Encryption.UNENCRYPTED)
{
sb.append(" USE ENCRYPTION:").append(encryption.name());
sb.append(" KEY:").append(getKeyId());
}
}
else
{
sb.append(" INCLUDE RADIOS:");
sb.append(getPatchGroup().getValue().getPatchedRadioIdentifiers().stream()
.map(radio -> radio.getValue().toString()).collect(Collectors.joining(",")));
}
}
}
else
{
sb.append(" DEACTIVATE SUPERGROUP:").append(getPatchGroup().getValue().getPatchGroup().getValue());
}
sb.append(" MSG LENGTH:").append(getLength());
return sb.toString();
}
/**
* Vendor ID. This should be L3Harris unless another vendor is also using this Opcode.
*/
public Vendor getVendor()
{
return Vendor.fromValue(getMessage().getInt(VENDOR, getOffset()));
}
/**
* Message length. Note: I'm unsure if this is the count of identifiers or if this is the quantity of bytes.
* @return length.
*/
public int getLength()
{
return getMessage().getInt(LENGTH, getOffset());
}
/**
* Regrouping options for this patch group (ie activate, deactivate, patch group version, etc.
* @return patch group options.
*/
public RegroupOptions getRegroupOptions()
{
if(mRegroupOptions == null)
{
mRegroupOptions = new RegroupOptions(getMessage().getInt(REGROUP_OPTIONS, getOffset()));
}
return mRegroupOptions;
}
/**
* Encryption key ID. Note: only available when the regroup options indicates this is a talkgroup supergroup.
* @return key ID.
*/
public int getKeyId()
{
return getMessage().getInt(KEY_ID, getOffset());
}
/**
* Encryption algorithm for talkgroup super-group.
*/
public Encryption getEncryptionAlgorithm()
{
return Encryption.fromValue(getMessage().getInt(ALGORITHM_ID, getOffset()));
}
/**
* Supergroup/Patch group referenced by this message including any patched talkgroups or individual radio identifiers.
* @return patch group.
*/
public PatchGroupIdentifier getPatchGroup()
{
if(mPatchGroupIdentifier == null)
{
TalkgroupIdentifier patchGroupId = APCO25Talkgroup.create(getMessage().getInt(SUPERGROUP, getOffset()));
PatchGroup patchGroup = new PatchGroup(patchGroupId, getRegroupOptions().getSupergroupSequenceNumber());
if(getRegroupOptions().isTalkgroupAddress())
{
int talkgroup1 = getMessage().getInt(TALKGROUP_1, getOffset());
if(talkgroup1 > 0)
{
patchGroup.addPatchedTalkgroup(APCO25Talkgroup.create(talkgroup1));
int talkgroup2 = getMessage().getInt(TALKGROUP_2, getOffset());
if(talkgroup2 > 0)
{
patchGroup.addPatchedTalkgroup(APCO25Talkgroup.create(talkgroup2));
int talkgroup3 = getMessage().getInt(TALKGROUP_3, getOffset());
if(talkgroup3 > 0)
{
patchGroup.addPatchedTalkgroup(APCO25Talkgroup.create(talkgroup3));
int talkgroup4 = getMessage().getInt(TALKGROUP_4, getOffset());
if(talkgroup4 > 0)
{
patchGroup.addPatchedTalkgroup(APCO25Talkgroup.create(talkgroup4));
}
}
}
}
}
else
{
int radio1 = getMessage().getInt(RADIO_1, getOffset());
if(radio1 > 0)
{
patchGroup.addPatchedRadio(APCO25RadioIdentifier.createTo(radio1));
int radio2 = getMessage().getInt(RADIO_2, getOffset());
if(radio2 > 0)
{
patchGroup.addPatchedRadio(APCO25RadioIdentifier.createTo(radio2));
int radio3 = getMessage().getInt(RADIO_3, getOffset());
if(radio3 > 0)
{
patchGroup.addPatchedRadio(APCO25RadioIdentifier.createTo(radio3));
}
}
}
}
mPatchGroupIdentifier = APCO25PatchGroup.create(patchGroup);
}
return mPatchGroupIdentifier;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getPatchGroup());
}
return mIdentifiers;
}
}

View File

@@ -0,0 +1,81 @@
package io.github.dsheirer.module.decode.p25.reference;
/**
* Harris group regroup options
*/
public class RegroupOptions
{
private static final int MASK_REGROUP_TYPE = 0x80;
private static final int MASK_GROUP_INDIVIDUAL = 0x40;
private static final int MASK_ACTIVATE = 0x20;
private static final int MASK_SUPERGROUP_SEQUENCE_NUMBER = 0x1F;
private int mValue;
/**
* Constructs an instance
* @param value
*/
public RegroupOptions(int value)
{
mValue = value;
}
/**
* Type of regrouping
* @return regroup type
*/
public RegroupType getRegroupType()
{
return (mValue & MASK_REGROUP_TYPE) == MASK_REGROUP_TYPE ?
RegroupType.SIMULSELECT_ONE_WAY_REGROUP : RegroupType.PATCH_TWO_WAY_REGROUP;
}
/**
* Indicates if this is a two-way patch (true) or one-way simul-select (false) regrouping
* @return patch (true) or simulselect (false)
*/
public boolean isPatch()
{
return getRegroupType() == RegroupType.PATCH_TWO_WAY_REGROUP;
}
/**
* Indicates if the regroup target address is a group address (true) or individual address (false).
* @return true for group or false for individual address.
*/
public boolean isTalkgroupAddress()
{
return (mValue & MASK_GROUP_INDIVIDUAL) == MASK_GROUP_INDIVIDUAL;
}
/**
* Indicates if this is an activation (true) or a deactivation (false) of the supergroup.
* @return activation (true) or deactivation (false)
*/
public boolean isActivate()
{
return (mValue & MASK_ACTIVATE) == MASK_ACTIVATE;
}
/**
* Unique identifier for the regrouping
* @return unique id SSN
*/
public int getSupergroupSequenceNumber()
{
return (mValue & MASK_SUPERGROUP_SEQUENCE_NUMBER);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(isActivate() ? "ACTIVATE" : "DEACTIVATE");
sb.append(isTalkgroupAddress() ? " TALKGROUP" : " INDIVIDUAL");
sb.append(isPatch() ? " PATCH" : " SIMUL-SELECT");
return sb.toString();
}
public enum RegroupType {PATCH_TWO_WAY_REGROUP, SIMULSELECT_ONE_WAY_REGROUP};
}

View File

@@ -1,23 +1,20 @@
/* /*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* *
* * ****************************************************************************** * This program is free software: you can redistribute it and/or modify
* * Copyright (C) 2014-2019 Dennis Sheirer * it under the terms of the GNU General Public License as published by
* * * the Free Software Foundation, either version 3 of the License, or
* * This program is free software: you can redistribute it and/or modify * (at your option) any later version.
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/ */
package io.github.dsheirer.preference.identifier.talkgroup; package io.github.dsheirer.preference.identifier.talkgroup;
@@ -83,10 +80,20 @@ public class APCO25TalkgroupFormatter extends AbstractIntegerFormatter
sb.append("["); sb.append("[");
int counter = 0; int counter = 0;
for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedGroupIdentifiers()) for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedTalkgroupIdentifiers())
{ {
sb.append(format(patchedGroup, format, fixedWidth)); sb.append(format(patchedGroup, format, fixedWidth));
if(counter++ < patchGroup.getPatchedGroupIdentifiers().size() - 1) if(counter++ < patchGroup.getPatchedTalkgroupIdentifiers().size() - 1)
{
sb.append(",");
}
}
counter = 0;
for(RadioIdentifier patchedRadio: patchGroup.getPatchedRadioIdentifiers())
{
sb.append(format(patchedRadio, format, fixedWidth));
if(counter++ < patchGroup.getPatchedRadioIdentifiers().size() - 1)
{ {
sb.append(","); sb.append(",");
} }

View File

@@ -55,9 +55,14 @@ import io.github.dsheirer.source.tuner.rtl.RTL2832UnknownTunerEditor;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KEmbeddedTuner; import io.github.dsheirer.source.tuner.rtl.e4k.E4KEmbeddedTuner;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerEditor; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerEditor;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TEmbeddedTuner; import io.github.dsheirer.source.tuner.rtl.fc0013.FC0013EmbeddedTuner;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerConfiguration; import io.github.dsheirer.source.tuner.rtl.fc0013.FC0013TunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerEditor; import io.github.dsheirer.source.tuner.rtl.fc0013.FC0013TunerEditor;
import io.github.dsheirer.source.tuner.rtl.r8x.R8xTunerEditor;
import io.github.dsheirer.source.tuner.rtl.r8x.r820t.R820TEmbeddedTuner;
import io.github.dsheirer.source.tuner.rtl.r8x.r820t.R820TTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r8x.r828d.R828DEmbeddedTuner;
import io.github.dsheirer.source.tuner.rtl.r8x.r828d.R828DTunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner; import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.RspTuner; import io.github.dsheirer.source.tuner.sdrplay.RspTuner;
import io.github.dsheirer.source.tuner.sdrplay.api.DeviceSelectionMode; import io.github.dsheirer.source.tuner.sdrplay.api.DeviceSelectionMode;
@@ -366,8 +371,10 @@ public class TunerFactory
return switch(tunerType) return switch(tunerType)
{ {
case ELONICS_E4000 -> new E4KEmbeddedTuner(adapter); case ELONICS_E4000 -> new E4KEmbeddedTuner(adapter);
case FITIPOWER_FC0013 -> new FC0013EmbeddedTuner(adapter);
case RAFAELMICRO_R820T -> new R820TEmbeddedTuner(adapter); case RAFAELMICRO_R820T -> new R820TEmbeddedTuner(adapter);
default -> throw new SourceException("Unsupported/Unrecognized Tuner Type"); case RAFAELMICRO_R828D -> new R828DEmbeddedTuner(adapter);
default -> throw new SourceException("Unsupported/Unrecognized Tuner Type: " + tunerType);
}; };
} }
@@ -384,6 +391,8 @@ public class TunerFactory
return new AirspyTunerConfiguration(uniqueID); return new AirspyTunerConfiguration(uniqueID);
case ELONICS_E4000: case ELONICS_E4000:
return new E4KTunerConfiguration(uniqueID); return new E4KTunerConfiguration(uniqueID);
case FITIPOWER_FC0013:
return new FC0013TunerConfiguration(uniqueID);
case FUNCUBE_DONGLE_PRO: case FUNCUBE_DONGLE_PRO:
return new FCD1TunerConfiguration(uniqueID); return new FCD1TunerConfiguration(uniqueID);
case FUNCUBE_DONGLE_PRO_PLUS: case FUNCUBE_DONGLE_PRO_PLUS:
@@ -394,6 +403,8 @@ public class TunerFactory
return new HackRFTunerConfiguration(uniqueID); return new HackRFTunerConfiguration(uniqueID);
case RAFAELMICRO_R820T: case RAFAELMICRO_R820T:
return new R820TTunerConfiguration(uniqueID); return new R820TTunerConfiguration(uniqueID);
case RAFAELMICRO_R828D:
return new R828DTunerConfiguration(uniqueID);
case RECORDING: case RECORDING:
return RecordingTunerConfiguration.create(); return RecordingTunerConfiguration.create();
case RSP_1: case RSP_1:
@@ -474,8 +485,11 @@ public class TunerFactory
{ {
case ELONICS_E4000: case ELONICS_E4000:
return new E4KTunerEditor(userPreferences, tunerManager, discoveredTuner); return new E4KTunerEditor(userPreferences, tunerManager, discoveredTuner);
case FITIPOWER_FC0013:
return new FC0013TunerEditor(userPreferences, tunerManager, discoveredTuner);
case RAFAELMICRO_R820T: case RAFAELMICRO_R820T:
return new R820TTunerEditor(userPreferences, tunerManager, discoveredTuner); case RAFAELMICRO_R828D:
return new R8xTunerEditor(userPreferences, tunerManager, discoveredTuner);
} }
} }
return new RTL2832UnknownTunerEditor(userPreferences, tunerManager, discoveredTuner); return new RTL2832UnknownTunerEditor(userPreferences, tunerManager, discoveredTuner);

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -31,7 +31,9 @@ import io.github.dsheirer.source.tuner.fcd.proplusV2.FCD2TunerConfiguration;
import io.github.dsheirer.source.tuner.hackrf.HackRFTunerConfiguration; import io.github.dsheirer.source.tuner.hackrf.HackRFTunerConfiguration;
import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration; import io.github.dsheirer.source.tuner.recording.RecordingTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerConfiguration; import io.github.dsheirer.source.tuner.rtl.fc0013.FC0013TunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r8x.r820t.R820TTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r8x.r828d.R828DTunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration; import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
/** /**
@@ -42,11 +44,13 @@ import io.github.dsheirer.source.tuner.sdrplay.RspTunerConfiguration;
@JsonSubTypes.Type(value = AirspyTunerConfiguration.class, name = "airspyTunerConfiguration"), @JsonSubTypes.Type(value = AirspyTunerConfiguration.class, name = "airspyTunerConfiguration"),
@JsonSubTypes.Type(value = AirspyHfTunerConfiguration.class, name = "airspyHfTunerConfiguration"), @JsonSubTypes.Type(value = AirspyHfTunerConfiguration.class, name = "airspyHfTunerConfiguration"),
@JsonSubTypes.Type(value = E4KTunerConfiguration.class, name = "e4KTunerConfiguration"), @JsonSubTypes.Type(value = E4KTunerConfiguration.class, name = "e4KTunerConfiguration"),
@JsonSubTypes.Type(value = FC0013TunerConfiguration.class, name = "fc0013TunerConfiguration"),
@JsonSubTypes.Type(value = FCD1TunerConfiguration.class, name = "fcd1TunerConfiguration"), @JsonSubTypes.Type(value = FCD1TunerConfiguration.class, name = "fcd1TunerConfiguration"),
@JsonSubTypes.Type(value = FCD2TunerConfiguration.class, name = "fcd2TunerConfiguration"), @JsonSubTypes.Type(value = FCD2TunerConfiguration.class, name = "fcd2TunerConfiguration"),
@JsonSubTypes.Type(value = HackRFTunerConfiguration.class, name = "hackRFTunerConfiguration"), @JsonSubTypes.Type(value = HackRFTunerConfiguration.class, name = "hackRFTunerConfiguration"),
@JsonSubTypes.Type(value = RecordingTunerConfiguration.class, name = "recordingTunerConfiguration"), @JsonSubTypes.Type(value = RecordingTunerConfiguration.class, name = "recordingTunerConfiguration"),
@JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"), @JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"),
@JsonSubTypes.Type(value = R828DTunerConfiguration.class, name = "r828DTunerConfiguration"),
@JsonSubTypes.Type(value = RspTunerConfiguration.class, name = "rspTunerConfiguration"), @JsonSubTypes.Type(value = RspTunerConfiguration.class, name = "rspTunerConfiguration"),
}) })
@JacksonXmlRootElement(localName = "tuner_configuration") @JacksonXmlRootElement(localName = "tuner_configuration")

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -24,7 +24,9 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration; import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration; import io.github.dsheirer.source.tuner.rtl.e4k.E4KTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerConfiguration; import io.github.dsheirer.source.tuner.rtl.fc0013.FC0013TunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r8x.r820t.R820TTunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.r8x.r828d.R828DTunerConfiguration;
/** /**
* RTL2832 tuner configuration * RTL2832 tuner configuration
@@ -32,12 +34,15 @@ import io.github.dsheirer.source.tuner.rtl.r820t.R820TTunerConfiguration;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({ @JsonSubTypes({
@JsonSubTypes.Type(value = E4KTunerConfiguration.class, name = "e4KTunerConfiguration"), @JsonSubTypes.Type(value = E4KTunerConfiguration.class, name = "e4KTunerConfiguration"),
@JsonSubTypes.Type(value = FC0013TunerConfiguration.class, name = "fc0013TunerConfiguration"),
@JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"), @JsonSubTypes.Type(value = R820TTunerConfiguration.class, name = "r820TTunerConfiguration"),
@JsonSubTypes.Type(value = R828DTunerConfiguration.class, name = "r828DTunerConfiguration"),
}) })
@JacksonXmlRootElement(localName = "tuner_configuration") @JacksonXmlRootElement(localName = "tuner_configuration")
public abstract class RTL2832TunerConfiguration extends TunerConfiguration public abstract class RTL2832TunerConfiguration extends TunerConfiguration
{ {
private RTL2832TunerController.SampleRate mSampleRate = RTL2832TunerController.SampleRate.RATE_2_400MHZ; private RTL2832TunerController.SampleRate mSampleRate = RTL2832TunerController.SampleRate.RATE_2_400MHZ;
private boolean mBiasTEnabled = false;
/** /**
* Default constructor to support Jackson * Default constructor to support Jackson
@@ -61,4 +66,23 @@ public abstract class RTL2832TunerConfiguration extends TunerConfiguration
{ {
mSampleRate = sampleRate; mSampleRate = sampleRate;
} }
/**
* Sets the enabled state of the Bias-T
* @param enabled true to turn-on the bias-T
*/
public void setBiasT(boolean enabled)
{
mBiasTEnabled = enabled;
}
/**
* Indicates if the Bias-T is enabled.
* @return true if enabled.
*/
@JacksonXmlProperty(isAttribute = true, localName = "bias_t")
public boolean isBiasT()
{
return mBiasTEnabled;
}
} }

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -66,6 +66,7 @@ public class RTL2832TunerController extends USBTunerController
private Descriptor mDescriptor; private Descriptor mDescriptor;
private EmbeddedTuner mEmbeddedTuner; private EmbeddedTuner mEmbeddedTuner;
private long mTunedFrequency = 0; private long mTunedFrequency = 0;
private boolean mBiasTEnabled = false;
private ReentrantLock mLock = new ReentrantLock(); private ReentrantLock mLock = new ReentrantLock();
/** /**
@@ -174,7 +175,7 @@ public class RTL2832TunerController extends USBTunerController
try try
{ {
writeRegister(Block.USB, Address.USB_SYSCTL.getAddress(), 0x09, 1); writeRegister(Block.USB, Address.USB_SYSCTL, 0x09, 1);
} }
catch(LibUsbException lue) catch(LibUsbException lue)
{ {
@@ -195,7 +196,7 @@ public class RTL2832TunerController extends USBTunerController
try try
{ {
writeRegister(Block.USB, Address.USB_SYSCTL.getAddress(), 0x09, 1); writeRegister(Block.USB, Address.USB_SYSCTL, 0x09, 1);
} }
catch(LibUsbException lue) catch(LibUsbException lue)
{ {
@@ -238,7 +239,7 @@ public class RTL2832TunerController extends USBTunerController
if(tunerType == TunerType.UNKNOWN) if(tunerType == TunerType.UNKNOWN)
{ {
throw new SourceException("Unrecognized embedded tuner type for RTL-2832"); throw new SourceException("Unrecognized RTL-2832 embedded tuner type: " + tunerType);
} }
mEmbeddedTuner = TunerFactory.getRtlEmbeddedTuner(tunerType, new ControllerAdapter(this)); mEmbeddedTuner = TunerFactory.getRtlEmbeddedTuner(tunerType, new ControllerAdapter(this));
@@ -299,6 +300,26 @@ public class RTL2832TunerController extends USBTunerController
return null; return null;
} }
/**
* Sets the enabled state of the bias-t
* @param enabled true to turn-on the bias-t or false to turn-off the bias-t.
*/
public void setBiasT(boolean enabled)
{
setGPIOOutput((byte)0x01);
setGPIOBit((byte)0x01, enabled);
mBiasTEnabled = enabled;
}
/**
* Indicates if the bias-t is enabled.
* @return true if enabled.
*/
public boolean isBiasT()
{
return mBiasTEnabled;
}
/** /**
* Overrides updates for measured frequency error so that the updates can also be applied to the * Overrides updates for measured frequency error so that the updates can also be applied to the
* frequency error correction manager for automatic PPM updating. * frequency error correction manager for automatic PPM updating.
@@ -333,21 +354,28 @@ public class RTL2832TunerController extends USBTunerController
*/ */
public String getUniqueID() public String getUniqueID()
{ {
if(mDescriptor != null && mDescriptor.hasSerial()) if(mDescriptor != null)
{ {
if(hasEmbeddedTuner()) if(mDescriptor.hasSerial())
{ {
return TunerClass.RTL2832 + "/" + getTunerType().getLabel() + " " + mDescriptor.getSerial(); if(hasEmbeddedTuner())
{
return TunerClass.RTL2832 + "/" + getTunerType().getLabel() + " " + mDescriptor.getSerial();
}
else
{
return TunerClass.RTL2832 + " " + mDescriptor.getSerial();
}
} }
else else
{ {
return TunerClass.RTL2832 + " " + mDescriptor.getSerial(); return "RTL-2832 USB Bus:" + mBus + " Port:" + mPortAddress;
} }
} }
else else
{ {
int serial = (0xFF & getDeviceDescriptor().iSerialNumber()); int serial = (0xFF & getDeviceDescriptor().iSerialNumber());
return TunerClass.RTL2832.toString() + " " + serial; return TunerClass.RTL2832 + " " + serial;
} }
} }
@@ -437,8 +465,8 @@ public class RTL2832TunerController extends USBTunerController
*/ */
public void resetUSBBuffer() throws LibUsbException public void resetUSBBuffer() throws LibUsbException
{ {
writeRegister(Block.USB, Address.USB_EPA_CTL.getAddress(), 0x1002, 2); writeRegister(Block.USB, Address.USB_EPA_CTL, 0x1002, 2);
writeRegister(Block.USB, Address.USB_EPA_CTL.getAddress(), 0x0000, 2); writeRegister(Block.USB, Address.USB_EPA_CTL, 0x0000, 2);
} }
/** /**
@@ -448,13 +476,13 @@ public class RTL2832TunerController extends USBTunerController
public void initBaseband() throws LibUsbException public void initBaseband() throws LibUsbException
{ {
/* Initialize USB */ /* Initialize USB */
writeRegister(Block.USB, Address.USB_SYSCTL.getAddress(), 0x09, 1); writeRegister(Block.USB, Address.USB_SYSCTL, 0x09, 1);
writeRegister(Block.USB, Address.USB_EPA_MAXPKT.getAddress(), 0x0002, 2); writeRegister(Block.USB, Address.USB_EPA_MAXPKT, 0x0002, 2);
writeRegister(Block.USB, Address.USB_EPA_CTL.getAddress(), 0x1002, 2); writeRegister(Block.USB, Address.USB_EPA_CTL, 0x1002, 2);
/* Power on demod */ /* Power on demod */
writeRegister(Block.SYS, Address.DEMOD_CTL_1.getAddress(), 0x22, 1); writeRegister(Block.SYS, Address.DEMOD_CTL_1, 0x22, 1);
writeRegister(Block.SYS, Address.DEMOD_CTL.getAddress(), 0xE8, 1); writeRegister(Block.SYS, Address.DEMOD_CTL, 0xE8, 1);
/* Reset demod */ /* Reset demod */
writeDemodRegister(Page.ONE, (short) 0x01, 0x14, 1); //Bit 3 = soft reset writeDemodRegister(Page.ONE, (short) 0x01, 0x14, 1); //Bit 3 = soft reset
@@ -513,7 +541,7 @@ public class RTL2832TunerController extends USBTunerController
*/ */
private void deinitBaseband() throws IllegalArgumentException, UsbDisconnectedException private void deinitBaseband() throws IllegalArgumentException, UsbDisconnectedException
{ {
writeRegister(Block.SYS, Address.DEMOD_CTL.getAddress(), 0x20, 1); writeRegister(Block.SYS, Address.DEMOD_CTL, 0x20, 1);
} }
/** /**
@@ -527,7 +555,7 @@ public class RTL2832TunerController extends USBTunerController
private void setGPIOBit(byte bitMask, boolean enabled) throws LibUsbException private void setGPIOBit(byte bitMask, boolean enabled) throws LibUsbException
{ {
//Get current register value //Get current register value
int value = readRegister(Block.SYS, Address.GPO.getAddress(), 1); int value = readRegister(Block.SYS, Address.GPO, 1);
//Update the masked bits //Update the masked bits
if(enabled) if(enabled)
@@ -540,7 +568,7 @@ public class RTL2832TunerController extends USBTunerController
} }
//Write the change back to the device //Write the change back to the device
writeRegister(Block.SYS, Address.GPO.getAddress(), value, 1); writeRegister(Block.SYS, Address.GPO, value, 1);
} }
/** /**
@@ -553,16 +581,16 @@ public class RTL2832TunerController extends USBTunerController
private void setGPIOOutput(byte bitMask) throws LibUsbException private void setGPIOOutput(byte bitMask) throws LibUsbException
{ {
//Get current register value //Get current register value
int value = readRegister(Block.SYS, Address.GPD.getAddress(), 1); int value = readRegister(Block.SYS, Address.GPD, 1);
//Mask the value and rewrite it //Mask the value and rewrite it
writeRegister(Block.SYS, Address.GPO.getAddress(), value & ~bitMask, 1); writeRegister(Block.SYS, Address.GPO, value & ~bitMask, 1);
//Get current register value //Get current register value
value = readRegister(Block.SYS, Address.GPOE.getAddress(), 1); value = readRegister(Block.SYS, Address.GPOE, 1);
//Mask the value and rewrite it //Mask the value and rewrite it
writeRegister(Block.SYS, Address.GPOE.getAddress(), value | bitMask, 1); writeRegister(Block.SYS, Address.GPOE, value | bitMask, 1);
} }
/** /**
@@ -572,10 +600,9 @@ public class RTL2832TunerController extends USBTunerController
*/ */
private void enableI2CRepeater(boolean enabled) throws LibUsbException private void enableI2CRepeater(boolean enabled) throws LibUsbException
{ {
Page page = Page.ONE;
short address = 1; short address = 1;
int value = (enabled ? 0x18 : 0x10); int value = (enabled ? 0x18 : 0x10);
writeDemodRegister(page, address, value, 1); writeDemodRegister(Page.ONE, address, value, 1);
} }
/** /**
@@ -720,7 +747,7 @@ public class RTL2832TunerController extends USBTunerController
* @param length of value in bytes * @param length of value in bytes
* @throws LibUsbException on error * @throws LibUsbException on error
*/ */
private void writeRegister(Block block, short address, int value, int length) throws LibUsbException private void writeRegister(Block block, Address address, int value, int length) throws LibUsbException
{ {
ByteBuffer buffer = ByteBuffer.allocateDirect(length); ByteBuffer buffer = ByteBuffer.allocateDirect(length);
buffer.order(ByteOrder.BIG_ENDIAN); buffer.order(ByteOrder.BIG_ENDIAN);
@@ -740,7 +767,7 @@ public class RTL2832TunerController extends USBTunerController
} }
buffer.rewind(); buffer.rewind();
write(address, block, buffer); write(address.getAddress(), block, buffer);
} }
/** /**
@@ -751,10 +778,10 @@ public class RTL2832TunerController extends USBTunerController
* @return value read * @return value read
* @throws LibUsbException on error * @throws LibUsbException on error
*/ */
private int readRegister(Block block, short address, int length) throws LibUsbException private int readRegister(Block block, Address address, int length) throws LibUsbException
{ {
ByteBuffer buffer = ByteBuffer.allocateDirect(2); ByteBuffer buffer = ByteBuffer.allocateDirect(2);
read(address, block, buffer); read(address.getAddress(), block, buffer);
buffer.order(ByteOrder.LITTLE_ENDIAN); buffer.order(ByteOrder.LITTLE_ENDIAN);
if(length == 2) if(length == 2)
@@ -869,11 +896,11 @@ public class RTL2832TunerController extends USBTunerController
if(type == TunerTypeCheck.FC2580) if(type == TunerTypeCheck.FC2580)
{ {
return ((value & 0x7F) == type.getCheckValue()); return ((value & 0x7F) == (type.getCheckValue() & 0xFF));
} }
else else
{ {
return (value == type.getCheckValue()); return (value == (type.getCheckValue() & 0xFF));
} }
} }
catch(LibUsbException e) catch(LibUsbException e)
@@ -973,7 +1000,7 @@ public class RTL2832TunerController extends USBTunerController
public void setSampleRateFrequencyCorrection(int ppm) throws SourceException public void setSampleRateFrequencyCorrection(int ppm) throws SourceException
{ {
int offset = -ppm * TWO_TO_22_POWER / 1000000; int offset = -ppm * TWO_TO_22_POWER / 1_000_000;
writeDemodRegister(Page.ONE, (short) 0x3F, (offset & 0xFF), 1); writeDemodRegister(Page.ONE, (short) 0x3F, (offset & 0xFF), 1);
writeDemodRegister(Page.ONE, (short) 0x3E, (Integer.rotateRight(offset, 8) & 0xFF), 1); writeDemodRegister(Page.ONE, (short) 0x3E, (Integer.rotateRight(offset, 8) & 0xFF), 1);
/* Test to retune controller to apply frequency correction */ /* Test to retune controller to apply frequency correction */
@@ -983,7 +1010,7 @@ public class RTL2832TunerController extends USBTunerController
} }
catch(Exception e) catch(Exception e)
{ {
throw new SourceException("couldn't set sample rate frequency correction", e); throw new SourceException("Couldn't set sample rate frequency correction", e);
} }
} }
@@ -1027,7 +1054,7 @@ public class RTL2832TunerController extends USBTunerController
try try
{ {
/* Tell the RTL-2832 to address the EEPROM */ /* Tell the RTL-2832 to address the EEPROM */
writeRegister(Block.I2C, EEPROM_ADDRESS, (byte) offset, 1); writeRegister(Block.I2C, Address.EEPROM, (byte) offset, 1);
} }
catch(LibUsbException e) catch(LibUsbException e)
{ {
@@ -1068,7 +1095,7 @@ public class RTL2832TunerController extends USBTunerController
} }
int offsetAndValue = Integer.rotateLeft((0xFF & offset), 8) | (0xFF & value); int offsetAndValue = Integer.rotateLeft((0xFF & offset), 8) | (0xFF & value);
writeRegister(Block.I2C, EEPROM_ADDRESS, offsetAndValue, 2); writeRegister(Block.I2C, Address.EEPROM, offsetAndValue, 2);
} }
public enum Address public enum Address
@@ -1093,7 +1120,8 @@ public class RTL2832TunerController extends USBTunerController
SYSINTE_1(0x3009), SYSINTE_1(0x3009),
SYSINTS_1(0x300A), SYSINTS_1(0x300A),
DEMOD_CTL_1(0x300B), DEMOD_CTL_1(0x300B),
IR_SUSPEND(0x300C); IR_SUSPEND(0x300C),
EEPROM(0xA0);
private int mAddress; private int mAddress;
@@ -1314,6 +1342,14 @@ public class RTL2832TunerController extends USBTunerController
getLabels(); getLabels();
} }
/**
* Indicates if this is a rtl-sdr.com V4 R828D dongle with notch filtering.
*/
public boolean isRtlSdrV4()
{
return "RTLSDRBlog".equals(getVendorLabel()) && "Blog V4".equals(getProductLabel());
}
public boolean isValid() public boolean isValid()
{ {
return mData[0] != (byte)0x0 && mData[1] != (byte)0x0; return mData[0] != (byte)0x0 && mData[1] != (byte)0x0;
@@ -1479,6 +1515,15 @@ public class RTL2832TunerController extends USBTunerController
return mController.isRunning(); return mController.isRunning();
} }
/**
* Indicates if this is a rtl-sdr.com V4 dongle with support for notch filtering.
* @return true if this is a V4 R828D tuner.
*/
public boolean isV4Dongle()
{
return mController.getDescriptor().isRtlSdrV4();
}
/** /**
* Device handle for the tuner controller. * Device handle for the tuner controller.
* @return * @return

View File

@@ -41,6 +41,7 @@ import javax.swing.JComboBox;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.JSeparator; import javax.swing.JSeparator;
import javax.swing.JToggleButton;
import javax.swing.SpinnerNumberModel; import javax.swing.SpinnerNumberModel;
import javax.usb.UsbException; import javax.usb.UsbException;
@@ -52,6 +53,7 @@ public class E4KTunerEditor extends TunerEditor<RTL2832Tuner, E4KTunerConfigurat
private final static Logger mLog = LoggerFactory.getLogger(E4KTunerEditor.class); private final static Logger mLog = LoggerFactory.getLogger(E4KTunerEditor.class);
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private JButton mTunerInfoButton; private JButton mTunerInfoButton;
private JToggleButton mBiasTButton;
private JComboBox<SampleRate> mSampleRateCombo; private JComboBox<SampleRate> mSampleRateCombo;
private JComboBox<E4KGain> mMasterGainCombo; private JComboBox<E4KGain> mMasterGainCombo;
private JComboBox<E4KMixerGain> mMixerGainCombo; private JComboBox<E4KMixerGain> mMixerGainCombo;
@@ -95,7 +97,8 @@ public class E4KTunerEditor extends TunerEditor<RTL2832Tuner, E4KTunerConfigurat
add(getTunerInfoButton()); add(getTunerInfoButton());
add(new JLabel("Status:")); add(new JLabel("Status:"));
add(getTunerStatusLabel(), "wrap"); add(getTunerStatusLabel());
add(getBiasTButton(), "wrap");
add(getButtonPanel(), "span,align left"); add(getButtonPanel(), "span,align left");
@@ -144,6 +147,8 @@ public class E4KTunerEditor extends TunerEditor<RTL2832Tuner, E4KTunerConfigurat
if(hasTuner()) if(hasTuner())
{ {
getBiasTButton().setEnabled(true);
getBiasTButton().setSelected(getConfiguration().isBiasT());
getTunerInfoButton().setEnabled(true); getTunerInfoButton().setEnabled(true);
getSampleRateCombo().setEnabled(true); getSampleRateCombo().setEnabled(true);
getSampleRateCombo().setSelectedItem(getConfiguration().getSampleRate()); getSampleRateCombo().setSelectedItem(getConfiguration().getSampleRate());
@@ -174,6 +179,8 @@ public class E4KTunerEditor extends TunerEditor<RTL2832Tuner, E4KTunerConfigurat
} }
else else
{ {
getBiasTButton().setEnabled(false);
getBiasTButton().setSelected(false);
getTunerInfoButton().setEnabled(false); getTunerInfoButton().setEnabled(false);
getSampleRateCombo().setEnabled(false); getSampleRateCombo().setEnabled(false);
getMasterGainCombo().setEnabled(false); getMasterGainCombo().setEnabled(false);
@@ -187,6 +194,28 @@ public class E4KTunerEditor extends TunerEditor<RTL2832Tuner, E4KTunerConfigurat
setLoading(false); setLoading(false);
} }
/**
* Bias-T toggle button
* @return
*/
private JToggleButton getBiasTButton()
{
if(mBiasTButton == null)
{
mBiasTButton = new JToggleButton("Bias-T");
mBiasTButton.setEnabled(false);
mBiasTButton.addActionListener(e -> {
if(!isLoading())
{
getTuner().getController().setBiasT(mBiasTButton.isSelected());
save();
}
});
}
return mBiasTButton;
}
private JComboBox getIfGainCombo() private JComboBox getIfGainCombo()
{ {
if(mIfGainCombo == null) if(mIfGainCombo == null)
@@ -459,7 +488,7 @@ public class E4KTunerEditor extends TunerEditor<RTL2832Tuner, E4KTunerConfigurat
double value = ((SpinnerNumberModel)getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue(); double value = ((SpinnerNumberModel)getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue();
config.setFrequencyCorrection(value); config.setFrequencyCorrection(value);
config.setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); config.setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
config.setBiasT(getTuner().getController().isBiasT());
config.setSampleRate((SampleRate)getSampleRateCombo().getSelectedItem()); config.setSampleRate((SampleRate)getSampleRateCombo().getSelectedItem());
config.setMasterGain((E4KGain)getMasterGainCombo().getSelectedItem()); config.setMasterGain((E4KGain)getMasterGainCombo().getSelectedItem());
config.setMixerGain((E4KMixerGain)getMixerGainCombo().getSelectedItem()); config.setMixerGain((E4KMixerGain)getMixerGainCombo().getSelectedItem());

View File

@@ -0,0 +1,842 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.rtl.fc0013;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.rtl.EmbeddedTuner;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
import java.text.DecimalFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.LibUsbException;
/**
* Fitipower FC0013 Tuner
* <p>
* Ported from: https://github.com/osmocom/rtl-sdr/blob/master/src/tuner_fc0013.c
*
* Register: Original Contents / Shadow Contents - Comments
* 0x00: .. / 00 - Dummy register for 0-index
* 0x01: 05 / 09
* 0x02: 10 / 16
* 0x03: 00 / 00
* 0x04: 00 / 00
* 0x05: 0A / 17
* 0x06: 0A / 02 - Low Pass Filter Bandwidth
* 0x07: 00 / 2A - 0x2x setup for 28.8MHz crystal
* 0x08: B0 / FF - AGC clock divide by 256, AGC gain 1/256, Loop bw 1/8
* 0x09: 6E / 6E - Disable loop through, enable loop through: 0x6F
* 0x0A: B8 / B8 - Disable LO Test Buffer
* 0x0B: 82 / 82
* 0x0C: 7C / FC - Depending on AGC Up-Down mode, may need 0xF8
* 0x0D: 00 / 01 - AGC not forcing & LNA forcing, we may need 0x02
* 0x0E: 00 / 00
* 0x0F: 00 / 00
* 0x10: 00 / 00
* 0x11: 08 / 00
* 0x12: 00 / 00
* 0x13: 00 / 00
* 0x14: C0 / 50 - DVB-t=High Gain, UHF=middle gain (0x48), or low gain (0x40)
* 0x15: 01 / 01
*/
public class FC0013EmbeddedTuner extends EmbeddedTuner
{
private final static Logger mLog = LoggerFactory.getLogger(FC0013EmbeddedTuner.class);
private DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.000000");
private static final long MINIMUM_SUPPORTED_FREQUENCY = 13_500_000;
private static final long MAXIMUM_SUPPORTED_FREQUENCY = 1_907_999_890l;
private static final double USABLE_BANDWIDTH_PERCENT = 0.95;
private static final int DC_SPIKE_AVOID_BUFFER = 15000;
//Hardware I2C address
private static final byte I2C_ADDRESS = (byte) 0xC6;
//Hardware crystal oscillator frequency - used by the frequency divider
private static final int XTAL_FREQUENCY = 28_800_000;
private static final int XTAL_FREQUENCY_DIVIDED_BY_2 = XTAL_FREQUENCY / 2;
//Initial register settings to apply to the tuner
private static byte[] REGISTERS = {(byte) 0x00, (byte) 0x09, (byte) 0x16, (byte) 0x00, (byte) 0x00, (byte) 0x17,
(byte) 0x02, (byte) 0x2A, (byte) 0xFF, (byte) 0x6E, (byte) 0xB8, (byte) 0x82, (byte) 0xFE, (byte) 0x01,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x50, (byte) 0x01};
private long mTunedFrequency = MINIMUM_SUPPORTED_FREQUENCY;
/**
* Constructs an instance
*
* @param adapter for controlling the RTL2832
*/
public FC0013EmbeddedTuner(RTL2832TunerController.ControllerAdapter adapter)
{
super(adapter);
}
@Override
public TunerType getTunerType()
{
return TunerType.FITIPOWER_FC0013;
}
@Override
public long getMinimumFrequencySupported()
{
return MINIMUM_SUPPORTED_FREQUENCY;
}
@Override
public long getMaximumFrequencySupported()
{
return MAXIMUM_SUPPORTED_FREQUENCY;
}
@Override
public int getDcSpikeHalfBandwidth()
{
return DC_SPIKE_AVOID_BUFFER;
}
@Override
public double getUsableBandwidthPercent()
{
return USABLE_BANDWIDTH_PERCENT;
}
/**
* Applies the tuner configuration settings.
* @param tunerConfig containing settings to apply
* @throws SourceException if there is an error.
*/
@Override
public void apply(TunerConfiguration tunerConfig) throws SourceException
{
if(tunerConfig instanceof FC0013TunerConfiguration config)
{
try
{
setGain(config.getAGC(), config.getLnaGain());
}
catch(Exception e)
{
throw new SourceException("Error while applying tuner config", e);
}
}
else
{
throw new IllegalArgumentException("Unrecognized tuner config [" + tunerConfig.getClass() + "]");
}
}
/**
* Writes the value to the FC0013 register and optionally controls the I2C repeater
*
* @param register to write
* @param value to write
* @param controlI2CRepeater true to turn on the I2C repeater before and after the write operation. If this is
* set to false, you need to have previously enabled the I2C repeater before invoking this method call.
*/
private void write(Register register, byte value, boolean controlI2CRepeater)
{
getAdapter().writeI2CRegister(I2C_ADDRESS, register.byteValue(), value, controlI2CRepeater);
}
/**
* Writes the value to the FC0013 register and optionally controls the I2C repeater
*
* @param register to write
* @param value to write
* @param mask to use when reading the full register byte that has ones in the bits that should be untouched and
* zeros in the bits that should be cleared prior to applying (or'ing) the value into the register.
* @param controlI2CRepeater true to turn on the I2C repeater before and after the write operation. If this is
* set to false, you need to have previously enabled the I2C repeater before invoking this method call.
*/
public void writeMaskedRegister(Register register, byte value, byte mask, boolean controlI2CRepeater)
{
byte content = (byte) (readRegister(register, controlI2CRepeater) & 0xFF);
content &= mask;
content |= value;
write(register, content, controlI2CRepeater);
}
/**
* Writes the value to a field within an FC0013 register and optionally controls the I2C repeater
*
* @param field to write
* @param value to write
* @param controlI2CRepeater true to turn on the I2C repeater before and after the write operation. If this is
* set to false, you need to have previously enabled the I2C repeater before invoking this method call.
*/
private void write(Field field, byte value, boolean controlI2CRepeater)
{
writeMaskedRegister(field.getRegister(), value, field.getMask(), controlI2CRepeater);
}
/**
* Reads the FC0013 tuner register value.
*
* @param register to read
* @param controlI2CRepeater true to turn on the I2C repeater before and after the write operation. If this is
* set to false, you need to have previously enabled the I2C repeater before invoking this method call.
* @return register value.
*/
private int readRegister(Register register, boolean controlI2CRepeater)
{
return getAdapter().readI2CRegister(I2C_ADDRESS, register.byteValue(), controlI2CRepeater);
}
@Override
public void setSamplingMode(RTL2832TunerController.SampleMode mode) throws LibUsbException
{
//No-op
}
@Override
public void setSampleRateFilters(int sampleRate) throws SourceException
{
//No-op
}
/**
* Currently tuned frequency.
* @return frequency
* @throws SourceException never
*/
public long getTunedFrequency() throws SourceException
{
return mTunedFrequency;
}
/**
* Sets the frequency value to the tuner. Note: this is managed by the frequency controller along with PPM adjust.
* @param frequency in Hertz to apply to the tuner
* @throws SourceException if there is an error.
*/
@Override
public synchronized void setTunedFrequency(long frequency) throws SourceException
{
getAdapter().getLock().lock();
try
{
//Capture current I2C repeater state and enable if necessary.
boolean i2CEnabledState = getAdapter().isI2CRepeaterEnabled();
if(!i2CEnabledState)
{
getAdapter().enableI2CRepeater();
}
setVhfTracking(frequency);
setFilters(frequency);
FrequencyDivider divider = FrequencyDivider.fromFrequency(frequency);
boolean vcoSelect = ((double)frequency / (double)divider.getIntegralFrequencyMultiplier()) >= 212.5;
int integral = (int)(frequency / divider.getIntegralFrequencyMultiplier());
int pm = integral / 8; //pm is units of 8x integrals
pm = Math.max(pm, 11);
pm = Math.min(pm, 31);
int am = integral - (pm * 8); //am is units of 1x integrals
if(am < 2)
{
am += 8;
pm--;
}
am = Math.min(am, 15);
integral = pm * 8 + am;
int residual = (int)(frequency - (integral * divider.getIntegralFrequencyMultiplier()));
//fractional is units of 1/65536th of 2x integrals
int fractional = (int)Math.round(residual / divider.getFractionalFrequencyMultiplier());
if(pm < 11 || pm > 31 || am < 2 || am > 15 || fractional < 0 || fractional > 65535)
{
String message = "FC0013 - no valid PLL combination for frequency [" + frequency + "] using divider [" +
divider + "] pm [" + pm + "] am [" + am + "] fractional [" + fractional + "]";
mLog.error(message);
throw new SourceException(message);
}
setFrequencyValues(divider, pm, am, fractional, vcoSelect);
//Restore the I2C repeater to previous state
if(!i2CEnabledState)
{
getAdapter().disableI2CRepeater();
}
}
catch(LibUsbException e)
{
mLog.error("FC0013 tuner error while setting tuned frequency [" + frequency + "]", e);
}
finally
{
getAdapter().getLock().unlock();
}
mTunedFrequency = frequency;
}
/**
* Apply initial settings to the tuner
*/
protected void initTuner()
{
getAdapter().enableI2CRepeater();
boolean i2CRepeaterControl = false;
//Write initial register settings to the tuner
for(int x = 1; x < REGISTERS.length; x++)
{
write(Register.values()[x], REGISTERS[x], i2CRepeaterControl);
}
write(Field.R0D_AGC, (byte)0x08, false);
write(Register.R13, (byte)0x0A, false);
getAdapter().disableI2CRepeater();
}
/**
* Applies the frequency settings and calibrates the PLL. Note: I2C repeater must be enabled before invoking.
* @param divider to use
* @param pm units of 8x integrals
* @param am units of 1x integrals
* @param fractional value.
* @param vcoSelect to enable (true) or disable (false)
* @throws SourceException if the settings are invalid or the PLL can't be calibrated with the settings.
* @return true if the frequency settings were applied successfully.
*/
private boolean setFrequencyValues(FrequencyDivider divider, int pm, int am, int fractional, boolean vcoSelect)
throws SourceException
{
if(pm < 11 || pm > 31)
{
throw new IllegalArgumentException("PM value [" + pm + "] must be in range 11-31");
}
if(am < 2 || am > 15)
{
throw new IllegalArgumentException("AM value [" + am + "] must be in range 2-15");
}
if(fractional < 0 || fractional > 65535)
{
throw new IllegalArgumentException("Fractional value [" + fractional + "] must be in range 0-65,535");
}
double exactFrequency = divider.calculate(pm, am, fractional);
long frequency = (long)exactFrequency;
byte register5 = divider.getRegister5();
register5 |= 0x07; //modified for Realtek demod
byte register6 = divider.getRegister6();
if(vcoSelect)
{
register6 |= 0x08;
}
//Write the integral units to the PLL
write(Register.R01, (byte)(am & 0xFF), false);
write(Register.R02, (byte)(pm & 0xFF), false);
//Write the fractional units to the PLL
write(Register.R03, (byte)((fractional >> 8) & 0xFF), false);
write(Register.R04, (byte)(fractional & 0xFF), false);
//Set the frequency divider, bandwidth, and enable the clock output
write(Register.R05, register5, false);
write(Register.R06, register6, false);
//Whatever register 11 does ... seems like it's a gain thing.
int tmp = readRegister(Register.R11, false);
if(divider == FrequencyDivider.D64)
{
write(Register.R11, (byte)(tmp | 0x04), false);
}
else
{
write(Register.R11, (byte)(tmp & 0xFB), false);
}
//Perform VCO Calibration
write(Register.R0E, (byte)0x80, false);
write(Register.R0E, (byte)0x00, false);
write(Register.R0E, (byte)0x00, false);
int calibration = readRegister(Register.R0E, false) & 0x3F;
boolean recalibrateRequired = false;
if(vcoSelect && calibration > 0x3C)
{
register6 &= ~0x08;
recalibrateRequired = true;
}
else if(!vcoSelect && calibration < 0x02)
{
register6 |= 0x08;
recalibrateRequired = true;
}
if(recalibrateRequired)
{
write(Register.R06, register6, false);
write(Register.R0E, (byte)0x80, false);
write(Register.R0E, (byte)0x00, false);
write(Register.R0E, (byte)0x00, false);
calibration = readRegister(Register.R0E, false) & 0x3F;
if((!vcoSelect & calibration < 0x02) || (vcoSelect & calibration > 0x3C))
{
String msg = "Unable to tune frequency [" + fractional + "] PLL calibration [" + Integer.toHexString(calibration).toUpperCase() + "] out of limits [02-3C]";
mLog.error(msg);
throw new SourceException(msg);
}
}
return true;
}
/**
* Sets the tuner filters according to the tuned frequency. Note: I2C repeater must be enabled before invoking.
* @param frequency of the tuner
*/
private void setFilters(long frequency)
{
byte vhf = (byte)0x00; //default to disabled
byte uhf = (byte)0x00; //default to disabled
if(frequency < 300_000_000)
{
vhf = (byte)0x10; //enable
}
else if(frequency < 862_000_000)
{
uhf = (byte)0x40;
}
else
{
//The original source code seems to target a GPS filter, but it's using the same uhf filter setting.
uhf = (byte)0x40;
}
write(Field.R07_VHF_FILTER, vhf, false);
write(Field.R14_UHF_FILTER, uhf, false);
}
/**
* Sets VHF tracking according to the tuned frequency. Note: I2C repeater must be enabled before invoking.
* @param frequency for the tuner
*/
private void setVhfTracking(long frequency)
{
int tmp = readRegister(Register.R1D, false);
//Clear the middle 3x bits that we're going to set and leave the remaining bits untouched.
tmp &= 0xE3;
if(frequency <= 177_500_000)
{
tmp |= 0x1C; //VHF Track 7
}
else if(frequency <= 184_500_000)
{
tmp |= 0x18; //VHF Track 6
}
else if(frequency <= 191_500_000)
{
tmp |= 0x14; //VHF Track 5
}
else if(frequency <= 198_500_000)
{
tmp |= 0x10; //VHF Track 4
}
else if(frequency <= 205_500_000)
{
tmp |= 0x0C; //VHF Track 3
}
else if(frequency <= 219_500_000)
{
tmp |= 0x08; //VHF Track 2
}
else if(frequency <= 300_000_000)
{
tmp |= 0x04; //VHF Track 1
}
else
{
tmp |= 0x1C; //UHF & GPS
}
write(Register.R1D, (byte) tmp, false);
}
/**
* Sets the gain for the tuner. Note: if agc=true, the manual gain setting is ignored.
* @param agc automatic gain control.
* @param manualGain setting.
*/
public void setGain(boolean agc, LNAGain manualGain)
{
getAdapter().getLock().lock();
try
{
boolean repeaterState = getAdapter().isI2CRepeaterEnabled();
if(!repeaterState)
{
getAdapter().enableI2CRepeater();
}
if(agc)
{
write(Field.R0D_AGC, (byte)0x00, false);
write(Register.R13, (byte)0x0A, false);
}
else
{
write(Field.R14_LNA_GAIN, manualGain.byteValue(), false);
write(Field.R0D_AGC, (byte)0x08, false);
write(Register.R13, (byte)0x0A, false);
}
if(!repeaterState)
{
getAdapter().disableI2CRepeater();
}
}
finally
{
getAdapter().getLock().unlock();
}
}
/**
* Enumeration of FC0013 registers. Provides convenience method for accessing the entry's ordinal position as a
* byte value for reading/writing.
*/
private enum Register
{
R00, R01, R02, R03, R04, R05, R06, R07, R08, R09, R0A, R0B, R0C, R0D, R0E, R0F, R10, R11, R12, R13, R14, R15,
R16, R17, R18, R19, R1A, R1B, R1C, R1D;
/**
* Returns the ordinal position of the entry within the enumeration as a byte value.
*/
public byte byteValue()
{
return (byte) ordinal();
}
}
/**
* Masked tuner register fields enumeration
*/
private enum Field
{
R06_BANDWIDTH(Register.R06, 0xC0),
R07_VHF_FILTER(Register.R07, 0xEF),
R0D_AGC(Register.R0D, 0xF7),
R14_LNA_GAIN(Register.R14, 0xE0),
R14_UHF_FILTER(Register.R14, 0x1F);
private Register mRegister;
private byte mMask;
/**
* Constructs an instance
* @param register for the field
* @param mask for the field
*/
Field(Register register, int mask)
{
mRegister = register;
mMask = (byte) mask;
}
/**
* Register for the field.
* @return register
*/
public Register getRegister()
{
return mRegister;
}
/**
* Bit field mask for the register that masks out the target field bits prior to applying a value.
* @return mask value.
*/
public byte getMask()
{
return mMask;
}
}
/**
* LNA gain values enumeration
*/
public enum LNAGain
{
G00("0 LOW", 0x02),
G01("1", 0x03),
G02("2", 0x05),
G03("3", 0x04),
G04("4", 0x00),
G05("5", 0x07),
G06("6", 0x01),
G07("7", 0x06),
G08("8", 0x0F),
G09("9", 0x0E),
G10("10", 0x0D),
G11("11", 0x0C),
G12("12", 0x0B),
G13("13", 0x0A),
G14("14", 0x09),
G15("15", 0x08),
G16("16", 0x17),
G17("17", 0x16),
G18("18", 0x15),
G19("19", 0x14),
G20("20", 0x13),
G21("21", 0x12),
G22("22", 0x11),
G23("23 HIGH", 0x10);
private String mLabel;
private byte mValue;
/**
* Constructs an instance
*
* @param label to show to the user
* @param value to apply to the register
*/
LNAGain(String label, int value)
{
mLabel = label;
mValue = (byte) value;
}
/**
* Byte value for the gain setting.
*
* @return byte value.
*/
public byte byteValue()
{
return mValue;
}
@Override
public String toString()
{
return mLabel;
}
}
/**
* SDM PLL Frequency Divider and Supported Frequency Ranges
*
* Note: each divider frequency range is overlapping with other divider frequency range(s) and also within each
* frequency divider there are overlapping frequency range across each PM value. Each unit of PM is 8x the integral
* and each unit of AM is 1x the integral. You can specify a range of 2-15 units of AM which causes overlap when
* each unit of PM represents 8x units of AM.
*
* So, for a requested frequency, there are likely multiple combinations of Divider/PM/AM/Fractional that will
* tune the frequency, each with varying degrees of correctness.
*
* For each divider range, minimum and maximum frequency is calculated as:
* Min: PM = 11, AM = 2, FRACTIONAL = 0
* Max: PM = 31, AM = 15, FRACTIONAL = 65,535, or the highest setting that can be calibrated by the VCO.
*
* integral = XTAL_FREQ / 2 / divider
*
* Where: (integral * ((PM * 8) + AM)) + (2 * integral / 65,536 * fractional)
*
* Minimum tunable frequency step size is (2 * integral / 65,536).
*/
public enum FrequencyDivider
{
D96(96, true, 0x82, 13_500_000, 39_749_997),
D64(64, false, 0x02, 20_250_000, 59_624_996), //Special one, why?
D48(48, true, 0x42, 27_000_000, 79_499_995),
D32(32, false, 0x82, 40_500_000, 119_249_993),
D24(24, true, 0x22, 54_000_000, 158_999_990),
D16(16, false, 0x42, 81_000_000, 238_499_986),
D12(12, true, 0x12, 108_000_000, 317_999_981),
D08(8, false, 0x22, 162_000_000, 476_999_972),
D06(6, true, 0x0A, 235_200_000, 635_999_963),
D04(4, false, 0x12, 514_900_000, 953_999_945),
D02(2, false, 0x0A, 648_000_000, 1_907_999_890);
private int mDivider;
private boolean mIs3xMode;
private int mRegister5;
private long mMinimumFrequency;
private long mMaximumFrequency;
/**
* Constructs an instance
* @param divider quantity
* @param is3xMode indicates 3x divider (true) or 2x divider (false)
* @param register5 setting
* @param minimumFrequency supported by the divider
* @param maximumFrequency supported by the divider
*/
FrequencyDivider(int divider, boolean is3xMode, int register5, long minimumFrequency, long maximumFrequency)
{
mDivider = divider;
mIs3xMode = is3xMode;
mRegister5 = register5;
mMinimumFrequency = minimumFrequency;
mMaximumFrequency = maximumFrequency;
}
/**
* Register 5 frequency multiplier setting
*/
public byte getRegister5()
{
return (byte)(mRegister5 & 0xFF);
}
/**
* Register 6 multiplier mode setting (2x or 3x)
*/
public byte getRegister6()
{
//0xAx OR Enable clock (0x20) OR bandwidth set to 6 MHz (0x80).
return is3xMode() ? (byte)0xA0 : (byte)0xA2;
}
/**
* Calculates the frequency for this frequency divider.
* @param pm - 8x integral units
* @param am - 1x integral units
* @param fractional integral units
* @param vcoSelect to indicate if the frequency requires VCO select (ie AM + 2)
* @return frequency
*/
public double getFrequency(int pm, int am, int fractional, boolean vcoSelect)
{
if(vcoSelect)
{
return calculate(pm, am, fractional);
}
else
{
return calculate(pm, am - 2, fractional);
}
}
/**
* Indicates if the frequency requires VCO select for this divider.
* @param frequency to evaluate
* @return true if vco select
*/
public boolean isVcoSelect(double frequency)
{
return (frequency * getDivider()) > 3_060_000_000l;
}
private double calculate(int pm, int am, int fractional)
{
return (((8l * pm) + am) * getIntegralFrequencyMultiplier()) + (getFractionalFrequencyMultiplier() * fractional);
}
/**
* Integral unit of frequency multiplier supported by the PLL for the specified frequency divider.
* @return integral frequency multiplier.
*/
public int getIntegralFrequencyMultiplier()
{
return XTAL_FREQUENCY_DIVIDED_BY_2 / mDivider;
}
/**
* Fractional (1/65536th) unit of frequency of 2x integrals that is supported by the 16-bit fractional PLL.
* @return fractional frequency multiplier
*/
public double getFractionalFrequencyMultiplier()
{
return XTAL_FREQUENCY / mDivider / 65_536d;
}
/**
* Indicates if this divider is mode 2x (false) or mode 3x (true) as a divider of the base crystal frequency.
* @return true if 3x mode.
*/
public boolean is3xMode()
{
return mIs3xMode;
}
/**
* Divider value (of the base crystal frequency).
* @return divider
*/
public int getDivider()
{
return mDivider;
}
/**
* Minimum frequency supported by this divider.
* @return minimum frequency (Hz).
*/
public long getMinimumFrequency()
{
return mMinimumFrequency;
}
/**
* Maximum frequency supported by this divider.
* @return maximum frequency (Hz).
*/
public long getMaximumFrequency()
{
return mMaximumFrequency;
}
/**
* Indicates if the frequency is contained by the min/max values of the current frequency divider entry.
* @param frequency to test
* @return true if contained.
*/
public boolean contains(long frequency)
{
return mMinimumFrequency <= frequency && frequency <= mMaximumFrequency;
}
/**
* Returns the matching frequency divider for the specified frequency, or returns divider 16x, if the frequency
* is outside any supported frequency ranges.
*
* @param frequency - desired frequency
* @return - FrequencyDivider to use for the specified frequency
*/
public static FrequencyDivider fromFrequency(long frequency)
{
for(FrequencyDivider divider : FrequencyDivider.values())
{
if(divider.contains(frequency))
{
return divider;
}
}
return FrequencyDivider.D16;
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.rtl.fc0013;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerConfiguration;
/**
* RTL-2832 with embedded FC0013 tuner configuration
*/
public class FC0013TunerConfiguration extends RTL2832TunerConfiguration
{
private FC0013EmbeddedTuner.LNAGain mLnaGain = FC0013EmbeddedTuner.LNAGain.G10;
private boolean mAgc = false;
/**
* Default constructor for JAXB
*/
public FC0013TunerConfiguration()
{
}
@JsonIgnore
@Override
public TunerType getTunerType()
{
return TunerType.FITIPOWER_FC0013;
}
/**
* Constructs an instance
*
* @param uniqueID for the tuner
*/
public FC0013TunerConfiguration(String uniqueID)
{
super(uniqueID);
}
/**
* LNA gain value.
* @return value
*/
@JacksonXmlProperty(isAttribute = true, localName = "lna_gain")
public FC0013EmbeddedTuner.LNAGain getLnaGain()
{
return mLnaGain;
}
/**
* Sets the LNA gain value.
* @param lnaGain value.
*/
public void setLnaGain(FC0013EmbeddedTuner.LNAGain lnaGain)
{
mLnaGain = lnaGain;
}
/**
* Automatic Gain Control (AGC) state
* @return AGC enabled state
*/
@JacksonXmlProperty(isAttribute = true, localName = "agc")
public boolean getAGC()
{
return mAgc;
}
/**
* Sets the AGC
* @param enabled state of the AGC
*/
public void setAGC(boolean enabled)
{
mAgc = enabled;
}
}

View File

@@ -0,0 +1,404 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.rtl.fc0013;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.rtl.RTL2832Tuner;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController.SampleRate;
import io.github.dsheirer.source.tuner.ui.TunerEditor;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.usb4java.LibUsbException;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JToggleButton;
import javax.swing.SpinnerNumberModel;
/**
* FC0013 Tuner Editor
*/
public class FC0013TunerEditor extends TunerEditor<RTL2832Tuner, FC0013TunerConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(FC0013TunerEditor.class);
private static final long serialVersionUID = 1L;
private static final FC0013EmbeddedTuner.LNAGain DEFAULT_LNA_GAIN = FC0013EmbeddedTuner.LNAGain.G14;
private JButton mTunerInfoButton;
private JToggleButton mBiasTButton;
private JComboBox<SampleRate> mSampleRateCombo;
private JToggleButton mAgcToggleButton;
private JComboBox<FC0013EmbeddedTuner.LNAGain> mLNAGainCombo;
/**
* Constructs an instance
* @param userPreferences for wide-band recordings
* @param tunerManager for saving configurations
* @param discoveredTuner to edit
*/
public FC0013TunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner)
{
super(userPreferences, tunerManager, discoveredTuner);
init();
tunerStatusUpdated();
}
/**
* Access the FC0013 embedded tuner
* @return tuner if there is a tuner, or null otherwise
*/
private FC0013EmbeddedTuner getEmbeddedTuner()
{
if(hasTuner())
{
return (FC0013EmbeddedTuner) getTuner().getController().getEmbeddedTuner();
}
return null;
}
private String getLogPrefix()
{
return getEmbeddedTuner().getTunerType().getLabel() + " Tuner Controller - ";
}
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
"[][][][][][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
add(getTunerIdLabel());
add(getTunerInfoButton());
add(new JLabel("Status:"));
add(getTunerStatusLabel());
add(getBiasTButton(), "wrap");
add(getButtonPanel(), "span,align left");
add(new JSeparator(), "span,growx,push");
add(new JLabel("Frequency (MHz):"));
add(getFrequencyPanel(), "wrap");
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JSeparator(), "span,growx,push");
JPanel gainPanel = new JPanel();
gainPanel.add(new JLabel("Gain"));
gainPanel.add(getAgcToggleButton());
gainPanel.add(new JLabel("LNA:"));
add(gainPanel);
add(getLNAGainCombo(), "wrap");
}
@Override
protected void tunerStatusUpdated()
{
setLoading(true);
if(hasTuner())
{
getTunerIdLabel().setText(getTuner().getPreferredName());
}
else
{
getTunerIdLabel().setText(getDiscoveredTuner().getId());
}
String status = getDiscoveredTuner().getTunerStatus().toString();
if(getDiscoveredTuner().hasErrorMessage())
{
status += " - " + getDiscoveredTuner().getErrorMessage();
}
getTunerStatusLabel().setText(status);
getButtonPanel().updateControls();
getFrequencyPanel().updateControls();
if(hasTuner())
{
getBiasTButton().setEnabled(true);
getBiasTButton().setSelected(getConfiguration().isBiasT());
getTunerInfoButton().setEnabled(true);
getSampleRateCombo().setEnabled(true);
getSampleRateCombo().setSelectedItem(getConfiguration().getSampleRate());
getAgcToggleButton().setEnabled(true);
getAgcToggleButton().setSelected(getConfiguration().getAGC());
getLNAGainCombo().setEnabled(!getConfiguration().getAGC());
getLNAGainCombo().setSelectedItem(getConfiguration().getLnaGain());
}
else
{
getBiasTButton().setEnabled(false);
getBiasTButton().setSelected(false);
getTunerInfoButton().setEnabled(false);
getSampleRateCombo().setEnabled(false);
getAgcToggleButton().setEnabled(false);
getLNAGainCombo().setEnabled(false);
}
updateSampleRateToolTip();
setLoading(false);
}
/**
* Bias-T toggle button
* @return bias-t button
*/
private JToggleButton getBiasTButton()
{
if(mBiasTButton == null)
{
mBiasTButton = new JToggleButton("Bias-T");
mBiasTButton.setEnabled(false);
mBiasTButton.addActionListener(e -> {
if(!isLoading())
{
getTuner().getController().setBiasT(mBiasTButton.isSelected());
save();
}
});
}
return mBiasTButton;
}
/**
* Hyperlink button that provides tuner information
*/
private JButton getTunerInfoButton()
{
if(mTunerInfoButton == null)
{
mTunerInfoButton = new JButton("Info");
mTunerInfoButton.setEnabled(false);
mTunerInfoButton.addActionListener(e -> JOptionPane.showMessageDialog(FC0013TunerEditor.this,
getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE));
}
return mTunerInfoButton;
}
private JComboBox getLNAGainCombo()
{
if(mLNAGainCombo == null)
{
mLNAGainCombo = new JComboBox<>(FC0013EmbeddedTuner.LNAGain.values());
mLNAGainCombo.setEnabled(false);
mLNAGainCombo.addActionListener(arg0 ->
{
if(!isLoading())
{
try
{
FC0013EmbeddedTuner.LNAGain lnaGain = (FC0013EmbeddedTuner.LNAGain) mLNAGainCombo.getSelectedItem();
if(lnaGain == null)
{
lnaGain = DEFAULT_LNA_GAIN;
}
if(mLNAGainCombo.isEnabled())
{
getEmbeddedTuner().setGain(getAgcToggleButton().isSelected(), lnaGain);
}
save();
}
catch(Exception e)
{
JOptionPane.showMessageDialog(FC0013TunerEditor.this, getLogPrefix() +
"couldn't apply the LNA gain setting - " + e.getLocalizedMessage());
mLog.error(getLogPrefix() + "couldn't apply LNA gain setting - ", e);
}
}
});
mLNAGainCombo.setToolTipText("<html>LNA Gain. Set master gain to <b>MANUAL</b> to enable adjustment</html>");
}
return mLNAGainCombo;
}
private JComboBox getSampleRateCombo()
{
if(mSampleRateCombo == null)
{
mSampleRateCombo = new JComboBox<>(SampleRate.values());
mSampleRateCombo.setEnabled(false);
mSampleRateCombo.addActionListener(e ->
{
if(!isLoading())
{
SampleRate sampleRate = (SampleRate) mSampleRateCombo.getSelectedItem();
try
{
getTuner().getController().setSampleRate(sampleRate);
save();
}
catch(SourceException | LibUsbException eSampleRate)
{
JOptionPane.showMessageDialog(FC0013TunerEditor.this,
getLogPrefix() + "couldn't apply the sample rate setting [" +
sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage());
mLog.error(getLogPrefix() + "couldn't apply sample rate setting [" + sampleRate.getLabel() +
"]", eSampleRate);
}
}
});
}
return mSampleRateCombo;
}
private JToggleButton getAgcToggleButton()
{
if(mAgcToggleButton == null)
{
mAgcToggleButton = new JToggleButton("AGC");
mAgcToggleButton.setEnabled(false);
mAgcToggleButton.addActionListener(arg0 ->
{
if(!isLoading())
{
try
{
boolean agc = getAgcToggleButton().isSelected();
FC0013EmbeddedTuner.LNAGain lnaGain = (FC0013EmbeddedTuner.LNAGain)getLNAGainCombo().getSelectedItem();
getEmbeddedTuner().setGain(agc, lnaGain);
getLNAGainCombo().setEnabled(!agc);
save();
}
catch(Exception e)
{
JOptionPane.showMessageDialog(FC0013TunerEditor.this, getLogPrefix() +
"couldn't set AGC" + e.getLocalizedMessage());
mLog.error(getLogPrefix() + "couldn't set AGC", e);
}
}
});
mAgcToggleButton.setToolTipText("<html>Automatic Gain Control (AGC). </html>");
}
return mAgcToggleButton;
}
/**
* Updates the sample rate tooltip according to the tuner controller's lock state.
*/
private void updateSampleRateToolTip()
{
if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
{
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else if(hasTuner())
{
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
else
{
getSampleRateCombo().setToolTipText("No tuner available");
}
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
private String getTunerInfo()
{
StringBuilder sb = new StringBuilder();
RTL2832TunerController.Descriptor descriptor = getTuner().getController().getDescriptor();
sb.append("<html><h3>RTL-2832 with " + getEmbeddedTuner().getTunerType().getLabel() + " Tuner</h3>");
if(descriptor == null)
{
sb.append("No EEPROM Descriptor Available");
}
else
{
sb.append("<b>USB ID: </b>");
sb.append(descriptor.getVendorID());
sb.append(":");
sb.append(descriptor.getProductID());
sb.append("<br>");
sb.append("<b>Vendor: </b>");
sb.append(descriptor.getVendorLabel());
sb.append("<br>");
sb.append("<b>Product: </b>");
sb.append(descriptor.getProductLabel());
sb.append("<br>");
sb.append("<b>Serial: </b>");
sb.append(descriptor.getSerial());
sb.append("<br>");
sb.append("<b>IR Enabled: </b>");
sb.append(descriptor.irEnabled());
sb.append("<br>");
sb.append("<b>Remote Wake: </b>");
sb.append(descriptor.remoteWakeupEnabled());
sb.append("<br>");
}
return sb.toString();
}
@Override
public void save()
{
if(hasConfiguration() && !isLoading())
{
FC0013TunerConfiguration config = getConfiguration();
config.setBiasT(getTuner().getController().isBiasT());
config.setFrequency(getFrequencyControl().getFrequency());
double value = ((SpinnerNumberModel)getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue();
config.setFrequencyCorrection(value);
config.setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
config.setSampleRate((SampleRate)getSampleRateCombo().getSelectedItem());
config.setAGC(getAgcToggleButton().isSelected());
FC0013EmbeddedTuner.LNAGain lnaGain = (FC0013EmbeddedTuner.LNAGain)getLNAGainCombo().getSelectedItem();
config.setLnaGain(lnaGain);
saveConfiguration();
}
}
}

View File

@@ -1,6 +1,6 @@
/* /*
* ***************************************************************************** * *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer * Copyright (C) 2014-2023 Dennis Sheirer
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@@ -16,87 +16,78 @@
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
* **************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.source.tuner.rtl.r820t; package io.github.dsheirer.source.tuner.rtl.r8x;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerConfiguration; import io.github.dsheirer.source.tuner.rtl.RTL2832TunerConfiguration;
/** /**
* RTL-2832 with embedded R820T tuner configuration * RTL-2832 with embedded R8xxx tuner configuration
*/ */
public class R820TTunerConfiguration extends RTL2832TunerConfiguration public abstract class R8xTunerConfiguration extends RTL2832TunerConfiguration
{ {
private R820TEmbeddedTuner.R820TGain mMasterGain = R820TEmbeddedTuner.R820TGain.GAIN_327; private R8xEmbeddedTuner.MasterGain mMasterMasterGain = R8xEmbeddedTuner.MasterGain.GAIN_327;
private R820TEmbeddedTuner.R820TMixerGain mMixerGain = R820TEmbeddedTuner.R820TMixerGain.GAIN_105; private R8xEmbeddedTuner.MixerGain mMixerGain = R8xEmbeddedTuner.MixerGain.GAIN_105;
private R820TEmbeddedTuner.R820TLNAGain mLNAGain = R820TEmbeddedTuner.R820TLNAGain.GAIN_222; private R8xEmbeddedTuner.LNAGain mLNAGain = R8xEmbeddedTuner.LNAGain.GAIN_222;
private R820TEmbeddedTuner.R820TVGAGain mVGAGain = R820TEmbeddedTuner.R820TVGAGain.GAIN_210; private R8xEmbeddedTuner.VGAGain mVGAGain = R8xEmbeddedTuner.VGAGain.GAIN_210;
/** /**
* Default constructor for JAXB * Default constructor for JAXB
*/ */
public R820TTunerConfiguration() public R8xTunerConfiguration()
{ {
} }
@JsonIgnore
@Override
public TunerType getTunerType()
{
return TunerType.RAFAELMICRO_R820T;
}
/** /**
* Constructs an instance * Constructs an instance
* *
* @param uniqueID for the tuner * @param uniqueID for the tuner
*/ */
public R820TTunerConfiguration(String uniqueID) public R8xTunerConfiguration(String uniqueID)
{ {
super(uniqueID); super(uniqueID);
} }
@JacksonXmlProperty(isAttribute = true, localName = "master_gain") @JacksonXmlProperty(isAttribute = true, localName = "master_gain")
public R820TEmbeddedTuner.R820TGain getMasterGain() public R8xEmbeddedTuner.MasterGain getMasterGain()
{ {
return mMasterGain; return mMasterMasterGain;
} }
public void setMasterGain(R820TEmbeddedTuner.R820TGain gain) public void setMasterGain(R8xEmbeddedTuner.MasterGain masterGain)
{ {
mMasterGain = gain; mMasterMasterGain = masterGain;
} }
@JacksonXmlProperty(isAttribute = true, localName = "mixer_gain") @JacksonXmlProperty(isAttribute = true, localName = "mixer_gain")
public R820TEmbeddedTuner.R820TMixerGain getMixerGain() public R8xEmbeddedTuner.MixerGain getMixerGain()
{ {
return mMixerGain; return mMixerGain;
} }
public void setMixerGain(R820TEmbeddedTuner.R820TMixerGain mixerGain) public void setMixerGain(R8xEmbeddedTuner.MixerGain mixerGain)
{ {
mMixerGain = mixerGain; mMixerGain = mixerGain;
} }
@JacksonXmlProperty(isAttribute = true, localName = "lna_gain") @JacksonXmlProperty(isAttribute = true, localName = "lna_gain")
public R820TEmbeddedTuner.R820TLNAGain getLNAGain() public R8xEmbeddedTuner.LNAGain getLNAGain()
{ {
return mLNAGain; return mLNAGain;
} }
public void setLNAGain(R820TEmbeddedTuner.R820TLNAGain lnaGain) public void setLNAGain(R8xEmbeddedTuner.LNAGain lnaGain)
{ {
mLNAGain = lnaGain; mLNAGain = lnaGain;
} }
@JacksonXmlProperty(isAttribute = true, localName = "vga_gain") @JacksonXmlProperty(isAttribute = true, localName = "vga_gain")
public R820TEmbeddedTuner.R820TVGAGain getVGAGain() public R8xEmbeddedTuner.VGAGain getVGAGain()
{ {
return mVGAGain; return mVGAGain;
} }
public void setVGAGain(R820TEmbeddedTuner.R820TVGAGain vgaGain) public void setVGAGain(R8xEmbeddedTuner.VGAGain vgaGain)
{ {
mVGAGain = vgaGain; mVGAGain = vgaGain;
} }

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/> * along with this program. If not, see <http://www.gnu.org/licenses/>
* **************************************************************************** * ****************************************************************************
*/ */
package io.github.dsheirer.source.tuner.rtl.r820t; package io.github.dsheirer.source.tuner.rtl.r8x;
import io.github.dsheirer.preference.UserPreferences; import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.SourceException; import io.github.dsheirer.source.SourceException;
@@ -25,10 +25,6 @@ import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.rtl.RTL2832Tuner; import io.github.dsheirer.source.tuner.rtl.RTL2832Tuner;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController; import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController.SampleRate; import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController.SampleRate;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TEmbeddedTuner.R820TGain;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TEmbeddedTuner.R820TLNAGain;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TEmbeddedTuner.R820TMixerGain;
import io.github.dsheirer.source.tuner.rtl.r820t.R820TEmbeddedTuner.R820TVGAGain;
import io.github.dsheirer.source.tuner.ui.TunerEditor; import io.github.dsheirer.source.tuner.ui.TunerEditor;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -40,20 +36,25 @@ import javax.swing.JComboBox;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JOptionPane; import javax.swing.JOptionPane;
import javax.swing.JSeparator; import javax.swing.JSeparator;
import javax.swing.JToggleButton;
import javax.swing.SpinnerNumberModel; import javax.swing.SpinnerNumberModel;
import javax.usb.UsbException; import javax.usb.UsbException;
public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfiguration> /**
* R8xxx Tuner Editor
*/
public class R8xTunerEditor extends TunerEditor<RTL2832Tuner, R8xTunerConfiguration>
{ {
private final static Logger mLog = LoggerFactory.getLogger(R820TTunerEditor.class); private final static Logger mLog = LoggerFactory.getLogger(R8xTunerEditor.class);
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private static final R820TGain DEFAULT_GAIN = R820TGain.GAIN_279; private static final R8xEmbeddedTuner.MasterGain DEFAULT_GAIN = R8xEmbeddedTuner.MasterGain.GAIN_279;
private JButton mTunerInfoButton; private JButton mTunerInfoButton;
private JToggleButton mBiasTButton;
private JComboBox<SampleRate> mSampleRateCombo; private JComboBox<SampleRate> mSampleRateCombo;
private JComboBox<R820TGain> mMasterGainCombo; private JComboBox<R8xEmbeddedTuner.MasterGain> mMasterGainCombo;
private JComboBox<R820TMixerGain> mMixerGainCombo; private JComboBox<R8xEmbeddedTuner.MixerGain> mMixerGainCombo;
private JComboBox<R820TLNAGain> mLNAGainCombo; private JComboBox<R8xEmbeddedTuner.LNAGain> mLNAGainCombo;
private JComboBox<R820TVGAGain> mVGAGainCombo; private JComboBox<R8xEmbeddedTuner.VGAGain> mVGAGainCombo;
/** /**
* Constructs an instance * Constructs an instance
@@ -61,7 +62,7 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
* @param tunerManager for saving configurations * @param tunerManager for saving configurations
* @param discoveredTuner to edit * @param discoveredTuner to edit
*/ */
public R820TTunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner) public R8xTunerEditor(UserPreferences userPreferences, TunerManager tunerManager, DiscoveredTuner discoveredTuner)
{ {
super(userPreferences, tunerManager, discoveredTuner); super(userPreferences, tunerManager, discoveredTuner);
init(); init();
@@ -69,19 +70,24 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
} }
/** /**
* Access the R820T embedded tuner * Access the R8xxx embedded tuner
* @return R820T tuner if there is a tuner, or null otherwise * @return R8xxx tuner if there is a tuner, or null otherwise
*/ */
private R820TEmbeddedTuner getEmbeddedTuner() private R8xEmbeddedTuner getEmbeddedTuner()
{ {
if(hasTuner()) if(hasTuner())
{ {
return (R820TEmbeddedTuner)getTuner().getController().getEmbeddedTuner(); return (R8xEmbeddedTuner) getTuner().getController().getEmbeddedTuner();
} }
return null; return null;
} }
private String getLogPrefix()
{
return getEmbeddedTuner().getTunerType().getLabel() + " Tuner Controller - ";
}
private void init() private void init()
{ {
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]", setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
@@ -92,7 +98,8 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
add(getTunerInfoButton()); add(getTunerInfoButton());
add(new JLabel("Status:")); add(new JLabel("Status:"));
add(getTunerStatusLabel(), "wrap"); add(getTunerStatusLabel());
add(getBiasTButton(), "wrap");
add(getButtonPanel(), "span,align left"); add(getButtonPanel(), "span,align left");
@@ -145,15 +152,17 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
if(hasTuner()) if(hasTuner())
{ {
getBiasTButton().setEnabled(true);
getBiasTButton().setSelected(getConfiguration().isBiasT());
getTunerInfoButton().setEnabled(true); getTunerInfoButton().setEnabled(true);
getSampleRateCombo().setEnabled(true); getSampleRateCombo().setEnabled(true);
getSampleRateCombo().setSelectedItem(getConfiguration().getSampleRate()); getSampleRateCombo().setSelectedItem(getConfiguration().getSampleRate());
getMasterGainCombo().setEnabled(true); getMasterGainCombo().setEnabled(true);
R820TGain gain = getConfiguration().getMasterGain(); R8xEmbeddedTuner.MasterGain gain = getConfiguration().getMasterGain();
getMasterGainCombo().setEnabled(true); getMasterGainCombo().setEnabled(true);
getMasterGainCombo().setSelectedItem(gain); getMasterGainCombo().setSelectedItem(gain);
if(gain == R820TGain.MANUAL) if(gain == R8xEmbeddedTuner.MasterGain.MANUAL)
{ {
getMixerGainCombo().setSelectedItem(getConfiguration().getMixerGain()); getMixerGainCombo().setSelectedItem(getConfiguration().getMixerGain());
getMixerGainCombo().setEnabled(true); getMixerGainCombo().setEnabled(true);
@@ -178,6 +187,8 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
} }
else else
{ {
getBiasTButton().setEnabled(false);
getBiasTButton().setSelected(false);
getTunerInfoButton().setEnabled(false); getTunerInfoButton().setEnabled(false);
getSampleRateCombo().setEnabled(false); getSampleRateCombo().setEnabled(false);
getMasterGainCombo().setEnabled(false); getMasterGainCombo().setEnabled(false);
@@ -191,6 +202,28 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
setLoading(false); setLoading(false);
} }
/**
* Bias-T toggle button
* @return
*/
private JToggleButton getBiasTButton()
{
if(mBiasTButton == null)
{
mBiasTButton = new JToggleButton("Bias-T");
mBiasTButton.setEnabled(false);
mBiasTButton.addActionListener(e -> {
if(!isLoading())
{
getTuner().getController().setBiasT(mBiasTButton.isSelected());
save();
}
});
}
return mBiasTButton;
}
/** /**
* Hyperlink button that provides tuner information * Hyperlink button that provides tuner information
*/ */
@@ -200,7 +233,7 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
mTunerInfoButton = new JButton("Info"); mTunerInfoButton = new JButton("Info");
mTunerInfoButton.setEnabled(false); mTunerInfoButton.setEnabled(false);
mTunerInfoButton.addActionListener(e -> JOptionPane.showMessageDialog(R820TTunerEditor.this, mTunerInfoButton.addActionListener(e -> JOptionPane.showMessageDialog(R8xTunerEditor.this,
getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE)); getTunerInfo(), "Tuner Info", JOptionPane.INFORMATION_MESSAGE));
} }
@@ -211,13 +244,13 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
if(mVGAGainCombo == null) if(mVGAGainCombo == null)
{ {
mVGAGainCombo = new JComboBox<>(R820TVGAGain.values()); mVGAGainCombo = new JComboBox<>(R8xEmbeddedTuner.VGAGain.values());
mVGAGainCombo.setEnabled(false); mVGAGainCombo.setEnabled(false);
mVGAGainCombo.addActionListener(arg0 -> mVGAGainCombo.addActionListener(arg0 ->
{ {
try try
{ {
R820TVGAGain vgaGain = (R820TVGAGain) mVGAGainCombo.getSelectedItem(); R8xEmbeddedTuner.VGAGain vgaGain = (R8xEmbeddedTuner.VGAGain) mVGAGainCombo.getSelectedItem();
if(vgaGain == null) if(vgaGain == null)
{ {
@@ -233,9 +266,9 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
} }
catch(UsbException e) catch(UsbException e)
{ {
JOptionPane.showMessageDialog(R820TTunerEditor.this, "R820T Tuner Controller - " + JOptionPane.showMessageDialog(R8xTunerEditor.this, getLogPrefix() +
"couldn't apply the VGA gain setting - " + e.getLocalizedMessage()); "couldn't apply the VGA gain setting - " + e.getLocalizedMessage());
mLog.error("R820T Tuner Controller - couldn't apply VGA gain setting", e); mLog.error(getLogPrefix() + "couldn't apply VGA gain setting", e);
} }
}); });
mVGAGainCombo.setToolTipText("<html>VGA Gain. Set master gain to <b>MANUAL</b> to enable adjustment</html>"); mVGAGainCombo.setToolTipText("<html>VGA Gain. Set master gain to <b>MANUAL</b> to enable adjustment</html>");
@@ -248,13 +281,13 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
if(mLNAGainCombo == null) if(mLNAGainCombo == null)
{ {
mLNAGainCombo = new JComboBox<>(R820TLNAGain.values()); mLNAGainCombo = new JComboBox<>(R8xEmbeddedTuner.LNAGain.values());
mLNAGainCombo.setEnabled(false); mLNAGainCombo.setEnabled(false);
mLNAGainCombo.addActionListener(arg0 -> mLNAGainCombo.addActionListener(arg0 ->
{ {
try try
{ {
R820TLNAGain lnaGain = (R820TLNAGain) mLNAGainCombo.getSelectedItem(); R8xEmbeddedTuner.LNAGain lnaGain = (R8xEmbeddedTuner.LNAGain) mLNAGainCombo.getSelectedItem();
if(lnaGain == null) if(lnaGain == null)
{ {
@@ -270,9 +303,9 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
} }
catch(UsbException e) catch(UsbException e)
{ {
JOptionPane.showMessageDialog(R820TTunerEditor.this, "R820T Tuner Controller - " + JOptionPane.showMessageDialog(R8xTunerEditor.this, getLogPrefix() +
"couldn't apply the LNA gain setting - " + e.getLocalizedMessage()); "couldn't apply the LNA gain setting - " + e.getLocalizedMessage());
mLog.error("R820T Tuner Controller - couldn't apply LNA " + "gain setting - ", e); mLog.error(getLogPrefix() + "couldn't apply LNA " + "gain setting - ", e);
} }
}); });
mLNAGainCombo.setToolTipText("<html>LNA Gain. Set master gain to <b>MANUAL</b> to enable adjustment</html>"); mLNAGainCombo.setToolTipText("<html>LNA Gain. Set master gain to <b>MANUAL</b> to enable adjustment</html>");
@@ -300,11 +333,11 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
} }
catch(SourceException | LibUsbException eSampleRate) catch(SourceException | LibUsbException eSampleRate)
{ {
JOptionPane.showMessageDialog(R820TTunerEditor.this, JOptionPane.showMessageDialog(R8xTunerEditor.this,
"R820T Tuner Controller - couldn't apply the sample rate setting [" + getLogPrefix() + "couldn't apply the sample rate setting [" +
sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage()); sampleRate.getLabel() + "] " + eSampleRate.getLocalizedMessage());
mLog.error("R820T Tuner Controller - couldn't apply sample rate setting [" + sampleRate.getLabel() + mLog.error(getLogPrefix() + "couldn't apply sample rate setting [" + sampleRate.getLabel() +
"]", eSampleRate); "]", eSampleRate);
} }
} }
@@ -318,7 +351,7 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
if(mMixerGainCombo == null) if(mMixerGainCombo == null)
{ {
mMixerGainCombo = new JComboBox<>(R820TMixerGain.values()); mMixerGainCombo = new JComboBox<>(R8xEmbeddedTuner.MixerGain.values());
mMixerGainCombo.setEnabled(false); mMixerGainCombo.setEnabled(false);
mMixerGainCombo.addActionListener(arg0 -> mMixerGainCombo.addActionListener(arg0 ->
{ {
@@ -326,7 +359,7 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
try try
{ {
R820TMixerGain mixerGain = (R820TMixerGain) mMixerGainCombo.getSelectedItem(); R8xEmbeddedTuner.MixerGain mixerGain = (R8xEmbeddedTuner.MixerGain) mMixerGainCombo.getSelectedItem();
if(mixerGain == null) if(mixerGain == null)
{ {
@@ -342,10 +375,10 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
} }
catch(UsbException e) catch(UsbException e)
{ {
JOptionPane.showMessageDialog(R820TTunerEditor.this, "R820T Tuner Controller - " + JOptionPane.showMessageDialog(R8xTunerEditor.this, getLogPrefix() +
"couldn't apply the mixer gain setting - " + e.getLocalizedMessage()); "couldn't apply the mixer gain setting - " + e.getLocalizedMessage());
mLog.error("R820T Tuner Controller - couldn't apply mixer gain setting - ", e); mLog.error(getLogPrefix() + "couldn't apply mixer gain setting - ", e);
} }
} }
}); });
@@ -359,7 +392,7 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
if(mMasterGainCombo == null) if(mMasterGainCombo == null)
{ {
mMasterGainCombo = new JComboBox<>(R820TGain.values()); mMasterGainCombo = new JComboBox<>(R8xEmbeddedTuner.MasterGain.values());
mMasterGainCombo.setEnabled(false); mMasterGainCombo.setEnabled(false);
mMasterGainCombo.addActionListener(arg0 -> mMasterGainCombo.addActionListener(arg0 ->
{ {
@@ -367,10 +400,10 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
try try
{ {
R820TGain gain = (R820TGain)getMasterGainCombo().getSelectedItem(); R8xEmbeddedTuner.MasterGain gain = (R8xEmbeddedTuner.MasterGain)getMasterGainCombo().getSelectedItem();
getEmbeddedTuner().setGain((R820TGain)getMasterGainCombo().getSelectedItem(), true); getEmbeddedTuner().setGain((R8xEmbeddedTuner.MasterGain)getMasterGainCombo().getSelectedItem(), true);
if(gain == R820TGain.MANUAL) if(gain == R8xEmbeddedTuner.MasterGain.MANUAL)
{ {
getMixerGainCombo().setSelectedItem(gain.getMixerGain()); getMixerGainCombo().setSelectedItem(gain.getMixerGain());
getMixerGainCombo().setEnabled(true); getMixerGainCombo().setEnabled(true);
@@ -397,9 +430,9 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
} }
catch(UsbException e) catch(UsbException e)
{ {
JOptionPane.showMessageDialog(R820TTunerEditor.this, "R820T Tuner Controller - " + JOptionPane.showMessageDialog(R8xTunerEditor.this, getLogPrefix() +
"couldn't apply the gain setting - " + e.getLocalizedMessage()); "couldn't apply the gain setting - " + e.getLocalizedMessage());
mLog.error("R820T Tuner Controller - couldn't apply gain setting - ", e); mLog.error(getLogPrefix() + "couldn't apply gain setting - ", e);
} }
} }
}); });
@@ -442,7 +475,7 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
RTL2832TunerController.Descriptor descriptor = getTuner().getController().getDescriptor(); RTL2832TunerController.Descriptor descriptor = getTuner().getController().getDescriptor();
sb.append("<html><h3>RTL-2832 with R820T Tuner</h3>"); sb.append("<html><h3>RTL-2832 with " + getEmbeddedTuner().getTunerType().getLabel() + " Tuner</h3>");
if(descriptor == null) if(descriptor == null)
{ {
@@ -485,21 +518,21 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
{ {
if(hasConfiguration() && !isLoading()) if(hasConfiguration() && !isLoading())
{ {
R820TTunerConfiguration config = getConfiguration(); R8xTunerConfiguration config = getConfiguration();
config.setBiasT(getTuner().getController().isBiasT());
config.setFrequency(getFrequencyControl().getFrequency()); config.setFrequency(getFrequencyControl().getFrequency());
double value = ((SpinnerNumberModel)getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue(); double value = ((SpinnerNumberModel)getFrequencyCorrectionSpinner().getModel()).getNumber().doubleValue();
config.setFrequencyCorrection(value); config.setFrequencyCorrection(value);
config.setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected()); config.setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
config.setSampleRate((SampleRate)getSampleRateCombo().getSelectedItem()); config.setSampleRate((SampleRate)getSampleRateCombo().getSelectedItem());
R820TGain gain = (R820TGain)getMasterGainCombo().getSelectedItem(); R8xEmbeddedTuner.MasterGain gain = (R8xEmbeddedTuner.MasterGain)getMasterGainCombo().getSelectedItem();
config.setMasterGain(gain); config.setMasterGain(gain);
R820TMixerGain mixerGain = (R820TMixerGain)getMixerGainCombo().getSelectedItem(); R8xEmbeddedTuner.MixerGain mixerGain = (R8xEmbeddedTuner.MixerGain)getMixerGainCombo().getSelectedItem();
config.setMixerGain(mixerGain); config.setMixerGain(mixerGain);
R820TLNAGain lnaGain = (R820TLNAGain)getLNAGainCombo().getSelectedItem(); R8xEmbeddedTuner.LNAGain lnaGain = (R8xEmbeddedTuner.LNAGain)getLNAGainCombo().getSelectedItem();
config.setLNAGain(lnaGain); config.setLNAGain(lnaGain);
R820TVGAGain vgaGain = (R820TVGAGain)getVGAGainCombo().getSelectedItem(); R8xEmbeddedTuner.VGAGain vgaGain = (R8xEmbeddedTuner.VGAGain)getVGAGainCombo().getSelectedItem();
config.setVGAGain(vgaGain); config.setVGAGain(vgaGain);
saveConfiguration(); saveConfiguration();
} }

View File

@@ -0,0 +1,92 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.rtl.r8x.r820t;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
import io.github.dsheirer.source.tuner.rtl.r8x.R8xEmbeddedTuner;
import javax.usb.UsbException;
/**
* Rafael Micro R820T and R820T2 Embedded Tuner implementation
*/
public class R820TEmbeddedTuner extends R8xEmbeddedTuner
{
private static final byte I2C_WRITE_ADDRESS = (byte) 0x34;
private static final byte I2C_READ_ADDRESS = (byte) 0x35;
private static final int VCO_POWER_REF = 2;
/**
* Constructs an instance
* @param adapter for accessing RTL2832USBController interfaces
*/
public R820TEmbeddedTuner(RTL2832TunerController.ControllerAdapter adapter)
{
super(adapter, VCO_POWER_REF);
}
@Override
public TunerType getTunerType()
{
return TunerType.RAFAELMICRO_R820T;
}
@Override
public byte getI2CWriteAddress()
{
return I2C_WRITE_ADDRESS;
}
@Override
public byte getI2CReadAddress()
{
return I2C_READ_ADDRESS;
}
/**
* Sets the center frequency. Setting the frequency is a two-part process
* of setting the multiplexer and then setting the Oscillator (PLL).
*/
@Override
public synchronized void setTunedFrequency(long frequency) throws SourceException
{
getAdapter().getLock().lock();
try
{
getAdapter().enableI2CRepeater();
boolean controlI2C = false;
long offsetFrequency = frequency + IF_FREQUENCY;
setMux(offsetFrequency, controlI2C);
setPLL(offsetFrequency, controlI2C);
getAdapter().disableI2CRepeater();
}
catch(UsbException e)
{
throw new SourceException("R820TTunerController - exception while setting frequency [" + frequency + "] - " +
e.getLocalizedMessage());
}
finally
{
getAdapter().getLock().unlock();
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.rtl.r8x.r820t;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.rtl.r8x.R8xTunerConfiguration;
/**
* RTL-2832 with embedded R820T tuner configuration
*/
public class R820TTunerConfiguration extends R8xTunerConfiguration
{
/**
* Empty constructor for Jackson serialization
*/
public R820TTunerConfiguration()
{
}
/**
* Constructs an instance
* @param uniqueId for the tuner
*/
public R820TTunerConfiguration(String uniqueId)
{
super(uniqueId);
}
@JsonIgnore
@Override
public TunerType getTunerType()
{
return TunerType.RAFAELMICRO_R820T;
}
}

View File

@@ -0,0 +1,134 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.rtl.r8x.r828d;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.rtl.RTL2832TunerController;
import io.github.dsheirer.source.tuner.rtl.r8x.R8xEmbeddedTuner;
import javax.usb.UsbException;
/**
* Rafael Micro R828D Embedded Tuner implementation
*/
public class R828DEmbeddedTuner extends R8xEmbeddedTuner
{
private static final byte I2C_WRITE_ADDRESS = (byte) 0x74;
private static final byte I2C_READ_ADDRESS = (byte) 0x75;
private static final int VCO_POWER_REF = 1;
/**
* RTL-SDR.com blog V4 dongle indicator.
*/
private final boolean mIsV4Dongle;
/**
* Constructs an instance
* @param adapter for accessing RTL2832USBController interfaces
*/
public R828DEmbeddedTuner(RTL2832TunerController.ControllerAdapter adapter)
{
super(adapter, VCO_POWER_REF);
//Set flag to indicate if this is a V4 dongle that supports notch filtering.
mIsV4Dongle = adapter.isV4Dongle();
}
@Override
public TunerType getTunerType()
{
return TunerType.RAFAELMICRO_R828D;
}
@Override
public byte getI2CWriteAddress()
{
return I2C_WRITE_ADDRESS;
}
@Override
public byte getI2CReadAddress()
{
return I2C_READ_ADDRESS;
}
/**
* Sets the center frequency. Setting the frequency is a two-part process
* of setting the multiplexer and then setting the Oscillator (PLL).
*/
@Override
public synchronized void setTunedFrequency(long frequency) throws SourceException
{
getAdapter().getLock().lock();
try
{
getAdapter().enableI2CRepeater();
boolean controlI2C = false;
long offsetFrequency = frequency + IF_FREQUENCY;
setMux(offsetFrequency, controlI2C);
setPLL(offsetFrequency, controlI2C);
//Select the RF input
if(mIsV4Dongle)
{
//Disable notch filtering if frequency falls into a notched band, otherwise enable it.
boolean isNotchFilterFrequency = (frequency < 2_200_000) ||
(85_000_000 < frequency && frequency < 112_000_000) ||
(172_000_000 < frequency && frequency < 242_000_000);
writeRegister(Register.DRAIN, isNotchFilterFrequency ? (byte)0x00 : (byte)0x08, controlI2C);
if(frequency <= 28_800_000) //Use cable 2 input
{
writeRegister(Register.INPUT_SELECTOR_AIR, (byte)0x20, controlI2C); //disabled
writeRegister(Register.INPUT_SELECTOR_CABLE_1, (byte)0x00, controlI2C); //disabled
writeRegister(Register.INPUT_SELECTOR_CABLE_2, (byte)0x08, controlI2C); //enabled
}
else if(frequency < 250_000_000)
{
writeRegister(Register.INPUT_SELECTOR_AIR, (byte)0x20, controlI2C); //disabled
writeRegister(Register.INPUT_SELECTOR_CABLE_1, (byte)0x40, controlI2C); //enabled
writeRegister(Register.INPUT_SELECTOR_CABLE_2, (byte)0x00, controlI2C); //disabled
}
else
{
writeRegister(Register.INPUT_SELECTOR_AIR, (byte)0x00, controlI2C); //enabled
writeRegister(Register.INPUT_SELECTOR_CABLE_1, (byte)0x00, controlI2C); //disabled
writeRegister(Register.INPUT_SELECTOR_CABLE_2, (byte)0x00, controlI2C); //disabled
}
}
else
{
byte air_cable_in = (frequency > 345_000_000) ? (byte)0x00 : (byte)0x60;
writeRegister(Register.INPUT_SELECTOR_AIR_AND_CABLE_1, air_cable_in, controlI2C);
}
getAdapter().disableI2CRepeater();
}
catch(UsbException e)
{
throw new SourceException("R820TTunerController - exception while setting frequency [" + frequency + "] - " +
e.getLocalizedMessage());
}
finally
{
getAdapter().getLock().unlock();
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 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.source.tuner.rtl.r8x.r828d;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.rtl.r8x.R8xTunerConfiguration;
/**
* RTL-2832 with embedded R828D tuner configuration
*/
public class R828DTunerConfiguration extends R8xTunerConfiguration
{
/**
* Empty constructor for Jackson serialization
*/
public R828DTunerConfiguration()
{
}
/**
* Constructs an instance
* @param uniqueId for the tuner
*/
public R828DTunerConfiguration(String uniqueId)
{
super(uniqueId);
}
@JsonIgnore
@Override
public TunerType getTunerType()
{
return TunerType.RAFAELMICRO_R828D;
}
}

View File

@@ -56,8 +56,8 @@ public abstract class USBTunerController extends TunerController
protected static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81; protected static final byte USB_BULK_TRANSFER_ENDPOINT = (byte) 0x81;
private static final long USB_BULK_TRANSFER_TIMEOUT_MS = 2000l; private static final long USB_BULK_TRANSFER_TIMEOUT_MS = 2000l;
private int mBus; protected int mBus;
private String mPortAddress; protected String mPortAddress;
private Context mDeviceContext = new Context(); private Context mDeviceContext = new Context();
private Device mDevice; private Device mDevice;
private DeviceHandle mDeviceHandle; private DeviceHandle mDeviceHandle;