Compare commits

...

9 Commits

Author SHA1 Message Date
Denny Sheirer
9abf64c6ab #1449 Updates version to 0.5.1 final (#1451)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-19 07:17:10 -05:00
Denny Sheirer
1702103f5d #1449 Updates version to 0.5.1 beta-2 (#1450)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-19 07:10:44 -05:00
Denny Sheirer
8f68a989b9 #1444 Adds: (#1447)
* DMR Recording viewer utility
* DMR talker aliases
* DMR SMS messages
* DMR Hytera Tier-III compressed talkgroups support
* DMR UDT packet sequences
* DMR Hytera Aloha and other unknown CSBK and Link Control messages

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-18 08:36:55 -05:00
Denny Sheirer
b51a0102ef #1422 Resolves issue where MotoTRBO IP Site Connect and Capacity Plus voice group user message not decoding Radio & Talkgroup IDs correctly. Creates standalone application for viewing DMR messages from recordings. (#1442)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-11 15:32:41 -05:00
Denny Sheirer
e177c2c6b0 #1426 Adds JRE accessibility module to support assistive technologies. (#1427)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2023-01-21 07:21:00 -05:00
Denny Sheirer
3567fdf51d #1393 Updates DMR Talkgroup and Radio ID editors to use correct formatting. Patch submitted by @hockeyref2 (#1425)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2023-01-20 06:50:18 -05:00
Denny Sheirer
323097ba65 #1423 Updates Icecast/Broadcastify feed to attempt reconnect following a mountpoint in use error response. (#1424)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2023-01-20 05:39:38 -05:00
Denny Sheirer
33f2bab7ac #1419 Reduce logging from USBTunerController for (temporary) failed transfer buffer resubmits. (#1420)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-18 04:38:30 -05:00
Denny Sheirer
c063cfb201 #1416 Updates P25-P1 To Expose ICMP & Unknown Packet Events in Event History View (#1417)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-16 05:31:27 -05:00
60 changed files with 3576 additions and 321 deletions

View File

@@ -28,7 +28,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.5.1-beta1'
version = '0.5.1'
sourceCompatibility = '17'
sourceSets {
@@ -212,7 +212,9 @@ runtime {
}
//Additional modules to include with auto-detected modules.
modules = ['jdk.crypto.ec'] //jdk.crypto.ec needed for HTTPS connections (broadcastify calls & map tile server)
//jdk.crypto.ec needed for HTTPS connections (broadcastify calls & map tile server)
//jdk.accessibility is used with assistive technologies like screen readers
modules = ['jdk.crypto.ec','jdk.accessibility']
//Use auto-detected modules and 'add' any specified modules.
additive = true

View File

@@ -68,7 +68,7 @@ public enum BroadcastState
/**
* Specified mount point is already in use
*/
MOUNT_POINT_IN_USE("Mount Point In Use", true),
MOUNT_POINT_IN_USE("Mount Point In Use", false),
/**
* Network is unavailable or the server address cannot be resolved

View File

@@ -19,13 +19,20 @@
package io.github.dsheirer.bits;
import io.github.dsheirer.edac.CRC;
import java.io.UnsupportedEncodingException;
import java.util.BitSet;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BinaryMessage extends BitSet
{
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = LoggerFactory.getLogger(BinaryMessage.class);
private static final int[] CHARACTER_7_BIT = new int[]{0, 1, 2, 3, 4, 5, 6};
private static final int[] CHARACTER_8_BIT = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
private static final String UTF_8 = "UTF-8";
/**
* Logical (ie constructed) size of this bitset, despite the actual size of
@@ -1174,19 +1181,101 @@ public class BinaryMessage extends BitSet
this.xor(mask);
}
public static void main(String[] args)
/**
* Parse ISO-7 bit characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseISO7(int offset, int characterCount)
{
BinaryMessage message = new BinaryMessage(10);
message.set(1);
message.set(4);
message.set(5);
message.set(8);
message.set(9);
byte[] bytes = new byte[characterCount];
System.out.println("Hex: " + message.toHexString() + " Size:" + message.size());
for(int x = 0; x < 10; x++)
for(int x = 0; x < characterCount; x++)
{
System.out.println(x + ": " + message.getHex(x, x + 10));
bytes[x] = getByte(CHARACTER_7_BIT, x * 7 + offset);
}
return new String(bytes);
}
/**
* Parse ISO-8 bit characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseISO8(int offset, int characterCount)
{
byte[] bytes = new byte[characterCount];
for(int x = 0; x < characterCount; x++)
{
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes);
}
/**
* Parse UTF-8 characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
* @throws UnsupportedEncodingException if UTF-8 encoding is not supported.
*/
public String parseUTF8(int offset, int characterCount) throws UnsupportedEncodingException
{
byte[] bytes = new byte[characterCount];
for(int x = 0; x < characterCount; x++)
{
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes, UTF_8);
}
/**
* Parse 16-bit unicode characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseUnicode(int offset, int characterCount)
{
int length = characterCount * 2;
byte[] bytes = new byte[length];
for(int x = 0; x < length; x++)
{
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes);
}
/**
* Parse 4-bit Binary Coded Decimal (BCD) characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseBCD4(int offset, int characterCount)
{
StringBuilder sb = new StringBuilder();
int count = 0;
while(count < characterCount)
{
int nibble = getNibble(offset);
//This will parse digits 0-9 and anything higher will parse as hexadecimal A-F
sb.append(Integer.toHexString(nibble).toUpperCase());
offset += 4;
count++;
}
return sb.toString();
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* 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.edac;
@@ -34,7 +31,7 @@ import org.slf4j.LoggerFactory;
*/
public class BerlekempMassey
{
public final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
private final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
/* Golay field size GF( 2 ** MM ) */
private int MM;

View File

@@ -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.edac;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements the 5-bit Checksum error detection algorithm specified in TS 102 361-1 paragraph B.3.11
*/
public class Checksum_5_DMR
{
private static final Logger LOGGER = LoggerFactory.getLogger(Checksum_5_DMR.class);
private static final int[] LC_0 = {0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] LC_1 = {8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] LC_2 = {16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] LC_3 = {24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] LC_4 = {32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] LC_5 = {40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] LC_6 = {48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] LC_7 = {56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] LC_8 = {64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] CHECKSUM = {72, 73, 74, 75, 76};
/**
* Indicates if the 77-bit Full Link Control message is valid.
* @param message to check
* @return true if the checksum matches.
*/
public static boolean isValid(CorrectedBinaryMessage message)
{
int accumulator = message.getInt(LC_0);
accumulator += message.getInt(LC_1);
accumulator += message.getInt(LC_2);
accumulator += message.getInt(LC_3);
accumulator += message.getInt(LC_4);
accumulator += message.getInt(LC_5);
accumulator += message.getInt(LC_6);
accumulator += message.getInt(LC_7);
accumulator += message.getInt(LC_8);
accumulator = accumulator % 31;
int checksum = message.getInt(CHECKSUM);
return accumulator == checksum;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,12 +20,16 @@
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Berlecamp-Massey decoder for Reed Solomon RS(12,9,4) protected messages using DMR generator polynomial
*/
public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR
{
private final static Logger mLog = LoggerFactory.getLogger(ReedSolomon_12_9_4_DMR.class);
private static final int[] FLC_CODEWORD_0 = {0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] FLC_CODEWORD_1 = {8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] FLC_CODEWORD_2 = {16, 17, 18, 19, 20, 21, 22, 23};
@@ -56,16 +60,16 @@ public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR
*
* @param message to be corrected.
* @param crcMask as defined in the TS 102-361-1 Table B.21
* @return true if the message was corrected.
* @return true if the message is correct or if the message was corrected.
*/
public boolean correctFullLinkControl(CorrectedBinaryMessage message, int crcMask)
{
int[] input = new int[255];
int[] output = new int[255];
input[0] = message.getInt(FLC_PARITY_2) ^ crcMask;
input[1] = message.getInt(FLC_PARITY_1) ^ crcMask;
input[2] = message.getInt(FLC_PARITY_0) ^ crcMask;
input[0] = (message.getInt(FLC_PARITY_2) ^ crcMask);
input[1] = (message.getInt(FLC_PARITY_1) ^ crcMask);
input[2] = (message.getInt(FLC_PARITY_0) ^ crcMask);
input[3] = message.getInt(FLC_CODEWORD_8);
input[4] = message.getInt(FLC_CODEWORD_7);
input[5] = message.getInt(FLC_CODEWORD_6);
@@ -106,4 +110,40 @@ public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR
return true;
}
public static void main(String[] args)
{
int[] output = new int[255];
int[] input = new int[255];
input[0] = 247;
input[1] = 236;
input[2] = 171;
input[3] = 24;
input[4] = 46;
input[5] = 177;
input[6] = 246;
input[7] = 205;
input[8] = 232;
input[9] = 32;
input[10] = 124;
input[11] = 0;
// input[254] = 247;
// input[253] = 236;
// input[252] = 171;
// input[251] = 24;
// input[250] = 46;
// input[249] = 177;
// input[248] = 246;
// input[247] = 205;
// input[246] = 232;
// input[245] = 32;
// input[244] = 124;
// input[243] = 0;
ReedSolomon_12_9_4_DMR rs = new ReedSolomon_12_9_4_DMR();
boolean irrecoverable = rs.decode(input, output);
mLog.info("Irrecoverable: " + irrecoverable);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,6 +24,8 @@ 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;
@@ -65,6 +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_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";
@@ -79,12 +82,14 @@ public class JavaFxWindowManager extends Application
private TunerManager mTunerManager;
private UserPreferences mUserPreferences;
private UserPreferencesEditor mUserPreferencesEditor;
private DMRRecordingViewer mDmrRecordingViewer;
private Stage mChannelMapStage;
private Stage mIconManagerStage;
private Stage mJmbeEditorStage;
private Stage mPlaylistStage;
private Stage mUserPreferencesStage;
private Stage mDmrRecordingViewerStage;
/**
* Constructs an instance. Note: this constructor is used for Swing applications.
@@ -189,6 +194,34 @@ public class JavaFxWindowManager extends Application
return new CalibrationDialog(userPreferences);
}
/**
* Stage for the DMR Message Viewer
*/
public Stage getDmrRecordingViewerStage()
{
if(mDmrRecordingViewerStage == 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);
}
return mDmrRecordingViewerStage;
}
public DMRRecordingViewer getDmrRecordingViewer()
{
if(mDmrRecordingViewer == null)
{
mDmrRecordingViewer = new DMRRecordingViewer();
}
return mDmrRecordingViewer;
}
public Stage getIconManagerStage()
{
if(mIconManagerStage == null)
@@ -416,6 +449,19 @@ public class JavaFxWindowManager extends Application
});
}
/**
* Process a channel map editor request
*/
@Subscribe
public void process(final ViewDmrRecordingViewerRequest request)
{
execute(() -> {
getDmrRecordingViewerStage().show();
getDmrRecordingViewerStage().requestFocus();
getDmrRecordingViewerStage().toFront();
});
}
@Override
public void start(Stage primaryStage) throws Exception
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -32,6 +32,7 @@ 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;
@@ -59,6 +60,26 @@ import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import jiconfont.icons.font_awesome.FontAwesome;
@@ -80,26 +101,6 @@ import javax.swing.UIManager;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.plaf.metal.MetalLookAndFeel;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
public class SDRTrunk implements Listener<TunerEvent>
{
@@ -413,9 +414,11 @@ public class SDRTrunk implements Listener<TunerEvent>
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
viewMenu.add(viewPlaylistItem);
JMenuItem preferencesItem = new JMenuItem("User Preferences");
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
viewMenu.add(preferencesItem);
viewMenu.add(new JSeparator());
JMenuItem dmrMessageViewerMenu = new JMenuItem("DMR Recording Viewer");
dmrMessageViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewDmrRecordingViewerRequest()));
viewMenu.add(dmrMessageViewerMenu);
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
@@ -436,15 +439,19 @@ public class SDRTrunk implements Listener<TunerEvent>
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " +
getHomePath().toString(),
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
"Can't launch file explorer - files are located at: " +
getHomePath().toString(),
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
}
});
viewMenu.add(logFilesMenu);
JMenuItem preferencesItem = new JMenuItem("User Preferences");
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
viewMenu.add(preferencesItem);
viewMenu.add(new JSeparator());
viewMenu.add(new TunersMenu());
viewMenu.add(new JSeparator());

View File

