#1422 Resolves issue where MotoTRBO IP Site Connect and Capacity Plus voice group user message not decoding Radio & Talkgroup IDs correctly. Creates standalone application for viewing DMR messages from recordings. (#1442)

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
Denny Sheirer
2023-02-11 15:32:41 -05:00
committed by GitHub
parent e177c2c6b0
commit b51a0102ef
14 changed files with 656 additions and 63 deletions

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.edac;
@@ -34,7 +31,7 @@ import org.slf4j.LoggerFactory;
*/
public class BerlekempMassey
{
public final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
private final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
/* Golay field size GF( 2 ** MM ) */
private int MM;

View File

@@ -0,0 +1,66 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements the 5-bit Checksum error detection algorithm specified in TS 102 361-1 paragraph B.3.11
*/
public class Checksum_5_DMR
{
private static final Logger LOGGER = LoggerFactory.getLogger(Checksum_5_DMR.class);
private static final int[] LC_0 = {0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] LC_1 = {8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] LC_2 = {16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] LC_3 = {24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] LC_4 = {32, 33, 34, 35, 36, 37, 38, 39};
private static final int[] LC_5 = {40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] LC_6 = {48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] LC_7 = {56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] LC_8 = {64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] CHECKSUM = {72, 73, 74, 75, 76};
/**
* Indicates if the 77-bit Full Link Control message is valid.
* @param message to check
* @return true if the checksum matches.
*/
public static boolean isValid(CorrectedBinaryMessage message)
{
int accumulator = message.getInt(LC_0);
accumulator += message.getInt(LC_1);
accumulator += message.getInt(LC_2);
accumulator += message.getInt(LC_3);
accumulator += message.getInt(LC_4);
accumulator += message.getInt(LC_5);
accumulator += message.getInt(LC_6);
accumulator += message.getInt(LC_7);
accumulator += message.getInt(LC_8);
accumulator = accumulator % 31;
int checksum = message.getInt(CHECKSUM);
return accumulator == checksum;
}
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.dmr;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.DMRMessageFramer;
import io.github.dsheirer.module.decode.dmr.DMRMessageProcessor;
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
import io.github.dsheirer.record.binary.BinaryReader;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.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<IMessage> 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<IMessage> 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<TableColumn.CellDataFeatures, ObservableValue>) 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);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,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;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.type.CapacityPlusServiceOptions;
/**
* Any Capacity Plus Voice Channel User link control message that contains vendor-specific service options.
*/
public abstract class CapacityPlusVoiceChannelUser extends FullLCMessage
{
private static final int[] SERVICE_OPTIONS = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private CapacityPlusServiceOptions mServiceOptions;
/**
* Constructs an instance.
*
* @param message for the link control payload
*/
public CapacityPlusVoiceChannelUser(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
/**
* Service options for the call
*/
public CapacityPlusServiceOptions getServiceOptions()
{
if(mServiceOptions == null)
{
mServiceOptions = new CapacityPlusServiceOptions(getMessage().getInt(SERVICE_OPTIONS));
}
return mServiceOptions;
}
}

View File

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

View File

@@ -0,0 +1,103 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.type;
import com.google.common.base.Joiner;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola Capacity Plus flavor of service options. Motorola uses the 2x reserved bits from the default implementation.
*/
public class CapacityPlusServiceOptions extends ServiceOptions
{
/**
* Constructs an instance
*
* @param value of the service options bitmap
*/
public CapacityPlusServiceOptions(int value)
{
super(value);
}
/**
* Reserved bit 1 is used to signal if the system is IP Site Connect or Conventional (true) or Capacity Plus (false).
* @return
*/
public boolean isCapacityPlus()
{
return !isReserved1();
}
/**
* Indicates if the transmission is interruptible.
* @return true if interruptible.
*/
public boolean isInterruptible()
{
return isReserved2();
}
@Override
public String toString()
{
List<String> flags = new ArrayList<>();
if(isEmergency())
{
flags.add("EMERGENCY");
}
if(isEncrypted())
{
flags.add("ENCRYPTED");
}
if(isBroadcastService())
{
flags.add("BROADCAST");
}
if(isOpenVoiceCallMode())
{
flags.add("OVCM");
}
if(isInterruptible())
{
flags.add("TXI CALL");
}
if(getPriority() > 0)
{
flags.add("PRIORITY-" + getPriority());
}
StringBuilder sb = new StringBuilder();
sb.append("SERVICE OPTIONS [");
if(!flags.isEmpty())
{
sb.append(Joiner.on(",").join(flags));
}
sb.append("]");
return sb.toString();
}
}

View File

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

View File

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