diff --git a/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java b/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java
index 1865f403..55d0e0f9 100644
--- a/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java
+++ b/src/main/java/io/github/dsheirer/edac/BerlekempMassey.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.edac;
@@ -34,7 +31,7 @@ import org.slf4j.LoggerFactory;
*/
public class BerlekempMassey
{
- public final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
+ private final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
/* Golay field size GF( 2 ** MM ) */
private int MM;
diff --git a/src/main/java/io/github/dsheirer/edac/Checksum_5_DMR.java b/src/main/java/io/github/dsheirer/edac/Checksum_5_DMR.java
new file mode 100644
index 00000000..c61677f3
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/Checksum_5_DMR.java
@@ -0,0 +1,66 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.edac;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implements the 5-bit Checksum error detection algorithm specified in TS 102 361-1 paragraph B.3.11
+ */
+public class Checksum_5_DMR
+{
+ private static final Logger LOGGER = LoggerFactory.getLogger(Checksum_5_DMR.class);
+
+ private static final int[] LC_0 = {0, 1, 2, 3, 4, 5, 6, 7};
+ private static final int[] LC_1 = {8, 9, 10, 11, 12, 13, 14, 15};
+ private static final int[] LC_2 = {16, 17, 18, 19, 20, 21, 22, 23};
+ private static final int[] LC_3 = {24, 25, 26, 27, 28, 29, 30, 31};
+ private static final int[] LC_4 = {32, 33, 34, 35, 36, 37, 38, 39};
+ private static final int[] LC_5 = {40, 41, 42, 43, 44, 45, 46, 47};
+ private static final int[] LC_6 = {48, 49, 50, 51, 52, 53, 54, 55};
+ private static final int[] LC_7 = {56, 57, 58, 59, 60, 61, 62, 63};
+ private static final int[] LC_8 = {64, 65, 66, 67, 68, 69, 70, 71};
+ private static final int[] CHECKSUM = {72, 73, 74, 75, 76};
+
+ /**
+ * Indicates if the 77-bit Full Link Control message is valid.
+ * @param message to check
+ * @return true if the checksum matches.
+ */
+ public static boolean isValid(CorrectedBinaryMessage message)
+ {
+ int accumulator = message.getInt(LC_0);
+ accumulator += message.getInt(LC_1);
+ accumulator += message.getInt(LC_2);
+ accumulator += message.getInt(LC_3);
+ accumulator += message.getInt(LC_4);
+ accumulator += message.getInt(LC_5);
+ accumulator += message.getInt(LC_6);
+ accumulator += message.getInt(LC_7);
+ accumulator += message.getInt(LC_8);
+
+ accumulator = accumulator % 31;
+ int checksum = message.getInt(CHECKSUM);
+
+ return accumulator == checksum;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java
index b0caa4f4..137521b6 100644
--- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.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,12 +20,16 @@
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Berlecamp-Massey decoder for Reed Solomon RS(12,9,4) protected messages using DMR generator polynomial
*/
public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR
{
+ private final static Logger mLog = LoggerFactory.getLogger(ReedSolomon_12_9_4_DMR.class);
+
private static final int[] FLC_CODEWORD_0 = {0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] FLC_CODEWORD_1 = {8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] FLC_CODEWORD_2 = {16, 17, 18, 19, 20, 21, 22, 23};
@@ -56,16 +60,16 @@ public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR
*
* @param message to be corrected.
* @param crcMask as defined in the TS 102-361-1 Table B.21
- * @return true if the message was corrected.
+ * @return true if the message is correct or if the message was corrected.
*/
public boolean correctFullLinkControl(CorrectedBinaryMessage message, int crcMask)
{
int[] input = new int[255];
int[] output = new int[255];
- input[0] = message.getInt(FLC_PARITY_2) ^ crcMask;
- input[1] = message.getInt(FLC_PARITY_1) ^ crcMask;
- input[2] = message.getInt(FLC_PARITY_0) ^ crcMask;
+ input[0] = (message.getInt(FLC_PARITY_2) ^ crcMask);
+ input[1] = (message.getInt(FLC_PARITY_1) ^ crcMask);
+ input[2] = (message.getInt(FLC_PARITY_0) ^ crcMask);
input[3] = message.getInt(FLC_CODEWORD_8);
input[4] = message.getInt(FLC_CODEWORD_7);
input[5] = message.getInt(FLC_CODEWORD_6);
@@ -106,4 +110,40 @@ public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR
return true;
}
+
+ public static void main(String[] args)
+ {
+ int[] output = new int[255];
+ int[] input = new int[255];
+ input[0] = 247;
+ input[1] = 236;
+ input[2] = 171;
+ input[3] = 24;
+ input[4] = 46;
+ input[5] = 177;
+ input[6] = 246;
+ input[7] = 205;
+ input[8] = 232;
+ input[9] = 32;
+ input[10] = 124;
+ input[11] = 0;
+
+// input[254] = 247;
+// input[253] = 236;
+// input[252] = 171;
+// input[251] = 24;
+// input[250] = 46;
+// input[249] = 177;
+// input[248] = 246;
+// input[247] = 205;
+// input[246] = 232;
+// input[245] = 32;
+// input[244] = 124;
+// input[243] = 0;
+
+ ReedSolomon_12_9_4_DMR rs = new ReedSolomon_12_9_4_DMR();
+ boolean irrecoverable = rs.decode(input, output);
+
+ mLog.info("Irrecoverable: " + irrecoverable);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/gui/dmr/DMRMessageViewer.java b/src/main/java/io/github/dsheirer/gui/dmr/DMRMessageViewer.java
new file mode 100644
index 00000000..bb842a77
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/dmr/DMRMessageViewer.java
@@ -0,0 +1,234 @@
+/*
+ * *****************************************************************************
+ * 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/module/decode/dmr/message/data/lc/LCMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java
index 3dfc78ca..54c7c855 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
@@ -1,6 +1,6 @@
/*
* *****************************************************************************
- * Copyright (C) 2014-2020 Dennis Sheirer
+ * Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,6 +21,7 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
+import io.github.dsheirer.edac.Checksum_5_DMR;
import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
@@ -71,9 +72,23 @@ public class LCMessageFactory
throw new IllegalArgumentException("Message cannot be null");
}
- //RS(12,9,4) can correct up to floor(4/2) = 1 bit error.
- boolean corrected = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message,
- isTerminator ? TERMINATOR_LINK_CONTROL_CRC_MASK : VOICE_LINK_CONTROL_CRC_MASK);
+ boolean valid = true;
+
+ if(message.size() == 77)
+ {
+ valid = Checksum_5_DMR.isValid(message);
+ }
+ else if(message.size() == 96)
+ {
+ //RS(12,9,4) can correct up to floor(4/2) = 1 bit error.
+ valid = REED_SOLOMON_12_9_4_DMR.correctFullLinkControl(message,
+ isTerminator ? TERMINATOR_LINK_CONTROL_CRC_MASK : VOICE_LINK_CONTROL_CRC_MASK);
+ mLog.warn("Doing the reed solomon thing .... valid: " + valid);
+ }
+ else
+ {
+ mLog.warn("Unrecognized Link Control Message Size: " + message.size());
+ }
LCOpcode opcode = FullLCMessage.getOpcode(message);
@@ -118,7 +133,8 @@ public class LCMessageFactory
break;
}
- flc.setValid(corrected);
+ flc.setValid(valid);
+
return flc;
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java
index 6f224799..2e1439ae 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FLCAssembler.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
@@ -93,7 +93,7 @@ public class FLCAssembler
}
/**
- * Dispatches a fully-received messsage
+ * Dispatches a fully-received message
*/
private FullLCMessage dispatch()
{
@@ -133,6 +133,7 @@ public class FLCAssembler
for(int row = 0; row < 8; row++)
{
int rowErrorCount = Hamming16.checkAndCorrect(descrambled, row * 16);
+
errorCount += rowErrorCount;
if(errorCount > 1)
@@ -179,6 +180,9 @@ public class FLCAssembler
{
extractedMessage.set(pointer++, descrambled.get(row * 16 + column));
}
+
+ //Transfer the checksum parity bit from this row
+ extractedMessage.set(70 + row, descrambled.get(row * 16 + 10));
}
extractedMessage.setCorrectedBitCount(errorCount);
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.java
index a3e40f9b..e9fb25cd 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/FullLCMessage.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
@@ -99,4 +99,13 @@ public abstract class FullLCMessage extends LCMessage
{
return getMessage().get(ENCRYPTION_FLAG);
}
+
+ /**
+ * Indicates if the reserved bit is set.
+ * @return true if set
+ */
+ public boolean isReservedBitSet()
+ {
+ return getMessage().get(RESERVED);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.java
index f4125249..80d3fba9 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/GroupVoiceChannelUser.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
@@ -25,7 +25,6 @@ import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
-
import java.util.ArrayList;
import java.util.List;
@@ -69,6 +68,7 @@ public class GroupVoiceChannelUser extends AbstractVoiceChannelUser
sb.append(getRadio());
sb.append(" TO:").append(getTalkgroup());
sb.append(" ").append(getServiceOptions());
+ sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.java
index 8819b6d9..7664b6d9 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusGroupVoiceChannelUser.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
@@ -28,20 +28,22 @@ import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
-import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
-
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus - Group Voice Channel User
*/
-public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser implements ITimeslotFrequencyReceiver
+public class CapacityPlusGroupVoiceChannelUser extends CapacityPlusVoiceChannelUser implements ITimeslotFrequencyReceiver
{
- private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
+ private static final int[] CAPACITY_PLUS_GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
+ private static final int[] CONVENTIONAL_GROUP_ADDRESS = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
+ 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] VOICE_CHANNEL_REPEATER = new int[]{51, 52, 53, 54};
private static final int[] VOICE_CHANNEL_TIMESLOT = new int[]{55};
- private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
+ private static final int[] CAPACITY_PLUS_SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
+ private static final int[] CONVENTIONAL_SOURCE_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
+ 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] UNKNOWN = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private RadioIdentifier mRadio;
@@ -69,20 +71,33 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
sb.append("[CRC-ERROR] ");
}
- sb.append("FLC MOTOROLA CAP+ GROUP VOICE CHANNEL USER FM:");
- sb.append(getRadio());
- sb.append(" TO:").append(getTalkgroup());
- sb.append(" ON CHANNEL:");
- if(hasVoiceChannel())
+ if(isEncrypted())
{
- sb.append(getVoiceChannel());
+ sb.append(" ENCRYPTED");
+ }
+
+ if(isReservedBitSet())
+ {
+ sb.append(" RESERVED-BIT");
+ }
+
+ if(getServiceOptions().isCapacityPlus())
+ {
+ sb.append("FLC MOTOROLA CAP+ GROUP VOICE CHANNEL USER");
}
else
{
- sb.append("--");
+ sb.append("FLC MOTOROLA CONV/IP SITE GROUP VOICE CHANNEL USER");
+ }
+ sb.append(" FM:").append(getRadio());
+ sb.append(" TO:").append(getTalkgroup());
+
+ if(hasVoiceChannel())
+ {
+ sb.append(" ON CHANNEL:");
+ sb.append(getVoiceChannel());
}
- sb.append(" UNK:").append(getUnknown());
sb.append(" ").append(getServiceOptions());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
@@ -119,6 +134,7 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
/**
* Rest timeslot
+ *
* @return
*/
public int getVoiceChannelTimeslot()
@@ -131,7 +147,7 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
*/
public boolean hasVoiceChannel()
{
- return getVoiceChannelRepeater() != 0;
+ return getServiceOptions().isCapacityPlus() && getVoiceChannelRepeater() != 0;
}
/**
@@ -141,7 +157,14 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
{
if(mRadio == null)
{
- mRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_ADDRESS));
+ if(getServiceOptions().isCapacityPlus())
+ {
+ mRadio = DMRRadio.createFrom(getMessage().getInt(CAPACITY_PLUS_SOURCE_ADDRESS));
+ }
+ else
+ {
+ mRadio = DMRRadio.createFrom(getMessage().getInt(CONVENTIONAL_SOURCE_ADDRESS));
+ }
}
return mRadio;
@@ -154,7 +177,14 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
{
if(mTalkgroup == null)
{
- mTalkgroup = DMRTalkgroup.create(getMessage().getInt(GROUP_ADDRESS));
+ if(getServiceOptions().isCapacityPlus())
+ {
+ mTalkgroup = DMRTalkgroup.create(getMessage().getInt(CAPACITY_PLUS_GROUP_ADDRESS));
+ }
+ else
+ {
+ mTalkgroup = DMRTalkgroup.create(getMessage().getInt(CONVENTIONAL_GROUP_ADDRESS));
+ }
}
return mTalkgroup;
@@ -189,12 +219,13 @@ public class CapacityPlusGroupVoiceChannelUser extends AbstractVoiceChannelUser
/**
* Applies the LSN to frequency map to the rest channel.
+ *
* @param timeslotFrequencies that match the logical timeslots
*/
@Override
public void apply(List timeslotFrequencies)
{
- for(TimeslotFrequency timeslotFrequency: timeslotFrequencies)
+ for(TimeslotFrequency timeslotFrequency : timeslotFrequencies)
{
if(getVoiceChannel().getLogicalSlotNumber() == timeslotFrequency.getNumber())
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusVoiceChannelUser.java
new file mode 100644
index 00000000..9c80a9e8
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusVoiceChannelUser.java
@@ -0,0 +1,57 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
+import io.github.dsheirer.module.decode.dmr.message.type.CapacityPlusServiceOptions;
+
+/**
+ * Any Capacity Plus Voice Channel User link control message that contains vendor-specific service options.
+ */
+public abstract class CapacityPlusVoiceChannelUser extends FullLCMessage
+{
+ private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
+
+ private CapacityPlusServiceOptions mServiceOptions;
+
+ /**
+ * Constructs an instance.
+ *
+ * @param message for the link control payload
+ */
+ public CapacityPlusVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot)
+ {
+ super(message, timestamp, timeslot);
+ }
+
+ /**
+ * Service options for the call
+ */
+ public CapacityPlusServiceOptions getServiceOptions()
+ {
+ if(mServiceOptions == null)
+ {
+ mServiceOptions = new CapacityPlusServiceOptions(getMessage().getInt(SERVICE_OPTIONS));
+ }
+
+ return mServiceOptions;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.java
index c7a9ec25..14bec291 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityPlusWideAreaVoiceChannelUser.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
@@ -28,22 +28,20 @@ import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
-import io.github.dsheirer.module.decode.dmr.message.data.lc.full.AbstractVoiceChannelUser;
-
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus - Wide Area (Multi-Site) Voice Channel User
*/
-public class CapacityPlusWideAreaVoiceChannelUser extends AbstractVoiceChannelUser implements ITimeslotFrequencyReceiver
+public class CapacityPlusWideAreaVoiceChannelUser extends CapacityPlusVoiceChannelUser implements ITimeslotFrequencyReceiver
{
private static final int[] UNKNOWN_1 = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] GROUP_ADDRESS = new int[]{40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] REST_REPEATER = new int[]{51, 52, 53, 54};
private static final int[] REST_TIMESLOT = new int[]{55};
private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
- private static final int[] UNKNOWN_2 = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
+ private static final int[] UNKNOWN_3 = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private RadioIdentifier mRadio;
private TalkgroupIdentifier mTalkgroup;
@@ -70,6 +68,16 @@ public class CapacityPlusWideAreaVoiceChannelUser extends AbstractVoiceChannelUs
sb.append("[CRC-ERROR] ");
}
+ if(isEncrypted())
+ {
+ sb.append(" *ENCRYPTED*");
+ }
+
+ if(isReservedBitSet())
+ {
+ sb.append(" *RESERVED-BIT*");
+ }
+
sb.append("FLC MOTOROLA CAP+ WIDE-AREA VOICE CHANNEL USER FM:");
sb.append(getRadio());
sb.append(" TO:").append(getTalkgroup());
@@ -102,7 +110,7 @@ public class CapacityPlusWideAreaVoiceChannelUser extends AbstractVoiceChannelUs
*/
public int getUnknown2()
{
- return getMessage().getInt(UNKNOWN_2);
+ return getMessage().getInt(UNKNOWN_3);
}
/**
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/CapacityPlusServiceOptions.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/CapacityPlusServiceOptions.java
new file mode 100644
index 00000000..b9a13e15
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/CapacityPlusServiceOptions.java
@@ -0,0 +1,103 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2023 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.module.decode.dmr.message.type;
+
+import com.google.common.base.Joiner;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Motorola Capacity Plus flavor of service options. Motorola uses the 2x reserved bits from the default implementation.
+ */
+public class CapacityPlusServiceOptions extends ServiceOptions
+{
+ /**
+ * Constructs an instance
+ *
+ * @param value of the service options bitmap
+ */
+ public CapacityPlusServiceOptions(int value)
+ {
+ super(value);
+ }
+
+ /**
+ * Reserved bit 1 is used to signal if the system is IP Site Connect or Conventional (true) or Capacity Plus (false).
+ * @return
+ */
+ public boolean isCapacityPlus()
+ {
+ return !isReserved1();
+ }
+
+ /**
+ * Indicates if the transmission is interruptible.
+ * @return true if interruptible.
+ */
+ public boolean isInterruptible()
+ {
+ return isReserved2();
+ }
+
+ @Override
+ public String toString()
+ {
+ List flags = new ArrayList<>();
+
+ if(isEmergency())
+ {
+ flags.add("EMERGENCY");
+ }
+
+ if(isEncrypted())
+ {
+ flags.add("ENCRYPTED");
+ }
+
+ if(isBroadcastService())
+ {
+ flags.add("BROADCAST");
+ }
+
+ if(isOpenVoiceCallMode())
+ {
+ flags.add("OVCM");
+ }
+
+ if(isInterruptible())
+ {
+ flags.add("TXI CALL");
+ }
+
+ if(getPriority() > 0)
+ {
+ flags.add("PRIORITY-" + getPriority());
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("SERVICE OPTIONS [");
+ if(!flags.isEmpty())
+ {
+ sb.append(Joiner.on(",").join(flags));
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.java
index 96e62a6d..9e3c0d1d 100644
--- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.java
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/type/ServiceOptions.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.type;
import com.google.common.base.Joiner;
-
import java.util.ArrayList;
import java.util.List;
@@ -31,6 +30,8 @@ public class ServiceOptions
{
private static final int EMERGENCY_MASK = 0x80;
private static final int ENCRYPTION_MASK = 0x40;
+ private static final int RESERVED_1 = 0x20;
+ private static final int RESERVED_2 = 0x10;
private static final int BROADCAST_SERVICE_MASK = 0x8;
private static final int OPEN_VOICE_CALL_MODE_MASK = 0x4;
private static final int PRIORITY_MASK = 0x3;
@@ -77,6 +78,24 @@ public class ServiceOptions
return isSet(OPEN_VOICE_CALL_MODE_MASK);
}
+ /**
+ * Indicates if Reserved 1 bit is set
+ * @return true if set
+ */
+ public boolean isReserved1()
+ {
+ return isSet(RESERVED_1);
+ }
+
+ /**
+ * Indicates if Reserved 2 bit is set
+ * @return true if set
+ */
+ public boolean isReserved2()
+ {
+ return isSet(RESERVED_2);
+ }
+
/**
* Priority of the communication
* @return priority 0-3, 0 = lowest/routine, 3 = highest
@@ -119,6 +138,16 @@ public class ServiceOptions
flags.add("OVCM");
}
+ if(isReserved1())
+ {
+ flags.add("RSVD1");
+ }
+
+ if(isReserved2())
+ {
+ flags.add("RSVD2");
+ }
+
if(getPriority() > 0)
{
flags.add("PRIORITY-" + getPriority());
diff --git a/src/main/java/io/github/dsheirer/record/binary/BinaryReader.java b/src/main/java/io/github/dsheirer/record/binary/BinaryReader.java
index 0168ba2f..6a2630eb 100644
--- a/src/main/java/io/github/dsheirer/record/binary/BinaryReader.java
+++ b/src/main/java/io/github/dsheirer/record/binary/BinaryReader.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
@@ -18,9 +18,6 @@
*/
package io.github.dsheirer.record.binary;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
@@ -29,6 +26,8 @@ import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Iterator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class BinaryReader implements Iterator, AutoCloseable
{
@@ -94,7 +93,7 @@ public class BinaryReader implements Iterator, AutoCloseable
byte[] readBytes = new byte[mBufferSize];
int bytesRead = mInputStream.read(readBytes);
- if(bytesRead < readBytes.length)
+ if(bytesRead < readBytes.length && bytesRead > 0)
{
readBytes = Arrays.copyOf(readBytes, bytesRead);
}