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); }