diff --git a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java index 1853eb1f..ff04b2d9 100644 --- a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java +++ b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java @@ -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(); } } diff --git a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java index 63de28a7..71108b94 100644 --- a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java +++ b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java @@ -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 { diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java index 70c5a114..cb9004e9 100644 --- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java +++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java @@ -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 { @@ -413,9 +414,11 @@ public class SDRTrunk implements Listener 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 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()); diff --git a/src/main/java/io/github/dsheirer/gui/dmr/DMRMessageViewer.java b/src/main/java/io/github/dsheirer/gui/dmr/DMRMessageViewer.java deleted file mode 100644 index bb842a77..00000000 --- a/src/main/java/io/github/dsheirer/gui/dmr/DMRMessageViewer.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * **************************************************************************** - */ - -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.prefs.Preferences; -import javafx.application.Application; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.value.ObservableValue; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; -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; - -public class DMRMessageViewer extends Application -{ - private static final Logger mLog = LoggerFactory.getLogger(DMRMessageViewer.class); - private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory"; - private Preferences mPreferences = Preferences.userNodeForPackage(DMRMessageViewer.class); - private Stage mStage; - private Scene mScene; - private VBox mContentBox; - private Button mSelectFileButton; - private Label mSelectedFileLabel; - private TableView mMessageTableView; - - @Override - public void start(Stage primaryStage) throws Exception - { - mStage = primaryStage; - mStage.setTitle("Recording Message Viewer (.bits)"); - mStage.setScene(getScene()); - mStage.show(); - } - - /** - * 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()); - getMessageTableView().getItems().clear(); - - DMRMessageFramer messageFramer = new DMRMessageFramer(null); - DecodeConfigDMR config = new DecodeConfigDMR(); - DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config); - messageFramer.setListener(messageProcessor); - messageProcessor.setMessageListener(message -> getMessageTableView().getItems().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(); - } - } - } - - /** - * Scene for this application - */ - private Scene getScene() - { - if(mScene == null) - { - mScene = new Scene(getContent(), 1100, 800); - } - - return mScene; - } - - /** - * Primary content pane. - */ - private VBox getContent() - { - if(mContentBox == null) - { - mContentBox = new VBox(); - mContentBox.setPadding(new Insets(5)); - mContentBox.setSpacing(5); - HBox fileBox = new HBox(); - fileBox.setSpacing(5); - HBox.setHgrow(getSelectFileButton(), Priority.NEVER); - HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS); - getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER); - fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel()); - - VBox.setVgrow(fileBox, Priority.NEVER); - VBox.setVgrow(getMessageTableView(), Priority.ALWAYS); - - mContentBox.getChildren().addAll(fileBox, getMessageTableView()); - } - - return mContentBox; - } - - /** - * List view control with DMR messages - */ - private TableView getMessageTableView() - { - if(mMessageTableView == null) - { - mMessageTableView = new TableView<>(); - - 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) param -> { - SimpleStringProperty property = new SimpleStringProperty(); - if(param.getValue() instanceof IMessage message) - { - property.set(message.toString()); - } - - return property; - }); - - mMessageTableView.getColumns().addAll(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(mStage); - - 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; - } - - - public static void main(String[] args) - { - launch(args); - } -} diff --git a/src/main/java/io/github/dsheirer/gui/dmr/DMRRecordingViewer.java b/src/main/java/io/github/dsheirer/gui/dmr/DMRRecordingViewer.java new file mode 100644 index 00000000..c8d0e574 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/dmr/DMRRecordingViewer.java @@ -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 + * **************************************************************************** + */ + +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 mMessageTableView; + private ObservableList mMessages = FXCollections.observableArrayList(); + private FilteredList 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 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 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 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) 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); + } +} diff --git a/src/main/java/io/github/dsheirer/gui/dmr/ViewDmrRecordingViewerRequest.java b/src/main/java/io/github/dsheirer/gui/dmr/ViewDmrRecordingViewerRequest.java new file mode 100644 index 00000000..1f3cba29 --- /dev/null +++ b/src/main/java/io/github/dsheirer/gui/dmr/ViewDmrRecordingViewerRequest.java @@ -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 + * **************************************************************************** + */ + +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() {} +} diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java index dfe88f03..5fa5b352 100644 --- a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java +++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java @@ -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 mTrafficChannelPoolSizeSpinner; private TableView 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 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); } diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java index 979c1e23..1505e3ea 100644 --- a/src/main/java/io/github/dsheirer/identifier/Form.java +++ b/src/main/java/io/github/dsheirer/identifier/Form.java @@ -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, diff --git a/src/main/java/io/github/dsheirer/identifier/Identifier.java b/src/main/java/io/github/dsheirer/identifier/Identifier.java index 324ed6a0..a5faa174 100644 --- a/src/main/java/io/github/dsheirer/identifier/Identifier.java +++ b/src/main/java/io/github/dsheirer/identifier/Identifier.java @@ -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 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 */ diff --git a/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAliasIdentifier.java b/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAliasIdentifier.java new file mode 100644 index 00000000..1e95ec90 --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/alias/DmrTalkerAliasIdentifier.java @@ -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 + * **************************************************************************** + */ + +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); + } +} diff --git a/src/main/java/io/github/dsheirer/identifier/alias/TalkerAliasIdentifier.java b/src/main/java/io/github/dsheirer/identifier/alias/TalkerAliasIdentifier.java new file mode 100644 index 00000000..12e7ad53 --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/alias/TalkerAliasIdentifier.java @@ -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 + * **************************************************************************** + */ + +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(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java index 48525a82..f0b4507c 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java @@ -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; } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java index 2eab6b6e..1661f27d 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java @@ -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 - * * ***************************************************************************** + * 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 + * **************************************************************************** */ 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 { 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 mMessageListener; private Map mTimeslotFrequencyMap = new TreeMap<>(); @@ -68,6 +73,8 @@ public class DMRMessageProcessor implements Listener */ 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 } //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 { 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); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java index 84b5ccfb..23aa12c5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DecodeConfigDMR.java @@ -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 - * ***************************************************************************** + * **************************************************************************** */ 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 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 diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java index dcc62263..1143bc29 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/identifier/DMRTalkgroup.java @@ -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 - * * ***************************************************************************** + * 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 + * **************************************************************************** */ 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 */ diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java index f208426a..7fd9ca57 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/CSBKMessageFactory.java @@ -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; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java index 5c898680..b44b778f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/Opcode.java @@ -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"); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAloha.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAloha.java new file mode 100644 index 00000000..15f495a8 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAloha.java @@ -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 + * **************************************************************************** + */ + +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 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 getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + if(hasRadioIdentifier()) + { + mIdentifiers.add(getRadioIdentifier()); + } + mIdentifiers.add(getSystemIdentityCode().getNetwork()); + mIdentifiers.add(getSystemIdentityCode().getSite()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java index 25338d93..f1be96ca 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraAnnouncement.java @@ -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()); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko32.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko32.java new file mode 100644 index 00000000..900e10be --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko32.java @@ -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 + * **************************************************************************** + */ + +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 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 getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationRadio()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko44.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko44.java new file mode 100644 index 00000000..09192d04 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko44.java @@ -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 + * **************************************************************************** + */ + +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 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 getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko47.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko47.java new file mode 100644 index 00000000..97da3585 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraCsbko47.java @@ -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 + * **************************************************************************** + */ + +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 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 getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationRadio()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraSmsWaitingNotification.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraSmsWaitingNotification.java new file mode 100644 index 00000000..6f0ae0cb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/hytera/HyteraSmsWaitingNotification.java @@ -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 + * **************************************************************************** + */ + +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 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 getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getDestinationRadio()); + mIdentifiers.add(getSourceRadio()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java index da594f45..0b822d9b 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/csbk/standard/Clear.java @@ -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(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java index 939e446c..ee5f96c4 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/header/UDTHeader.java @@ -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() { diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java index 54c7c855..8f45f936 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java @@ -26,6 +26,10 @@ 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; @@ -83,7 +87,6 @@ public class LCMessageFactory //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); - mLog.warn("Doing the reed solomon thing .... valid: " + valid); } else { @@ -125,9 +128,17 @@ 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; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java index 0f694886..38dcde62 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java @@ -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 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; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssembler.java new file mode 100644 index 00000000..1629b59d --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssembler.java @@ -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 + * **************************************************************************** + */ + +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; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock1.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock1.java new file mode 100644 index 00000000..87893c9b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock1.java @@ -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 + * **************************************************************************** + */ + +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 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(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock2.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock2.java new file mode 100644 index 00000000..a1d44dd5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock2.java @@ -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 + * **************************************************************************** + */ + +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 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(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock3.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock3.java new file mode 100644 index 00000000..3525a9a5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasBlock3.java @@ -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 + * **************************************************************************** + */ + +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 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(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasComplete.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasComplete.java new file mode 100644 index 00000000..f37c6868 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasComplete.java @@ -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 + * **************************************************************************** + */ + +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 getIdentifiers() + { + if(hasTalkerAliasIdentifier()) + { + List identifiers = new ArrayList<>(); + identifiers.add(getTalkerAliasIdentifier()); + return identifiers; + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasHeader.java new file mode 100644 index 00000000..0a043325 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasHeader.java @@ -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 + * **************************************************************************** + */ + +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 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(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java index 92234576..9136b151 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequence.java @@ -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 mPreambles = new ArrayList<>(); private List 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 */ diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java index f00e47af..1a9a3a62 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceAssembler.java @@ -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 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. */ diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java index 6515c4b1..778db3e5 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/PacketSequenceMessageFactory.java @@ -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 dataBlocks) + { + if(!dataBlocks.isEmpty()) + { + int length = 0; + + List 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; } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/UDTShortMessageService.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/UDTShortMessageService.java new file mode 100644 index 00000000..4049f461 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/packet/UDTShortMessageService.java @@ -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 + * **************************************************************************** + */ + +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 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)"; + } + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java index 128f2227..36a852c7 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/AnnouncementType.java @@ -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"), diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/TalkerAliasDataFormat.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/TalkerAliasDataFormat.java new file mode 100644 index 00000000..5a05aa4a --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/TalkerAliasDataFormat.java @@ -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 + * **************************************************************************** + */ + +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; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java index 2f77ec6a..9a6fbdb6 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/voice/VoiceAMessage.java @@ -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(); diff --git a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java index c368a00e..a7b1f47e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java +++ b/src/main/java/io/github/dsheirer/module/decode/event/DecodeEventType.java @@ -63,6 +63,7 @@ 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"),