@@ -0,0 +1,479 @@
/*
* *****************************************************************************
* 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.dmr;
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 java.io.File;
import java.nio.ByteBuffer;
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;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
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.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
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.
*/
public class DMRRecordingViewer extends VBox
{
private static final Logger mLog = LoggerFactory.getLogger(DMRRecordingViewer.class);
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory";
private Preferences mPreferences = Preferences.userNodeForPackage(DMRRecordingViewer.class);
private Scene mScene;
private Button mSelectFileButton;
private Label mSelectedFileLabel;
private TableView<IMessage> mMessageTableView;
private ObservableList<IMessage> mMessages = FXCollections.observableArrayList();
private FilteredList<IMessage> mFilteredMessages = new FilteredList<>(mMessages);
private CheckBox mShowTS0;
private CheckBox mShowTS1;
private CheckBox mShowTS2;
private CheckBox mUseCompressedTalkgroups;
private TextField mSearchText;
private TextField mFindText;
private Button mFindButton;
private Button mFindNextButton;
public DMRRecordingViewer()
{
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);
HBox compressedBox = new HBox();
compressedBox.setAlignment(Pos.CENTER_RIGHT);
compressedBox.setMaxWidth(Double.MAX_VALUE);
compressedBox.getChildren().addAll(getUseCompressedTalkgroups());
HBox.setHgrow(compressedBox, Priority.ALWAYS);
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel(), compressedBox);
HBox filterBox = new HBox();
filterBox.setMaxWidth(Double.MAX_VALUE);
filterBox.setAlignment(Pos.BASELINE_CENTER);
filterBox.setSpacing(5);
Label showLabel = new Label("Show:");
HBox.setMargin(showLabel, new Insets(0,0,0,15));
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(), showLabel, getShowTS0(), getShowTS1(), getShowTS2());
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
* @param file containing a .bits recording of decoded DMR data.
*/
private void load(File file)
{
if(file != null && file.exists())
{
getSelectedFileLabel().setText(file.getName());
mMessages.clear();
DMRMessageFramer messageFramer = new DMRMessageFramer(null);
DecodeConfigDMR config = new DecodeConfigDMR();
if(getUseCompressedTalkgroups().isSelected())
{
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())
{
ByteBuffer buffer = reader.next();
messageFramer.receive(buffer);
}
}
catch(Exception ioe)
{
ioe.printStackTrace();
}
}
}
/**
* Updates the filter(s) applies to the list of messages
*/
private void updateFilters()
{
Predicate<IMessage> timeslotPredicate = message ->
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
String filterText = getSearchText().getText();
if(filterText == null || filterText.isEmpty())
{
mFilteredMessages.setPredicate(timeslotPredicate);
}
else
{
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
mFilteredMessages.setPredicate(timeslotPredicate.and(textPredicate));
}
}
/**
* 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<>(mFilteredMessages);
TableColumn timestampColumn = new TableColumn();
timestampColumn.setPrefWidth(110);
timestampColumn.setText("Time");
timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
TableColumn timeslotColumn = new TableColumn();
timeslotColumn.setPrefWidth(35);
timeslotColumn.setText("TS");
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
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, timeslotColumn, 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"));
File selected = fileChooser.showOpenDialog(getScene().getWindow());
if(selected != null)
{
mLog.info("Last Selected Directory updated to [" + selected.getParent() +"]");
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
load(selected);
}
});
}
return mSelectFileButton;
}
/**
* Selected file path label.
*/
private Label getSelectedFileLabel()
{
if(mSelectedFileLabel == null)
{
mSelectedFileLabel = new Label(" ");
}
return mSelectedFileLabel;
}
/**
* Check box to apply filter to show/hide TS0 messages
* @return check box control
*/
private CheckBox getShowTS0()
{
if(mShowTS0 == null)
{
mShowTS0 = new CheckBox("TS0");
mShowTS0.setSelected(true);
mShowTS0.setOnAction(event -> updateFilters());
}
return mShowTS0;
}
/**
* Check box to apply filter to show/hide TS0 messages
* @return check box control
*/
private CheckBox getShowTS1()
{
if(mShowTS1 == null)
{
mShowTS1 = new CheckBox("TS1");
mShowTS1.setSelected(true);
mShowTS1.setOnAction(event -> updateFilters());
}
return mShowTS1;
}
/**
* Check box to apply filter to show/hide TS0 messages
* @return check box control
*/
private CheckBox getShowTS2()
{
if(mShowTS2 == null)
{
mShowTS2 = new CheckBox("TS2");
mShowTS2.setSelected(true);
mShowTS2.setOnAction(event -> updateFilters());
}
return mShowTS2;
}
/**
* 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.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;
}
/**
* Use Hytera Tier 3 Compressed Talkgroups
* @return
*/
private CheckBox getUseCompressedTalkgroups()
{
if(mUseCompressedTalkgroups == null)
{
mUseCompressedTalkgroups = new CheckBox("Use 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);
}
}

View File

@@ -0,0 +1,33 @@
/*
* *****************************************************************************
* 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.dmr;
import io.github.dsheirer.gui.JavaFxWindowRequest;
/**
* Request to show the DMR recording viewer
*/
public class ViewDmrRecordingViewerRequest extends JavaFxWindowRequest
{
/**
* Constructs an instance
*/
public ViewDmrRecordingViewerRequest() {}
}

View File

@@ -217,6 +217,9 @@ public class RadioIdEditor extends IdentifierEditor<Radio>
}
}
mLog.warn("Unable to find radio id editor for protocol [" + protocol + "] and format [" + integerFormat +
"] - using default editor");
//Use a default instance
for(RadioDetail detail: mRadioDetails)
{

View File

@@ -159,7 +159,7 @@ public class RadioIdRangeEditor extends IdentifierEditor<RadioRange>
else
{
mLog.warn("Couldn't find radio ID detail for protocol [" + getItem().getProtocol() +
"] and format [" + format + "]");
"] and format [" + format + "]");
getFormatLabel().setText(" ");
}
}
@@ -267,6 +267,9 @@ public class RadioIdRangeEditor extends IdentifierEditor<RadioRange>
}
}
mLog.warn("Unable to find radio id range editor for protocol [" + protocol + "] and format [" + integerFormat +
"] - using default editor");
//Use a default instance
for(RadioIdDetail detail: mRadioDetails)
{
@@ -287,20 +290,23 @@ public class RadioIdRangeEditor extends IdentifierEditor<RadioRange>
{
List<RadioIdDetail> details = new ArrayList<>();
details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFFFF),
new IntegerFormatter(0,0xFFFFFF), "Format: 0 - 16777215"));
new IntegerFormatter(0,0xFFFFFF), "Format: 0 - 16777215"));
details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF"));
new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF"));
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607"));
new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607"));
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
new HexFormatter(0,0x7FFFFF), "Format: 0 - 7FFFFF"));
new HexFormatter(0,0x7FFFFF), "Format: 0 - 7FFFFF"));
details.add(new RadioIdDetail(Protocol.UNKNOWN, IntegerFormat.DECIMAL, new IntegerFormatter(0,16777215),
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
details.add(new RadioIdDetail(Protocol.UNKNOWN, IntegerFormat.FORMATTED, new IntegerFormatter(0,16777215),
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
details.add(new RadioIdDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
details.add(new RadioIdDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(0,16777215),
new IntegerFormatter(0,16777215), "Format: 0 - 16777215"));
details.add(new RadioIdDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
return details;
}

View File

@@ -135,7 +135,7 @@ public class TalkgroupEditor extends IdentifierEditor<Talkgroup>
else
{
mLog.warn("Couldn't find talkgroup detail for protocol [" + getItem().getProtocol() +
"] and format [" + format + "]");
"] and format [" + format + "]");
}
getFormatLabel().setText(talkgroupDetail.getTooltip());
@@ -219,6 +219,9 @@ public class TalkgroupEditor extends IdentifierEditor<Talkgroup>
}
}
mLog.warn("Unable to find talkgroup editor for protocol [" + protocol + "] and format [" + integerFormat +
"] - using default editor");
//Use a default instance
for(TalkgroupDetail detail: mTalkgroupDetails)
{
@@ -236,37 +239,37 @@ public class TalkgroupEditor extends IdentifierEditor<Talkgroup>
{
mTalkgroupDetails.clear();
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535),
"Format: 0 - 65535"));
"Format: 0 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
"Format: 0 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFFFF),
"Format: 0 - 16,777,215"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
"Format: 0 - FFFFFF"));
"Format: 0 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFFFF),
"Format: 1 - 16,777,215"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFFFF),
"Format: 1 - FFFFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.LTR, IntegerFormat.FORMATTED, new LtrFormatter(0,0x3FFF),
"Format: A-HH-TTT = Area (0-1), Home (1-31), Talkgroup (1-255)"));
"Format: A-HH-TTT = Area (0-1), Home (1-31), Talkgroup (1-255)"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.MDC1200, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
"Format: 0 - 65535"));
"Format: 0 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.MDC1200, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
"Format: 0 - FFFF"));
"Format: 0 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.MPT1327, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF),
"Format: 1 - 65535"));
"Format: 1 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
"Format: 1 - FFFF"));
"Format: 1 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
"Format: 0 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
"Format: 0 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.DECIMAL, new IntegerFormatter(0,16777215),
"Format: 0 - FFFFFF"));
"Format: 0 - FFFFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.FORMATTED, new IntegerFormatter(0,16777215),
"Format: 0 - FFFFFF"));
"Format: 0 - FFFFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
"Format: 0 - FFFFFF"));
"Format: 0 - FFFFFF"));
}
public static class TalkgroupDetail

View File

