mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
Compare commits
27 Commits
v0.6.0-bet
...
v0.6.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02363c3113 | ||
|
|
cfaac0fcbc | ||
|
|
bee0405e00 | ||
|
|
50bdf0ee81 | ||
|
|
a27ddbe0d0 | ||
|
|
0d652b9153 | ||
|
|
9468fd189b | ||
|
|
215737ad9b | ||
|
|
123c25b142 | ||
|
|
af56198f0e | ||
|
|
3c73fdfd6e | ||
|
|
b391ebbb5f | ||
|
|
bad5a1f9f0 | ||
|
|
a7470b463f | ||
|
|
d6c706cb35 | ||
|
|
a0d6950c75 | ||
|
|
2495200b36 | ||
|
|
bfe7ae0b1b | ||
|
|
4912abbf07 | ||
|
|
2f2998d02e | ||
|
|
c96349fa1c | ||
|
|
f640576cc3 | ||
|
|
77d725a488 | ||
|
|
2c0305f928 | ||
|
|
21fd61d507 | ||
|
|
8fb56a5342 | ||
|
|
5d7b73f876 |
@@ -53,7 +53,8 @@ repositories {
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
|
||||
version = '0.6.0-beta1'
|
||||
//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 {
|
||||
|
||||
@@ -388,6 +388,8 @@ public class AliasList
|
||||
}
|
||||
break;
|
||||
case PATCH_GROUP:
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
@@ -395,8 +397,6 @@ public class AliasList
|
||||
|
||||
if(patchGroupAliasList != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
|
||||
|
||||
if(alias != null)
|
||||
@@ -404,19 +404,36 @@ public class AliasList
|
||||
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:
|
||||
RadioIdentifier radio = (RadioIdentifier)identifier;
|
||||
|
||||
@@ -483,9 +500,7 @@ public class AliasList
|
||||
{
|
||||
if(alias != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
aliases.add(alias);
|
||||
return aliases;
|
||||
return Collections.singletonList(alias);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
|
||||
@@ -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
|
||||
* 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.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -48,12 +45,13 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
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.
|
||||
@@ -475,7 +473,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,7 +20,6 @@
|
||||
package io.github.dsheirer.audio.broadcast.rdioscanner;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
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.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
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.talkgroup.TalkgroupIdentifier;
|
||||
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.PrintWriter;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
@@ -67,6 +53,8 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
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());
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -844,13 +844,18 @@ public class BinaryMessage extends BitSet
|
||||
*/
|
||||
public int getTwosComplement(int start, int end)
|
||||
{
|
||||
BinaryMessage sub = getSubMessage(start, end);
|
||||
boolean negative = sub.get(start);
|
||||
sub.flip(0, sub.size());
|
||||
|
||||
int value = sub.getInt(0, sub.size()) + 1;
|
||||
value *= (negative ? -1 : 1);
|
||||
return value;
|
||||
if(get(start))
|
||||
{
|
||||
//Negative value - flip and add one
|
||||
BinaryMessage fragment = getSubMessage(start, end);
|
||||
fragment.flip(0, fragment.size());
|
||||
return fragment.getInt(1, fragment.size()) + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Positive value - return the contents.
|
||||
return getInt(start + 1, end);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.buffer.INativeBufferProvider;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
@@ -65,6 +66,7 @@ import org.slf4j.LoggerFactory;
|
||||
public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
{
|
||||
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LoggerFactory.getLogger(PolyphaseChannelManager.class));
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
||||
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
|
||||
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
||||
@@ -203,7 +205,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
|
||||
LOGGING_SUPPRESSOR.error(iae.getMessage(), 3, "Couldn't allocate channel. " + iae.getMessage());
|
||||
channelSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.ChannelRange;
|
||||
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.ViewIconManagerRequest;
|
||||
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.ViewUserPreferenceEditorRequest;
|
||||
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.jmbe.JmbeEditor;
|
||||
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 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_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_JMBE_EDITOR = "jmbe.editor";
|
||||
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
|
||||
@@ -82,14 +82,14 @@ public class JavaFxWindowManager extends Application
|
||||
private TunerManager mTunerManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
private UserPreferencesEditor mUserPreferencesEditor;
|
||||
private DMRRecordingViewer mDmrRecordingViewer;
|
||||
private RecordingViewer mRecordingViewer;
|
||||
|
||||
private Stage mChannelMapStage;
|
||||
private Stage mIconManagerStage;
|
||||
private Stage mJmbeEditorStage;
|
||||
private Stage mPlaylistStage;
|
||||
private Stage mUserPreferencesStage;
|
||||
private Stage mDmrRecordingViewerStage;
|
||||
private Stage mRecordingViewerStage;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
Scene scene = new Scene(getDmrRecordingViewer(), 1100, 800);
|
||||
mDmrRecordingViewerStage = new Stage();
|
||||
mDmrRecordingViewerStage.setTitle("sdrtrunk - DMR Recording Viewer");
|
||||
mDmrRecordingViewerStage.setScene(scene);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mDmrRecordingViewerStage, STAGE_MONITOR_KEY_DMR_MESSAGE_VIEWER);
|
||||
Scene scene = new Scene(getRecordingViewer(), 1100, 800);
|
||||
mRecordingViewerStage = new Stage();
|
||||
mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
|
||||
mRecordingViewerStage.setScene(scene);
|
||||
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()
|
||||
@@ -453,12 +453,12 @@ public class JavaFxWindowManager extends Application
|
||||
* Process a channel map editor request
|
||||
*/
|
||||
@Subscribe
|
||||
public void process(final ViewDmrRecordingViewerRequest request)
|
||||
public void process(final ViewRecordingViewerRequest request)
|
||||
{
|
||||
execute(() -> {
|
||||
getDmrRecordingViewerStage().show();
|
||||
getDmrRecordingViewerStage().requestFocus();
|
||||
getDmrRecordingViewerStage().toFront();
|
||||
getRecordingViewerStage().show();
|
||||
getRecordingViewerStage().requestFocus();
|
||||
getRecordingViewerStage().toFront();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
|
||||
import io.github.dsheirer.controller.channel.ChannelException;
|
||||
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
|
||||
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.playlist.ViewPlaylistRequest;
|
||||
import io.github.dsheirer.gui.preference.CalibrateRequest;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
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.log.ApplicationLog;
|
||||
import io.github.dsheirer.map.MapService;
|
||||
@@ -420,9 +420,9 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
|
||||
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer");
|
||||
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest()));
|
||||
viewMenu.add(dmrMessageViewerMenu);
|
||||
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
|
||||
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
|
||||
viewMenu.add(recordingViewerMenu);
|
||||
|
||||
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
|
||||
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||
|
||||
@@ -85,7 +85,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
private ToggleSwitch mUseCompressedTalkgroupsToggle;
|
||||
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
|
||||
private TableView<TimeslotFrequency> mTimeslotFrequencyTable;
|
||||
private IntegerTextField mLogicalSlotNumberField;
|
||||
private IntegerTextField mLogicalChannelNumberField;
|
||||
private FrequencyField mDownlinkFrequencyField;
|
||||
// private FrequencyField mUplinkFrequencyField;
|
||||
private Button mAddTimeslotFrequencyButton;
|
||||
@@ -172,7 +172,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
GridPane.setConstraints(useCompressedTalkgroupsLabel, 7, row);
|
||||
gridPane.getChildren().add(useCompressedTalkgroupsLabel);
|
||||
|
||||
Label timeslotTableLabel = new Label("Logical Slot Number (LSN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies");
|
||||
Label timeslotTableLabel = new Label("Logical Channel Number (LCN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies. LSN = Logical Slot Number");
|
||||
GridPane.setHalignment(timeslotTableLabel, HPos.LEFT);
|
||||
GridPane.setConstraints(timeslotTableLabel, 0, ++row, 6, 1);
|
||||
gridPane.getChildren().add(timeslotTableLabel);
|
||||
@@ -194,17 +194,13 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
editorBox.setAlignment(Pos.CENTER_LEFT);
|
||||
editorBox.setSpacing(5);
|
||||
|
||||
Label lsnLabel = new Label("LSN");
|
||||
editorBox.getChildren().addAll(lsnLabel,getLogicalSlotNumberField());
|
||||
Label lcnLabel = new Label("LCN");
|
||||
editorBox.getChildren().addAll(lcnLabel, getLogicalChannelNumberField());
|
||||
|
||||
Label downlinkLabel = new Label("Frequency (MHz)");
|
||||
downlinkLabel.setPadding(new Insets(0,0,0,5));
|
||||
editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
|
||||
|
||||
// Label uplinkLabel = new Label("Uplink");
|
||||
// uplinkLabel.setPadding(new Insets(0,0,0,5));
|
||||
// editorBox.getChildren().addAll(uplinkLabel,getUplinkFrequencyField());
|
||||
|
||||
GridPane.setConstraints(editorBox, 0, row, 4, 1);
|
||||
gridPane.getChildren().add(editorBox);
|
||||
|
||||
@@ -287,20 +283,22 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor()));
|
||||
mTimeslotFrequencyTable.setPrefHeight(100.0);
|
||||
|
||||
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LSN");
|
||||
numberColumn.setPrefWidth(70);
|
||||
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LCN");
|
||||
numberColumn.setPrefWidth(75);
|
||||
numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty());
|
||||
mTimeslotFrequencyTable.getColumns().addAll(numberColumn);
|
||||
mTimeslotFrequencyTable.getSortOrder().add(numberColumn);
|
||||
|
||||
TableColumn<TimeslotFrequency,Number> downlinkColumn = new TableColumn("Frequency (MHz)");
|
||||
downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz());
|
||||
downlinkColumn.setPrefWidth(175);
|
||||
downlinkColumn.setPrefWidth(150);
|
||||
mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn);
|
||||
|
||||
// TableColumn<TimeslotFrequency,Number> uplinkColumn = new TableColumn("Uplink (MHz)");
|
||||
// uplinkColumn.setCellValueFactory(cellData -> cellData.getValue().getUplinkMHz());
|
||||
// mTimeslotFrequencyTable.getColumns().addAll(uplinkColumn);
|
||||
TableColumn<TimeslotFrequency,String> lsnColumn = new TableColumn("IDs (TS1/TS2)");
|
||||
lsnColumn.setPrefWidth(225);
|
||||
lsnColumn.setCellValueFactory(new PropertyValueFactory<>("logicalSlotNumbers"));
|
||||
mTimeslotFrequencyTable.getColumns().addAll(lsnColumn);
|
||||
|
||||
|
||||
mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> setTimeslot(newValue));
|
||||
@@ -317,20 +315,20 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
//Preserve the current modified flag state since setting values in the editor will change it.
|
||||
boolean modified = modifiedProperty().get();
|
||||
|
||||
getLogicalSlotNumberField().setDisable(timeslot == null);
|
||||
getLogicalChannelNumberField().setDisable(timeslot == null);
|
||||
getDownlinkFrequencyField().setDisable(timeslot == null);
|
||||
// getUplinkFrequencyField().setDisable(timeslot == null);
|
||||
getDeleteTimeslotFrequencyButton().setDisable(timeslot == null);
|
||||
|
||||
if(timeslot != null)
|
||||
{
|
||||
getLogicalSlotNumberField().set(timeslot.getNumber());
|
||||
getLogicalChannelNumberField().set(timeslot.getNumber());
|
||||
getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency());
|
||||
// getUplinkFrequencyField().set(timeslot.getUplinkFrequency());
|
||||
}
|
||||
else
|
||||
{
|
||||
getLogicalSlotNumberField().set(0);
|
||||
getLogicalChannelNumberField().set(0);
|
||||
getDownlinkFrequencyField().set(0);
|
||||
// getUplinkFrequencyField().set(0);
|
||||
}
|
||||
@@ -412,19 +410,19 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
return mDeleteTimeslotFrequencyButton;
|
||||
}
|
||||
|
||||
private IntegerTextField getLogicalSlotNumberField()
|
||||
private IntegerTextField getLogicalChannelNumberField()
|
||||
{
|
||||
if(mLogicalSlotNumberField == null)
|
||||
if(mLogicalChannelNumberField == null)
|
||||
{
|
||||
mLogicalSlotNumberField = new IntegerTextField();
|
||||
mLogicalSlotNumberField.setDisable(true);
|
||||
mLogicalSlotNumberField.setPrefWidth(65);
|
||||
mLogicalSlotNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
mLogicalChannelNumberField = new IntegerTextField();
|
||||
mLogicalChannelNumberField.setDisable(true);
|
||||
mLogicalChannelNumberField.setPrefWidth(65);
|
||||
mLogicalChannelNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
Integer value = mLogicalSlotNumberField.get();
|
||||
Integer value = mLogicalChannelNumberField.get();
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
@@ -436,7 +434,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
});
|
||||
}
|
||||
|
||||
return mLogicalSlotNumberField;
|
||||
return mLogicalChannelNumberField;
|
||||
}
|
||||
|
||||
private FrequencyField getDownlinkFrequencyField()
|
||||
@@ -610,8 +608,8 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
getTimeslotTable().setDisable(config == null);
|
||||
getAddTimeslotFrequencyButton().setDisable(config == null);
|
||||
getDeleteTimeslotFrequencyButton().setDisable(true);
|
||||
getLogicalSlotNumberField().set(0);
|
||||
getLogicalSlotNumberField().setDisable(true);
|
||||
getLogicalChannelNumberField().set(0);
|
||||
getLogicalChannelNumberField().setDisable(true);
|
||||
getDownlinkFrequencyField().set(0);
|
||||
getDownlinkFrequencyField().setDisable(true);
|
||||
// getUplinkFrequencyField().set(0);
|
||||
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -41,13 +41,12 @@ import io.github.dsheirer.rrapi.type.Tag;
|
||||
import io.github.dsheirer.rrapi.type.Talkgroup;
|
||||
import io.github.dsheirer.rrapi.type.Type;
|
||||
import io.github.dsheirer.rrapi.type.Voice;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Utility class for decoding type, flavor and voice for systems and formatting talkgroups according to user preferences.
|
||||
@@ -374,16 +373,10 @@ public class RadioReferenceDecoder
|
||||
}
|
||||
}
|
||||
|
||||
int lsn = (lcn - 1) * 2 + 1;
|
||||
TimeslotFrequency timeslot1Frequency = new TimeslotFrequency();
|
||||
timeslot1Frequency.setNumber(lsn);
|
||||
timeslot1Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
|
||||
frequencies.add(timeslot1Frequency);
|
||||
|
||||
TimeslotFrequency timeslot2Frequency = new TimeslotFrequency();
|
||||
timeslot2Frequency.setNumber(lsn + 1);
|
||||
timeslot2Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
|
||||
frequencies.add(timeslot2Frequency);
|
||||
TimeslotFrequency timeslotFrequency = new TimeslotFrequency();
|
||||
timeslotFrequency.setNumber(lcn);
|
||||
timeslotFrequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
|
||||
frequencies.add(timeslotFrequency);
|
||||
}
|
||||
|
||||
return frequencies;
|
||||
|
||||
@@ -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.module.decode.dmr.DMRMessageFramer;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
|
||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||
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.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -40,10 +42,10 @@ import javafx.collections.transformation.FilteredList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
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;
|
||||
@@ -58,21 +60,19 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
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 String LAST_SELECTED_DIRECTORY = "last.selected.directory";
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(DMRRecordingViewer.class);
|
||||
private Scene mScene;
|
||||
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.dmr";
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(DmrViewer.class);
|
||||
private Button mSelectFileButton;
|
||||
private Label mSelectedFileLabel;
|
||||
private TableView<IMessage> mMessageTableView;
|
||||
@@ -86,8 +86,9 @@ public class DMRRecordingViewer extends VBox
|
||||
private TextField mFindText;
|
||||
private Button mFindButton;
|
||||
private Button mFindNextButton;
|
||||
private ProgressIndicator mLoadingIndicator;
|
||||
|
||||
public DMRRecordingViewer()
|
||||
public DmrViewer()
|
||||
{
|
||||
setPadding(new Insets(5));
|
||||
setSpacing(5);
|
||||
@@ -132,6 +133,21 @@ public class DMRRecordingViewer extends VBox
|
||||
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
|
||||
* @param file containing a .bits recording of decoded DMR data.
|
||||
@@ -140,33 +156,45 @@ public class DMRRecordingViewer extends VBox
|
||||
{
|
||||
if(file != null && file.exists())
|
||||
{
|
||||
getSelectedFileLabel().setText(file.getName());
|
||||
mMessages.clear();
|
||||
getLoadingIndicator().setVisible(true);
|
||||
getSelectedFileLabel().setText("Loading ...");
|
||||
final boolean useCompressed = getUseCompressedTalkgroups().isSelected();
|
||||
|
||||
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
|
||||
DecodeConfigDMR config = new DecodeConfigDMR();
|
||||
if(getUseCompressedTalkgroups().isSelected())
|
||||
ThreadPool.CACHED.submit(new Runnable()
|
||||
{
|
||||
config.setUseCompressedTalkgroups(true);
|
||||
}
|
||||
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())
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
messageFramer.receive(buffer);
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
List<IMessage> messages = new ArrayList<>();
|
||||
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
|
||||
DecodeConfigDMR config = new DecodeConfigDMR();
|
||||
config.setUseCompressedTalkgroups(useCompressed);
|
||||
DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
|
||||
messageFramer.setListener(messageProcessor);
|
||||
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()
|
||||
{
|
||||
Predicate<IMessage> timeslotPredicate = message ->
|
||||
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
|
||||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
|
||||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
|
||||
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
|
||||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
|
||||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
|
||||
|
||||
String filterText = getSearchText().getText();
|
||||
|
||||
@@ -256,6 +284,7 @@ public class DMRRecordingViewer extends VBox
|
||||
if(mMessageTableView == null)
|
||||
{
|
||||
mMessageTableView = new TableView<>();
|
||||
mMessageTableView.setPlaceholder(getLoadingIndicator());
|
||||
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
|
||||
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
|
||||
mMessageTableView.setItems(sortedList);
|
||||
@@ -509,37 +538,9 @@ public class DMRRecordingViewer extends VBox
|
||||
{
|
||||
if(mUseCompressedTalkgroups == null)
|
||||
{
|
||||
mUseCompressedTalkgroups = new CheckBox("Use Compressed Talkgroups");
|
||||
mUseCompressedTalkgroups = new CheckBox("Use Hytera Tier III Compressed Talkgroups");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
462
src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java
Normal file
462
src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java
Normal 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;
|
||||
}
|
||||
}
|
||||
180
src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java
Normal file
180
src/main/java/io/github/dsheirer/gui/viewer/RecordingViewer.java
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -17,17 +17,17 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.dmr;
|
||||
package io.github.dsheirer.gui.viewer;
|
||||
|
||||
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
|
||||
*/
|
||||
public ViewDmrRecordingViewerRequest() {}
|
||||
public ViewRecordingViewerRequest() {}
|
||||
}
|
||||
@@ -1,69 +1,161 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier.patch;
|
||||
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Temporary grouping or regrouping of talkgroups or individual radios.
|
||||
*/
|
||||
public class PatchGroup
|
||||
{
|
||||
// private final static Logger mLog = LoggerFactory.getLogger(PatchGroup.class);
|
||||
|
||||
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;
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
@@ -72,9 +164,14 @@ public class PatchGroup
|
||||
StringBuilder sb = new StringBuilder();
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
* 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.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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
|
||||
* and maintains a map of the current patch groups.
|
||||
*
|
||||
* Traffic channel calls will sometimes reference a patch group by simply using the patch group identifier. This
|
||||
* 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.
|
||||
* Traffic channel calls will reference a patch group by simply using the patch group identifier. This
|
||||
* 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
|
||||
* patch group reference on the control or traffic channel.
|
||||
*/
|
||||
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<>();
|
||||
|
||||
/**
|
||||
@@ -58,13 +54,13 @@ public class PatchGroupManager
|
||||
public void clear()
|
||||
{
|
||||
mPatchGroupMap.clear();
|
||||
mPatchGroupLastUpdatedMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 patch group.
|
||||
* managed, the new patch group argument is checked for additional patched talkgroups and they are added to the
|
||||
* 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
|
||||
*/
|
||||
@@ -76,8 +72,19 @@ public class PatchGroupManager
|
||||
{
|
||||
if(mPatchGroupMap.containsKey(update.getPatchGroup().getValue()))
|
||||
{
|
||||
PatchGroup current = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
|
||||
current.addPatchedGroups(update.getPatchedGroupIdentifiers());
|
||||
PatchGroup existingPatchGroup = mPatchGroupMap.get(update.getPatchGroup().getValue()).getValue();
|
||||
|
||||
//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
|
||||
{
|
||||
@@ -109,7 +116,6 @@ public class PatchGroupManager
|
||||
{
|
||||
int id = patchGroupIdentifier.getValue().getPatchGroup().getValue();
|
||||
mPatchGroupMap.remove(id);
|
||||
mPatchGroupLastUpdatedMap.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,23 +159,28 @@ public class PatchGroupManager
|
||||
*/
|
||||
public Identifier update(Identifier identifier)
|
||||
{
|
||||
if(identifier == null)
|
||||
{
|
||||
return identifier;
|
||||
}
|
||||
|
||||
if(identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
|
||||
if(identifier != null && identifier.getIdentifierClass() == IdentifierClass.USER && identifier.getRole() == Role.TO)
|
||||
{
|
||||
switch(identifier.getForm())
|
||||
{
|
||||
case PATCH_GROUP:
|
||||
if(identifier instanceof PatchGroupIdentifier)
|
||||
case TALKGROUP:
|
||||
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();
|
||||
|
||||
Identifier mapValue = mPatchGroupMap.get(patchGroupId);
|
||||
if (mapValue != null) {
|
||||
if (mapValue != null)
|
||||
{
|
||||
return mapValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,7 @@ public abstract class DecodeConfigAnalog extends DecodeConfiguration implements
|
||||
{
|
||||
BW_3_0("3.0 kHz", 3000.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_12_5("12.5 kHz", 12500.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);
|
||||
|
||||
//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.
|
||||
|
||||
@@ -57,6 +57,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcem
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
|
||||
@@ -74,19 +75,24 @@ 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.VoiceMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.Arc4EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
||||
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
|
||||
import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket;
|
||||
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket;
|
||||
import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
|
||||
import io.github.dsheirer.util.PacketUtil;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.jdesktop.swingx.mapviewer.GeoPosition;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -105,7 +111,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
private DecodeEvent mCurrentCallEvent;
|
||||
private long mCurrentFrequency;
|
||||
private boolean mIgnoreCRCChecksums;
|
||||
private Map<Integer,DecodeEvent> mDetectedCallEventsMap = new TreeMap<>();
|
||||
private Map<DMRChannel,DecodeEvent> mDetectedCallEventsMap = new HashMap<>();
|
||||
private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
|
||||
new AddChannelRotationActiveStateRequest(State.ACTIVE);
|
||||
|
||||
@@ -320,6 +326,46 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
.build();
|
||||
broadcast(smsEvent);
|
||||
}
|
||||
//Hytera Radio Registration Service (RRS)
|
||||
else if(packet.getPacket() instanceof HyteraRrsPacket rrs)
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("HYTERA RRS REGISTER RADIO:");
|
||||
sb.append(rrs.getDestination());
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(sb.toString())
|
||||
.build();
|
||||
broadcast(shortDataEvent);
|
||||
}
|
||||
//Hytera Short Data
|
||||
else if(packet.getPacket() instanceof HyteraShortDataPacket hsdp)
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("HYTERA");
|
||||
|
||||
if(hsdp.getPacketSequence().isEncrypted())
|
||||
{
|
||||
HyteraDataEncryptionHeader hdeh = (HyteraDataEncryptionHeader)hsdp.getPacketSequence().getProprietaryDataHeader();
|
||||
sb.append(" ENCRYPTED ALGORITHM:").append(hdeh.getAlgorithm());
|
||||
sb.append(" KEY:").append(hdeh.getKeyId());
|
||||
sb.append(" IV:").append(hdeh.getIV());
|
||||
}
|
||||
|
||||
sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(sb.toString())
|
||||
.build();
|
||||
broadcast(shortDataEvent);
|
||||
}
|
||||
//Unknown Hytera Long Data Service Token Message
|
||||
else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket)
|
||||
{
|
||||
@@ -328,32 +374,69 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details("HYTERA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
|
||||
.details("HYTERA LONG DATA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
|
||||
.build();
|
||||
broadcast(unknownTokenEvent);
|
||||
}
|
||||
//Motorola ARS
|
||||
else if(packet.getPacket() instanceof ARSPacket ars)
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(ars.toString())
|
||||
.build();
|
||||
broadcast(shortDataEvent);
|
||||
}
|
||||
//Motorola LRRP
|
||||
else if(packet.getPacket() instanceof LRRPPacket lrrp)
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.LRRP, packet.getTimestamp())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(lrrp.toString())
|
||||
.build();
|
||||
broadcast(shortDataEvent);
|
||||
}
|
||||
//Motorola XCMP
|
||||
else if(packet.getPacket() instanceof XCMPPacket xcmp)
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
|
||||
|
||||
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.XCMP, packet.getTimestamp())
|
||||
.identifiers(mic)
|
||||
.timeslot(getTimeslot())
|
||||
.details(xcmp.toString())
|
||||
.build();
|
||||
broadcast(shortDataEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp())
|
||||
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
|
||||
.identifiers(new IdentifierCollection(packet.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
.details(packet.toString())
|
||||
.build();
|
||||
|
||||
broadcast(packetEvent);
|
||||
|
||||
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
|
||||
}
|
||||
|
||||
if (geoPosition != null) {
|
||||
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, packet.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
|
||||
.protocol(Protocol.LRRP)
|
||||
.location(geoPosition)
|
||||
.build();
|
||||
GeoPosition geoPosition = PacketUtil.extractGeoPosition(packet.getPacket());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +463,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
EmbeddedParameters embedded = voiceEmb.getEmbeddedParameters();
|
||||
|
||||
if(embedded.getShortBurst() instanceof Arc4EncryptionParameters arc4)
|
||||
if(embedded.getShortBurst() instanceof EncryptionParameters arc4)
|
||||
{
|
||||
updateEncryptedCall(arc4, true, voiceEmb.getTimestamp());
|
||||
}
|
||||
@@ -653,21 +736,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
ChannelGrant dataGrant = (ChannelGrant)csbk;
|
||||
DMRChannel channel = dataGrant.getChannel();
|
||||
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers);
|
||||
mDetectedCallEventsMap.put(channel.getValue(), event);
|
||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -686,21 +770,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
ChannelGrant tgGrant = (ChannelGrant)csbk;
|
||||
DMRChannel channel = tgGrant.getChannel();
|
||||
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
||||
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers);
|
||||
mDetectedCallEventsMap.put(channel.getValue(), event);
|
||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -718,21 +803,23 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
ChannelGrant channelGrant = (ChannelGrant)csbk;
|
||||
DMRChannel channel = channelGrant.getChannel();
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers,
|
||||
csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel, mergedIdentifiers);
|
||||
mDetectedCallEventsMap.put(channel.getValue(), event);
|
||||
event = getDecodeEvent(csbk, DecodeEventType.CALL_UNIT_TO_UNIT, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -763,21 +850,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk;
|
||||
DMRChannel channel = cpdcg.getChannel();
|
||||
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel.getValue());
|
||||
DecodeEvent event = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(event, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel, mergedIdentifiers);
|
||||
mDetectedCallEventsMap.put(channel.getValue(), event);
|
||||
event = getDecodeEvent(csbk, DecodeEventType.DATA_CALL, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -793,7 +881,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
case MOTOROLA_CONPLUS_REGISTRATION_REQUEST:
|
||||
DecodeEvent event = DMRDecodeEvent.builder(DecodeEventType.REQUEST, csbk.getTimestamp())
|
||||
.details("Registration Request")
|
||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
.build();
|
||||
broadcast(event);
|
||||
@@ -802,7 +890,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE:
|
||||
DecodeEvent regRespEvent = DMRDecodeEvent.builder(DecodeEventType.RESPONSE, csbk.getTimestamp())
|
||||
.details("Registration Response")
|
||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
.build();
|
||||
broadcast(regRespEvent);
|
||||
@@ -814,21 +902,22 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
ConnectPlusVoiceChannelUser cpvcu = (ConnectPlusVoiceChannelUser)csbk;
|
||||
DMRChannel channel = cpvcu.getChannel();
|
||||
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
if(hasTrafficChannelManager())
|
||||
{
|
||||
IdentifierCollection mergedIdentifiers = getMergedIdentifierCollection(csbk.getIdentifiers());
|
||||
|
||||
mTrafficChannelManager.processChannelGrant(channel, mergedIdentifiers, csbk.getOpcode(),
|
||||
csbk.getTimestamp(), csbk.isEncrypted());
|
||||
}
|
||||
else
|
||||
{
|
||||
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel.getValue());
|
||||
DecodeEvent detectedEvent = mDetectedCallEventsMap.get(channel);
|
||||
|
||||
if(isStale(detectedEvent, csbk.getTimestamp(), csbk.getIdentifiers()))
|
||||
{
|
||||
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel, mergedIdentifiers);
|
||||
mDetectedCallEventsMap.put(channel.getValue(), detectedEvent);
|
||||
detectedEvent = getDecodeEvent(csbk, DecodeEventType.CALL_GROUP, channel,
|
||||
new IdentifierCollection(csbk.getIdentifiers()));
|
||||
mDetectedCallEventsMap.put(channel, detectedEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -844,7 +933,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION:
|
||||
DecodeEvent affiliateEvent = DMRDecodeEvent.builder(DecodeEventType.AFFILIATE, csbk.getTimestamp())
|
||||
.details("TALKGROUP AFFILIATION")
|
||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
.build();
|
||||
broadcast(affiliateEvent);
|
||||
@@ -857,18 +946,18 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
|
||||
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, DMRChannel channel,
|
||||
IdentifierCollection mergedIdentifiers) {
|
||||
IdentifierCollection identifierCollection) {
|
||||
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
||||
.channel(channel)
|
||||
.details(csbk.getOpcode().getLabel())
|
||||
.identifiers(mergedIdentifiers)
|
||||
.identifiers(identifierCollection)
|
||||
.timeslot(getTimeslot())
|
||||
.build();
|
||||
}
|
||||
|
||||
private DecodeEvent getDecodeEvent(CSBKMessage csbk, DecodeEventType decodeEventType, String details) {
|
||||
return DMRDecodeEvent.builder(decodeEventType, csbk.getTimestamp())
|
||||
.identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
|
||||
.identifiers(new IdentifierCollection(csbk.getIdentifiers()))
|
||||
.timeslot(getTimeslot())
|
||||
.details(details)
|
||||
.build();
|
||||
@@ -972,6 +1061,15 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
switch(message.getOpcode())
|
||||
{
|
||||
case FULL_ENCRYPTION_PARAMETERS:
|
||||
if(message instanceof io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters ep)
|
||||
{
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
mCurrentCallEvent.setDetails(ep.getDetails());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION:
|
||||
if(message instanceof CapacityPlusRestChannel)
|
||||
{
|
||||
@@ -1112,19 +1210,17 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
}
|
||||
break;
|
||||
case FULL_STANDARD_GPS_INFO:
|
||||
if(message instanceof GPSInformation)
|
||||
if(message instanceof GPSInformation gpsInformation)
|
||||
{
|
||||
GPSInformation gpsInformation = (GPSInformation)message;
|
||||
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
|
||||
ic.update(gpsInformation.getGPSLocation());
|
||||
PlottableDecodeEvent plottableGPS = PlottableDecodeEvent.plottableBuilder(DecodeEventType.GPS, message.getTimestamp())
|
||||
.channel(getCurrentChannel())
|
||||
.details("LOCATION:" + gpsInformation.getGPSLocation())
|
||||
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
|
||||
.protocol(Protocol.DMR)
|
||||
.location(gpsInformation.getPosition())
|
||||
.build();
|
||||
|
||||
DecodeEvent gpsEvent = DMRDecodeEvent.builder(DecodeEventType.GPS, message.getTimestamp())
|
||||
.identifiers(ic)
|
||||
.timeslot(getTimeslot())
|
||||
.details("LOCATION:" + gpsInformation.getGPSLocation())
|
||||
.build();
|
||||
|
||||
broadcast(gpsEvent);
|
||||
broadcast(plottableGPS);
|
||||
}
|
||||
break;
|
||||
case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
|
||||
@@ -1141,7 +1237,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
* @param encryptionParameters decoded from the Voice Frame F
|
||||
* @param isGroup true for group or false for individual call.
|
||||
*/
|
||||
private void updateEncryptedCall(Arc4EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
|
||||
private void updateEncryptedCall(EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
|
||||
{
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
@@ -1151,7 +1247,7 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
details = encryptionParameters.toString();
|
||||
}
|
||||
else if(!details.contains(encryptionParameters.toString()))
|
||||
else if(!details.contains(encryptionParameters.toString()) && !details.contains("ENCRYPTION"))
|
||||
{
|
||||
details += " " + encryptionParameters;
|
||||
}
|
||||
|
||||
@@ -127,19 +127,19 @@ public class DMRMessageProcessor implements Listener<IMessage>
|
||||
}
|
||||
}
|
||||
|
||||
//Enrich messages that carry DMR Logical Slot Number channels with LCN to frequency mappings
|
||||
//Enrich messages that carry DMR Logical Channel Numbers with LCN to frequency mappings
|
||||
if(message instanceof ITimeslotFrequencyReceiver)
|
||||
{
|
||||
ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
|
||||
int[] lsns = receiver.getLogicalSlotNumbers();
|
||||
int[] lcns = receiver.getLogicalChannelNumbers();
|
||||
|
||||
List<TimeslotFrequency> timeslotFrequencies = new ArrayList<>();
|
||||
|
||||
for(int lsn: lsns)
|
||||
for(int lcn: lcns)
|
||||
{
|
||||
if(mTimeslotFrequencyMap.containsKey(lsn))
|
||||
if(mTimeslotFrequencyMap.containsKey(lcn))
|
||||
{
|
||||
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lsn));
|
||||
timeslotFrequencies.add(mTimeslotFrequencyMap.get(lcn));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr;
|
||||
@@ -41,14 +38,13 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusCo
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.Model;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Tracks the network configuration details of a DMR network from the broadcast messages
|
||||
@@ -250,7 +246,7 @@ public class DMRNetworkConfigurationMonitor
|
||||
ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
|
||||
mNeighborSites.addAll(cpnr.getNeighbors());
|
||||
}
|
||||
if(mBrand == null)
|
||||
if(mBrand == null || mBrand != BRAND_MOTOROLA_CONNECT_PLUS)
|
||||
{
|
||||
mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
private List<Channel> mManagedTrafficChannels;
|
||||
|
||||
private Map<Long,Channel> mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
|
||||
private Map<Integer,DMRChannelGrantEvent> mLSNGrantEventMap = new ConcurrentHashMap<>();
|
||||
private Map<DMRChannel,DMRChannelGrantEvent> mChannelGrantEventMap = new ConcurrentHashMap<>();
|
||||
|
||||
private Listener<ChannelEvent> mChannelEventListener;
|
||||
private Listener<IDecodeEvent> mDecodeEventListener;
|
||||
@@ -322,9 +322,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection,
|
||||
Opcode opcode, long timestamp, boolean encrypted)
|
||||
{
|
||||
int lsn = channel.getValue();
|
||||
|
||||
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
|
||||
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
|
||||
DecodeEventType decodeEventType = getEventType(opcode, identifierCollection, encrypted);
|
||||
|
||||
if(isStale(event, timestamp, identifierCollection)) //Create new event
|
||||
@@ -335,7 +333,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
.identifiers(identifierCollection)
|
||||
.build();
|
||||
|
||||
mLSNGrantEventMap.put(lsn, event);
|
||||
mChannelGrantEventMap.put(channel, event);
|
||||
|
||||
}
|
||||
else //Update current event
|
||||
@@ -355,7 +353,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
.identifiers(identifierCollection)
|
||||
.build();
|
||||
|
||||
mLSNGrantEventMap.put(lsn, event);
|
||||
mChannelGrantEventMap.put(channel, event);
|
||||
broadcast(event);
|
||||
}
|
||||
}
|
||||
@@ -643,37 +641,37 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I
|
||||
*/
|
||||
private void removeCallEvents(long frequency)
|
||||
{
|
||||
List<Integer> lsnsToRemove = new ArrayList<>();
|
||||
List<DMRChannel> channelsToRemove = new ArrayList<>();
|
||||
|
||||
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet())
|
||||
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
|
||||
{
|
||||
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
|
||||
if(entry.getKey().getDownlinkFrequency() == frequency)
|
||||
{
|
||||
lsnsToRemove.add(entry.getKey());
|
||||
channelsToRemove.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for(Integer lsn: lsnsToRemove)
|
||||
for(DMRChannel channel: channelsToRemove)
|
||||
{
|
||||
mLSNGrantEventMap.remove(lsn);
|
||||
mChannelGrantEventMap.remove(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCallEventDetails(long frequency, String detailFragment)
|
||||
{
|
||||
List<Integer> lsnsToUpdate = new ArrayList<>();
|
||||
List<DMRChannel> channelsToUpdate = new ArrayList<>();
|
||||
|
||||
for(Map.Entry<Integer,DMRChannelGrantEvent> entry: mLSNGrantEventMap.entrySet())
|
||||
for(Map.Entry<DMRChannel,DMRChannelGrantEvent> entry: mChannelGrantEventMap.entrySet())
|
||||
{
|
||||
if(entry.getValue().getChannelDescriptor().getDownlinkFrequency() == frequency)
|
||||
{
|
||||
lsnsToUpdate.add(entry.getKey());
|
||||
channelsToUpdate.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for(Integer lsn: lsnsToUpdate)
|
||||
for(DMRChannel channel: channelsToUpdate)
|
||||
{
|
||||
DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
|
||||
DMRChannelGrantEvent event = mChannelGrantEventMap.get(channel);
|
||||
|
||||
if(event != null)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
* 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.FullLCMessage;
|
||||
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.VoiceMessage;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
@@ -105,63 +104,54 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
{
|
||||
if(hasAudioCodec() && message.getTimeslot() == getTimeslot())
|
||||
{
|
||||
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
|
||||
if(!mEncryptedCallStateEstablished && message instanceof VoiceMessage vm &&
|
||||
vm.getSyncPattern().isDirectMode())
|
||||
//Attempt to set the audio encryption state from certain types of messages
|
||||
if(!mEncryptedCallStateEstablished)
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = false;
|
||||
List<byte[]> frames = vm.getAMBEFrames();
|
||||
for(byte[] frame: frames)
|
||||
//DCDM doesn't provide FLCs or EMBs ... assume that the call is unencrypted.
|
||||
if(message instanceof VoiceMessage vm && vm.getSyncPattern().isDirectMode())
|
||||
{
|
||||
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
|
||||
//in the EMB field.
|
||||
else if(!mEncryptedCallStateEstablished && message instanceof VoiceEMBMessage voiceMessage)
|
||||
{
|
||||
if(voiceMessage.getEMB().isValid())
|
||||
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
|
||||
//in the EMB field.
|
||||
else if(message instanceof VoiceEMBMessage voiceMessage && voiceMessage.getEMB().isValid())
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = voiceMessage.getEMB().isEncrypted();
|
||||
}
|
||||
else if(message instanceof VoiceHeader voiceHeader)
|
||||
{
|
||||
LCMessage lc = voiceHeader.getLCMessage();
|
||||
|
||||
List<byte[]> frames = voiceMessage.getAMBEFrames();
|
||||
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())
|
||||
if(lc instanceof FullLCMessage flc) //We know it is, but this null checks as well
|
||||
{
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = flc.isEncrypted();
|
||||
if(flc.isValid())
|
||||
{
|
||||
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
|
||||
// an ongoing call and can be used to establish encryption state when the FLC is a voice channel user.
|
||||
else if(!mEncryptedCallStateEstablished && message instanceof AbstractVoiceChannelUser avcu)
|
||||
|
||||
//Queue or process audio frames. Note: audio frames are held/queued until encrypted state is established
|
||||
//before any audio is generated for the audio segment.
|
||||
if(message instanceof VoiceMessage voiceMessage)
|
||||
{
|
||||
ServiceOptions serviceOptions = avcu.getServiceOptions();
|
||||
mEncryptedCallStateEstablished = true;
|
||||
mEncryptedCall = serviceOptions.isEncrypted();
|
||||
List<byte[]> frames = voiceMessage.getAMBEFrames();
|
||||
for(byte[] frame: frames)
|
||||
{
|
||||
processAudio(frame, message.getTimestamp());
|
||||
}
|
||||
}
|
||||
else if(message instanceof Terminator)
|
||||
{
|
||||
|
||||
@@ -26,6 +26,8 @@ import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
/**
|
||||
@@ -33,8 +35,9 @@ import org.apache.commons.lang3.Validate;
|
||||
*
|
||||
* Note: timeslots are tracked as 1 and 2
|
||||
*/
|
||||
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor
|
||||
public abstract class DMRChannel extends IntegerIdentifier implements IChannelDescriptor, ITimeslotFrequencyReceiver
|
||||
{
|
||||
private TimeslotFrequency mTimeslotFrequency;
|
||||
private int mTimeslot;
|
||||
|
||||
/**
|
||||
@@ -56,13 +59,23 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeater number or channel number for the channel.
|
||||
* Repeater number aka Logical Channel Number (LCN) for the channel.
|
||||
*/
|
||||
public int getChannel()
|
||||
public int getChannelNumber()
|
||||
{
|
||||
return getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to format the single channel number as an array for TimeslotFrequency matching.
|
||||
* @return channel number as an integer array.
|
||||
*/
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return new int[]{getChannelNumber()};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Timeslot for the channel.
|
||||
* @return timeslot as zero-based index with values in range: 0 or 1.
|
||||
@@ -95,7 +108,7 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "CHAN:" + getChannel() + ":" + getTimeslot();
|
||||
return "CHAN:" + getChannelNumber() + ":" + getTimeslot();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,4 +128,72 @@ public abstract class DMRChannel extends IntegerIdentifier implements IChannelDe
|
||||
{
|
||||
throw new IllegalArgumentException("This method is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDownlinkFrequency()
|
||||
{
|
||||
if(mTimeslotFrequency != null)
|
||||
{
|
||||
return mTimeslotFrequency.getDownlinkFrequency();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUplinkFrequency()
|
||||
{
|
||||
if(mTimeslotFrequency != null)
|
||||
{
|
||||
return mTimeslotFrequency.getUplinkFrequency();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lsn to frequency mapper value.
|
||||
* @param timeslotFrequency to set
|
||||
*/
|
||||
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
|
||||
{
|
||||
mTimeslotFrequency = timeslotFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(timeslotFrequency.getChannelNumber() == getChannelNumber())
|
||||
{
|
||||
setTimeslotFrequency(timeslotFrequency);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(!(o instanceof DMRChannel that))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if(!super.equals(o))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return getTimeslot() == that.getTimeslot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(super.hashCode(), getTimeslot());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.channel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* DMR Logical Slot Number (LSN) channel.
|
||||
*
|
||||
@@ -44,10 +42,8 @@ import java.util.List;
|
||||
* 15: 8/1
|
||||
* 16: 8/2
|
||||
*/
|
||||
public class DMRLsn extends DMRChannel implements ITimeslotFrequencyReceiver
|
||||
public class DMRLsn extends DMRChannel
|
||||
{
|
||||
private TimeslotFrequency mTimeslotFrequency;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
@@ -61,7 +57,7 @@ public class DMRLsn extends DMRChannel implements ITimeslotFrequencyReceiver
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "LSN:" + getLsn();
|
||||
return "LSN:" + getLsn() + " LCN:" + getChannelNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,65 +66,7 @@ public class DMRLsn extends DMRChannel implements ITimeslotFrequencyReceiver
|
||||
*/
|
||||
public int getLsn()
|
||||
{
|
||||
return ((getChannel() - 1) * 2) + getTimeslot();
|
||||
return ((getChannelNumber() - 1) * 2) + getTimeslot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDownlinkFrequency()
|
||||
{
|
||||
if(mTimeslotFrequency != null)
|
||||
{
|
||||
return mTimeslotFrequency.getDownlinkFrequency();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUplinkFrequency()
|
||||
{
|
||||
if(mTimeslotFrequency != null)
|
||||
{
|
||||
return mTimeslotFrequency.getUplinkFrequency();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
{
|
||||
return new int[]{getLsn()};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the lsn to frequency mapper value.
|
||||
* @param timeslotFrequency to set
|
||||
*/
|
||||
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
|
||||
{
|
||||
mTimeslotFrequency = timeslotFrequency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(timeslotFrequency.getNumber() == getLsn())
|
||||
{
|
||||
setTimeslotFrequency(timeslotFrequency);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
for(int x = 1; x <= 16; x++)
|
||||
{
|
||||
DMRLsn lsn = new DmrRestLsn(x);
|
||||
System.out.println(lsn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,12 @@ package io.github.dsheirer.module.decode.dmr.channel;
|
||||
*/
|
||||
public class DMRTier3Channel extends DMRChannel
|
||||
{
|
||||
private TimeslotFrequency mTimeslotFrequency;
|
||||
|
||||
/**
|
||||
* Constructs an instance. Note: radio reference uses a one based index, so we add a value of one to the
|
||||
* calculated logical slot value for visual compatibility for users.
|
||||
*
|
||||
* @param channel number or repeater number
|
||||
* @param logicalSlotNumber - zero based index.
|
||||
* @param timeslot either 1 or 2.
|
||||
*/
|
||||
public DMRTier3Channel(int channel, int timeslot)
|
||||
{
|
||||
@@ -39,42 +37,12 @@ public class DMRTier3Channel extends DMRChannel
|
||||
}
|
||||
|
||||
/**
|
||||
* Downlink frequency
|
||||
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
|
||||
* Tier III Channel ID
|
||||
* @return channel ID
|
||||
*/
|
||||
@Override
|
||||
public long getDownlinkFrequency()
|
||||
public int getChannelId()
|
||||
{
|
||||
if(mTimeslotFrequency != null)
|
||||
{
|
||||
return mTimeslotFrequency.getDownlinkFrequency();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uplink frequency
|
||||
* @return value in Hertz, or 0 if this channel doesn't have a timeslot frequency mapping
|
||||
*/
|
||||
@Override
|
||||
public long getUplinkFrequency()
|
||||
{
|
||||
if(mTimeslotFrequency != null)
|
||||
{
|
||||
return mTimeslotFrequency.getUplinkFrequency();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timeslot frequency mapping
|
||||
* @param timeslotFrequency
|
||||
*/
|
||||
public void setTimeslotFrequency(TimeslotFrequency timeslotFrequency)
|
||||
{
|
||||
mTimeslotFrequency = timeslotFrequency;
|
||||
return getChannelNumber() * 2 + getTimeslot();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +51,8 @@ public class DMRTier3Channel extends DMRChannel
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(" LCN:").append(getValue());
|
||||
sb.append(" LCN:").append(getChannelNumber());
|
||||
sb.append(" CHANID:").append(getChannelId());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ import java.util.List;
|
||||
public interface ITimeslotFrequencyReceiver
|
||||
{
|
||||
/**
|
||||
* Provides the logical slot number(s) that require a matching timeslot frequency mapping
|
||||
* Provides the logical channel number(s) that require a matching timeslot frequency mapping
|
||||
*/
|
||||
public int[] getLogicalSlotNumbers();
|
||||
public int[] getLogicalChannelNumbers();
|
||||
|
||||
/**
|
||||
* Applies the list of timeslot frequency mappings to the implementer
|
||||
|
||||
@@ -31,7 +31,7 @@ import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.util.Callback;
|
||||
|
||||
/**
|
||||
* Maps a logical slot number (LSN) to a pair of channel frequency values
|
||||
* Maps a logical channel number (LCN) to a pair of channel frequency values
|
||||
*/
|
||||
public class TimeslotFrequency
|
||||
{
|
||||
@@ -100,7 +100,10 @@ public class TimeslotFrequency
|
||||
}
|
||||
|
||||
/**
|
||||
* Logical slot number (LSN) as a 1-index based counter
|
||||
* Logical channel number (LCN) as a 1-index based counter
|
||||
*
|
||||
* Note: lsn (logical slot number) is a legacy name for this field. The field currently holds the LCN or logical
|
||||
* channel number. To avoid breaking serialization, we leave this field labeled as lsn.
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "lsn")
|
||||
public int getNumber()
|
||||
@@ -109,12 +112,52 @@ public class TimeslotFrequency
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the logical slot number (LSN) as a 1-index start
|
||||
* @param lsn where LSN 1 is the first slot, 2 the second, etc
|
||||
* Logical channel number which is the same value as getNumber.
|
||||
* @return
|
||||
*/
|
||||
public void setNumber(int lsn)
|
||||
@JsonIgnore
|
||||
public int getChannelNumber()
|
||||
{
|
||||
mNumberProperty.set(lsn);
|
||||
return getNumber();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public String getLogicalSlotNumbers()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int channel = getChannelNumber();
|
||||
|
||||
if(channel > 0)
|
||||
{
|
||||
if(channel < 32) //Connect Plus uses 5-bits for LSN for a maximum value of 31
|
||||
{
|
||||
//Cap+ & Con+ Logical Slot Numbers
|
||||
int lsn1 = (channel - 1) * 2 + 1;
|
||||
int lsn2 = (channel - 1) * 2 + 2;
|
||||
sb.append("LSN:").append(lsn1).append("/").append(lsn2).append(" or ");
|
||||
}
|
||||
|
||||
//Tier III channel IDs
|
||||
int chanId1 = channel * 2 + 1;
|
||||
int chanId2 = channel * 2 + 2;
|
||||
sb.append("Tier3 Chan:").append(chanId1).append("/").append(chanId2);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("(empty)");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the logical channel number (LCN) as a 1-index start
|
||||
* @param lcn where LCN 1 is logical slot numbers (LSN) 1 and 2, LCN2=LSN3/4, etc.
|
||||
*/
|
||||
public void setNumber(int lcn)
|
||||
{
|
||||
mNumberProperty.set(lcn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,7 +203,7 @@ public class TimeslotFrequency
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "TIMESLOT LSN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency();
|
||||
return "LCN:" + getNumber() + " DOWNLINK:" + getDownlinkFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,8 +33,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessageFactory
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.ConfirmedDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.DataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.DefinedShortDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.PiHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.RawShortDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.ResponseDataHeader;
|
||||
@@ -43,7 +43,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.header.StatusDataHeader
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.UnconfirmedDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
|
||||
@@ -98,7 +98,7 @@ public class DMRDataMessageFactory
|
||||
return new MBCHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
case CHANNEL_CONTROL_ENC_HEADER:
|
||||
case PI_HEADER:
|
||||
return new HeaderMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
return new PiHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
case VOICE_HEADER:
|
||||
return new VoiceHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
|
||||
case DATA_ENC_HEADER:
|
||||
@@ -138,10 +138,10 @@ public class DMRDataMessageFactory
|
||||
return mprdh;
|
||||
}
|
||||
case HYTERA_68:
|
||||
HyteraProprietaryDataHeader hpdh = new HyteraProprietaryDataHeader(pattern, payload,
|
||||
cach, slotType, timestamp, timeslot);
|
||||
hpdh.setValid(valid);
|
||||
return hpdh;
|
||||
HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
|
||||
cach, slotType, timestamp, timeslot);
|
||||
hsdh.setValid(valid);
|
||||
return hsdh;
|
||||
default:
|
||||
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
|
||||
slotType, timestamp, timeslot);
|
||||
|
||||
@@ -34,7 +34,7 @@ import java.util.List;
|
||||
*/
|
||||
public abstract class DataMessageWithLinkControl extends DataMessage
|
||||
{
|
||||
private LCMessage mLCMessage;
|
||||
protected LCMessage mLCMessage;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
|
||||
@@ -56,6 +56,10 @@ public class DataBlock1Rate extends DataBlock
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
if(hasRAS())
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" [CRC ERROR]");
|
||||
|
||||
@@ -56,6 +56,10 @@ public class DataBlock1_2Rate extends DataBlock
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
if(hasRAS())
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" [CRC ERROR]");
|
||||
|
||||
@@ -57,6 +57,10 @@ public class DataBlock3_4Rate extends DataBlock
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
if(hasRAS())
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits());
|
||||
}
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" [CRC ERROR]");
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Control Signalling Block (CSBK) Opcode enumeration
|
||||
*
|
||||
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B
|
||||
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B
|
||||
*/
|
||||
@@ -91,6 +90,8 @@ public enum Opcode
|
||||
|
||||
HYTERA_08_ACKNOWLEDGE(Vendor.HYTERA_8, 32, "HYTERA 08 ACKNOWLEDGE"),
|
||||
HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "HYTERA 08 ANNOUNCEMENT"),
|
||||
//CSBKO 44 & 47 observed on Tier3 interleaved in voice group call terminator sequence. 44 was continuously transmitted
|
||||
//and 47 was only transmitted 3x times in succession in the middle of the terminators and CSBKO 44 messages.
|
||||
HYTERA_08_CSBKO_44(Vendor.HYTERA_8, 44, "HYTERA 08 CSBKO 44"),
|
||||
HYTERA_08_CSBKO_47(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
|
||||
|
||||
@@ -104,9 +105,9 @@ public enum Opcode
|
||||
|
||||
UNKNOWN(Vendor.UNKNOWN, -1, "UNKNOWN");
|
||||
|
||||
private Vendor mVendor;
|
||||
private int mValue;
|
||||
private String mLabel;
|
||||
private final Vendor mVendor;
|
||||
private final int mValue;
|
||||
private final String mLabel;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
@@ -299,9 +299,9 @@ public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequ
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getRestChannel().getLogicalSlotNumbers();
|
||||
return getRestChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,13 +312,7 @@ public class CapacityPlusNeighbors extends CSBKMessage implements ITimeslotFrequ
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
|
||||
{
|
||||
if(timeslotFrequency.getNumber() == getRestChannel().getValue())
|
||||
{
|
||||
getRestChannel().setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getRestChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -299,9 +299,9 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
|
||||
* Logical slot numbers that require slot to frequency mappings.
|
||||
*/
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getRestChannel().getLogicalSlotNumbers();
|
||||
return getRestChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,13 +312,7 @@ public class CapacityPlusSiteStatus extends CSBKMessage implements ITimeslotFreq
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
|
||||
{
|
||||
if(getRestChannel().getValue() == timeslotFrequency.getNumber())
|
||||
{
|
||||
getRestChannel().setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getRestChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -40,7 +40,7 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
|
||||
{
|
||||
private static final int[] TARGET_ADDRESS = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
|
||||
32, 33, 34, 35, 36, 37, 38, 39};
|
||||
private static final int[] REPEATER = new int[]{40, 41, 42, 43, 44};
|
||||
private static final int[] LOGICAL_SLOT_NUMBER = new int[]{40, 41, 42, 43, 44};
|
||||
|
||||
//Analysis: this field correlates to UNKNOWN_FIELD_1(bits: 40-48) in ConnectPlusTerminateChannelGrant.
|
||||
private static final int[] UNKNOWN_FIELD = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
|
||||
@@ -109,11 +109,11 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel grant repeater number
|
||||
* Channel grant logical slot number
|
||||
*/
|
||||
public int getRepeater()
|
||||
public int getLsn()
|
||||
{
|
||||
return getMessage().getInt(REPEATER);
|
||||
return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Don't change this ... always subtract 1 to get the repeater LSN
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,16 +123,16 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
|
||||
{
|
||||
if(mDmrLsn == null)
|
||||
{
|
||||
mDmrLsn = new DMRLsn(getRepeater());
|
||||
mDmrLsn = new DMRLsn(getLsn());
|
||||
}
|
||||
|
||||
return mDmrLsn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getChannel().getLogicalSlotNumbers();
|
||||
return getChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,13 +142,7 @@ public class ConnectPlusDataChannelGrant extends CSBKMessage implements ITimeslo
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(timeslotFrequency.getNumber() == getChannel().getValue())
|
||||
{
|
||||
getChannel().setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,7 +45,7 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
|
||||
private static final int[] VERSION = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39};
|
||||
private static final int[] UNKNOWN = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56,
|
||||
57, 58, 59, 60, 61, 62};
|
||||
private static final int[] DATA_REPEATER = new int[]{63, 64, 65, 66, 67};
|
||||
private static final int[] LOGICAL_SLOT_NUMBER = new int[]{63, 64, 65, 66, 67};
|
||||
|
||||
private DMRLsn mDataChannel;
|
||||
private List<Identifier> mIdentifiers;
|
||||
@@ -99,11 +99,11 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
|
||||
}
|
||||
|
||||
/**
|
||||
* Data repeater number
|
||||
* Data logical slot numberrepeater number
|
||||
*/
|
||||
public int getDataRepeater()
|
||||
public int getDataLsn()
|
||||
{
|
||||
return getMessage().getInt(DATA_REPEATER);
|
||||
return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Leave this as minus 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,16 +113,16 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
|
||||
{
|
||||
if(mDataChannel == null)
|
||||
{
|
||||
mDataChannel = new DMRLsn(getDataRepeater());
|
||||
mDataChannel = new DMRLsn(getDataLsn());
|
||||
}
|
||||
|
||||
return mDataChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getDataChannel().getLogicalSlotNumbers();
|
||||
return getDataChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,13 +133,7 @@ public class ConnectPlusOTAAnnouncement extends CSBKMessage implements ITimeslot
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
|
||||
{
|
||||
if(timeslotFrequency.getNumber() == getDataChannel().getValue())
|
||||
{
|
||||
getDataChannel().setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getDataChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -44,7 +44,7 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
|
||||
32, 33, 34, 35, 36, 37, 38, 39};
|
||||
private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
|
||||
56, 57, 58, 59, 60, 61, 62, 63};
|
||||
private static final int[] TRAFFIC_CHANNEL_REPEATER = new int[]{64, 65, 66, 67, 68};
|
||||
private static final int[] LOGICAL_SLOT_NUMBER = new int[]{64, 65, 66, 67, 68};
|
||||
private static final int[] UNKNOWN_FIELD = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
|
||||
|
||||
private RadioIdentifier mRadio;
|
||||
@@ -126,9 +126,9 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
|
||||
/**
|
||||
* Traffic channel repeater
|
||||
*/
|
||||
public int getTrafficChannelRepeater()
|
||||
public int getTrafficLsn()
|
||||
{
|
||||
return getMessage().getInt(TRAFFIC_CHANNEL_REPEATER);
|
||||
return getMessage().getInt(LOGICAL_SLOT_NUMBER) - 1; //Always leave as minus 1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,16 +138,16 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
|
||||
{
|
||||
if(mDmrLsn == null)
|
||||
{
|
||||
mDmrLsn = new DMRLsn(getTrafficChannelRepeater());
|
||||
mDmrLsn = new DMRLsn(getTrafficLsn());
|
||||
}
|
||||
|
||||
return mDmrLsn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getChannel().getLogicalSlotNumbers();
|
||||
return getChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,13 +157,7 @@ public class ConnectPlusVoiceChannelUser extends CSBKMessage implements ITimeslo
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(timeslotFrequency.getNumber() == getChannel().getValue())
|
||||
{
|
||||
getChannel().setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -25,7 +25,6 @@ import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
@@ -224,11 +223,11 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
|
||||
* Logical Slot Number(s) for channels contained in this message
|
||||
*/
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
if(getMoveToChannel() instanceof DMRLsn dmrLsn)
|
||||
if(getMoveToChannel() != null)
|
||||
{
|
||||
return dmrLsn.getLogicalSlotNumbers();
|
||||
return getMoveToChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
return new int[0];
|
||||
@@ -241,17 +240,9 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
if(getMoveToChannel() instanceof DMRTier3Channel)
|
||||
if(getMoveToChannel() != null)
|
||||
{
|
||||
DMRTier3Channel channel = (DMRTier3Channel)getMoveToChannel();
|
||||
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(channel.getValue() == timeslotFrequency.getNumber())
|
||||
{
|
||||
channel.setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getMoveToChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ public class MoveTSCC extends CSBKMessage
|
||||
|
||||
if(multiBlock != null)
|
||||
{
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
|
||||
//Timeslot hard-coded to 1 since this is a control channel.
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,8 +84,8 @@ public class AdjacentSiteInformation extends Announcement
|
||||
|
||||
if(multiBlock != null)
|
||||
{
|
||||
//Timeslot hard-coded to zero for control channel
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
|
||||
//Timeslot hard-coded to one for control channel
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -27,7 +27,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.AbsoluteChannelParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -56,7 +55,8 @@ public class AnnounceChannelFrequency extends Announcement
|
||||
|
||||
if(multiBlock != null)
|
||||
{
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
|
||||
//Timeslot hard-coded to one
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ public class AnnounceWithdrawTSCC extends Announcement
|
||||
|
||||
if(multiBlock != null)
|
||||
{
|
||||
//Control channel timeslot hard-coded to 0
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
|
||||
//Control channel timeslot hard-coded to 1
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
@@ -90,7 +89,8 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
|
||||
|
||||
if(multiBlock != null)
|
||||
{
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 0);
|
||||
//Timeslot hard-coded to one for control channel
|
||||
mAbsoluteChannelParameters = new AbsoluteChannelParameters(multiBlock.getMessage(), 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,11 +239,11 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
|
||||
* Logical Slot Number(s) for channels contained in this message
|
||||
*/
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
if(getChannel() instanceof DMRLsn dmrLsn)
|
||||
if(getChannel() != null)
|
||||
{
|
||||
return dmrLsn.getLogicalSlotNumbers();
|
||||
return getChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
return new int[0];
|
||||
@@ -256,17 +256,9 @@ public class VoteNowAdvice extends Announcement implements ITimeslotFrequencyRec
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
if(getChannel() instanceof DMRTier3Channel)
|
||||
if(getChannel() != null)
|
||||
{
|
||||
DMRTier3Channel channel = (DMRTier3Channel)getChannel();
|
||||
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(channel.getValue() == timeslotFrequency.getNumber())
|
||||
{
|
||||
channel.setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ public class BroadcastTalkgroupVoiceChannelGrant extends ChannelGrant
|
||||
|
||||
sb.append(" BROADCAST TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationTalkgroup());
|
||||
sb.append(" CHAN:").append(getChannel());
|
||||
sb.append(" ").append(getChannel());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ package io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRLsn;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.DMRTier3Channel;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
@@ -158,11 +157,11 @@ public abstract class ChannelGrant extends CSBKMessage implements ITimeslotFrequ
|
||||
* Logical Slot Number(s) for channels contained in this message
|
||||
*/
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
if(getChannel() instanceof DMRLsn dmrLsn)
|
||||
if(getChannel() != null)
|
||||
{
|
||||
return dmrLsn.getLogicalSlotNumbers();
|
||||
return getChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
return new int[0];
|
||||
@@ -175,17 +174,9 @@ public abstract class ChannelGrant extends CSBKMessage implements ITimeslotFrequ
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
if(getChannel() instanceof DMRTier3Channel)
|
||||
if(getChannel() != null)
|
||||
{
|
||||
DMRTier3Channel channel = (DMRTier3Channel)getChannel();
|
||||
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(channel.getValue() == timeslotFrequency.getNumber())
|
||||
{
|
||||
channel.setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class DuplexPrivateDataChannelGrant extends ChannelGrant
|
||||
|
||||
sb.append(" DUPLEX PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationRadio());
|
||||
sb.append(" CHAN:").append(getChannel());
|
||||
sb.append(" ").append(getChannel());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ public class DuplexPrivateVoiceChannelGrant extends ChannelGrant
|
||||
|
||||
sb.append(" DUPLEX PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationRadio());
|
||||
sb.append(" CHAN:").append(getChannel());
|
||||
sb.append(" ").append(getChannel());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ public class PrivateDataChannelGrant extends ChannelGrant
|
||||
|
||||
sb.append(" PRIVATE DATA CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationRadio());
|
||||
sb.append(" CHAN:").append(getChannel());
|
||||
sb.append(" ").append(getChannel());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ public class PrivateVoiceChannelGrant extends ChannelGrant
|
||||
|
||||
sb.append(" PRIVATE VOICE CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationRadio());
|
||||
sb.append(" CHAN:").append(getChannel());
|
||||
sb.append(" ").append(getChannel());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ public class TalkgroupDataChannelGrant extends ChannelGrant
|
||||
|
||||
sb.append(" TALKGROUP DATA CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationTalkgroup());
|
||||
sb.append(" CHAN:").append(getChannel());
|
||||
sb.append(" ").append(getChannel());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ public class TalkgroupVoiceChannelGrant extends ChannelGrant
|
||||
|
||||
sb.append(" TALKGROUP VOICE CHANNEL GRANT FM:").append(getSourceRadio());
|
||||
sb.append(" TO:").append(getDestinationTalkgroup());
|
||||
sb.append(" CHAN:").append(getChannel());
|
||||
sb.append(" ").append(getChannel());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,18 +17,19 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.header.hytera;
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.header;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory;
|
||||
|
||||
/**
|
||||
* Hytera Proprietary Data Header
|
||||
* PI/Encryption Header with Link Control
|
||||
*/
|
||||
public class HyteraProprietaryDataHeader extends ProprietaryDataHeader
|
||||
public class PiHeader extends HeaderMessage
|
||||
{
|
||||
/**
|
||||
* Constructs an instance.
|
||||
@@ -40,23 +41,42 @@ public class HyteraProprietaryDataHeader extends ProprietaryDataHeader
|
||||
* @param timestamp message was received
|
||||
* @param timeslot for the DMR burst
|
||||
*/
|
||||
public HyteraProprietaryDataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
public PiHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
{
|
||||
super(syncPattern, message, cach, slotType, timestamp, timeslot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the embedded link control message
|
||||
*/
|
||||
public LCMessage getLCMessage()
|
||||
{
|
||||
if(mLCMessage == null)
|
||||
{
|
||||
//Overrides default to ignore opcode and construct an encryption parameters message.
|
||||
mLCMessage = LCMessageFactory.createFullEncryption(getMessage(), getTimestamp(), getTimeslot());
|
||||
}
|
||||
|
||||
return mLCMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" [CRC ERROR]");
|
||||
}
|
||||
sb.append(" HYTERA PROPRIETARY DATA HEADER");
|
||||
sb.append(" SAP:").append(getServiceAccessPoint());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
|
||||
sb.append(getSlotType());
|
||||
|
||||
if(hasRAS())
|
||||
{
|
||||
sb.append(" RAS:").append(getBPTCReservedBits()).append(" ");
|
||||
}
|
||||
sb.append(" ").append(getLCMessage());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.dmr.message.data.header.hytera;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
|
||||
import io.github.dsheirer.module.decode.dmr.message.CACH;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
|
||||
|
||||
/**
|
||||
* Hytera Proprietary Data Header for SAP Short Data
|
||||
*/
|
||||
public class HyteraDataEncryptionHeader extends ProprietaryDataHeader
|
||||
{
|
||||
private static final int[] ALGORITHM = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
|
||||
private static final int[] KEY = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
|
||||
private static final int[] INITIALIZATION_VECTOR = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
|
||||
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63};
|
||||
private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
*
|
||||
* @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA
|
||||
* @param message containing extracted 196-bit payload.
|
||||
* @param cach for the DMR burst
|
||||
* @param slotType for this data message
|
||||
* @param timestamp message was received
|
||||
* @param timeslot for the DMR burst
|
||||
*/
|
||||
public HyteraDataEncryptionHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
|
||||
{
|
||||
super(syncPattern, message, cach, slotType, timestamp, timeslot);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CC:").append(getSlotType().getColorCode());
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" [CRC ERROR]");
|
||||
}
|
||||
sb.append(" HYTERA DATA ENCRYPTION HEADER - SAP:").append(getServiceAccessPoint());
|
||||
|
||||
switch(getAlgorithm())
|
||||
{
|
||||
case NO_ENCRYPTION:
|
||||
sb.append(" ALGORITHM:UNENCRYPTED");
|
||||
break;
|
||||
case UNKNOWN:
|
||||
sb.append(" ALGORITHM:").append(getMessage().getInt(ALGORITHM));
|
||||
break;
|
||||
default:
|
||||
sb.append(" ALGORITHM:").append(getAlgorithm());
|
||||
break;
|
||||
}
|
||||
|
||||
if(getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION)
|
||||
{
|
||||
sb.append(" KEY:").append(getKeyId());
|
||||
sb.append(" IV:").append(getIV());
|
||||
sb.append(" UNK:").append(getUnknown());
|
||||
}
|
||||
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption algorithm.
|
||||
* @return algorithm or UNKNOWN if the value is not known.
|
||||
*/
|
||||
public EncryptionAlgorithm getAlgorithm()
|
||||
{
|
||||
return EncryptionAlgorithm.fromValue(getMessage().getInt(ALGORITHM));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption key ID (1-255).
|
||||
* @return key ID
|
||||
*/
|
||||
public int getKeyId()
|
||||
{
|
||||
return getMessage().getInt(KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hex string representation of the initialization vector
|
||||
* @return IV in hex string.
|
||||
*/
|
||||
public String getIV()
|
||||
{
|
||||
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown field.
|
||||
* @return hex string from the field contents.
|
||||
*/
|
||||
public String getUnknown()
|
||||
{
|
||||
return getMessage().getHex(UNKNOWN, 4);
|
||||
}
|
||||
}
|
||||
@@ -64,8 +64,8 @@ public class MotorolaDataEncryptionHeader extends ProprietaryDataHeader
|
||||
sb.append(" MOTOROLA DATA ENCRYPTION HEADER");
|
||||
sb.append(" SAP:").append(getServiceAccessPoint());
|
||||
sb.append(" ALGORITHM?:").append(getAlgorithm());
|
||||
sb.append(" KEY?:").append(getKeyId());
|
||||
sb.append(" IV?:").append(getInitializationVector());
|
||||
sb.append(" KEY:").append(getKeyId());
|
||||
sb.append(" IV:").append(getInitializationVector());
|
||||
sb.append(" UNK:").append(getUnknown());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.CRCDMR;
|
||||
import io.github.dsheirer.edac.Checksum_5_DMR;
|
||||
import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
|
||||
@@ -33,13 +34,11 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasHead
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TerminatorData;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnknownFullLCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraArc4EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaArc4EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ActivityUpdateMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
|
||||
@@ -61,8 +60,20 @@ public class LCMessageFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(LCMessageFactory.class);
|
||||
private static final ReedSolomon_12_9_4_DMR REED_SOLOMON_12_9_4_DMR = new ReedSolomon_12_9_4_DMR();
|
||||
private static int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99;
|
||||
private static int VOICE_LINK_CONTROL_CRC_MASK = 0x96;
|
||||
private static final int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99;
|
||||
private static final int VOICE_LINK_CONTROL_CRC_MASK = 0x96;
|
||||
|
||||
/**
|
||||
* Creates a full link control message specifically for the PI_HEADER message type.
|
||||
* @param message bits
|
||||
* @param timestamp of the original message
|
||||
* @param timeslot of the original message
|
||||
* @return encryption parameters link control.
|
||||
*/
|
||||
public static FullLCMessage createFullEncryption(CorrectedBinaryMessage message, long timestamp, int timeslot)
|
||||
{
|
||||
return new EncryptionParameters(message, timestamp, timeslot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a full link control message
|
||||
@@ -123,34 +134,9 @@ public class LCMessageFactory
|
||||
case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER:
|
||||
flc = new CapacityPlusWideAreaVoiceChannelUser(message, timestamp, timeslot);
|
||||
break;
|
||||
case FULL_ARC4_ENCRYPTION_PARAMETERS:
|
||||
boolean isHytera = false;
|
||||
|
||||
//This is apparently now a DMR standard FLC opcode, even though it's using Motorola's vendor ID (0x10).
|
||||
//As observed on a known Hytera system, it's not using the standard FLC RS-12/9/4 check, which fails.
|
||||
//It's apparently using the CRC-CCITT with 0x9696 initial fill. Not sure if both Hytera and Motorola
|
||||
// are using this FEC. But, by using only 16 bits for checksum versus the standad 24, they're able to
|
||||
// include a full 24-bit group/radio ID in the message. For now we'll identify as either Motorola
|
||||
//(using reed solomon) or Hytera (using crc-ccitt).
|
||||
if(!valid)
|
||||
{
|
||||
int bitErrors = CRCDMR.correctCCITT80(message, 0, 80, 0x9696);
|
||||
|
||||
if(bitErrors < 2)
|
||||
{
|
||||
valid = true;
|
||||
isHytera = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(isHytera)
|
||||
{
|
||||
flc = new HyteraArc4EncryptionParameters(message, timestamp, timeslot);
|
||||
}
|
||||
else
|
||||
{
|
||||
flc = new MotorolaArc4EncryptionParameters(message, timestamp, timeslot);
|
||||
}
|
||||
case FULL_ENCRYPTION_PARAMETERS:
|
||||
flc = new EncryptionParameters(message, timestamp, timeslot);
|
||||
valid = flc.isValid();
|
||||
break;
|
||||
case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER:
|
||||
flc = new HyteraGroupVoiceChannelUser(message, timestamp, timeslot);
|
||||
|
||||
@@ -27,7 +27,6 @@ import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Full and Short Link Control Opcode enumeration
|
||||
*
|
||||
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B.1 & B.2
|
||||
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B.1 & B.2
|
||||
*/
|
||||
@@ -49,7 +48,7 @@ public enum LCOpcode
|
||||
//Observed on Cap+ Multi-Site System during an encrypted voice call
|
||||
FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"),
|
||||
//Observed on Cap+ Multi-Site System during an encrypted voice call
|
||||
FULL_ARC4_ENCRYPTION_PARAMETERS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 33, "ARC4/EP ENCRYPTION PARAMETERS"),
|
||||
FULL_ENCRYPTION_PARAMETERS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 33, "ENCRYPTION PARAMETERS"),
|
||||
//Cap+ opcodes from https://forums.radioreference.com/threads/understanding-capacity-plus-trunking-some-more.452566/
|
||||
//FLCO 0: Group Call Maintenance
|
||||
//FLCO 3: Private Call Maintenance (TermLC)
|
||||
@@ -81,10 +80,10 @@ public enum LCOpcode
|
||||
private static final EnumSet<LCOpcode> TALKER_ALIAS_OPCODES = EnumSet.of(FULL_STANDARD_TALKER_ALIAS_HEADER,
|
||||
FULL_STANDARD_TALKER_ALIAS_BLOCK_1, FULL_STANDARD_TALKER_ALIAS_BLOCK_2, FULL_STANDARD_TALKER_ALIAS_BLOCK_3);
|
||||
|
||||
private Vendor mVendor;
|
||||
private boolean mFull;
|
||||
private int mValue;
|
||||
private String mLabel;
|
||||
private final Vendor mVendor;
|
||||
private final boolean mFull;
|
||||
private final int mValue;
|
||||
private final String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
|
||||
@@ -17,29 +17,30 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
|
||||
package io.github.dsheirer.module.decode.dmr.message.data.lc.full;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.edac.CRCDMR;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hytera ARC4/EP Encryption Parameters
|
||||
* <p>
|
||||
* Note: observed as FLC payload for a PI_HEADER slot type.
|
||||
* Note: observed on a Hytera system that was configured as IP Site Connect compatible.
|
||||
* Encryption Parameters, carried by a PI_HEADER.
|
||||
*
|
||||
* Note: this message repurposes the OPCODE field to carry the encryption algorithm.
|
||||
*/
|
||||
public class HyteraArc4EncryptionParameters extends FullLCMessage
|
||||
public class EncryptionParameters extends FullLCMessage
|
||||
{
|
||||
private static final int[] KEY_ID = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
|
||||
private static final int[] INITIALIZATION_VECTOR = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
|
||||
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
|
||||
private static final int[] DESTINATION_GROUP = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
|
||||
71, 72, 73, 74, 75, 76, 77, 78, 79};
|
||||
//Hytera version uses CRC-CCITT with 0x9696 initial fill, bits: 80-95
|
||||
//CRC-CCITT with 0x9696 initial fill, bits: 80-95
|
||||
|
||||
private DMRTalkgroup mTalkgroup;
|
||||
private List<Identifier> mIdentifiers;
|
||||
@@ -50,9 +51,23 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
|
||||
* @param timestamp for the message
|
||||
* @param timeslot of the message
|
||||
*/
|
||||
public HyteraArc4EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
|
||||
public EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
|
||||
{
|
||||
super(message, timestamp, timeslot);
|
||||
int bitErrors = CRCDMR.correctCCITT80(message, 0, 80, 0x9696);
|
||||
setValid(bitErrors < 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the default method for opcode to return a static value, because the opcode field is repurposed to
|
||||
* be the algorithm field in both Motorola and Hytera variants of this message, both carried by the Encryption
|
||||
* Header message.
|
||||
* @return full encryption parameters opcode.
|
||||
*/
|
||||
@Override
|
||||
public LCOpcode getOpcode()
|
||||
{
|
||||
return LCOpcode.FULL_ENCRYPTION_PARAMETERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +90,17 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
|
||||
sb.append(" *RESERVED-BIT*");
|
||||
}
|
||||
|
||||
sb.append("FLC HYTERA ARC4/EP ENCRYPTION PARAMETERS");
|
||||
sb.append("FLC ENCRYPTION PARAMETERS");
|
||||
sb.append(" VENDOR:").append(getVendor());
|
||||
if(getAlgorithm() == EncryptionAlgorithm.UNKNOWN)
|
||||
{
|
||||
//List the algorithm (aka opcod) value for unknown algorithms
|
||||
sb.append(" ALGORITHM:").append(getOpcodeValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(" ALGORITHM:").append(getAlgorithm());
|
||||
}
|
||||
sb.append(" KEY:").append(getKeyId());
|
||||
sb.append(" IV:").append(getInitializationVector());
|
||||
sb.append(" TALKGROUP:").append(getTalkgroup());
|
||||
@@ -83,6 +108,29 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption parameter details
|
||||
* @return details
|
||||
*/
|
||||
public String getDetails()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ENCRYPTION ALGORITHM:").append(getAlgorithm());
|
||||
sb.append(" KEY:").append(getKeyId());
|
||||
sb.append(" IV:").append(getInitializationVector());
|
||||
sb.append(" VENDOR:").append(getVendor());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption algorithm. Note: this message repurposes the OPCODE field to carry the algorithm ID.
|
||||
* @return algorithm or UNKNOWN.
|
||||
*/
|
||||
public EncryptionAlgorithm getAlgorithm()
|
||||
{
|
||||
return EncryptionAlgorithm.fromValue(getOpcodeValue());
|
||||
}
|
||||
|
||||
public DMRTalkgroup getTalkgroup()
|
||||
{
|
||||
if(mTalkgroup == null)
|
||||
@@ -30,7 +30,7 @@ public abstract class FullLCMessage extends LCMessage
|
||||
{
|
||||
private static final int ENCRYPTION_FLAG = 0;
|
||||
private static final int RESERVED = 1;
|
||||
private static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7};
|
||||
protected static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7};
|
||||
private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
* 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.module.decode.dmr.identifier.DMRLocation;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.PositionError;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.jdesktop.swingx.mapviewer.GeoPosition;
|
||||
|
||||
/**
|
||||
* GPS Information
|
||||
@@ -81,16 +81,30 @@ public class GPSInformation extends FullLCMessage
|
||||
return PositionError.fromValue(getMessage().getInt(POSITION_ERROR));
|
||||
}
|
||||
|
||||
/**
|
||||
* Latitude in decimal degrees
|
||||
*/
|
||||
public double getLatitude()
|
||||
{
|
||||
return getMessage().getTwosComplement(LATITUDE_START, LATITUDE_END) * LATITUDE_UNITS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Longitude in decimal degrees
|
||||
*/
|
||||
public double getLongitude()
|
||||
{
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -37,11 +37,12 @@ public abstract class HyteraFullLC extends FullLCMessage implements IServiceOpti
|
||||
protected static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
|
||||
private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
|
||||
private static final int[] UNKNOWN_1 = new int[]{72, 73, 74, 75};
|
||||
private static final int[] UNKNOWN_2 = new int[]{76, 77, 78, 79};
|
||||
//FEC: 72-96
|
||||
//TODO: Target and Source address fields may be full 24-bit values. How do we distinguish between Hytera repeater
|
||||
//TODO: and a Hytera XPT repeater? Do the service options identify when it's XPT?
|
||||
|
||||
private ServiceOptions mServiceOptions;
|
||||
private RadioIdentifier mSourceRadio;
|
||||
protected RadioIdentifier mSourceRadio;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
@@ -110,24 +111,6 @@ public abstract class HyteraFullLC extends FullLCMessage implements IServiceOpti
|
||||
return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown nibble one. This value remains consistent for the FLC between the Voice Header and the Terminator
|
||||
* for calls. It does not seem to have correlation to the channel/repeater number
|
||||
*/
|
||||
public int getUnknownField1()
|
||||
{
|
||||
return getMessage().getInt(UNKNOWN_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown nibble two. For each talkgroup, this field shows one value for the Voice Header and the same value but
|
||||
* bit-inverted in the Terminator.
|
||||
*/
|
||||
public int getUnknownField2()
|
||||
{
|
||||
return getMessage().getInt(UNKNOWN_2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Source radio address
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,9 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -32,6 +33,11 @@ import java.util.List;
|
||||
*/
|
||||
public class HyteraGroupVoiceChannelUser extends HyteraFullLC
|
||||
{
|
||||
protected static final int[] TALKGROUP = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
|
||||
39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
private static final int[] SOURCE_RADIO = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
|
||||
63, 64, 65, 66, 67, 68, 69, 70, 71};
|
||||
|
||||
private TalkgroupIdentifier mTalkgroup;
|
||||
private List<Identifier> mIdentifiers;
|
||||
|
||||
@@ -55,26 +61,11 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
|
||||
sb.append("[CRC-ERROR] ");
|
||||
}
|
||||
|
||||
sb.append("FLC HYTERA XPT GROUP VOICE CHANNEL USER FM:");
|
||||
sb.append("FLC HYTERA GROUP VOICE CHANNEL USER FM:");
|
||||
sb.append(getSourceRadio());
|
||||
sb.append(" TO:").append(getTalkgroup());
|
||||
if(isAllChannelsBusy())
|
||||
{
|
||||
sb.append(" ALL REPEATERS BUSY");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(" FREE REPEATER:").append(getFreeRepeater());
|
||||
}
|
||||
|
||||
if(hasPriorityCall())
|
||||
{
|
||||
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
|
||||
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
|
||||
}
|
||||
sb.append(" ").append(getServiceOptions());
|
||||
sb.append(" UNK1:").append(getUnknownField1());
|
||||
sb.append(" UNK2:").append(getUnknownField2());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -85,12 +76,25 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
|
||||
{
|
||||
if(mTalkgroup == null)
|
||||
{
|
||||
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS));
|
||||
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TALKGROUP));
|
||||
}
|
||||
|
||||
return mTalkgroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Source radio address
|
||||
*/
|
||||
public RadioIdentifier getSourceRadio()
|
||||
{
|
||||
if(mSourceRadio == null)
|
||||
{
|
||||
mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO));
|
||||
}
|
||||
|
||||
return mSourceRadio;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -73,8 +72,6 @@ public class HyteraTerminator extends HyteraFullLC
|
||||
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
|
||||
}
|
||||
sb.append(" ").append(getServiceOptions());
|
||||
sb.append(" UNK1:").append(getUnknownField1());
|
||||
sb.append(" UNK2:").append(getUnknownField2());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -72,8 +71,6 @@ public class HyteraUnitToUnitVoiceChannelUser extends HyteraFullLC
|
||||
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
|
||||
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
|
||||
}
|
||||
sb.append(" UNK1:").append(getUnknownField1());
|
||||
sb.append(" UNK2:").append(getUnknownField2());
|
||||
sb.append(" ").append(getServiceOptions());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -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.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;
|
||||
|
||||
/**
|
||||
* 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};
|
||||
//Reed Solomon FEC: 72-95
|
||||
|
||||
@@ -176,9 +176,9 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
|
||||
* Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message.
|
||||
*/
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getRestChannel().getLogicalSlotNumbers();
|
||||
return getRestChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,12 +189,6 @@ public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChann
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
|
||||
{
|
||||
if(getRestChannel().getValue() == timeslotFrequency.getNumber())
|
||||
{
|
||||
getRestChannel().setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getRestChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.dmr.message.data.lc.full.motorola;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Motorola ARC4/EP Encryption Parameters
|
||||
* <p>
|
||||
* Note: observed as FLC payload for a PI_HEADER slot type.
|
||||
* Note: observed on a possible Hytera (clone) system that was configured as IP Site Connect compatible.
|
||||
*/
|
||||
public class MotorolaArc4EncryptionParameters extends FullLCMessage
|
||||
{
|
||||
private static final int[] KEY_ID = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
|
||||
private static final int[] INITIALIZATION_VECTOR = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
|
||||
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
|
||||
private static final int[] DESTINATION_GROUP = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
|
||||
//Motorola Reed Solomon FEC bits: 72-95
|
||||
|
||||
private DMRTalkgroup mTalkgroup;
|
||||
private List<Identifier> mIdentifiers;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param message bits
|
||||
* @param timestamp for the message
|
||||
* @param timeslot of the message
|
||||
*/
|
||||
public MotorolaArc4EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
|
||||
{
|
||||
super(message, timestamp, timeslot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append("[CRC-ERROR] ");
|
||||
}
|
||||
|
||||
if(isEncrypted())
|
||||
{
|
||||
sb.append(" *ENCRYPTED*");
|
||||
}
|
||||
|
||||
if(isReservedBitSet())
|
||||
{
|
||||
sb.append(" *RESERVED-BIT*");
|
||||
}
|
||||
|
||||
sb.append("FLC MOTOROLA ARC4/EP ENCRYPTION PARAMETERS");
|
||||
sb.append(" KEY:").append(getKeyId());
|
||||
sb.append(" IV:").append(getInitializationVector());
|
||||
sb.append(" TALKGROUP:").append(getTalkgroup());
|
||||
sb.append(" MSG:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public DMRTalkgroup getTalkgroup()
|
||||
{
|
||||
if(mTalkgroup == null)
|
||||
{
|
||||
mTalkgroup = new DMRTalkgroup(getMessage().getInt(DESTINATION_GROUP));
|
||||
}
|
||||
|
||||
return mTalkgroup;
|
||||
}
|
||||
|
||||
public int getKeyId()
|
||||
{
|
||||
return getMessage().getInt(KEY_ID);
|
||||
}
|
||||
|
||||
public String getInitializationVector()
|
||||
{
|
||||
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
if(mIdentifiers == null)
|
||||
{
|
||||
mIdentifiers = new ArrayList<>();
|
||||
mIdentifiers.add(getTalkgroup());
|
||||
}
|
||||
|
||||
return mIdentifiers;
|
||||
}
|
||||
}
|
||||
@@ -105,9 +105,9 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
|
||||
* Exposes the rest channel logical slot number so that a LSN to frequency map can be applied to this message.
|
||||
*/
|
||||
@Override
|
||||
public int[] getLogicalSlotNumbers()
|
||||
public int[] getLogicalChannelNumbers()
|
||||
{
|
||||
return getRestChannel().getLogicalSlotNumbers();
|
||||
return getRestChannel().getLogicalChannelNumbers();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,13 +117,7 @@ public class CapacityPlusRestChannel extends ShortLCMessage implements ITimeslot
|
||||
@Override
|
||||
public void apply(List<TimeslotFrequency> timeslotFrequencies)
|
||||
{
|
||||
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
|
||||
{
|
||||
if(getRestChannel().getValue() == timeslotFrequency.getNumber())
|
||||
{
|
||||
getRestChannel().setTimeslotFrequency(timeslotFrequency);
|
||||
}
|
||||
}
|
||||
getRestChannel().apply(timeslotFrequencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,7 +24,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,12 +35,12 @@ import java.util.List;
|
||||
*/
|
||||
public class PacketSequence
|
||||
{
|
||||
private int mTimeslot;
|
||||
private final int mTimeslot;
|
||||
private PacketSequenceHeader mPacketSequenceHeader;
|
||||
private UDTHeader mUDTHeader;
|
||||
private ProprietaryDataHeader mProprietaryDataHeader;
|
||||
private List<Preamble> mPreambles = new ArrayList<>();
|
||||
private List<DataBlock> mDataBlocks = new ArrayList<>();
|
||||
private final List<Preamble> mPreambles = new ArrayList<>();
|
||||
private final List<DataBlock> mDataBlocks = new ArrayList<>();
|
||||
|
||||
public PacketSequence(int timeslot)
|
||||
{
|
||||
@@ -96,7 +98,19 @@ public class PacketSequence
|
||||
*/
|
||||
public boolean isEncrypted()
|
||||
{
|
||||
return hasProprietaryDataHeader() && getProprietaryDataHeader() instanceof MotorolaDataEncryptionHeader;
|
||||
if(hasProprietaryDataHeader())
|
||||
{
|
||||
if(getProprietaryDataHeader() instanceof MotorolaDataEncryptionHeader)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(getProprietaryDataHeader() instanceof HyteraDataEncryptionHeader hdeh)
|
||||
{
|
||||
return hdeh.getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getTimeslot()
|
||||
|
||||
@@ -25,17 +25,19 @@ import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.DataPacketFormat;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint;
|
||||
import io.github.dsheirer.module.decode.ip.DefinedShortDataPacket;
|
||||
import io.github.dsheirer.module.decode.ip.UnknownPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.rrs.HyteraRrsPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
|
||||
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header;
|
||||
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
|
||||
@@ -167,19 +169,30 @@ public class PacketSequenceMessageFactory
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
}
|
||||
}
|
||||
else if(secondaryHeader instanceof HyteraProprietaryDataHeader)
|
||||
else if(secondaryHeader instanceof HyteraDataEncryptionHeader hdeh)
|
||||
{
|
||||
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
|
||||
ServiceAccessPoint sap = hdeh.getServiceAccessPoint();
|
||||
|
||||
if(hyteraTokenHeader.isSMSMessage())
|
||||
switch(sap)
|
||||
{
|
||||
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
case SHORT_DATA:
|
||||
return new DMRPacketMessage(packetSequence, new HyteraShortDataPacket(packetSequence, packet), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
case PROPRIETARY_DATA:
|
||||
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
|
||||
if(hyteraTokenHeader.isSMSMessage())
|
||||
{
|
||||
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
}
|
||||
default:
|
||||
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
}
|
||||
}
|
||||
else if(secondaryHeader instanceof MotorolaDataEncryptionHeader &&
|
||||
@@ -231,8 +244,19 @@ public class PacketSequenceMessageFactory
|
||||
*/
|
||||
public static IMessage createDefinedShortData(PacketSequence packetSequence, CorrectedBinaryMessage packet)
|
||||
{
|
||||
return new DMRPacketMessage(packetSequence, new DefinedShortDataPacket(packet, 0), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
//Test to see if this is Hytera Radio Registration Service (RRS)
|
||||
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
|
||||
|
||||
if(hyteraTokenHeader.isRRSMessage())
|
||||
{
|
||||
return new DMRPacketMessage(packetSequence, new HyteraRrsPacket(hyteraTokenHeader), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
}
|
||||
else
|
||||
{
|
||||
return new DMRPacketMessage(packetSequence, new DefinedShortDataPacket(packet, 0), packet,
|
||||
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,7 +272,7 @@ public class PacketSequenceMessageFactory
|
||||
|
||||
if(sequence.hasProprietaryDataHeader())
|
||||
{
|
||||
CorrectedBinaryMessage prefix = ((ProprietaryDataHeader)sequence.getProprietaryDataHeader()).getPacketPrefix();
|
||||
CorrectedBinaryMessage prefix = sequence.getProprietaryDataHeader().getPacketPrefix();
|
||||
|
||||
if(prefix != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.dmr.message.type;
|
||||
|
||||
/**
|
||||
* Enumeration of encryption algorithms
|
||||
*/
|
||||
public enum EncryptionAlgorithm
|
||||
{
|
||||
NO_ENCRYPTION(0x00, "NO ENCRYPTION"),
|
||||
HYTERA_BASIC_PRIVACY(0x01, "HYTERA BP"),
|
||||
HYTERA_ENHANCED_PRIVACY(0x02, "HYTERA RC4/EP"),
|
||||
DMRA_RC4(0x21, "DMRA RC4/EP"),
|
||||
DMRA_AES128(0x24, "DMRA AES128"),
|
||||
DMRA_AES256(0x25, "DMRA AES256" ),
|
||||
HYTERA_ENHANCED_PRIVACY_2(0x26, "HYTERA RC4/EP"),
|
||||
UNKNOWN(-1, "UNKNOWN");
|
||||
|
||||
private final int mValue;
|
||||
private final String mLabel;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param value for the algorithm
|
||||
* @param label to display
|
||||
*/
|
||||
EncryptionAlgorithm(int value, String label)
|
||||
{
|
||||
mValue = value;
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric value for the algorithm
|
||||
* @return value.
|
||||
*/
|
||||
public int getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to lookup the encryption from the value.
|
||||
* @param value to lookup
|
||||
* @return matching entry or UNKNOWN
|
||||
*/
|
||||
public static EncryptionAlgorithm fromValue(int value)
|
||||
{
|
||||
for(EncryptionAlgorithm algorithm: EncryptionAlgorithm.values())
|
||||
{
|
||||
if(algorithm.getValue() == value)
|
||||
{
|
||||
return algorithm;
|
||||
}
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ import io.github.dsheirer.edac.BPTC_16_2;
|
||||
import io.github.dsheirer.edac.Golay24;
|
||||
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.voice.embedded.Arc4EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.NonStandardShortBurst;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.NullShortBurst;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.ShortBurst;
|
||||
@@ -38,8 +38,8 @@ import io.github.dsheirer.module.decode.dmr.message.voice.embedded.UnknownShortB
|
||||
* Monitors audio call voice frame messaging to detect encrypted audio calls and extract the encryption parameters
|
||||
* that are embedded into the six voice frames that comprise a voice super-frame.
|
||||
* <p>
|
||||
* See patent: https://patents.google.com/patent/EP2347540B1/en - embedding encryption parameters in voice super frame
|
||||
* See patent: https://patents.google.com/patent/US8271009B2 - TXI - interrupting voice transmissions
|
||||
* See patent: <a href="https://patents.google.com/patent/EP2347540B1/en">...</a> - embedding encryption parameters in voice super frame
|
||||
* See patent: <a href="https://patents.google.com/patent/US8271009B2">...</a> - TXI - interrupting voice transmissions
|
||||
*/
|
||||
public class VoiceSuperFrameProcessor
|
||||
{
|
||||
@@ -47,8 +47,6 @@ public class VoiceSuperFrameProcessor
|
||||
33, 34, 35, 48, 49, 50, 51, 52, 53, 54, 55};
|
||||
private static final int[] CRC4 = new int[]{56, 57, 58, 59};
|
||||
private boolean mEncrypted = false;
|
||||
private int mAlgorithm;
|
||||
private int mKey;
|
||||
private byte[] mFragmentA;
|
||||
private byte[] mFragmentB;
|
||||
private byte[] mFragmentC;
|
||||
@@ -83,8 +81,6 @@ public class VoiceSuperFrameProcessor
|
||||
|
||||
private void softReset()
|
||||
{
|
||||
mAlgorithm = 0;
|
||||
mKey = 0;
|
||||
mFragmentA = null;
|
||||
mFragmentB = null;
|
||||
mFragmentC = null;
|
||||
@@ -124,9 +120,6 @@ public class VoiceSuperFrameProcessor
|
||||
{
|
||||
if(voiceMessage instanceof VoiceEMBMessage voiceEMB)
|
||||
{
|
||||
boolean valid = voiceEMB.getEMB().isValid();
|
||||
boolean encrypted = voiceEMB.getEMB().isEncrypted();
|
||||
|
||||
if(voiceEMB.getEMB().isValid() && voiceEMB.getEMB().isEncrypted())
|
||||
{
|
||||
mEncrypted = true;
|
||||
@@ -175,7 +168,6 @@ public class VoiceSuperFrameProcessor
|
||||
* initialization vector to return a parameters object.
|
||||
*
|
||||
* @param frameFFragment containing the short-burst FLC fragment
|
||||
* @param iv that was previously extracted from the voice super-frame
|
||||
* @return embedded parameters.
|
||||
*/
|
||||
private ShortBurst extractShortBurst(BinaryMessage frameFFragment)
|
||||
@@ -191,7 +183,7 @@ public class VoiceSuperFrameProcessor
|
||||
return switch(opcode)
|
||||
{
|
||||
case NULL -> new NullShortBurst(decoded);
|
||||
case ARC4_ENCRYPTION -> new Arc4EncryptionParameters(decoded);
|
||||
case ARC4_ENCRYPTION, AES128_ENCRYPTION, AES256_ENCRYPTION -> new EncryptionParameters(decoded);
|
||||
case TXI_DELAY -> new TransmitInterrupt(decoded);
|
||||
default -> new UnknownShortBurst(decoded);
|
||||
};
|
||||
@@ -233,11 +225,11 @@ public class VoiceSuperFrameProcessor
|
||||
|
||||
if(passes)
|
||||
{
|
||||
return String.format("%04X", iv);
|
||||
return String.format("%08X", iv);
|
||||
}
|
||||
else
|
||||
{
|
||||
return String.format("%04X", iv) + "(CRC-FAIL " + check1 + "/" + check2 + "/" + check3 + "/" +
|
||||
return String.format("%08X", iv) + "(CRC-FAIL " + check1 + "/" + check2 + "/" + check3 + "/" +
|
||||
reassembled.getCorrectedBitCount() + ")";
|
||||
}
|
||||
}
|
||||
@@ -262,10 +254,10 @@ public class VoiceSuperFrameProcessor
|
||||
*/
|
||||
public static boolean crc4(int value, int crc)
|
||||
{
|
||||
long checksum = (value & 0x0FFFFFFFFl) << 4;
|
||||
long checksum = (value & 0x0FFFFFFFFL) << 4;
|
||||
checksum ^= 0xF; //Initial fill
|
||||
long polynomial = 0x013l << 31;
|
||||
long checkBit = 0x1l << 35;
|
||||
long polynomial = 0x013L << 31;
|
||||
long checkBit = 0x1L << 35;
|
||||
|
||||
for(int x = 31; x >= 0; x--)
|
||||
{
|
||||
|
||||
@@ -20,11 +20,12 @@
|
||||
package io.github.dsheirer.module.decode.dmr.message.voice.embedded;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
|
||||
|
||||
/**
|
||||
* Encryption parameters short burst payload.
|
||||
*/
|
||||
public class Arc4EncryptionParameters extends ShortBurst
|
||||
public class EncryptionParameters extends ShortBurst
|
||||
{
|
||||
private static final int[] KEY = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
|
||||
private static final int[] ALGORITHM = new int[]{8, 9, 10};
|
||||
@@ -34,7 +35,7 @@ public class Arc4EncryptionParameters extends ShortBurst
|
||||
*
|
||||
* @param message containing the de-interleaved and error-corrected short burst payload.
|
||||
*/
|
||||
public Arc4EncryptionParameters(CorrectedBinaryMessage message)
|
||||
public EncryptionParameters(CorrectedBinaryMessage message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
@@ -49,31 +50,45 @@ public class Arc4EncryptionParameters extends ShortBurst
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption algorithm
|
||||
* @return algorithm (0 - 7). 1 = ARC4
|
||||
* Encryption algorithm value
|
||||
* @return algorithm (0 - 7).
|
||||
*/
|
||||
public int getAlgorithmValue()
|
||||
{
|
||||
return getMessage().getInt(ALGORITHM);
|
||||
}
|
||||
|
||||
public String getAlgorithm()
|
||||
/**
|
||||
* Encryption algorithm.
|
||||
* @return algorithm
|
||||
*/
|
||||
public EncryptionAlgorithm getAlgorithm()
|
||||
{
|
||||
int algorithm = getAlgorithmValue();
|
||||
|
||||
if(algorithm == 1)
|
||||
return switch(algorithm)
|
||||
{
|
||||
return "EP/ARC4";
|
||||
}
|
||||
|
||||
return "ALGORITHM:" + algorithm;
|
||||
case 1 -> EncryptionAlgorithm.DMRA_RC4;
|
||||
case 4 -> EncryptionAlgorithm.DMRA_AES128;
|
||||
case 5 -> EncryptionAlgorithm.DMRA_AES256;
|
||||
default -> EncryptionAlgorithm.UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ENCRYPTION:").append(getAlgorithm());
|
||||
|
||||
sb.append("ENCRYPTION ALGORITHM:");
|
||||
if(getAlgorithm() == EncryptionAlgorithm.UNKNOWN)
|
||||
{
|
||||
sb.append(getAlgorithmValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append(getAlgorithm());
|
||||
}
|
||||
sb.append(" KEY:").append(getKey());
|
||||
return sb.toString();
|
||||
}
|
||||
@@ -21,17 +21,18 @@ package io.github.dsheirer.module.decode.dmr.message.voice.embedded;
|
||||
|
||||
/**
|
||||
* Short Burst opcode
|
||||
*
|
||||
* See: https://patents.google.com/patent/US8271009B2 Page 12
|
||||
* See: <a href="https://patents.google.com/patent/US8271009B2">...</a> Page 12
|
||||
*/
|
||||
public enum ShortBurstOpcode
|
||||
{
|
||||
NULL(0),
|
||||
ARC4_ENCRYPTION(1),
|
||||
TXI_DELAY(3),
|
||||
AES128_ENCRYPTION(4),
|
||||
AES256_ENCRYPTION(5),
|
||||
UNKNOWN(-1);
|
||||
|
||||
private int mValue;
|
||||
private final int mValue;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
||||
@@ -221,7 +221,7 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
||||
sb.append(",");
|
||||
}
|
||||
|
||||
if(identifier.getForm() == Form.TALKGROUP)
|
||||
if(identifier.getForm() == Form.TALKGROUP || identifier.getForm() == Form.RADIO || identifier.getForm() == Form.PATCH_GROUP)
|
||||
{
|
||||
sb.append(mUserPreferences.getTalkgroupFormatPreference().format(identifier));
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ public enum DecodeEventType
|
||||
PAGE("Page"),
|
||||
QUERY("Query"),
|
||||
RADIO_CHECK("Radio Check"),
|
||||
RADIO_REGISTRATION_SERVICE("Hytera RRS"),
|
||||
REGISTER("Register"),
|
||||
REGISTER_ESN("ESN"),
|
||||
REQUEST("Request"),
|
||||
@@ -78,9 +79,10 @@ public enum DecodeEventType
|
||||
STATUS("Status"),
|
||||
UDP_PACKET("UDP/IP Packet"),
|
||||
UNKNOWN_PACKET("Unknown Packet"),
|
||||
XCMP("Motorola XCMP"),
|
||||
UNKNOWN("Unknown");
|
||||
|
||||
private String mLabel;
|
||||
private final String mLabel;
|
||||
|
||||
/**
|
||||
* Encrypted voice call event types for filtering
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.ip.hytera.rrs;
|
||||
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
|
||||
import io.github.dsheirer.module.decode.ip.IPacket;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.DestinationId;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraToken;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader;
|
||||
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenType;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.ipv4.DMRIpAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hytera SDS Long Data Message with SMS encoded payload
|
||||
*/
|
||||
public class HyteraRrsPacket implements IPacket
|
||||
{
|
||||
private HyteraTokenHeader mHeader;
|
||||
private Identifier mDestination;
|
||||
private List<Identifier> mIdentifiers;
|
||||
|
||||
/**
|
||||
* Constructs a parser for a header contained within a binary message starting at the offset.
|
||||
*
|
||||
* @param header to the packet within the message
|
||||
*/
|
||||
public HyteraRrsPacket(HyteraTokenHeader header)
|
||||
{
|
||||
mHeader = header;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("HYTERA RRS REGISTER");
|
||||
if(hasDestination())
|
||||
{
|
||||
sb.append(" RADIO:").append(getDestination());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HyteraTokenHeader getHeader()
|
||||
{
|
||||
return mHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destination radio identifier for the SMS message
|
||||
*/
|
||||
public Identifier getDestination()
|
||||
{
|
||||
if(mDestination == null)
|
||||
{
|
||||
HyteraToken destinationToken = getHeader().getTokenByType(HyteraTokenType.ID_DESTINATION);
|
||||
|
||||
if(destinationToken instanceof DestinationId destinationId)
|
||||
{
|
||||
if(((DestinationId) destinationToken).isIp())
|
||||
{
|
||||
mDestination = DMRIpAddress.createFrom(((DestinationId) destinationToken).getId());
|
||||
}
|
||||
else
|
||||
{
|
||||
mDestination = DMRRadio.createTo(destinationId.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mDestination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the SMS message has a destination identifier
|
||||
*/
|
||||
public boolean hasDestination()
|
||||
{
|
||||
return getDestination() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
if(mIdentifiers == null)
|
||||
{
|
||||
mIdentifiers = new ArrayList<>();
|
||||
if(hasDestination())
|
||||
{
|
||||
mIdentifiers.add(getDestination());
|
||||
}
|
||||
}
|
||||
|
||||
return mIdentifiers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPacket getPayload()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPayload()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
package io.github.dsheirer.module.decode.ip.hytera.sds;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.ipv4.IPV4Address;
|
||||
|
||||
/**
|
||||
* Destination Identifier
|
||||
@@ -45,10 +46,19 @@ public class DestinationId extends HyteraToken
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("DESTINATION:").append(getId());
|
||||
sb.append("DESTINATION:").append(getIdFormatted());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this is an IP address (true) or a Radio ID (false).
|
||||
* @return true if IP
|
||||
*/
|
||||
public boolean isIp()
|
||||
{
|
||||
return getId() > 0x00FFFFFF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the content field in bytes.
|
||||
* @return content field length.
|
||||
@@ -69,6 +79,18 @@ public class DestinationId extends HyteraToken
|
||||
return mMessage.getInt(ID);
|
||||
}
|
||||
|
||||
public String getIdFormatted()
|
||||
{
|
||||
if(isIp())
|
||||
{
|
||||
return new IPV4Address(getId()).toString() + " [" + (getId() & 0xFFFFFF) + "]";
|
||||
}
|
||||
else
|
||||
{
|
||||
return String.valueOf(getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
HyteraTokenType getTokenType()
|
||||
{
|
||||
|
||||
@@ -37,11 +37,13 @@ public class HyteraTokenFactory
|
||||
return switch(token)
|
||||
{
|
||||
case ID_DESTINATION -> new DestinationId(message);
|
||||
case ENCODING -> new Encoding(message);
|
||||
case ID_MESSAGE -> new MessageId(message);
|
||||
case PAYLOAD -> new Payload(message);
|
||||
case MESSAGE_HEADER -> new MessageHeader(message);
|
||||
case ID_SOURCE -> new SourceId(message);
|
||||
case ENCODING -> new Encoding(message);
|
||||
case PAYLOAD -> new Payload(message);
|
||||
case RRS_UNKNOWN -> new RrsUnknown(message);
|
||||
case TEXT_MESSAGE_SERVICE_HEADER -> new TmsMessageHeader(message);
|
||||
case RADIO_REGISTRATION_SERVICE_HEADER -> new RrsMessageHeader(message);
|
||||
default -> new Unknown(message);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,7 +72,15 @@ public class HyteraTokenHeader implements IHeader
|
||||
*/
|
||||
public boolean isSMSMessage()
|
||||
{
|
||||
return hasTokens(HyteraTokenType.MESSAGE_HEADER, HyteraTokenType.ENCODING, HyteraTokenType.PAYLOAD);
|
||||
return hasTokens(HyteraTokenType.TEXT_MESSAGE_SERVICE_HEADER, HyteraTokenType.ENCODING, HyteraTokenType.PAYLOAD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this header has RRS service message.
|
||||
*/
|
||||
public boolean isRRSMessage()
|
||||
{
|
||||
return hasTokens(HyteraTokenType.RADIO_REGISTRATION_SERVICE_HEADER, HyteraTokenType.ID_DESTINATION);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +161,7 @@ public class HyteraTokenHeader implements IHeader
|
||||
length += contentLength * 8; //Actual content
|
||||
}
|
||||
|
||||
HyteraToken field = HyteraTokenFactory.getField(token, mMessage.getSubMessage(offset, offset + length + 1));
|
||||
HyteraToken field = HyteraTokenFactory.getField(token, mMessage.getSubMessage(offset, offset + length));
|
||||
offset += length;
|
||||
mParsedTokens.add(field);
|
||||
}
|
||||
|
||||
@@ -29,13 +29,15 @@ package io.github.dsheirer.module.decode.ip.hytera.sds;
|
||||
*/
|
||||
public enum HyteraTokenType
|
||||
{
|
||||
//Note: this may be the message type, but is definitely followed by 4-byte total length field. Need more examples
|
||||
MESSAGE_HEADER(0x0201, 4), //First 2 bytes are total message length, 2nd 2 bytes = unknown
|
||||
RADIO_REGISTRATION_SERVICE_HEADER(0x0200, 1), //Total message length
|
||||
TEXT_MESSAGE_SERVICE_HEADER(0x0201, 4), //First 2 bytes are total message length, 2nd 2 bytes = unknown
|
||||
|
||||
ID_MESSAGE(0x0001, -1),
|
||||
ID_SOURCE(0x0002, -1),
|
||||
ID_DESTINATION(0x0003, -1),
|
||||
ENCODING(0x0101, -1),
|
||||
PAYLOAD(0x0102, -1),
|
||||
RRS_UNKNOWN(0x0703, 0),
|
||||
UNKNOWN(-1, -1);
|
||||
|
||||
private int mOpcode;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.ip.hytera.sds;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
|
||||
/**
|
||||
* Radio Registration Service (RRS) Message Header
|
||||
*/
|
||||
public class RrsMessageHeader extends HyteraToken
|
||||
{
|
||||
private static int[] MESSAGE_LENGTH_BCD_TENS = new int[]{16, 17, 18, 19};
|
||||
private static int[] MESSAGE_LENGTH_BCD_ONES = new int[]{20, 21, 22, 23};
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param message containing both the token, run-length, and content
|
||||
*/
|
||||
public RrsMessageHeader(CorrectedBinaryMessage message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("RRS HEADER LENGTH:").append(getMessageLength());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
HyteraTokenType getTokenType()
|
||||
{
|
||||
return HyteraTokenType.RADIO_REGISTRATION_SERVICE_HEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overall message length in bytes
|
||||
* @return message length.
|
||||
*/
|
||||
public int getMessageLength()
|
||||
{
|
||||
int length = mMessage.getInt(MESSAGE_LENGTH_BCD_TENS) * 10;
|
||||
length += mMessage.getInt(MESSAGE_LENGTH_BCD_ONES);
|
||||
return length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.ip.hytera.sds;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
|
||||
/**
|
||||
* Unknown RRS field parsed from a Hytera Radio Registration Service (RRS) message
|
||||
*/
|
||||
public class RrsUnknown extends HyteraToken
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param message containing both the token, run-length, and content
|
||||
*/
|
||||
public RrsUnknown(CorrectedBinaryMessage message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "RRS UNKNOWN TOKEN";
|
||||
}
|
||||
|
||||
@Override
|
||||
HyteraTokenType getTokenType()
|
||||
{
|
||||
return HyteraTokenType.RRS_UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -22,16 +22,19 @@ package io.github.dsheirer.module.decode.ip.hytera.sds;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
|
||||
/**
|
||||
* Message Header
|
||||
*
|
||||
* Text Message Service (TMS) Message Header
|
||||
* <p>
|
||||
* Initial token that specifies the overall message length and some other unidentified bit fields.
|
||||
*
|
||||
* <p>
|
||||
* Note: I've only confirmed this is the start token for long SMS messages, but it may also be the start token for
|
||||
* other packet data messages like GPS or any other packet overlay application.
|
||||
*/
|
||||
public class MessageHeader extends HyteraToken
|
||||
public class TmsMessageHeader extends HyteraToken
|
||||
{
|
||||
private static int[] MESSAGE_LENGTH = new int[]{16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31};
|
||||
private static int[] MESSAGE_LENGTH_BCD_THOUSANDS = new int[]{16, 17, 18, 19};
|
||||
private static int[] MESSAGE_LENGTH_BCD_HUNDREDS = new int[]{20, 21, 22, 23};
|
||||
private static int[] MESSAGE_LENGTH_BCD_TENS = new int[]{24, 25, 26, 27};
|
||||
private static int[] MESSAGE_LENGTH_BCD_ONES = new int[]{28, 29, 30, 31};
|
||||
private static int[] MESSAGE_FLAGS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
|
||||
|
||||
/**
|
||||
@@ -39,7 +42,7 @@ public class MessageHeader extends HyteraToken
|
||||
*
|
||||
* @param message containing both the token, run-length, and content
|
||||
*/
|
||||
public MessageHeader(CorrectedBinaryMessage message)
|
||||
public TmsMessageHeader(CorrectedBinaryMessage message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
@@ -56,7 +59,7 @@ public class MessageHeader extends HyteraToken
|
||||
@Override
|
||||
HyteraTokenType getTokenType()
|
||||
{
|
||||
return HyteraTokenType.MESSAGE_HEADER;
|
||||
return HyteraTokenType.TEXT_MESSAGE_SERVICE_HEADER;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +68,7 @@ public class MessageHeader extends HyteraToken
|
||||
* - Priority/Flash (boolean)
|
||||
* - Encryption Type (enum of at least 4 values, ie 2+ bits)
|
||||
* - Emergency (boolean)
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getFlags()
|
||||
@@ -78,6 +82,10 @@ public class MessageHeader extends HyteraToken
|
||||
*/
|
||||
public int getMessageLength()
|
||||
{
|
||||
return mMessage.getInt(MESSAGE_LENGTH);
|
||||
int length = mMessage.getInt(MESSAGE_LENGTH_BCD_THOUSANDS) * 1000;
|
||||
length += mMessage.getInt(MESSAGE_LENGTH_BCD_HUNDREDS) * 100;
|
||||
length += mMessage.getInt(MESSAGE_LENGTH_BCD_TENS) * 10;
|
||||
length += mMessage.getInt(MESSAGE_LENGTH_BCD_ONES);
|
||||
return length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.ip.hytera.shortdata;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequence;
|
||||
import io.github.dsheirer.module.decode.ip.IHeader;
|
||||
import io.github.dsheirer.module.decode.ip.IPacket;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hytera Short Data Packet Sequence
|
||||
*/
|
||||
public class HyteraShortDataPacket implements IPacket
|
||||
{
|
||||
private final PacketSequence mPacketSequence;
|
||||
private CorrectedBinaryMessage mMessage;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param packetSequence containing the headers and data blocks
|
||||
* @param payload containing the reconstructed short data message from the data blocks.
|
||||
*/
|
||||
public HyteraShortDataPacket(PacketSequence packetSequence, CorrectedBinaryMessage payload)
|
||||
{
|
||||
mPacketSequence = packetSequence;
|
||||
mMessage = payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("CC:").append(getPacketSequence().getPacketSequenceHeader().getSlotType().getColorCode());
|
||||
sb.append(" HYTERA SHORT DATA MESSAGE FM:").append(getPacketSequence().getPacketSequenceHeader().getSourceLLID());
|
||||
sb.append(" TO:").append(getPacketSequence().getPacketSequenceHeader().getDestinationLLID());
|
||||
|
||||
if(getPacketSequence().isEncrypted())
|
||||
{
|
||||
HyteraDataEncryptionHeader hdeh = (HyteraDataEncryptionHeader)getPacketSequence().getProprietaryDataHeader();
|
||||
sb.append(" ENCRYPTED ALGORITHM:").append(hdeh.getAlgorithm());
|
||||
sb.append(" KEY:").append(hdeh.getKeyId());
|
||||
sb.append(" IV:").append(hdeh.getIV());
|
||||
}
|
||||
|
||||
sb.append(" SHORT DATA:").append(getMessage().toHexString());
|
||||
return sb.toString();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public IHeader getHeader()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPacket getPayload()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPayload()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload message
|
||||
* @return message
|
||||
*/
|
||||
public CorrectedBinaryMessage getMessage()
|
||||
{
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captured packet sequence
|
||||
* @return packet sequence
|
||||
*/
|
||||
public PacketSequence getPacketSequence()
|
||||
{
|
||||
return mPacketSequence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this packet is encrypted, meaning it contains a Hytera Data Encryption Header.
|
||||
* @return true if encrypted
|
||||
*/
|
||||
public boolean isEncrypted()
|
||||
{
|
||||
return getPacketSequence().isEncrypted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
return getPacketSequence().getPacketSequenceHeader().getIdentifiers();
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,8 @@ public class DecodeConfigNBFM extends DecodeConfigAnalog
|
||||
{
|
||||
switch(getBandwidth())
|
||||
{
|
||||
case BW_7_5:
|
||||
return new ChannelSpecification(25000.0, 7500, 3500.0, 3750.0);
|
||||
case BW_12_5:
|
||||
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
|
||||
case BW_25_0:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.identifier.ipv4;
|
||||
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.ipv4.IPV4Address;
|
||||
import io.github.dsheirer.identifier.ipv4.IPV4Identifier;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* DMR IPV4 Address
|
||||
*/
|
||||
public class DMRIpAddress extends IPV4Identifier
|
||||
{
|
||||
public DMRIpAddress(IPV4Address ipAddress, Role role)
|
||||
{
|
||||
super(ipAddress, role);
|
||||
}
|
||||
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.DMR;
|
||||
}
|
||||
|
||||
public static DMRIpAddress createTo(int value)
|
||||
{
|
||||
return new DMRIpAddress(new IPV4Address(value), Role.TO);
|
||||
}
|
||||
|
||||
public static DMRIpAddress createFrom(int value)
|
||||
{
|
||||
return new DMRIpAddress(new IPV4Address(value), Role.FROM);
|
||||
}
|
||||
}
|
||||
@@ -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.HeaderData;
|
||||
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.LCExtendedFunctionCommand;
|
||||
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.tsbk.Opcode;
|
||||
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.PatchGroupVoiceChannelGrant;
|
||||
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());
|
||||
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
|
||||
case ISP_GROUP_VOICE_SERVICE_REQUEST:
|
||||
if(tsbk instanceof GroupVoiceServiceRequest)
|
||||
@@ -1885,6 +1902,22 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
case UNIT_TO_UNIT_ANSWER_REQUEST:
|
||||
processBroadcast(lcw.getIdentifiers(), timestamp, DecodeEventType.PAGE, "Unit-to-Unit Answer Request");
|
||||
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:
|
||||
// mLog.debug("Unrecognized LCW Opcode: " + lcw.getOpcode().name() + " VENDOR:" + lcw.getVendor() +
|
||||
// " OPCODE:" + lcw.getOpcodeNumber());
|
||||
|
||||
@@ -42,6 +42,7 @@ public class TerminatorMessageFilterSet extends FilterSet<IMessage>
|
||||
addFilter(new TerminatorMessageFilter("Command/Status", LinkControlOpcode.COMMAND_STATUS_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("Network/Channel", LinkControlOpcode.NETWORK_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("Voice", LinkControlOpcode.VOICE_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("L3Harris", LinkControlOpcode.L3HARRIS_OPCODES));
|
||||
addFilter(new TerminatorMessageFilter("Motorola", LinkControlOpcode.MOTOROLA_OPCODES));
|
||||
|
||||
List<LinkControlOpcode> others = new ArrayList<>();
|
||||
|
||||
@@ -96,8 +96,14 @@ public enum LinkControlOpcode
|
||||
MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE("PATCH GROUP VOICE CHANNEL UPDATE", 1),
|
||||
MOTOROLA_PATCH_GROUP_ADD("PATCH GROUP ADD", 3),
|
||||
MOTOROLA_PATCH_GROUP_DELETE("PATCH GROUP DELETE", 4),
|
||||
MOTOROLA_UNIT_GPS("UNIT GPS", 6),
|
||||
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);
|
||||
|
||||
@@ -129,6 +135,11 @@ public enum LinkControlOpcode
|
||||
public static final EnumSet<LinkControlOpcode> MOTOROLA_OPCODES =
|
||||
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
|
||||
@@ -190,6 +201,18 @@ public enum LinkControlOpcode
|
||||
return values()[value];
|
||||
}
|
||||
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:
|
||||
switch(value)
|
||||
{
|
||||
@@ -201,16 +224,18 @@ public enum LinkControlOpcode
|
||||
return MOTOROLA_PATCH_GROUP_ADD;
|
||||
case 4:
|
||||
return MOTOROLA_PATCH_GROUP_DELETE;
|
||||
case 6:
|
||||
return MOTOROLA_UNIT_GPS;
|
||||
case 15:
|
||||
return MOTOROLA_TALK_COMPLETE;
|
||||
default:
|
||||
return MOTOROLA_UNKNOWN;
|
||||
}
|
||||
default:
|
||||
if(0 <= value && value <= 63)
|
||||
{
|
||||
return values()[value];
|
||||
}
|
||||
}
|
||||
|
||||
if(0 <= value && value <= 63)
|
||||
{
|
||||
return values()[value];
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* 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
|
||||
@@ -15,17 +14,21 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.module.decode.p25.phase1.message.lc;
|
||||
|
||||
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.LCMotorolaPatchGroupDelete;
|
||||
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.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.standard.LCAdjacentSiteStatusBroadcast;
|
||||
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:
|
||||
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:
|
||||
return new LCMotorolaPatchGroupAdd(binaryMessage);
|
||||
case MOTOROLA_PATCH_GROUP_DELETE:
|
||||
@@ -138,6 +150,8 @@ public class LinkControlWordFactory
|
||||
return new LCMotorolaTalkComplete(binaryMessage);
|
||||
case MOTOROLA_PATCH_GROUP_VOICE_CHANNEL_UPDATE:
|
||||
return new LCMotorolaPatchGroupVoiceChannelUpdate(binaryMessage);
|
||||
case MOTOROLA_UNIT_GPS:
|
||||
return new LCMotorolaUnitGPS(binaryMessage);
|
||||
case MOTOROLA_UNKNOWN:
|
||||
return new LCMotorolaUnknownOpcode(binaryMessage);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class LCMotorolaPatchGroupAdd extends MotorolaLinkControlWord
|
||||
if(mPatchGroup == null)
|
||||
{
|
||||
PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getMessage().getInt(PATCH_GROUP)));
|
||||
patchGroup.addPatchedGroups(getPatchedGroups());
|
||||
patchGroup.addPatchedTalkgroups(getPatchedGroups());
|
||||
mPatchGroup = APCO25PatchGroup.create(patchGroup);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ public class LCMotorolaPatchGroupDelete extends MotorolaLinkControlWord
|
||||
if(mPatchGroup == null)
|
||||
{
|
||||
PatchGroup patchGroup = new PatchGroup(APCO25Talkgroup.create(getMessage().getInt(PATCH_GROUP)));
|
||||
patchGroup.addPatchedGroups(getPatchedGroups());
|
||||
patchGroup.addPatchedTalkgroups(getPatchedGroups());
|
||||
mPatchGroup = APCO25PatchGroup.create(patchGroup);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* 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
|
||||
@@ -15,7 +14,7 @@
|
||||
*
|
||||
* 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.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.ReadyTimer;
|
||||
import io.github.dsheirer.module.decode.p25.reference.StandbyTimer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -81,7 +79,7 @@ public class ActivateTdsContextAccept extends SNDCPMessage
|
||||
}
|
||||
sb.append(" READY TIMER:").append(getReadyTimer().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());
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -176,11 +176,11 @@ public enum Opcode
|
||||
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"),
|
||||
|
||||
//Vendor: motorola, Inbound Service Packet (ISP)
|
||||
//Vendor: L3Harris, Inbound Service Packet (ISP)
|
||||
HARRIS_ISP_UNKNOWN(-1, "HARRIS ISP UNKNOWN OPCODE", "HARRIS ISP UNKNOWN OPCODE"),
|
||||
|
||||
//Vendor: harris, Outbound Service Packet (OSP)
|
||||
HARRIS_OSP_TDMA_SYNC(48, "HARRIS TDMA SYNC", "HARRIS TDMA SYNC BROADCAST"),
|
||||
//Vendor: L3Harris, Outbound Service Packet (OSP)
|
||||
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"),
|
||||
|
||||
//Vendor: unknown, Inbound Service Packet (ISP)
|
||||
@@ -294,7 +294,7 @@ public enum Opcode
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
@@ -425,7 +425,7 @@ public enum Opcode
|
||||
switch(value)
|
||||
{
|
||||
case 48:
|
||||
return HARRIS_OSP_TDMA_SYNC;
|
||||
return HARRIS_OSP_GRG_EXENC_CMD;
|
||||
default:
|
||||
return HARRIS_OSP_UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -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.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.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.motorola.isp.UnknownMotorolaISPMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.motorola.osp.ChannelLoading;
|
||||
@@ -298,8 +298,8 @@ public class TSBKMessageFactory
|
||||
|
||||
case HARRIS_ISP_UNKNOWN:
|
||||
return new UnknownHarrisISPMessage(dataUnitID, message, nac, timestamp);
|
||||
case HARRIS_OSP_TDMA_SYNC:
|
||||
return new HarrisTDMASyncBroadcast(dataUnitID, message, nac, timestamp);
|
||||
case HARRIS_OSP_GRG_EXENC_CMD:
|
||||
return new HarrisGroupRegroupExplicitEncryptionCommand(dataUnitID, message, nac, timestamp);
|
||||
case HARRIS_OSP_UNKNOWN:
|
||||
return new UnknownHarrisOSPMessage(dataUnitID, message, nac, timestamp);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user