@@ -161,7 +161,7 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
else
{
mLog.warn("Couldn't find talkgroup detail for protocol [" + getItem().getProtocol() +
"] and format [" + format + "]");
"] and format [" + format + "]");
getFormatLabel().setText(" ");
}
}
@@ -269,6 +269,9 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
}
}
mLog.warn("Unable to find talkgroup range editor for protocol [" + protocol + "] and format [" + integerFormat +
"] - using default editor");
//Use a default instance
for(TalkgroupDetail detail: mTalkgroupDetails)
{
@@ -289,31 +292,35 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
{
List<TalkgroupDetail> details = new ArrayList<>();
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535),
new IntegerFormatter(0,65535), "Format: 0 - 65535"));
new IntegerFormatter(0,65535), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
new HexFormatter(0,65535), "Format: 0 - FFFF"));
new HexFormatter(0,65535), "Format: 0 - FFFF"));
details.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
"Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
"Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
details.add(new TalkgroupDetail(Protocol.LTR, IntegerFormat.FORMATTED, new LtrFormatter(0,0x3FFF),
new LtrFormatter(0,0x3FFF), "Format: A-HH-TTT = Area (0-1), Home (1-31), Talkgroup (1-255)"));
new LtrFormatter(0,0x3FFF), "Format: A-HH-TTT = Area (0-1), Home (1-31), Talkgroup (1-255)"));
details.add(new TalkgroupDetail(Protocol.MDC1200, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.MDC1200, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
new HexFormatter(0,0xFFFF), "Format: 0 - FFFF"));
new HexFormatter(0,0xFFFF), "Format: 0 - FFFF"));
details.add(new TalkgroupDetail(Protocol.MPT1327, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
"Format: PPP-IIII = Prefix (0-127), Ident (1-8191)"));
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
"Format: PPP-IIII = Prefix (0-127), Ident (1-8191)"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
new HexFormatter(0,0xFFFF), "Format: 0 - FFFF"));
new HexFormatter(0,0xFFFF), "Format: 0 - FFFF"));
details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.DECIMAL, new IntegerFormatter(0,16777215),
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
new IntegerFormatter(0,16777215), "Format: 0 - 16777215"));
details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.FORMATTED, new IntegerFormatter(0,16777215),
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,16777215),
new IntegerFormatter(1,16777215), "Format: 1 - 16777215"));
details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0XFFFFFF),
new HexFormatter(1, 0xFFFFFF), "Format: 0 - FFFFFF"));
return details;
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -38,6 +38,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
@@ -65,9 +67,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* DMR channel configuration editor
*/
@@ -83,6 +82,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
private RecordConfigurationEditor mRecordConfigurationEditor;
private ToggleSwitch mIgnoreDataCallsButton;
private ToggleSwitch mIgnoreCRCChecksumsButton;
private ToggleSwitch mUseCompressedTalkgroupsToggle;
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
private TableView<TimeslotFrequency> mTimeslotFrequencyTable;
private IntegerTextField mLogicalSlotNumberField;
@@ -164,6 +164,14 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
GridPane.setConstraints(ignoreCRCLabel, 5, row);
gridPane.getChildren().add(ignoreCRCLabel);
GridPane.setConstraints(getUseCompressedTalkgroupsToggle(), 6, row);
gridPane.getChildren().add(getUseCompressedTalkgroupsToggle());
Label useCompressedTalkgroupsLabel = new Label("Use Compressed Talkgroups");
GridPane.setHalignment(useCompressedTalkgroupsLabel, HPos.LEFT);
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");
GridPane.setHalignment(timeslotTableLabel, HPos.LEFT);
GridPane.setConstraints(timeslotTableLabel, 0, ++row, 6, 1);
@@ -512,6 +520,24 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
return mIgnoreCRCChecksumsButton;
}
/**
* Use compressed talkgroups toggle switch. Let's the user select to turn on compressed talkgroups for Hytera
* Tier-III systems.
* @return toggle.
*/
private ToggleSwitch getUseCompressedTalkgroupsToggle()
{
if(mUseCompressedTalkgroupsToggle == null)
{
mUseCompressedTalkgroupsToggle = new ToggleSwitch();
mUseCompressedTalkgroupsToggle.setTooltip(new Tooltip("Use Compressed Talkgroups. This is only for Hytera Tier-III Trunked Systems"));
mUseCompressedTalkgroupsToggle.setDisable(true);
mUseCompressedTalkgroupsToggle.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mUseCompressedTalkgroupsToggle;
}
private Spinner<Integer> getTrafficChannelPoolSizeSpinner()
{
if(mTrafficChannelPoolSizeSpinner == null)
@@ -578,6 +604,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
{
getIgnoreCRCChecksumsButton().setDisable(config == null);
getIgnoreDataCallsButton().setDisable(config == null);
getUseCompressedTalkgroupsToggle().setDisable(config == null);
getTrafficChannelPoolSizeSpinner().setDisable(config == null);
getTimeslotTable().getItems().clear();
getTimeslotTable().setDisable(config == null);
@@ -597,6 +624,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
getIgnoreDataCallsButton().setSelected(decodeConfig.getIgnoreDataCalls());
getIgnoreCRCChecksumsButton().setSelected(decodeConfig.getIgnoreCRCChecksums());
getUseCompressedTalkgroupsToggle().setSelected(decodeConfig.isUseCompressedTalkgroups());
getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(decodeConfig.getTrafficChannelPoolSize());
for(TimeslotFrequency timeslotFrequency: decodeConfig.getTimeslotMap())
@@ -608,6 +636,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
{
getIgnoreCRCChecksumsButton().setSelected(false);
getIgnoreDataCallsButton().setSelected(false);
getUseCompressedTalkgroupsToggle().setSelected(false);
getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(0);
getChannelRotationDelaySpinner().getValueFactory().setValue(200);
}
@@ -630,6 +659,7 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
config.setIgnoreCRCChecksums(getIgnoreCRCChecksumsButton().isSelected());
config.setIgnoreDataCalls(getIgnoreDataCallsButton().isSelected());
config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
config.setUseCompressedTalkgroups(getUseCompressedTalkgroupsToggle().isSelected());
config.setTimeslotMap(new ArrayList<>(getTimeslotTable().getItems()));
getItem().setDecodeConfiguration(config);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -53,6 +53,7 @@ public enum Form
SITE,
STATE,
SYSTEM,
TALKER_ALIAS,
TALKGROUP,
TELEPHONE_NUMBER,
TONE,

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,6 @@
package io.github.dsheirer.identifier;
import io.github.dsheirer.protocol.Protocol;
import java.util.Objects;
/**
@@ -70,6 +69,15 @@ public abstract class Identifier<T>
return mValue;
}
/**
* Allow subclass implementations to set or update the value after construction.
* @param value to update or set.
*/
protected void setValue(T value)
{
mValue = value;
}
/**
* Class/type of identifier
*/

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* 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.identifier.alias;
import io.github.dsheirer.protocol.Protocol;
/**
* Talker alias value provided by the network for the current talker (ie FROM).
*/
public class DmrTalkerAliasIdentifier extends TalkerAliasIdentifier
{
/**
* Constructs an instance.
* @param value of the talker alias
*/
public DmrTalkerAliasIdentifier(String value)
{
super(value);
}
@Override
public boolean isValid()
{
return getValue() != null && !getValue().isEmpty();
}
@Override
public Protocol getProtocol()
{
return Protocol.DMR;
}
public static DmrTalkerAliasIdentifier create(String value)
{
return new DmrTalkerAliasIdentifier(value);
}
}

View File

@@ -0,0 +1,46 @@
/*
* *****************************************************************************
* 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.identifier.alias;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.StringIdentifier;
/**
* Talker alias value provided by the network for the current talker (ie FROM).
*/
public abstract class TalkerAliasIdentifier extends StringIdentifier
{
/**
* Constructs an instance.
* @param value of the string/alias identifier
*/
public TalkerAliasIdentifier(String value)
{
super(value, IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
}
@Override
public boolean isValid()
{
return getValue() != null && !getValue().isEmpty();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -59,6 +59,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
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;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasComplete;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
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.HyteraUnitToUnitVoiceChannelUser;
@@ -66,6 +67,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.Capaci
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage;
import io.github.dsheirer.module.decode.dmr.message.data.packet.UDTShortMessageService;
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.VoiceMessage;
@@ -75,13 +77,12 @@ import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
import io.github.dsheirer.util.PacketUtil;
import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
/**
* Decoder state for an DMR channel. Maintains the call/data/idle state of the channel and produces events by
@@ -207,6 +208,10 @@ public class DMRDecoderState extends TimeslotDecoderState
{
processPacket((DMRPacketMessage)message);
}
else if(message instanceof UDTShortMessageService sms)
{
processSMS(sms);
}
else if(message instanceof DMRMessage)
{
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
@@ -269,6 +274,24 @@ public class DMRDecoderState extends TimeslotDecoderState
}
}
/**
* Processes a short data message carrying SMS text
* @param sms
*/
private void processSMS(UDTShortMessageService sms)
{
broadcast(new DecoderStateEvent(this, Event.START, State.DATA, getTimeslot()));
DecodeEvent smsEvent = DMRDecodeEvent.builder(sms.getTimestamp())
.eventType(DecodeEventType.SMS)
.eventDescription(DecodeEventType.SMS.toString())
.details("MESSAGE: " + sms.getSMS())
.identifiers(new IdentifierCollection(sms.getIdentifiers()))
.timeslot(sms.getTimeslot())
.build();
broadcast(smsEvent);
}
/**
* Processes a packet message
*/
@@ -1127,6 +1150,12 @@ public class DMRDecoderState extends TimeslotDecoderState
broadcast(gpsEvent);
}
break;
case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
if(message instanceof TalkerAliasComplete tac && tac.hasTalkerAliasIdentifier())
{
getIdentifierCollection().update(tac.getTalkerAliasIdentifier());
}
break;
}
}

View File

@@ -1,52 +1,56 @@
/*
* *****************************************************************************
* 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.module.decode.dmr;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
import io.github.dsheirer.module.decode.dmr.message.data.IDLEMessage;
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader;
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.lc.full.FLCAssembler;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasAssembler;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.SLCAssembler;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ShortLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCAssembler;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler;
import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Processes DMR messages and performs re-assembly of link control fragments
@@ -54,12 +58,13 @@ import java.util.TreeMap;
public class DMRMessageProcessor implements Listener<IMessage>
{
private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class);
private DecodeConfigDMR mConfigDMR;
private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1);
private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2);
private MBCAssembler mMBCAssembler = new MBCAssembler();
private PacketSequenceAssembler mPacketSequenceAssembler;
private SLCAssembler mSLCAssembler = new SLCAssembler();
private TalkerAliasAssembler mTalkerAliasAssembler = new TalkerAliasAssembler();
private Listener<IMessage> mMessageListener;
private Map<Integer,TimeslotFrequency> mTimeslotFrequencyMap = new TreeMap<>();
@@ -68,6 +73,8 @@ public class DMRMessageProcessor implements Listener<IMessage>
*/
public DMRMessageProcessor(DecodeConfigDMR config)
{
mConfigDMR = config;
for(TimeslotFrequency timeslotFrequency: config.getTimeslotMap())
{
mTimeslotFrequencyMap.put(timeslotFrequency.getNumber(), timeslotFrequency);
@@ -157,22 +164,43 @@ public class DMRMessageProcessor implements Listener<IMessage>
}
//Packet Sequence Message Assembly ...
if(message instanceof Preamble)
if(message instanceof Preamble preamble)
{
mPacketSequenceAssembler.process((Preamble)message);
mPacketSequenceAssembler.process(preamble);
}
else if(message instanceof PacketSequenceHeader)
else if(message instanceof PacketSequenceHeader header)
{
mPacketSequenceAssembler.process((PacketSequenceHeader)message);
mPacketSequenceAssembler.process(header);
}
else if(message instanceof ProprietaryDataHeader)
else if(message instanceof ProprietaryDataHeader header)
{
mPacketSequenceAssembler.process(((ProprietaryDataHeader)message));
mPacketSequenceAssembler.process(header);
}
else if(message instanceof DataBlock)
else if(message instanceof DataBlock dataBlock)
{
mPacketSequenceAssembler.process((DataBlock)message);
mPacketSequenceAssembler.process(dataBlock);
}
else if(message instanceof UDTHeader header)
{
mPacketSequenceAssembler.process(header);
}
else if((message instanceof IDLEMessage || message instanceof Aloha || message instanceof Announcement) &&
message.getTimeslot() != 0)
{
mPacketSequenceAssembler.dispatchPacketSequence(message.getTimeslot());
}
//Reset talker alias assembler on Idle or Terminator
if(message.isValid() && (message instanceof IDLEMessage || message instanceof Terminator))
{
mTalkerAliasAssembler.reset(message.getTimeslot());
}
}
//Assemble Talker Alias from FLC message fragments (header & blocks 1-3)
if(message instanceof FullLCMessage flc && flc.getOpcode().isTalkerAliasOpcode() && message.isValid())
{
dispatch(mTalkerAliasAssembler.process(flc));
}
//Now that the message has been (potentially) enriched, dispatch it to the modules
@@ -186,6 +214,26 @@ public class DMRMessageProcessor implements Listener<IMessage>
{
if(mMessageListener != null && message != null)
{
//Configure compressed talkgroups for the DMR talkgroup identifiers in the message.
if(mConfigDMR.isUseCompressedTalkgroups())
{
try
{
for(Identifier identifier: message.getIdentifiers())
{
if(identifier instanceof DMRTalkgroup talkgroup)
{
talkgroup.setCompressed(true);
}
}
}
catch(Exception e)
{
mLog.error("Error applying compressed talkgroup setting to identifier in message: " + message +
" [" + message.getClass() + "]");
}
}
mMessageListener.receive(message);
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao
* *****************************************************************************
* 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.dmr;
@@ -25,7 +24,6 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import java.util.ArrayList;
import java.util.List;
@@ -40,6 +38,7 @@ public class DecodeConfigDMR extends DecodeConfiguration
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
private boolean mIgnoreDataCalls = true;
private boolean mIgnoreCRCChecksums = false;
private boolean mUseCompressedTalkgroups = false;
private List<TimeslotFrequency> mTimeslotMap = new ArrayList<>();
public DecodeConfigDMR()
@@ -109,6 +108,28 @@ public class DecodeConfigDMR extends DecodeConfiguration
mIgnoreCRCChecksums = ignore;
}
/**
* Indicates if decoder should use compressed talkgroups
*/
@JacksonXmlProperty(isAttribute = true, localName = "use_compressed_talkgroups")
public boolean isUseCompressedTalkgroups()
{
return mUseCompressedTalkgroups;
}
/**
* Sets flag to use compressed talkgroups.
*
* Converts the air interface value to a compressed value described in ETSI 102 361-2, V.2.4.1, Paragraph C2.1.2
* This has been observed in use on Hytera Tier III systems.
* See: https://cwh050.blogspot.com/2021/03/converting-flat-number-to-dmr-id.html?m=1
* @param useCompressedTalkgroups true to use compressed talkgroups.
*/
public void setUseCompressedTalkgroups(boolean useCompressedTalkgroups)
{
mUseCompressedTalkgroups = useCompressedTalkgroups;
}
/**
* Traffic channel pool size.
* @return

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* 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.module.decode.dmr.identifier;
@@ -26,16 +23,33 @@ import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.protocol.Protocol;
/**
* DMR Talkgroup identifier
*/
public class DMRTalkgroup extends TalkgroupIdentifier
{
public DMRTalkgroup(Integer value)
private boolean mCompressed = false;
private int mOriginalValue;
/**
* Constructs an instance
* @param value of the talkgroup
*/
public DMRTalkgroup(int value)
{
super(value, Role.TO);
mOriginalValue = value;
}
public DMRTalkgroup(Integer value, Role role)
/**
* Constructs an instance
* @param value of the talkgroup
* @param role of the talkgroup
*/
public DMRTalkgroup(int value, Role role)
{
super(value, role);
mOriginalValue = value;
}
@Override
@@ -44,6 +58,84 @@ public class DMRTalkgroup extends TalkgroupIdentifier
return Protocol.DMR;
}
@Override
public String toString()
{
if(mCompressed)
{
return super.toString() + "(c)";
}
else
{
return super.toString();
}
}
/**
* Sets the compressed flag for this value.
*
* Talkgroup compression is an alternate format for DMR systems that allows the end user to dial a 7-digit number
* on their mobile keypad using just the digits 0 - 9, to address a talkgroup. The downside to this is that the
* total available talkgroup address space is reduced to just those dialable talkgroups.
*
* @param compressed true when using compressed talkgroup format.
*/
public void setCompressed(boolean compressed)
{
mCompressed = compressed;
if(mCompressed)
{
setValue(getCompressed(mOriginalValue));
}
else
{
setValue(mOriginalValue);
}
}
/**
* Converts the air interface value to a compressed value described in ETSI 102 361-2, V.2.4.1, Paragraph C2.1.2
*
* This has been observed in use on Hytera Tier III systems.
*
* See: https://cwh050.blogspot.com/2021/03/converting-flat-number-to-dmr-id.html?m=1
*
* @param original value to convert
* @return converted value.
*/
public static int getCompressed(int original)
{
int converted = 0;
int units = original / 1_464_100;
converted += (units * 1_000_000);
original -= (units * 1_464_100);
units = original / 146_410;
converted += (units * 100_000);
original -= (units * 146_410);
units = original / 14_641;
converted += (units * 10_000);
original -= (units * 14_641);
units = original / 1_331;
converted += (units * 1_000);
original -= (units * 1_331);
units = original / 121;
converted += (units * 100);
original -= (units * 121);
units = original / 11;
converted += (units * 10);
original -= (units * 11);
converted += original;
return converted;
}
/**
* Creates a DMR TO talkgroup identifier
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,7 +24,12 @@ 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.csbk.hytera.HyteraAdjacentSiteInformation;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAloha;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko32;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko44;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraCsbko47;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraSmsWaitingNotification;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTPreamble;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraXPTSiteState;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
@@ -49,6 +54,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Clear;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.MoveTSCC;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Protect;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.Acknowledge;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.AcknowledgeStatus;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.RegistrationAccepted;
@@ -78,7 +84,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBloc
import io.github.dsheirer.module.decode.dmr.message.data.mbc.UnknownMultiCSBK;
import io.github.dsheirer.module.decode.dmr.message.type.AnnouncementType;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceKind;
import java.util.List;
/**
@@ -200,6 +205,9 @@ public class CSBKMessageFactory
case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT:
csbk = new PrivateVoiceChannelGrant(pattern, message, cach, slotType, timestamp, timeslot);
break;
case STANDARD_PROTECT:
csbk = new Protect(pattern, message, cach, slotType, timestamp, timeslot);
break;
case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM:
csbk = new TalkgroupDataChannelGrant(pattern, message, cach, slotType, timestamp, timeslot);
break;
@@ -213,6 +221,9 @@ public class CSBKMessageFactory
csbk = new Preamble(pattern, message, cach, slotType, timestamp, timeslot);
break;
case HYTERA_ALOHA:
csbk = new HyteraAloha(pattern, message, cach, slotType, timestamp, timeslot);
break;
case HYTERA_08_ANNOUNCEMENT:
case HYTERA_68_ANNOUNCEMENT:
AnnouncementType announcementType = HyteraAnnouncement.getAnnouncementType(message);
@@ -232,7 +243,18 @@ public class CSBKMessageFactory
case HYTERA_XPT_SITE_STATE:
csbk = new HyteraXPTSiteState(pattern, message, cach, slotType, timestamp, timeslot);
break;
case HYTERA_CSBKO_32:
csbk = new HyteraCsbko32(pattern, message, cach, slotType, timestamp, timeslot);
break;
case HYTERA_CSBKO_44:
csbk = new HyteraCsbko44(pattern, message, cach, slotType, timestamp, timeslot);
break;
case HYTERA_CSBKO_47:
csbk = new HyteraCsbko47(pattern, message, cach, slotType, timestamp, timeslot);
break;
case HYTERA_CSBKO_62:
csbk = new HyteraSmsWaitingNotification(pattern, message, cach, slotType, timestamp, timeslot);
break;
case MOTOROLA_CAPMAX_ALOHA:
csbk = new CapacityMaxAloha(pattern, message, cach, slotType, timestamp, timeslot);
break;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
package io.github.dsheirer.module.decode.dmr.message.data.csbk;
import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
@@ -50,8 +49,8 @@ public enum Opcode
STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_PAYLOAD(Vendor.STANDARD, 35, "ACKNOWLEDGE RESPONSE INBOUND PAYLOAD"),
STANDARD_UNIFIED_DATA_TRANSPORT_FOR_DGNA_OUTBOUND_HEADER(Vendor.STANDARD, 36, "UNIFIED DATA TRANSPORT OUTBOUND HEADER"),
STANDARD_UNIFIED_DATA_TRANSPORT_FOR_DGNA_INBOUND_HEADER(Vendor.STANDARD, 37, "UNIFIED DATA TRANSPORT OUTBOUND HEADER"),
STANDARD_ANNOUNCEMENT(Vendor.STANDARD, 40, "ANNOUNCEMENT"),
STANDARD_NEGATIVE_ACKNOWLEDGE_RESPONSE(Vendor.STANDARD, 38, "NEGATIVE ACKNOWLEDGE RESPONSE"),
STANDARD_ANNOUNCEMENT(Vendor.STANDARD, 40, "ANNOUNCEMENT"),
STANDARD_MAINTENANCE(Vendor.STANDARD, 42, "MAINTENANCE"),
STANDARD_CLEAR(Vendor.STANDARD, 46, "CLEAR"),
STANDARD_PROTECT(Vendor.STANDARD, 47, "PROTECT"),
@@ -90,12 +89,17 @@ public enum Opcode
MOTOROLA_CAPPLUS_PREAMBLE(Vendor.MOTOROLA_CAPACITY_PLUS, 61, "PREAMBLE"),
MOTOROLA_CAPPLUS_SYSTEM_STATUS(Vendor.MOTOROLA_CAPACITY_PLUS, 62, "SYSTEM STATUS"),
HYTERA_XPT_SITE_STATE(Vendor.HYTERA_68, 10, "HYTERA XPT SITE STATE"),
HYTERA_CSBKO_32(Vendor.HYTERA_8, 32, "T-III HYTERA 08 CSBKO 32"),
HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "T-III 08 ANNOUNCEMENT"),
HYTERA_68_ANNOUNCEMENT(Vendor.HYTERA_68, 40, "T-III 68 ANNOUNCEMENT"),
HYTERA_CSBKO_44(Vendor.HYTERA_8, 44, "T-III HYTERA 08 CSBKO 44"),
HYTERA_CSBKO_47(Vendor.HYTERA_8, 47, "T-III HYTERA 08 CSBKO 47"),
HYTERA_XPT_SITE_STATE(Vendor.HYTERA_68, 10, "HYTERA XPT SITE STATE"),
HYTERA_ALOHA(Vendor.HYTERA_68, 25, "HYTERA ALOHA"),
HYTERA_68_ANNOUNCEMENT(Vendor.HYTERA_68, 40, "T-III 68 ANNOUNCEMENT"),
HYTERA_XPT_PREAMBLE(Vendor.HYTERA_68, 61, "HYTERA XPT PREAMBLE"), //Opcode 54 and/or 55 - See patent on Hytera call forwarding
HYTERA_CSBKO_62(Vendor.HYTERA_68, 62, "HYTERA CSBKO 62"),
//Opcode 54 and/or 55 - See patent on Hytera call forwarding
HYTERA_XPT_PREAMBLE(Vendor.HYTERA_68, 61, "HYTERA XPT PREAMBLE"),
UNKNOWN(Vendor.UNKNOWN, -1, "UNKNOWN");

View File

@@ -0,0 +1,198 @@
/*
* *****************************************************************************
* 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.csbk.hytera;
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.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
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.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceFunction;
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
import io.github.dsheirer.module.decode.dmr.message.type.Version;
import java.util.ArrayList;
import java.util.List;
/**
* Hytera DMR Tier III - Aloha Message
*/
public class HyteraAloha extends CSBKMessage
{
//TODO: not sure what bits 16 to 31 are used for ... awaiting documentation.
//TODO: bits 32 to 79 seem to align with the standard aloha.
private static final int[] RESERVED = new int[]{16, 17};
private static final int SITE_TIMESLOT_SYNCHRONIZATION = 18;
private static final int[] VERSION = new int[]{19, 20, 21};
private static final int TIMING_OFFSET = 22;
private static final int ACTIVE_NETWORK_CONNECTION_FLAG = 23;
private static final int[] MASK = new int[]{24, 25, 26, 27, 28};
private static final int[] SERVICE_FUNCTION = new int[]{29, 30};
private static final int[] N_RAND_WAIT = new int[]{31, 32, 33, 34};
private static final int REGISTRATION_REQUIRED_FLAG = 35;
private static final int[] BACKOFF = new int[]{36, 37, 38, 39};
private static final int SYSTEM_IDENTITY_CODE_OFFSET = 40;
private static final int[] RADIO = 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};
private SystemIdentityCode mSystemIdentityCode;
private RadioIdentifier mRadioIdentifier;
private List<Identifier> mIdentifiers;
/**
* Constructs an instance
*
* @param syncPattern for the CSBK
* @param message bits
* @param cach for the DMR burst
* @param slotType for this message
* @param timestamp
* @param timeslot
*/
public HyteraAloha(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();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" HYTERA ALOHA");
if(hasRadioIdentifier())
{
sb.append(" TO:").append(getRadioIdentifier());
}
sb.append(" ").append(getSystemIdentityCode().getModel());
sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork());
sb.append(" SITE:").append(getSystemIdentityCode().getSite());
if(hasActiveNetworkConnection())
{
sb.append(" NET-CONNECTED");
}
else
{
sb.append(" NET-DISCONNECTED");
}
sb.append(" SERVICES:").append(getServiceFunction());
sb.append(" ETSI VER:").append(getVersion());
sb.append(" MASK:").append(getMask());
if(getSystemIdentityCode().getPAR().isMultipleControlChannels())
{
sb.append(" ").append(getSystemIdentityCode().getPAR());
}
sb.append(" ").append(getMessage().toHexString());
return sb.toString();
}
/**
* Services provided by the control channel.
*/
public ServiceFunction getServiceFunction()
{
return ServiceFunction.fromValue(getMessage().getInt(SERVICE_FUNCTION));
}
/**
* Mobile subscriber ID masking value. See: 102 361-4 p6.1.3
*/
public int getMask()
{
return getMessage().getInt(MASK);
}
public boolean hasActiveNetworkConnection()
{
return getMessage().get(ACTIVE_NETWORK_CONNECTION_FLAG);
}
/**
* DMR Tier III ETSI 102 361-4 ICD Version Number supported by this system
*/
public Version getVersion()
{
return Version.fromValue(getMessage().getInt(VERSION));
}
/**
* Acknowledged radio identifier
*/
public RadioIdentifier getRadioIdentifier()
{
if(mRadioIdentifier == null)
{
mRadioIdentifier = DMRRadio.createTo(getMessage().getInt(RADIO));
}
return mRadioIdentifier;
}
public boolean hasRadioIdentifier()
{
return getMessage().getInt(RADIO) != 0;
}
/**
* System Identity Code structure
*/
public SystemIdentityCode getSystemIdentityCode()
{
if(mSystemIdentityCode == null)
{
mSystemIdentityCode = new SystemIdentityCode(getMessage(), SYSTEM_IDENTITY_CODE_OFFSET, true);
}
return mSystemIdentityCode;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
if(hasRadioIdentifier())
{
mIdentifiers.add(getRadioIdentifier());
}
mIdentifiers.add(getSystemIdentityCode().getNetwork());
mIdentifiers.add(getSystemIdentityCode().getSite());
}
return mIdentifiers;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,7 +28,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.type.AnnouncementType;
import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
import java.util.ArrayList;
import java.util.List;
@@ -78,7 +77,7 @@ public class HyteraAnnouncement extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" HYTERA ").append(getAnnouncementType());
sb.append(" HYTERA ANNOUNCEMENT-TYPE:").append(getAnnouncementType());
sb.append(" ").append(getSystemIdentityCode().getModel());
sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork());
sb.append(" SITE:").append(getSystemIdentityCode().getSite());

View File

@@ -0,0 +1,137 @@
/*
* *****************************************************************************
* 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.csbk.hytera;
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.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
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.csbk.CSBKMessage;
import java.util.ArrayList;
import java.util.List;
/**
* Hytera DMR Tier III - CSBKO 32 - Possibly Acknowledge Response
*
* Analysis: ... this was transmitted on the traffic channel while the mobile was at the end of a call, in the terminator
* sequence. Since this comes from the SMS gateway 16776906, I wonder if this is a traffic channel message waiting notification?
*/
public class HyteraCsbko32 extends CSBKMessage
{
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] DESTINATION_RADIO = 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};
private static final int[] SOURCE_RADIO = 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};
private List<Identifier> mIdentifiers;
private RadioIdentifier mDestinationRadio;
private RadioIdentifier mSourceRadio;
/**
* Constructs an instance
*
* @param syncPattern for the CSBK
* @param message bits
* @param cach for the DMR burst
* @param slotType for this message
* @param timestamp
* @param timeslot
*/
public HyteraCsbko32(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();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" HYTERA CSBKO=32 PROBABLE ACKNOWLEDGE RESPONSE");
sb.append(" FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio());
sb.append(" UNK1:").append(getUnknown1());
sb.append(" UNK2:").append(getUnknown2());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Addressed (ie TO) radio identifier
* @return radio identifier
*/
public RadioIdentifier getDestinationRadio()
{
if(mDestinationRadio == null)
{
mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION_RADIO));
}
return mDestinationRadio;
}
public String getUnknown1()
{
return getMessage().getHex(UNKNOWN_1, 2);
}
public String getUnknown2()
{
return getMessage().getHex(UNKNOWN_2, 2);
}
/**
* Source radio. Should be 16,777,906 the Hytera SMS gateway.
*/
public RadioIdentifier getSourceRadio()
{
if(mSourceRadio == null)
{
mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO));
}
return mSourceRadio;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getDestinationRadio());
mIdentifiers.add(getSourceRadio());
}
return mIdentifiers;
}
}

View File

@@ -0,0 +1,80 @@
/*
* *****************************************************************************
* 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.csbk.hytera;
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.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import java.util.Collections;
import java.util.List;
/**
* Hytera DMR Tier III - CSBKO 44 -
*
* Analysis: ... this was transmitted on the Tier III traffic channel during call termination. This CSBK was
* interleaved with every other Terminator message, starting after the first 2x terminator messages. This continued
* through the final teardown which was 3x Clear messages.
*/
public class HyteraCsbko44 extends CSBKMessage
{
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private List<Identifier> mIdentifiers;
/**
* Constructs an instance
*
* @param syncPattern for the CSBK
* @param message bits
* @param cach for the DMR burst
* @param slotType for this message
* @param timestamp
* @param timeslot
*/
public HyteraCsbko44(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();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" HYTERA UNKNOWN CSBKO=44");
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,138 @@
/*
* *****************************************************************************
* 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.csbk.hytera;
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.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
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.csbk.CSBKMessage;
import java.util.ArrayList;
import java.util.List;
/**
* Hytera DMR Tier III - CSBKO 47 -
*
* Analysis: ... this was transmitted on the traffic channel while the mobile was at the end of a call, in the terminator
* sequence. Since this comes from the SMS gateway 16776906, I wonder if this is a traffic channel message waiting notification?
*/
public class HyteraCsbko47 extends CSBKMessage
{
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] DESTINATION_RADIO = 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};
private static final int[] SOURCE_RADIO = 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};
private List<Identifier> mIdentifiers;
private RadioIdentifier mDestinationRadio;
private RadioIdentifier mSourceRadio;
/**
* Constructs an instance
*
* @param syncPattern for the CSBK
* @param message bits
* @param cach for the DMR burst
* @param slotType for this message
* @param timestamp
* @param timeslot
*/
public HyteraCsbko47(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();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" HYTERA CSBKO=47 ######### UNKNOWN");
sb.append(" FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio());
sb.append(" UNK1:").append(getUnknown1());
sb.append(" UNK2:").append(getUnknown2());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Addressed (ie TO) radio identifier
* @return radio identifier
*/
public RadioIdentifier getDestinationRadio()
{
if(mDestinationRadio == null)
{
mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION_RADIO));
}
return mDestinationRadio;
}
public String getUnknown1()
{
return getMessage().getHex(UNKNOWN_1, 2);
}
public String getUnknown2()
{
return getMessage().getHex(UNKNOWN_2, 2);
}
/**
* Source radio. Should be 16,777,906 the Hytera SMS gateway.
*/
public RadioIdentifier getSourceRadio()
{
if(mSourceRadio == null)
{
mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO));
}
return mSourceRadio;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getDestinationRadio());
mIdentifiers.add(getSourceRadio());
}
return mIdentifiers;
}
}

View File

@@ -0,0 +1,138 @@
/*
* *****************************************************************************
* 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.csbk.hytera;
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.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
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.csbk.CSBKMessage;
import java.util.ArrayList;
import java.util.List;
/**
* Hytera DMR Tier III - CSBKO 62 -
*
* Analysis: this message gets sent on timeslot 2 (not the Tier III control timeslot 1) from the SMS gateway (16776906)
* and gets sent to each of the radios involved in the SMS transfer (sender/receiver). I suspect that the network tells
* each of the radios to join the control channel and prepare for the SMS message.
*/
public class HyteraSmsWaitingNotification extends CSBKMessage
{
private static final int[] UNKNOWN_1 = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] UNKNOWN_2 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] DESTINATION_RADIO = 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};
private static final int[] SOURCE_RADIO = 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};
private List<Identifier> mIdentifiers;
private RadioIdentifier mDestinationRadio;
private RadioIdentifier mSourceRadio;
/**
* Constructs an instance
*
* @param syncPattern for the CSBK
* @param message bits
* @param cach for the DMR burst
* @param slotType for this message
* @param timestamp
* @param timeslot
*/
public HyteraSmsWaitingNotification(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();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
sb.append("CC:").append(getSlotType().getColorCode());
sb.append(" HYTERA PREPARE FOR SMS MESSAGE FOR:").append(getDestinationRadio());
sb.append(" FM:").append(getSourceRadio());
sb.append(" UNK1:").append(getUnknown1());
sb.append(" UNK2:").append(getUnknown2());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Addressed (ie TO) radio identifier
* @return radio identifier
*/
public RadioIdentifier getDestinationRadio()
{
if(mDestinationRadio == null)
{
mDestinationRadio = DMRRadio.createTo(getMessage().getInt(DESTINATION_RADIO));
}
return mDestinationRadio;
}
public String getUnknown1()
{
return getMessage().getHex(UNKNOWN_1, 2);
}
public String getUnknown2()
{
return getMessage().getHex(UNKNOWN_2, 2);
}
/**
* Source radio. Should be 16,777,906 the Hytera SMS gateway.
*/
public RadioIdentifier getSourceRadio()
{
if(mSourceRadio == null)
{
mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO));
}
return mSourceRadio;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getDestinationRadio());
mIdentifiers.add(getSourceRadio());
}
return mIdentifiers;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,7 +37,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
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;
@@ -118,6 +117,7 @@ public class Clear extends CSBKMessage implements ITimeslotFrequencyReceiver
sb.append(" CLEAR - RETURN TO ").append(getMoveToChannel());
sb.append(" FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationId());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -32,7 +32,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.Opcode;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint;
import io.github.dsheirer.module.decode.dmr.message.type.UnifiedDataTransportFormat;
import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
import java.util.ArrayList;
import java.util.List;
@@ -84,13 +83,13 @@ public class UDTHeader extends DataHeader
sb.append(" [CRC ERROR]");
}
sb.append(" UDT HEADER");
if(isSupplementaryData())
if(isShortData())
{
sb.append(" SUPPLEMENTARY DATA");
sb.append(" SHORT DATA");
}
else
{
sb.append(" SHORT DATA");
sb.append(" SUPPLEMENTARY DATA");
}
sb.append(" FM:").append(getSourceLLID());
sb.append(" TO:").append(getDestinationLLID());
@@ -106,7 +105,7 @@ public class UDTHeader extends DataHeader
sb.append(" OPCODE:").append(getOpcode());
}
sb.append(" PAD NIBBLES:").append(getPadNibbleCount());
sb.append(" ").append(getServiceAccessPoint());
sb.append(" SAP:").append(getServiceAccessPoint());
return sb.toString();
}
@@ -121,7 +120,15 @@ public class UDTHeader extends DataHeader
}
/**
* Indicates if the header contains supplementary data (true) or short data (false)
* Indicates if the header is for a short data sequence
*/
public boolean isShortData()
{
return !isSupplementaryData();
}
/**
* Indicates if the header is for supplementary data
*/
public boolean isSupplementaryData()
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,10 +21,15 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc;
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.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;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasBlock1;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasBlock2;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasBlock3;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasHeader;
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;
@@ -71,9 +76,22 @@ public class LCMessageFactory
throw new IllegalArgumentException("Message cannot be null");
}
//RS(12,9,4) can correct up to floor(4/2) = 1 bit error.
boolean corrected = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message,
isTerminator ? TERMINATOR_LINK_CONTROL_CRC_MASK : VOICE_LINK_CONTROL_CRC_MASK);
boolean valid = true;
if(message.size() == 77)
{
valid = Checksum_5_DMR.isValid(message);
}
else if(message.size() == 96)
{
//RS(12,9,4) can correct up to floor(4/2) = 1 bit error.
valid = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message,
isTerminator ? TERMINATOR_LINK_CONTROL_CRC_MASK : VOICE_LINK_CONTROL_CRC_MASK);
}
else
{
mLog.warn("Unrecognized Link Control Message Size: " + message.size());
}
LCOpcode opcode = FullLCMessage.getOpcode(message);
@@ -110,15 +128,24 @@ public class LCMessageFactory
flc = new HyteraTerminator(message, timestamp, timeslot);
break;
case FULL_STANDARD_TALKER_ALIAS_HEADER:
flc = new TalkerAliasHeader(message, timestamp, timeslot);
break;
case FULL_STANDARD_TALKER_ALIAS_BLOCK_1:
flc = new TalkerAliasBlock1(message, timestamp, timeslot);
break;
case FULL_STANDARD_TALKER_ALIAS_BLOCK_2:
flc = new TalkerAliasBlock2(message, timestamp, timeslot);
break;
case FULL_STANDARD_TALKER_ALIAS_BLOCK_3:
flc = new TalkerAliasBlock3(message, timestamp, timeslot);
break;
default:
flc = new UnknownFullLCMessage(message, timestamp, timeslot);
break;
}
flc.setValid(corrected);
flc.setValid(valid);
return flc;
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,8 +20,8 @@
package io.github.dsheirer.module.decode.dmr.message.data.lc;
import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.TreeMap;
@@ -39,6 +39,7 @@ public enum LCOpcode
FULL_STANDARD_TALKER_ALIAS_BLOCK_1(Vendor.STANDARD,true, 5, "TALKER ALIAS BLOCK 1"),
FULL_STANDARD_TALKER_ALIAS_BLOCK_2(Vendor.STANDARD,true, 6, "TALKER ALIAS BLOCK 2"),
FULL_STANDARD_TALKER_ALIAS_BLOCK_3(Vendor.STANDARD,true, 7, "TALKER ALIAS BLOCK 3"),
FULL_STANDARD_TALKER_ALIAS_COMPLETE(Vendor.STANDARD, true, -1, "TALKER ALIAS COMPLETE"), //Not part of ICD
FULL_STANDARD_GPS_INFO(Vendor.STANDARD,true, 8, "GPS INFO"),
FULL_STANDARD_TERMINATOR_DATA(Vendor.STANDARD, true, 48, "TERMINATOR DATA"),
FULL_STANDARD_UNKNOWN(Vendor.STANDARD,true, -1, "FULL UNKNOWN"),
@@ -67,6 +68,9 @@ public enum LCOpcode
SHORT_STANDARD_UNKNOWN(Vendor.STANDARD,false,-1, "SHORT UNKNOWN");
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;
@@ -111,6 +115,14 @@ public enum LCOpcode
return !mFull;
}
/**
* Indicates if this entry is one of the four talker alias opcodes.
*/
public boolean isTalkerAliasOpcode()
{
return TALKER_ALIAS_OPCODES.contains(this);
}
public int getValue()
{
return mValue;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -93,7 +93,7 @@ public class FLCAssembler
}
/**
* Dispatches a fully-received messsage
* Dispatches a fully-received message
*/
private FullLCMessage dispatch()
{
@@ -133,6 +133,7 @@ public class FLCAssembler
for(int row = 0; row < 8; row++)
{
int rowErrorCount = Hamming16.checkAndCorrect(descrambled, row * 16);
errorCount += rowErrorCount;
if(errorCount > 1)
@@ -179,6 +180,9 @@ public class FLCAssembler
{
extractedMessage.set(pointer++, descrambled.get(row * 16 + column));
}
//Transfer the checksum parity bit from this row
extractedMessage.set(70 + row, descrambled.get(row * 16 + 10));
}
extractedMessage.setCorrectedBitCount(errorCount);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -99,4 +99,13 @@ public abstract class FullLCMessage extends LCMessage
{
return getMessage().get(ENCRYPTION_FLAG);
}
/**
* Indicates if the reserved bit is set.
* @return true if set
*/
public boolean isReservedBitSet()
{
return getMessage().get(RESERVED);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
@@ -69,6 +68,7 @@ public class GroupVoiceChannelUser extends AbstractVoiceChannelUser
sb.append(getRadio());
sb.append(" TO:").append(getTalkgroup());
sb.append(" ").append(getServiceOptions());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}

View File

@@ -0,0 +1,224 @@
/*
* *****************************************************************************
* 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;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/**
* Aggregates a sequence of talker alias full link control messages to reconstruct the talker alias.
*/
public class TalkerAliasAssembler
{
//Timeslot 1 fragments
private TalkerAliasHeader mHeaderTs1;
private TalkerAliasBlock1 mBlock1Ts1;
private TalkerAliasBlock2 mBlock2Ts1;
private TalkerAliasBlock3 mBlock3Ts1;
//Timeslot 2 fragments
private TalkerAliasHeader mHeaderTs2;
private TalkerAliasBlock1 mBlock1Ts2;
private TalkerAliasBlock2 mBlock2Ts2;
private TalkerAliasBlock3 mBlock3Ts2;
//Flags indicating if we have any non-null fragments for the specified timeslot to support resetting.
private boolean mEmptyTs1 = true;
private boolean mEmptyTs2 = true;
/**
* Constructs an instance.
*/
public TalkerAliasAssembler()
{
}
/**
* Resets the contents of either timeslot 1 or 2 when a terminator or idle message is received.
* @param timeslot 1 or 2
*/
public void reset(int timeslot)
{
if(timeslot == 1 && !mEmptyTs1)
{
mHeaderTs1 = null;
mBlock1Ts1 = null;
mBlock2Ts1 = null;
mBlock3Ts1 = null;
mEmptyTs1 = true;
}
else if(timeslot == 2 && !mEmptyTs2)
{
mHeaderTs2 = null;
mBlock1Ts2 = null;
mBlock2Ts2 = null;
mBlock3Ts2 = null;
mEmptyTs2 = true;
}
}
/**
* Processes the FLC talker alias header or block and returns a fully assembled alias when all fragments are received.
* @param message containing FLC talker alias header or blocks 1-3
* @return fully assembled talker alias, if available, or null.
*/
public TalkerAliasComplete process(FullLCMessage message)
{
switch(message.getOpcode())
{
case FULL_STANDARD_TALKER_ALIAS_HEADER:
if(message instanceof TalkerAliasHeader tah)
{
if(message.getTimeslot() == 1)
{
mHeaderTs1 = tah;
mEmptyTs1 = false;
return assemble(1);
}
else
{
mHeaderTs2 = tah;
mEmptyTs2 = false;
return assemble(2);
}
}
case FULL_STANDARD_TALKER_ALIAS_BLOCK_1:
if(message instanceof TalkerAliasBlock1 tab1)
{
if(message.getTimeslot() == 1)
{
mBlock1Ts1 = tab1;
mEmptyTs1 = false;
return assemble(1);
}
else
{
mBlock1Ts2 = tab1;
mEmptyTs2 = false;
return assemble(2);
}
}
case FULL_STANDARD_TALKER_ALIAS_BLOCK_2:
if(message instanceof TalkerAliasBlock2 tab2)
{
if(message.getTimeslot() == 1)
{
mBlock2Ts1 = tab2;
mEmptyTs1 = false;
return assemble(1);
}
else
{
mBlock2Ts2 = tab2;
mEmptyTs2 = false;
return assemble(2);
}
}
case FULL_STANDARD_TALKER_ALIAS_BLOCK_3:
if(message instanceof TalkerAliasBlock3 tab3)
{
if(message.getTimeslot() == 1)
{
mBlock3Ts1 = tab3;
mEmptyTs1 = false;
return assemble(1);
}
else
{
mBlock3Ts2 = tab3;
mEmptyTs2 = false;
return assemble(2);
}
}
}
return null;
}
/**
* Assembles the complete alias.
* @param timeslot to assemble.
* @return assembled alias or null.
*/
private TalkerAliasComplete assemble(int timeslot)
{
if(timeslot == 1)
{
return assemble(mHeaderTs1, mBlock1Ts1, mBlock2Ts1, mBlock3Ts1);
}
else
{
return assemble(mHeaderTs2, mBlock1Ts2, mBlock2Ts2, mBlock3Ts2);
}
}
/**
* Assembles a complete alias from the header and block fragments.
* @param header of the alias
* @param block1 of the alias, optional
* @param block2 of the alias, optional
* @param block3 of the alias, optional
* @return assembled alias or null.
*/
private TalkerAliasComplete assemble(TalkerAliasHeader header,
TalkerAliasBlock1 block1,
TalkerAliasBlock2 block2,
TalkerAliasBlock3 block3)
{
if(header != null)
{
int bitLength = header.getTotalBitLength();
if(1 <= bitLength && bitLength <= 49)
{
return new TalkerAliasComplete(header.getPayloadFragment(), header.getFormat(), header.getCharacterLength(),
header.getTimestamp(), header.getTimeslot());
}
else if(50 <= bitLength && bitLength <= 105 && block1 != null)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(105);
message.load(0, header.getPayloadFragment());
message.load(49, block1.getPayloadFragment());
return new TalkerAliasComplete(message, header.getFormat(), header.getCharacterLength(),
header.getTimestamp(), header.getTimeslot());
}
else if(106 <= bitLength && bitLength <= 161 && block1 != null && block2 != null)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(161);
message.load(0, header.getPayloadFragment());
message.load(49, block1.getPayloadFragment());
message.load(105, block2.getPayloadFragment());
return new TalkerAliasComplete(message, header.getFormat(), header.getCharacterLength(),
header.getTimestamp(), header.getTimeslot());
}
else if(162 <= bitLength && bitLength <= 217 && block1 != null && block2 != null && block3 != null)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(217);
message.load(0, header.getPayloadFragment());
message.load(49, block1.getPayloadFragment());
message.load(105, block2.getPayloadFragment());
message.load(161, block3.getPayloadFragment());
return new TalkerAliasComplete(message, header.getFormat(), header.getCharacterLength(),
header.getTimestamp(), header.getTimeslot());
}
}
return null;
}
}

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* 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;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import java.util.Collections;
import java.util.List;
/**
* Talker alias continuation block 1.
*/
public class TalkerAliasBlock1 extends FullLCMessage
{
private static final int PAYLOAD_START = 16;
private static final int PAYLOAD_END = 72;
/**
* Constructs an instance
*
* @param message for link control payload
* @param timestamp for the message
* @param timeslot where the message was transmitted
*/
public TalkerAliasBlock1(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
/**
* Payload fragment carried by the header.
* @return payload fragment.
*/
public CorrectedBinaryMessage getPayloadFragment()
{
return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("FLC TALKER ALIAS BLOCK 1");
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
}

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* 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;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import java.util.Collections;
import java.util.List;
/**
* Talker alias continuation block 2.
*/
public class TalkerAliasBlock2 extends FullLCMessage
{
private static final int PAYLOAD_START = 16;
private static final int PAYLOAD_END = 72;
/**
* Constructs an instance
*
* @param message for link control payload
* @param timestamp for the message
* @param timeslot where the message was transmitted
*/
public TalkerAliasBlock2(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
/**
* Payload fragment carried by the header.
* @return payload fragment.
*/
public CorrectedBinaryMessage getPayloadFragment()
{
return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("FLC TALKER ALIAS BLOCK 2");
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
}

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* 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;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import java.util.Collections;
import java.util.List;
/**
* Talker alias continuation block 3.
*/
public class TalkerAliasBlock3 extends FullLCMessage
{
private static final int PAYLOAD_START = 16;
private static final int PAYLOAD_END = 72;
/**
* Constructs an instance
*
* @param message for link control payload
* @param timestamp for the message
* @param timeslot where the message was transmitted
*/
public TalkerAliasBlock3(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
/**
* Payload fragment carried by the header.
* @return payload fragment.
*/
public CorrectedBinaryMessage getPayloadFragment()
{
return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("FLC TALKER ALIAS BLOCK 3");
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
}

View File

@@ -0,0 +1,180 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.lc.full;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode;
import io.github.dsheirer.module.decode.dmr.message.type.TalkerAliasDataFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Complete talker alias assembled from talker alias header and 0 or more talker alias blocks.
*/
public class TalkerAliasComplete extends FullLCMessage
{
private static final Logger LOGGER = LoggerFactory.getLogger(TalkerAliasComplete.class);
private TalkerAliasDataFormat mDataFormat;
private int mCharacterCount;
private String mAlias;
private DmrTalkerAliasIdentifier mTalkerAliasIdentifier;
/**
* Constructs an instance
*
* @param message for link control payload
* @param timestamp for the header
* @param timeslot for the header and blocks
*/
public TalkerAliasComplete(CorrectedBinaryMessage message, TalkerAliasDataFormat format, int characterCount,
long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
mDataFormat = format;
mCharacterCount = characterCount;
}
@Override
public LCOpcode getOpcode()
{
return LCOpcode.FULL_STANDARD_TALKER_ALIAS_COMPLETE;
}
@Override
public int getOpcodeValue()
{
return getOpcode().getValue();
}
/**
* Alias value
* @return alias value if it can be decoded or empty string.
*/
public String getAliasValue()
{
if(mAlias == null)
{
switch(mDataFormat)
{
//Note: there are 31 possible 7-bit characters, 27 possible 8-bit characters and 13.5 possible 16-bit
//characters. We check each format to ensure character count is at least one and no more than max.
case BIT_7:
if(1 <= mCharacterCount && mCharacterCount <= 31)
{
mAlias = getMessage().parseISO7(0, mCharacterCount);
}
break;
case BIT_8:
if(1 <= mCharacterCount && mCharacterCount <= 27)
{
mAlias = getMessage().parseISO8(1, mCharacterCount);
}
break;
case UTF_8:
if(1 <= mCharacterCount && mCharacterCount <= 27)
{
try
{
mAlias = getMessage().parseUTF8(1, mCharacterCount);
}
catch(Exception e)
{
LOGGER.info("Error decoding talker alias as UTF-8: " + e.getLocalizedMessage());
}
}
break;
case UNICODE_UTF_16_BE:
if(1 <= mCharacterCount && mCharacterCount <= 13)
{
mAlias = getMessage().parseUnicode(1, mCharacterCount);
}
break;
}
//If we couldn't decode, set it to empty so that we don't reprocess every time.
if(mAlias == null)
{
mAlias = "";
}
}
return mAlias;
}
/**
* Talker Alias identifier.
* @return identifier or null if the alias string is null or empty, meaning it could not be decoded.
*/
public DmrTalkerAliasIdentifier getTalkerAliasIdentifier()
{
if(mTalkerAliasIdentifier == null && getAliasValue() != null && !getAliasValue().isEmpty())
{
mTalkerAliasIdentifier = DmrTalkerAliasIdentifier.create(getAliasValue());
}
return mTalkerAliasIdentifier;
}
/**
* Indicates if the talker alias identifier is non-null.
* @return true if non-null.
*/
public boolean hasTalkerAliasIdentifier()
{
return getTalkerAliasIdentifier() != null;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("FLC TALKER ALIAS:");
if(hasTalkerAliasIdentifier())
{
sb.append(getTalkerAliasIdentifier());
}
else
{
sb.append("(empty/error)");
}
return sb.toString();
}
@Override
public List<Identifier> getIdentifiers()
{
if(hasTalkerAliasIdentifier())
{
List<Identifier> identifiers = new ArrayList<>();
identifiers.add(getTalkerAliasIdentifier());
return identifiers;
}
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,109 @@
/*
* *****************************************************************************
* 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;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.message.type.TalkerAliasDataFormat;
import java.util.Collections;
import java.util.List;
/**
* Talker alias header with format and length description and a fragment of the alias data.
*/
public class TalkerAliasHeader extends FullLCMessage
{
private static final int[] FORMAT = new int[]{16, 17};
private static final int[] LENGTH = new int[]{18, 19, 20, 21, 22};
private static final int PAYLOAD_START = 23;
private static final int PAYLOAD_END = 72;
/**
* Constructs an instance
*
* @param message for link control payload
* @param timestamp for the message
* @param timeslot where the message was transmitted
*/
public TalkerAliasHeader(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
/**
* Formatting used for the alias characters.
* @return format
*/
public TalkerAliasDataFormat getFormat()
{
return TalkerAliasDataFormat.fromValue(getMessage().getInt(FORMAT));
}
/**
* Length of the alias in characters.
* @return length in characters.
*/
public int getCharacterLength()
{
return getMessage().getInt(LENGTH);
}
/**
* Total bit length of the alias characters.
*/
public int getTotalBitLength()
{
int total = getFormat().getBitsPerCharacter() * getCharacterLength();
//Max payload bit length is 217 (49 + 56 + 56 + 56)
if(total > 217)
{
return 217;
}
return total;
}
/**
* Payload fragment carried by the header.
* @return payload fragment.
*/
public CorrectedBinaryMessage getPayloadFragment()
{
return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END);
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("FLC TALKER ALIAS HEADER");
sb.append(" FORMAT:").append(getFormat());
sb.append(" CHARACTERS:").append(getCharacterLength());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,20 +28,22 @@ import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus - Group Voice Channel User
*/
public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser implements ITimeslotFrequencyReceiver
public class CapacityPlusGroupVoiceChannelUser extends CapacityPlusVoiceChannelUser implements ITimeslotFrequencyReceiver
{
private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] CAPACITY_PLUS_GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] CONVENTIONAL_GROUP_ADDRESS = 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[] VOICE_CHANNEL_REPEATER = new int[]{51, 52, 53, 54};
private static final int[] VOICE_CHANNEL_TIMESLOT = new int[]{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[] CAPACITY_PLUS_SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] CONVENTIONAL_SOURCE_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] UNKNOWN = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private RadioIdentifier mRadio;
@@ -69,20 +71,33 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
sb.append("[CRC-ERROR] ");
}
sb.append("FLC MOTOROLA CAP+ GROUP VOICE CHANNEL USER FM:");
sb.append(getRadio());
sb.append(" TO:").append(getTalkgroup());
sb.append(" ON CHANNEL:");
if(hasVoiceChannel())
if(isEncrypted())
{
sb.append(getVoiceChannel());
sb.append(" ENCRYPTED");
}
if(isReservedBitSet())
{
sb.append(" RESERVED-BIT");
}
if(getServiceOptions().isCapacityPlus())
{
sb.append("FLC MOTOROLA CAP+ GROUP VOICE CHANNEL USER");
}
else
{
sb.append("--");
sb.append("FLC MOTOROLA CONV/IP SITE GROUP VOICE CHANNEL USER");
}
sb.append(" FM:").append(getRadio());
sb.append(" TO:").append(getTalkgroup());
if(hasVoiceChannel())
{
sb.append(" ON CHANNEL:");
sb.append(getVoiceChannel());
}
sb.append(" UNK:").append(getUnknown());
sb.append(" ").append(getServiceOptions());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
@@ -119,6 +134,7 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
/**
* Rest timeslot
*
* @return
*/
public int getVoiceChannelTimeslot()
@@ -131,7 +147,7 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
*/
public boolean hasVoiceChannel()
{
return getVoiceChannelRepeater() != 0;
return getServiceOptions().isCapacityPlus() && getVoiceChannelRepeater() != 0;
}
/**
@@ -141,7 +157,14 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
{
if(mRadio == null)
{
mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS));
if(getServiceOptions().isCapacityPlus())
{
mRadio = DMRRadio.createFrom(getMessage().getInt(CAPACITY_PLUS_SOURCE_ADDRESS));
}
else
{
mRadio = DMRRadio.createFrom(getMessage().getInt(CONVENTIONAL_SOURCE_ADDRESS));
}
}
return mRadio;
@@ -154,7 +177,14 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
{
if(mTalkgroup == null)
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS));
if(getServiceOptions().isCapacityPlus())
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(CAPACITY_PLUS_GROUP_ADDRESS));
}
else
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(CONVENTIONAL_GROUP_ADDRESS));
}
}
return mTalkgroup;
@@ -189,12 +219,13 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
/**
* Applies the LSN to frequency map to the rest channel.
*
* @param timeslotFrequencies that match the logical timeslots
*/
@Override
public void apply(List<TimeslotFrequency> timeslotFrequencies)
{
for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
{
if(getVoiceChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber())
{

View File

@@ -0,0 +1,57 @@
/*
* *****************************************************************************
* 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.module.decode.dmr.message.data.lc.full.FullLCMessage;
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
{
private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private CapacityPlusServiceOptions mServiceOptions;
/**
* Constructs an instance.
*
* @param message for the link control payload
*/
public CapacityPlusVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
/**
* Service options for the call
*/
public CapacityPlusServiceOptions getServiceOptions()
{
if(mServiceOptions == null)
{
mServiceOptions = new CapacityPlusServiceOptions(getMessage().getInt(SERVICE_OPTIONS));
}
return mServiceOptions;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,22 +28,20 @@ import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus - Wide Area (Multi-Site) Voice Channel User
*/
public class CapacityPlusWideAreaVoiceChannelUser extends AbstractVoiceChannelUser implements ITimeslotFrequencyReceiver
public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChannelUser implements ITimeslotFrequencyReceiver
{
private static final int[] UNKNOWN_1 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] REST_REPEATER = new int[]{51, 52, 53, 54};
private static final int[] REST_TIMESLOT = new int[]{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_2 = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private static final int[] UNKNOWN_3 = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private RadioIdentifier mRadio;
private TalkgroupIdentifier mTalkgroup;
@@ -70,6 +68,16 @@ public class CapacityPlusWideAreaVoiceChannelUser extends AbstractVoiceChannelUs
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" *ENCRYPTED*");
}
if(isReservedBitSet())
{
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC MOTOROLA CAP+ WIDE-AREA VOICE CHANNEL USER FM:");
sb.append(getRadio());
sb.append(" TO:").append(getTalkgroup());
@@ -102,7 +110,7 @@ public class CapacityPlusWideAreaVoiceChannelUser extends AbstractVoiceChannelUs
*/
public int getUnknown2()
{
return getMessage().getInt(UNKNOWN_2);
return getMessage().getInt(UNKNOWN_3);
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
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 java.util.ArrayList;
import java.util.List;
@@ -34,6 +34,7 @@ public class PacketSequence
{
private int mTimeslot;
private PacketSequenceHeader mPacketSequenceHeader;
private UDTHeader mUDTHeader;
private ProprietaryDataHeader mProprietaryDataHeader;
private List<Preamble> mPreambles = new ArrayList<>();
private List<DataBlock> mDataBlocks = new ArrayList<>();
@@ -51,7 +52,11 @@ public class PacketSequence
sb.append(" TS:").append(getTimeslot());
if(mPacketSequenceHeader != null)
{
sb.append(mPacketSequenceHeader.toString());
sb.append(mPacketSequenceHeader);
}
else if(mUDTHeader != null)
{
sb.append(mUDTHeader);
}
sb.append(" HEADER:").append(hasPacketSequenceHeader());
sb.append(" PROPRIETARY HEADER:").append(hasProprietaryDataHeader());
@@ -97,6 +102,15 @@ public class PacketSequence
mPacketSequenceHeader = header;
}
/**
* Unified Data Transport (UDT) header
* @param header for a UDT sequence
*/
public void setUDTHeader(UDTHeader header)
{
mUDTHeader = header;
}
/**
* Proprietary data header
*/
@@ -138,6 +152,23 @@ public class PacketSequence
return mPacketSequenceHeader != null;
}
/**
* UDT header
* @return header or null.
*/
public UDTHeader getUDTHeader()
{
return mUDTHeader;
}
/**
* Indicates if this packet sequence has a UDT header
*/
public boolean hasUDTHeader()
{
return getUDTHeader() != null;
}
/**
* Secondary/Proprietary packet sequence header
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2021 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,6 +24,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
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.sample.Listener;
/**
@@ -70,7 +71,7 @@ public class PacketSequenceAssembler
}
/**
* Sets the listener to receive packet packet sequence messages
* Sets the listener to receive packet sequence messages
* @param listener to receive messages
*/
public void setMessageListener(Listener<IMessage> listener)
@@ -82,11 +83,11 @@ public class PacketSequenceAssembler
* Dispatches the packet sequence for the specified timeslot and sets the sequence to null.
* @param timeslot to dispatch
*/
private void dispatchPacketSequence(int timeslot)
public void dispatchPacketSequence(int timeslot)
{
PacketSequence packetSequence = (timeslot == 1 ? mTimeslot1Sequence : mTimeslot2Sequence);
if(mMessageListener != null && packetSequence != null && packetSequence.isComplete())
if(packetSequence != null && mMessageListener != null)
{
IMessage message = PacketSequenceMessageFactory.create(packetSequence);
@@ -154,6 +155,27 @@ public class PacketSequenceAssembler
packetSequence.setPacketSequenceHeader(header);
}
/**
* Process a UDT header.
* @param header
*/
public void process(UDTHeader header)
{
int timeslot = header.getTimeslot();
PacketSequence packetSequence = getPacketSequence(timeslot);
//If we already have headers or data blocks for the current sequence, then this is a new sequence
if(packetSequence.hasPacketSequenceHeader() || packetSequence.hasProprietaryDataHeader() ||
packetSequence.hasDataBlocks())
{
dispatchPacketSequence(timeslot);
packetSequence = getPacketSequence(timeslot);
}
packetSequence.setUDTHeader(header);
}
/**
* Processes a proprietary packet sequence header.
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,6 +26,7 @@ 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.motorola.MNISProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType;
@@ -35,11 +36,10 @@ import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
import io.github.dsheirer.module.decode.ip.lrrp.LRRPPacket;
import io.github.dsheirer.module.decode.ip.xcmp.XCMPPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converts a DMR packet sequence into a message
@@ -55,30 +55,73 @@ public class PacketSequenceMessageFactory
*/
public static IMessage create(PacketSequence packetSequence)
{
if(packetSequence != null && packetSequence.isComplete())
if(packetSequence != null)
{
PacketSequenceHeader primaryHeader = packetSequence.getPacketSequenceHeader();
CorrectedBinaryMessage packet = getPacket(packetSequence, primaryHeader.isConfirmedData());
if(packet != null)
if(packetSequence.hasPacketSequenceHeader())
{
switch(primaryHeader.getServiceAccessPoint())
PacketSequenceHeader primaryHeader = packetSequence.getPacketSequenceHeader();
CorrectedBinaryMessage packet = getPacket(packetSequence, primaryHeader.isConfirmedData());
if(packet != null)
{
case IP_PACKET_DATA:
return createIPPacketData(packetSequence, packet);
case PROPRIETARY_DATA:
return createProprietary(packetSequence, packet);
case SHORT_DATA:
return createDefinedShortData(packetSequence, packet);
default:
mLog.info("Unknown Packet SAP: " + primaryHeader.getServiceAccessPoint() + " - returning unknown packet");
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
switch(primaryHeader.getServiceAccessPoint())
{
case IP_PACKET_DATA:
return createIPPacketData(packetSequence, packet);
case PROPRIETARY_DATA:
return createProprietary(packetSequence, packet);
case SHORT_DATA:
return createDefinedShortData(packetSequence, packet);
default:
mLog.info("Unknown Packet SAP: " + primaryHeader.getServiceAccessPoint() + " - returning unknown packet");
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
}
}
else if(packetSequence.hasUDTHeader() && packetSequence.hasDataBlocks())
{
UDTHeader header = packetSequence.getUDTHeader();
if(header.isShortData())
{
CorrectedBinaryMessage payload = getUnconfirmedPayload(packetSequence.getDataBlocks());
return new UDTShortMessageService(header, payload);
}
}
}
//TODO: NOTE: Don't create a DMRPacketMessage unless we have a)Packet and b)Packet Sequence Header
return null;
}
/**
* Assembles the unconfirmed payload bytes from each of the data blocks into a contiguous binary message.
* @param dataBlocks to assemble
* @return assembled unconfirmed payload.
*/
public static CorrectedBinaryMessage getUnconfirmedPayload(List<DataBlock> dataBlocks)
{
if(!dataBlocks.isEmpty())
{
int length = 0;
List<CorrectedBinaryMessage> fragments = new ArrayList<>();
for(DataBlock dataBlock: dataBlocks)
{
CorrectedBinaryMessage fragment = dataBlock.getUnConfirmedPayload();
length += fragment.size();
fragments.add(fragment);
}
CorrectedBinaryMessage combined = new CorrectedBinaryMessage(length);
int pointer = 0;
for(CorrectedBinaryMessage fragment: fragments)
{
combined.load(pointer, fragment);
pointer += fragment.size();
}
return combined;
}
return null;
}

View File

@@ -0,0 +1,188 @@
/*
* *****************************************************************************
* 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.packet;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Short Message Service (SMS) using Unified Data Transport
*/
public class UDTShortMessageService extends DMRMessage
{
private static final Logger LOGGER = LoggerFactory.getLogger(UDTShortMessageService.class);
private UDTHeader mHeader;
private String mSMS;
/**
* Constructs an instance
*
* @param header for the sequence
* @param payload from the data blocks
*/
public UDTShortMessageService(UDTHeader header, CorrectedBinaryMessage payload)
{
super(payload, header.getTimestamp(), header.getTimeslot());
mHeader = header;
}
@Override
public List<Identifier> getIdentifiers()
{
return mHeader.getIdentifiers();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(mHeader.getSlotType().getColorCode());
sb.append(" SMS MESSAGE:").append(getSMS());
sb.append(" FROM:").append(mHeader.getSourceLLID());
sb.append(" TO:").append(mHeader.getDestinationLLID());
sb.append(" HEX:").append(getMessage().toHexString());
return sb.toString();
}
public String getSMS()
{
if(mSMS == null)
{
switch(mHeader.getFormat())
{
case UNICODE_16:
mSMS = parseUnicodePayload();
break;
case ASCII_7:
mSMS = parseISO7Payload();
break;
case ASCII_8:
mSMS = parseISO8Payload();
break;
case BCD_4_BITS:
mSMS = parseBCD4Payload();
break;
case BINARY:
case MOBILE_SUBSCRIBER_OR_TALKGROUP_ADDRESS:
case NMEA_GPS_LOCATION_CODED:
case IP_ADDRESS:
case VENDOR_PROPRIETARY_8:
case VENDOR_PROPRIETARY_9:
case MIXED_FORMAT:
case UNKNOWN:
LOGGER.warn("Unrecognized UDT Short Data Format: " + mHeader.getFormat() +
" - Please send this to the sdrtrunk developer - Hex:" + getMessage().toHexString() +
" As String:" + new String(getMessage().getBytes()));
mSMS = "Error:" + new String(getMessage().getBytes());
break;
}
}
if(mSMS == null)
{
mSMS = "error - unrecognized format: " + mHeader.getFormat();
}
return mSMS;
}
/**
* Parses a unicode 16-bit payload from the message.
* @return parsed message
*/
private String parseUnicodePayload()
{
int length = getMessage().size();
length -= (mHeader.getPadNibbleCount() * 4);
length -= 16; //Exclude the dangling 32-bit CRC
length -= 16; //Exclude the dangling 0x0006 ACKNOWLEDGE character
if(length > 16)
{
return getMessage().parseUnicode(0, (length / 16));
}
else
{
return "(insufficient data)";
}
}
/**
* Parses an ISO-7 payload from the message.
* @return parsed message
*/
private String parseISO7Payload()
{
int length = getMessage().size();
length -= (mHeader.getPadNibbleCount() * 4);
if(length > 7)
{
return getMessage().parseISO7(0, (length / 7));
}
else
{
return "(insufficient data)";
}
}
/**
* Parses an ISO-8 payload from the message.
* @return parsed message
*/
private String parseISO8Payload()
{
int length = getMessage().size();
length -= (mHeader.getPadNibbleCount() * 4);
if(length > 8)
{
return getMessage().parseISO8(0, (length / 8));
}
else
{
return "(insufficient data)";
}
}
/**
* Parses a BCD 4-bit payload from the message.
* @return parsed message
*/
private String parseBCD4Payload()
{
int length = getMessage().size();
length -= (mHeader.getPadNibbleCount() * 4);
if(length > 4)
{
return getMessage().parseBCD4(0, (length / 4));
}
else
{
return "(insufficient data)";
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -36,7 +36,7 @@ public enum AnnouncementType
ADJACENT_SITE_INFORMATION(6, "NEIGHBOR SITE INFORMATION"),
GENERAL_SITE_INFORMATION(7, "SITE INFORMATION"),
RESERVED_8(8, "RESERVED 8"),
RESERVED_9(9, "RESERVED 8"),
RESERVED_9(9, "RESERVED 9"),
RESERVED_10(10, "RESERVED 10"),
RESERVED_11(11, "RESERVED 11"),
RESERVED_12(12, "RESERVED 12"),

View File

@@ -0,0 +1,103 @@
/*
* *****************************************************************************
* 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;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus flavor of service options. Motorola uses the 2x reserved bits from the default implementation.
*/
public class CapacityPlusServiceOptions extends ServiceOptions
{
/**
* Constructs an instance
*
* @param value of the service options bitmap
*/
public CapacityPlusServiceOptions(int value)
{
super(value);
}
/**
* Reserved bit 1 is used to signal if the system is IP Site Connect or Conventional (true) or Capacity Plus (false).
* @return
*/
public boolean isCapacityPlus()
{
return !isReserved1();
}
/**
* Indicates if the transmission is interruptible.
* @return true if interruptible.
*/
public boolean isInterruptible()
{
return isReserved2();
}
@Override
public String toString()
{
List<String> flags = new ArrayList<>();
if(isEmergency())
{
flags.add("EMERGENCY");
}
if(isEncrypted())
{
flags.add("ENCRYPTED");
}
if(isBroadcastService())
{
flags.add("BROADCAST");
}
if(isOpenVoiceCallMode())
{
flags.add("OVCM");
}
if(isInterruptible())
{
flags.add("TXI CALL");
}
if(getPriority() > 0)
{
flags.add("PRIORITY-" + getPriority());
}
StringBuilder sb = new StringBuilder();
sb.append("SERVICE OPTIONS [");
if(!flags.isEmpty())
{
sb.append(Joiner.on(",").join(flags));
}
sb.append("]");
return sb.toString();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,7 +20,6 @@
package io.github.dsheirer.module.decode.dmr.message.type;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.List;
@@ -31,6 +30,8 @@ public class ServiceOptions
{
private static final int EMERGENCY_MASK = 0x80;
private static final int ENCRYPTION_MASK = 0x40;
private static final int RESERVED_1 = 0x20;
private static final int RESERVED_2 = 0x10;
private static final int BROADCAST_SERVICE_MASK = 0x8;
private static final int OPEN_VOICE_CALL_MODE_MASK = 0x4;
private static final int PRIORITY_MASK = 0x3;
@@ -77,6 +78,24 @@ public class ServiceOptions
return isSet(OPEN_VOICE_CALL_MODE_MASK);
}
/**
* Indicates if Reserved 1 bit is set
* @return true if set
*/
public boolean isReserved1()
{
return isSet(RESERVED_1);
}
/**
* Indicates if Reserved 2 bit is set
* @return true if set
*/
public boolean isReserved2()
{
return isSet(RESERVED_2);
}
/**
* Priority of the communication
* @return priority 0-3, 0 = lowest/routine, 3 = highest
@@ -119,6 +138,16 @@ public class ServiceOptions
flags.add("OVCM");
}
if(isReserved1())
{
flags.add("RSVD1");
}
if(isReserved2())
{
flags.add("RSVD2");
}
if(getPriority() > 0)
{
flags.add("PRIORITY-" + getPriority());

View File

@@ -0,0 +1,87 @@
/*
* *****************************************************************************
* 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 talker alias data formatting options.
*
* ETSI 102 361-2 v2.4.1 paragraph 7.2.18
*/
public enum TalkerAliasDataFormat
{
BIT_7(0, 7, "ASCII-7"),
BIT_8(1, 8, "ASCII-8"),
UTF_8(2, 8, "UTF-8"),
UNICODE_UTF_16_BE(3, 16, "UNICODE-16");
private int mValue;
private int mBitsPerCharacter;
private String mLabel;
/**
* Constructs an instance
* @param value ordinal
*/
TalkerAliasDataFormat(int value, int bitsPerCharacter, String label)
{
mValue = value;
mBitsPerCharacter = bitsPerCharacter;
mLabel = label;
}
/**
* Ordinal value of the format
* @return ordinal value.
*/
public int getValue()
{
return mValue;
}
/**
* Number of bits per character for the format.
* @return bits per character
*/
public int getBitsPerCharacter()
{
return mBitsPerCharacter;
}
/**
* Lookup the format from the ordinal value.
* @param value to lookup in range 0-3
* @return format for the value.
*/
public static TalkerAliasDataFormat fromValue(int value)
{
if(value < 0 || value > 3)
{
throw new IllegalArgumentException("Invalid value - out of range (0-3): " + value);
}
return TalkerAliasDataFormat.values()[value];
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* 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.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import java.util.Collections;
import java.util.List;
@@ -55,7 +54,7 @@ public class VoiceAMessage extends VoiceMessage
}
else
{
sb.append(" ").append(getSyncPattern());
sb.append("CC:- ").append(getSyncPattern());
}
return sb.toString();

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 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.event;
@@ -50,6 +49,7 @@ public enum DecodeEventType
EMERGENCY("EMERGENCY"),
FUNCTION("Function"),
GPS("GPS"),
ICMP_PACKET("ICMP Packet"),
ID_ANI("ANI"),
ID_UNIQUE("Unique ID"),
IP_PACKET("IP Packet"),
@@ -63,9 +63,11 @@ public enum DecodeEventType
RESPONSE("Response"),
RESPONSE_PACKET("Response Packet"),
SDM("Short Data Message"),
SMS("SMS"),
STATION_ID("Station ID"),
STATUS("Status"),
UDP_PACKET("UDP/IP Packet"),
UNKNOWN_PACKET("Unknown Packet"),
UNKNOWN("Unknown");
private String mLabel;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -43,8 +43,10 @@ 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.IPacket;
import io.github.dsheirer.module.decode.ip.UnknownPacket;
import io.github.dsheirer.module.decode.ip.ars.ARSPacket;
import io.github.dsheirer.module.decode.ip.cellocator.MCGPPacket;
import io.github.dsheirer.module.decode.ip.icmp.ICMPPacket;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
import io.github.dsheirer.module.decode.ip.lrrp.LRRPPacket;
import io.github.dsheirer.module.decode.ip.udp.UDPPacket;
@@ -87,7 +89,6 @@ import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCUn
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCUnitToUnitVoiceServiceChannelGrant;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.ambtc.osp.AMBTCUnitToUnitVoiceServiceChannelGrantUpdate;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp.SNDCPMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp.SNDCPPacketMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.umbtc.isp.UMBTCTelephoneInterconnectRequestExplicitDialing;
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage;
@@ -139,12 +140,11 @@ import io.github.dsheirer.module.decode.p25.reference.ServiceOptions;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.PacketUtil;
import java.util.List;
import org.jdesktop.swingx.mapviewer.GeoPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* Decoder state for an APCO25 channel. Maintains the call/data/idle state of the channel and produces events by
* monitoring the decoded message stream.
@@ -877,12 +877,12 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
if(message instanceof SNDCPPacketMessage)
{
SNDCPPacketMessage sndcp = (SNDCPPacketMessage)message;
SNDCPPacketMessage sndcp = (SNDCPPacketMessage) message;
getIdentifierCollection().update(sndcp.getIdentifiers());
}
else if(message instanceof PacketMessage)
{
PacketMessage packetMessage = (PacketMessage)message;
PacketMessage packetMessage = (PacketMessage) message;
getIdentifierCollection().remove(IdentifierClass.USER);
getIdentifierCollection().update(packetMessage.getIdentifiers());
@@ -890,41 +890,41 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
if(packet instanceof IPV4Packet)
{
IPV4Packet ipv4 = (IPV4Packet)packet;
IPV4Packet ipv4 = (IPV4Packet) packet;
IPacket ipPayload = ipv4.getPayload();
if(ipPayload instanceof UDPPacket)
{
UDPPacket udpPacket = (UDPPacket)ipPayload;
UDPPacket udpPacket = (UDPPacket) ipPayload;
IPacket udpPayload = udpPacket.getPayload();
if(udpPayload instanceof ARSPacket)
{
ARSPacket arsPacket = (ARSPacket)udpPayload;
ARSPacket arsPacket = (ARSPacket) udpPayload;
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier: packet.getIdentifiers())
for(Identifier identifier : packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent packetEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
.eventDescription(DecodeEventType.AUTOMATIC_REGISTRATION_SERVICE.toString())
.identifiers(ic)
.details(arsPacket.toString() + " " + ipv4.toString())
.build();
.channel(getCurrentChannel())
.eventDescription(DecodeEventType.AUTOMATIC_REGISTRATION_SERVICE.toString())
.identifiers(ic)
.details(arsPacket.toString() + " " + ipv4.toString())
.build();
broadcast(packetEvent);
}
else if(udpPayload instanceof MCGPPacket)
{
MCGPPacket mcgpPacket = (MCGPPacket)udpPayload;
MCGPPacket mcgpPacket = (MCGPPacket) udpPayload;
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier: packet.getIdentifiers())
for(Identifier identifier : packet.getIdentifiers())
{
ic.update(identifier);
}
@@ -954,7 +954,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
GeoPosition geoPosition = PacketUtil.extractGeoPosition(lrrpPacket);
if (geoPosition != null)
if(geoPosition != null)
{
PlottableDecodeEvent plottableDecodeEvent = PlottableDecodeEvent.plottableBuilder(message.getTimestamp())
.channel(getCurrentChannel())
@@ -970,21 +970,72 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
else
{
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier: packet.getIdentifiers())
for(Identifier identifier : packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent packetEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
.eventDescription(DecodeEventType.UDP_PACKET.toString())
.identifiers(ic)
.details(ipv4.toString())
.build();
.channel(getCurrentChannel())
.eventDescription(DecodeEventType.UDP_PACKET.toString())
.identifiers(ic)
.details(ipv4.toString())
.build();
broadcast(packetEvent);
}
}
else if(ipPayload instanceof ICMPPacket)
{
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier : packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent packetEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
.eventDescription(DecodeEventType.ICMP_PACKET.toString())
.identifiers(ic)
.details(ipv4.toString())
.build();
broadcast(packetEvent);
}
else
{
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier : packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent packetEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
.eventDescription(DecodeEventType.IP_PACKET.toString())
.identifiers(ic)
.details(ipv4.toString())
.build();
broadcast(packetEvent);
}
}
else if(packet instanceof UnknownPacket)
{
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier : packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent packetEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
.eventDescription(DecodeEventType.UNKNOWN_PACKET.toString())
.identifiers(ic)
.details(packet.toString())
.build();
broadcast(packetEvent);
}
}
}
@@ -992,18 +1043,14 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
/**
* Sub-Network Dependent Convergence Protocol (SNDCP)
*
* @param message
* @param message to process
*/
private void processSNDCP(P25Message message)
{
broadcast(new DecoderStateEvent(this, Event.DECODE, State.DATA));
if(message.isValid() && message instanceof SNDCPPacketMessage)
if(message.isValid() && message instanceof SNDCPPacketMessage sndcpPacket)
{
SNDCPPacketMessage sndcpPacket = (SNDCPPacketMessage)message;
SNDCPMessage sndcpMessage = sndcpPacket.getSNDCPMessage();
MutableIdentifierCollection ic = getMutableIdentifierCollection(sndcpPacket.getIdentifiers());
switch(sndcpPacket.getSNDCPPacketHeader().getPDUType())

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,9 +18,6 @@
*/
package io.github.dsheirer.record.binary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
@@ -29,6 +26,8 @@ import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BinaryReader implements Iterator<ByteBuffer>, AutoCloseable
{
@@ -94,7 +93,7 @@ public class BinaryReader implements Iterator<ByteBuffer>, AutoCloseable
byte[] readBytes = new byte[mBufferSize];
int bytesRead = mInputStream.read(readBytes);
if(bytesRead < readBytes.length)
if(bytesRead < readBytes.length && bytesRead > 0)
{
readBytes = Arrays.copyOf(readBytes, bytesRead);
}

View File

@@ -574,7 +574,7 @@ public abstract class USBTunerController extends TunerController
{
for(Transfer transfer: transfers)
{
submitTransfer(transfer, LibUsb.SUCCESS, 0);
submitTransfer(transfer);
}
}
@@ -582,7 +582,7 @@ public abstract class USBTunerController extends TunerController
* (Re)Submits the transfer for stream processing
* @param transfer to (re)submit
*/
private void submitTransfer(Transfer transfer, int previousStatus, int previousBytesTransferred)
private void submitTransfer(Transfer transfer)
{
int status = LibUsb.submitTransfer(transfer);
@@ -599,21 +599,26 @@ public abstract class USBTunerController extends TunerController
if(resubmitStatus == LibUsb.SUCCESS)
{
mInProgressTransfers.add(transfer);
mLog.error("Successfully resubmitted previous error USB transfer buffer. Current transfer buffer" +
" status (error queue/total available) [" + mErrorTransfers.size() + "/" +
mAvailableTransfers.size() + "]");
//Only log this if more than half of the total transfer buffers are in error-holding
if(mErrorTransfers.size() >= (mAvailableTransfers.size() / 2))
{
mLog.info("Successfully resubmitted previous error USB transfer buffer. Current transfer buffer" +
" status (error queue/total available) [" + mErrorTransfers.size() + "/" +
mAvailableTransfers.size() + "]");
}
}
else
{
mErrorTransfers.add(transfer);
mTransferErrorCount++;
//Only log this a few times in case we get transfer buffer(s) that are perpetually stuck in error
if(mTransferErrorCount < mAvailableTransfers.size())
//Only log this if more than half of the total transfer buffers are in error-holding
if(mErrorTransfers.size() >= (mAvailableTransfers.size() / 2))
{
mLog.error("Attempt to resubmit previous error USB transfer buffer failed with status [" +
LibUsb.errorName(resubmitStatus) + "] - this may be temporary and it has happened [" +
mTransferErrorCount + "] times so far. Transfer buffer status (error queue/total available) [" +
LibUsb.errorName(resubmitStatus) + "] - this may be temporary and has happened [" +
mTransferErrorCount + "] times so far. Transfer buffer holding queue (error/total) [" +
mErrorTransfers.size() + "/" + mAvailableTransfers.size() + "]");
}
}
@@ -621,24 +626,23 @@ public abstract class USBTunerController extends TunerController
}
else
{
//For ERROR_BUSY error, there is a weird issue/bug with libusb on Windows where libusb tells us the
//transfer is complete and then tells us that the transfer is already submitted when we attempt to
//re-submit the transfer. We track the number of times this occurs and log when the anomaly count
//exceeds the number of transfer buffers in case the application gets to a state where all transfers
//are no longer usable ... so that we know post-mortem what happened to the application.
mErrorTransfers.add(transfer);
mTransferErrorCount++;
mLog.error("Attempt to submit USB transfer buffer failed with status [" +
LibUsb.errorName(status) + "] - this may be temporary and it has happened [" +
mTransferErrorCount + "] time(s) so far. Transfer buffer status (error queue/total available) [" +
mErrorTransfers.size() + "/" + mAvailableTransfers.size() + "]");
//Only log this if more than half of the total transfer buffers are in error-holding
if(mErrorTransfers.size() >= (mAvailableTransfers.size() / 2))
{
mLog.error("Attempt to submit USB transfer buffer failed with status [" +
LibUsb.errorName(status) + "] - this may be temporary and has happened [" +
mTransferErrorCount + "] time(s) so far. Transfer buffer holding queue (error/total) [" +
mErrorTransfers.size() + "/" + mAvailableTransfers.size() + "]");
}
}
if(mErrorTransfers.size() >= mAvailableTransfers.size())
{
mLog.error("Maximum USB transfer buffer errors reached - transfer buffers exhausted - shutting down USB tuner");
ThreadPool.CACHED.submit(() -> setErrorMessage("USB Error - Transfer Buffer Errors"));
ThreadPool.CACHED.submit(() -> setErrorMessage("USB Error - Transfer Buffers Exhausted"));
}
}
@@ -707,7 +711,7 @@ public abstract class USBTunerController extends TunerController
if(mAutoResubmitTransfers)
{
submitTransfer(transfer, transfer.status(), transferLength);
submitTransfer(transfer);
}
break;
case LibUsb.TRANSFER_CANCELLED: