diff --git a/.idea/dictionaries/denny.xml b/.idea/dictionaries/denny.xml
index 2c06cff8..2eb640e2 100644
--- a/.idea/dictionaries/denny.xml
+++ b/.idea/dictionaries/denny.xml
@@ -4,6 +4,8 @@
bahroo
broadcastify
correlator
+ deinterleave
+ descrambled
dinf
esnid
fleetsync
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 08f2781a..6746d627 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,7 +1,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index b386659b..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 10b6dc28..8db480ff 100644
--- a/build.gradle
+++ b/build.gradle
@@ -110,6 +110,7 @@ idea {
}
javafx {
+ version = "13.0.2"
modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing']
}
diff --git a/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java b/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java
index 39a13842..681ccc9c 100644
--- a/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java
+++ b/src/main/java/io/github/dsheirer/alias/action/AliasActionManager.java
@@ -93,12 +93,6 @@ public class AliasActionManager extends Module implements IMessageListener, List
//No actions neeeded
}
- @Override
- public void dispose()
- {
- //No actions neeeded
- }
-
/**
* IMessageListener interface ... delegates to this class implementation of Listener
*/
diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java
index 54233f64..54318179 100644
--- a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java
+++ b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormat.java
@@ -36,6 +36,8 @@ public enum RadioFormat
{
APCO25("********", 0, 0xFFFFFF, "0 to 16,777,215",
"APCO25 valid range is 0 to 16,777,215"),
+ DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
+ "DMR unit id valid range is 0 to 16,777,215"),
PASSPORT("********", 0, 0x7FFFFF, "0 to 8,388,607",
"PASSPORT valid range is 0 to 8,388,607"),
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
@@ -112,6 +114,8 @@ public enum RadioFormat
{
case APCO25:
return APCO25;
+ case DMR:
+ return DMR;
case PASSPORT:
return PASSPORT;
default:
diff --git a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java
index 3dc8f333..56cb74c6 100644
--- a/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java
+++ b/src/main/java/io/github/dsheirer/alias/id/radio/RadioFormatter.java
@@ -25,6 +25,7 @@ package io.github.dsheirer.alias.id.radio;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
+import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
import org.slf4j.Logger;
@@ -32,7 +33,6 @@ import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.EnumMap;
-import java.util.HashMap;
import java.util.Map;
/**
@@ -46,6 +46,7 @@ public class RadioFormatter
static
{
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
+ mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}
diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
index 3ecc3adf..2dcebb7c 100644
--- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
+++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormat.java
@@ -34,8 +34,10 @@ import io.github.dsheirer.protocol.Protocol;
*/
public enum TalkgroupFormat
{
- APCO25("********", 0, 0xFFFFFF, "0 to 65,535",
+ APCO25("********", 0, 0xFFFF, "0 to 65,535",
"APCO25 talkgroup valid range is 0 to 65,535"),
+ DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
+ "DMR talkgroup valid range is 0 to 16,777,215"),
FLEETSYNC("###-####", 0, 0x7FFFFF, "001-0001 to 127-8192",
"Fleetsync valid ranges are 1-127(prefix)
and 1-8192(ident) (ie. 001-0001 to 127-8192)"),
LTR("##-###", 0x101, 0x3FFF, "01-001 to 20-255",
diff --git a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
index 7e1b94d2..fe0bc823 100644
--- a/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
+++ b/src/main/java/io/github/dsheirer/alias/id/talkgroup/TalkgroupFormatter.java
@@ -23,6 +23,7 @@ package io.github.dsheirer.alias.id.talkgroup;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
+import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
@@ -35,7 +36,6 @@ import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.EnumMap;
-import java.util.HashMap;
import java.util.Map;
/**
@@ -49,6 +49,7 @@ public class TalkgroupFormatter
static
{
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
+ mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
mFormatterMap.put(Protocol.FLEETSYNC, new FleetsyncTalkgroupFormatter());
LTRTalkgroupFormatter ltr = new LTRTalkgroupFormatter();
mFormatterMap.put(Protocol.LTR, ltr);
diff --git a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java
index f4bc74c7..770c4d17 100644
--- a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java
+++ b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java
@@ -36,16 +36,17 @@ import org.slf4j.LoggerFactory;
public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener
{
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class);
- private static final int DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz
-
+ public static final long DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz
+ public static final int DEFAULT_TIMESLOT = 0;
private final int mMaxSegmentAudioSampleLength;
private Listener mAudioSegmentListener;
- protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
+ protected MutableIdentifierCollection mIdentifierCollection;
private Broadcaster mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
private AliasList mAliasList;
private AudioSegment mAudioSegment;
private int mAudioSampleCount = 0;
private boolean mRecordAudioOverride;
+ private int mTimeslot;
/**
* Constructs an abstract audio module
@@ -53,22 +54,30 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
* @param aliasList for aliasing identifiers
* @param maxSegmentAudioSampleLength in milliseconds
*/
- public AbstractAudioModule(AliasList aliasList, long maxSegmentAudioSampleLength)
+ public AbstractAudioModule(AliasList aliasList, int timeslot, long maxSegmentAudioSampleLength)
{
mAliasList = aliasList;
mMaxSegmentAudioSampleLength = (int)(maxSegmentAudioSampleLength * 8); //Convert milliseconds to samples
+ mTimeslot = timeslot;
+ mIdentifierCollection = new MutableIdentifierCollection(getTimeslot());
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
}
/**
- * Constructs an abstract audio module
+ * Constructs an abstract audio module with a default maximum audio segment length and a default timeslot 0.
*/
public AbstractAudioModule(AliasList aliasList)
{
- this(aliasList, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
+ this(aliasList, DEFAULT_TIMESLOT, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
}
- protected abstract int getTimeslot();
+ /**
+ * Timeslot for this audio module
+ */
+ protected int getTimeslot()
+ {
+ return mTimeslot;
+ }
/**
* Closes the current audio segment
diff --git a/src/main/java/io/github/dsheirer/audio/AudioModule.java b/src/main/java/io/github/dsheirer/audio/AudioModule.java
index 93bc363e..13d3d2e3 100644
--- a/src/main/java/io/github/dsheirer/audio/AudioModule.java
+++ b/src/main/java/io/github/dsheirer/audio/AudioModule.java
@@ -83,9 +83,9 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
* @param aliasList for aliasing identifiers
* @param maxAudioSegmentLength in milliseconds
*/
- public AudioModule(AliasList aliasList, long maxAudioSegmentLength)
+ public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength)
{
- super(aliasList, maxAudioSegmentLength);
+ super(aliasList, timeslot, maxAudioSegmentLength);
}
/**
@@ -102,13 +102,6 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
return 0;
}
- @Override
- public void dispose()
- {
- removeAudioSegmentListener();
- mSquelchStateListener = null;
- }
-
@Override
public void reset()
{
diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java
index 5e298ae9..43cd7098 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/AmbeAudioModule.java
@@ -33,9 +33,9 @@ public abstract class AmbeAudioModule extends JmbeAudioModule
private static final String AMBE_CODEC = "AMBE 3600 x 2450";
private static boolean sLibraryStatusLogged = false;
- public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
+ public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
{
- super(userPreferences, aliasList);
+ super(userPreferences, aliasList, timeslot);
if(!sLibraryStatusLogged)
{
diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java
index 35004da5..7e99f432 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/ImbeAudioModule.java
@@ -35,7 +35,7 @@ public abstract class ImbeAudioModule extends JmbeAudioModule
public ImbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
{
- super(userPreferences, aliasList);
+ super(userPreferences, aliasList, DEFAULT_TIMESLOT);
if(!sLibraryStatusLogged)
{
diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java
index cc5565e9..cf387e85 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/JmbeAudioModule.java
@@ -54,14 +54,22 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
private IAudioCodec mAudioCodec;
private UserPreferences mUserPreferences;
- public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
+ public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
{
- super(aliasList);
+ super(aliasList, timeslot, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
mUserPreferences = userPreferences;
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
loadConverter();
}
+ @Override
+ public void dispose()
+ {
+ super.dispose();
+ MyEventBus.getGlobalEventBus().unregister(this);
+ mAudioCodec = null;
+ }
+
protected IAudioCodec getAudioCodec()
{
return mAudioCodec;
@@ -236,10 +244,4 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
mAudioCodec = null;
}
}
-
- @Override
- public void dispose()
- {
- mAudioCodec = null;
- }
}
diff --git a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java
index 459a9a2f..a947933f 100644
--- a/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java
+++ b/src/main/java/io/github/dsheirer/audio/codec/mbe/MBECallSequenceRecorder.java
@@ -77,11 +77,6 @@ public abstract class MBECallSequenceRecorder extends Module implements IMessage
{
}
- @Override
- public void dispose()
- {
- }
-
/**
* Writes an MBE call sequence recording to the recording directory
* @param sequence to write
diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java b/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java
index 8b132cda..913090dc 100644
--- a/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java
+++ b/src/main/java/io/github/dsheirer/audio/playback/AudioChannelPanel.java
@@ -129,7 +129,7 @@ public class AudioChannelPanel extends JPanel implements Listener, S
public void dispose()
{
//Deregister from receiving preference update notifications
- MyEventBus.getEventBus().unregister(this);
+ MyEventBus.getGlobalEventBus().unregister(this);
if(mAudioOutput != null)
{
@@ -141,7 +141,7 @@ public class AudioChannelPanel extends JPanel implements Listener, S
private void init()
{
//Register to receive preference updates
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
setLayout(new MigLayout("align center center, insets 0 0 0 0",
"[][][align right]0[grow,fill]", ""));
diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java
index 42d84de5..7738927f 100644
--- a/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java
+++ b/src/main/java/io/github/dsheirer/audio/playback/AudioOutput.java
@@ -168,7 +168,7 @@ public abstract class AudioOutput implements LineListener, Listener
@Override
public void actionPerformed(ActionEvent e)
{
- MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
+ MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
}
});
popup.add(outputMenu);
diff --git a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java
index 58387c1b..fa281094 100644
--- a/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java
+++ b/src/main/java/io/github/dsheirer/audio/playback/AudioPlaybackManager.java
@@ -73,7 +73,7 @@ public class AudioPlaybackManager implements Listener, IAudioContr
public AudioPlaybackManager(UserPreferences userPreferences)
{
mUserPreferences = userPreferences;
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
@@ -254,6 +254,7 @@ public class AudioPlaybackManager implements Listener, IAudioContr
public void dispose()
{
+ MyEventBus.getGlobalEventBus().unregister(this);
if(mProcessingTask != null)
{
mProcessingTask.cancel(true);
diff --git a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java
index 1b9acb83..af2b1b94 100644
--- a/src/main/java/io/github/dsheirer/bits/BinaryMessage.java
+++ b/src/main/java/io/github/dsheirer/bits/BinaryMessage.java
@@ -834,6 +834,24 @@ public class BinaryMessage extends BitSet
return value;
}
+ /**
+ * Returns the twos-complement value of the bits between start and end, inclusive
+ * @param start bit position
+ * @param end bit position
+ * @return twos complement value
+ */
+ public int getTwosComplement(int start, int end)
+ {
+ BinaryMessage sub = getSubMessage(start, end);
+ boolean negative = sub.get(start);
+ sub.flip(0, sub.size());
+
+ int value = sub.getInt(0, sub.size()) + 1;
+ value *= (negative ? -1 : 1);
+ return value;
+ }
+
+
/**
* Returns the long value represented by the bit range. This method will
* parse the bits in big endian or little endian format. The start value
diff --git a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
index 4322dbad..a5abee55 100644
--- a/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
+++ b/src/main/java/io/github/dsheirer/bits/SoftSyncDetector.java
@@ -33,6 +33,10 @@ public class SoftSyncDetector implements ISyncProcessor
mThreshold = threshold;
}
+ public SoftSyncDetector(long pattern, int threshold) {
+ mPattern = pattern;
+ mThreshold = threshold;
+ }
public void dispose()
{
mListener = null;
diff --git a/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java b/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java
index e93a4234..ddb88d4d 100644
--- a/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java
+++ b/src/main/java/io/github/dsheirer/channel/details/ChannelDetailPanel.java
@@ -25,9 +25,13 @@ import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.sample.Listener;
import net.miginfocom.swing.MigLayout;
-import javax.swing.*;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
import javax.swing.text.DefaultCaret;
-import java.awt.*;
+import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelAndMetadata.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelAndMetadata.java
new file mode 100644
index 00000000..794e4e88
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelAndMetadata.java
@@ -0,0 +1,60 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.channel.metadata;
+
+import io.github.dsheirer.controller.channel.Channel;
+
+import java.util.List;
+
+/**
+ * Channel and associated channel metadata
+ */
+public class ChannelAndMetadata
+{
+ private Channel mChannel;
+ private List mChannelMetadata;
+
+ /**
+ * Connstructs an instance
+ * @param channel
+ * @param metadata
+ */
+ public ChannelAndMetadata(Channel channel, List metadata)
+ {
+ mChannel = channel;
+ mChannelMetadata = metadata;
+ }
+
+ /**
+ * Channel
+ */
+ public Channel getChannel()
+ {
+ return mChannel;
+ }
+
+ /**
+ * Channel Metadata
+ */
+ public List getChannelMetadata()
+ {
+ return mChannelMetadata;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java
index 2b142889..f98f774a 100644
--- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java
+++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataModel.java
@@ -28,6 +28,7 @@ import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
import io.github.dsheirer.preference.PreferenceType;
+import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -57,10 +58,11 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
private List mChannelMetadata = new ArrayList();
private Map mMetadataChannelMap = new HashMap();
+ private Listener mChannelAddListener;
public ChannelMetadataModel()
{
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
}
/**
@@ -82,21 +84,66 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
}
}
- public void add(Collection channelMetadatas, Channel channel)
+ public void dispose()
+ {
+ MyEventBus.getGlobalEventBus().unregister(this);
+ }
+
+ /**
+ * Lookup model row for the specified channel metadata
+ */
+ public int getRow(ChannelMetadata channelMetadata)
+ {
+ if(mChannelMetadata.contains(channelMetadata))
+ {
+ return mChannelMetadata.indexOf(channelMetadata);
+ }
+
+ return -1;
+ }
+
+ /**
+ * Registers the listener to be notified when channel metadata(s) are added to the model.
+ * @param listener to receive channel associated with the channel metadata(s) that were added
+ */
+ public void setChannelAddListener(Listener listener)
+ {
+ mChannelAddListener = listener;
+ }
+
+ public void add(ChannelAndMetadata channelAndMetadata)
{
//Execute on the swing thread to avoid threading issues
EventQueue.invokeLater(() -> {
- for(ChannelMetadata channelMetadata: channelMetadatas)
+ for(ChannelMetadata channelMetadata: channelAndMetadata.getChannelMetadata())
{
mChannelMetadata.add(channelMetadata);
- mMetadataChannelMap.put(channelMetadata, channel);
+ mMetadataChannelMap.put(channelMetadata, channelAndMetadata.getChannel());
int index = mChannelMetadata.indexOf(channelMetadata);
fireTableRowsInserted(index, index);
channelMetadata.setUpdateEventListener(ChannelMetadataModel.this);
}
+
+ if(mChannelAddListener != null)
+ {
+ mChannelAddListener.receive(channelAndMetadata);
+ }
});
}
+ /**
+ * Updates the mapping of channel metadata to the channel used to create the channel metadatas (and processing chain)
+ * @param channelMetadatas
+ * @param channel
+ */
+ public void updateChannelMetadataToChannelMap(Collection channelMetadatas, Channel channel)
+ {
+ for(ChannelMetadata channelMetadata: channelMetadatas)
+ {
+ mMetadataChannelMap.put(channelMetadata, channel);
+ }
+ }
+
public void remove(ChannelMetadata channelMetadata)
{
//Execute on the swing thread to avoid threading issues
diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java
index 4043c540..661739d4 100644
--- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java
+++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java
@@ -38,6 +38,8 @@ import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import net.miginfocom.swing.MigLayout;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
@@ -60,6 +62,8 @@ import java.util.Map;
public class ChannelMetadataPanel extends JPanel implements ListSelectionListener
{
+ private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class);
+
private static final String TABLE_PREFERENCE_KEY = "channel.metadata.panel";
private ChannelModel mChannelModel;
private ChannelProcessingManager mChannelProcessingManager;
@@ -70,6 +74,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
private Map mBackgroundColors = new EnumMap<>(State.class);
private Map mForegroundColors = new EnumMap<>(State.class);
private JTableColumnWidthMonitor mTableColumnMonitor;
+ private Channel mUserSelectedChannel;
/**
* Table view for currently decoding channel metadata
@@ -91,6 +96,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
mTable = new JTable(mChannelProcessingManager.getChannelMetadataModel());
+ mChannelProcessingManager.getChannelMetadataModel().setChannelAddListener(new ChannelAddListener());
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class);
renderer.setHorizontalAlignment(SwingConstants.CENTER);
@@ -165,9 +171,10 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
if(selectedMetadata != null)
{
- Channel selectedChannel = mChannelProcessingManager.getChannelMetadataModel()
+ mUserSelectedChannel = mChannelProcessingManager.getChannelMetadataModel()
.getChannelFromMetadata(selectedMetadata);
- processingChain = mChannelProcessingManager.getProcessingChain(selectedChannel);
+
+ processingChain = mChannelProcessingManager.getProcessingChain(mUserSelectedChannel);
}
}
@@ -422,4 +429,39 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
}
}
}
+
+ /**
+ * Listener to be notified when a channel and associated channel metadata(s) are added to the underlying
+ * channel metadata model.
+ *
+ * When a channel is added, it is compared the the last user selected channel and if they are the same, it
+ * invokes a selection event on the channel metadata row so that the channel metadata is re-selected. This is
+ * primarily a hack to counter-act the DMR Capacity+ REST channel rotation where a channel is converted to a
+ * traffic channel and the previous channel is restarted. The UI effect is that the user selected channel row
+ * in the Now Playing window continually loses selection over the channel and causes the user to perpetually
+ * chase the channel row.
+ */
+ public class ChannelAddListener implements Listener
+ {
+ @Override
+ public void receive(ChannelAndMetadata channelAndMetadata)
+ {
+ if(mUserSelectedChannel != null &&
+ mUserSelectedChannel.getChannelID() == channelAndMetadata.getChannel().getChannelID())
+ {
+ List metadata = channelAndMetadata.getChannelMetadata();
+
+ if(metadata.size() > 0)
+ {
+ int modelRow = mChannelProcessingManager.getChannelMetadataModel().getRow(metadata.get(0));
+
+ if(modelRow >= 0)
+ {
+ int tableRow = mTable.convertRowIndexToView(modelRow);
+ mTable.getSelectionModel().setSelectionInterval(tableRow, tableRow);
+ }
+ }
+ }
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java b/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java
index 2c93486c..10a5772c 100644
--- a/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/AbstractChannelState.java
@@ -41,7 +41,7 @@ import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Collection;
+import java.util.List;
public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider,
IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider,
@@ -53,15 +53,35 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
protected Listener mDecodeEventListener;
protected Listener mDecoderStateListener;
protected Listener mExternalSourceEventListener;
- protected Channel mChannel;
+ private Channel mChannel;
protected boolean mSourceOverflow = false;
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
+ /**
+ * Constructs an instance
+ * @param channel configuration
+ */
public AbstractChannelState(Channel channel)
{
mChannel = channel;
}
+ /**
+ * Updates/replaces the current channel configuration with the argument.
+ */
+ protected void updateChannelConfiguration(Channel channel)
+ {
+ mChannel = channel;
+ }
+
+ /**
+ * Channel configuration for this channel state
+ */
+ protected Channel getChannel()
+ {
+ return mChannel;
+ }
+
/**
* Invoked each time that a heartbeat is received so that sub-class implementations can check current timers and
* adjust channel state as necessary. The heartbeat arrives on a periodic basis independent of any decoded
@@ -69,7 +89,7 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
*/
protected abstract void checkState();
- public abstract Collection getChannelMetadata();
+ public abstract List getChannelMetadata();
public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification);
diff --git a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java
index f594003c..4a0f933c 100644
--- a/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/AlwaysUnsquelchedDecoderState.java
@@ -24,10 +24,8 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
-import io.github.dsheirer.identifier.string.StringIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
-import io.github.dsheirer.protocol.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -78,7 +76,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
@Override
public void receiveDecoderStateEvent(DecoderStateEvent event)
{
- if(event.getEvent() == Event.RESET)
+ if(event.getEvent() == Event.REQUEST_RESET)
{
getIdentifierCollection().update(mChannelNameIdentifier);
}
@@ -93,7 +91,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
@Override
public void start()
{
- broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
+ broadcast(new DecoderStateEvent(this, Event.REQUEST_ALWAYS_UNSQUELCH, State.IDLE));
getIdentifierCollection().update(mChannelNameIdentifier);
}
diff --git a/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java b/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java
index f885b73b..91240529 100644
--- a/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java
+++ b/src/main/java/io/github/dsheirer/channel/state/ChangeChannelTimeoutEvent.java
@@ -41,7 +41,7 @@ public class ChangeChannelTimeoutEvent extends DecoderStateEvent
*/
public ChangeChannelTimeoutEvent(Object source, ChannelType channelType, long timeout, int timeslot)
{
- super(source, Event.CHANGE_CALL_TIMEOUT, State.IDLE, timeslot);
+ super(source, Event.REQUEST_CHANGE_CALL_TIMEOUT, State.IDLE, timeslot);
mChannelType = channelType;
mCallTimeoutMilliseconds = timeout;
}
diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java
index fbb24a8b..a50c8f6e 100644
--- a/src/main/java/io/github/dsheirer/channel/state/DecoderState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/DecoderState.java
@@ -125,14 +125,6 @@ public abstract class DecoderState extends AbstractDecoderState
*/
public abstract void init();
- /**
- * Disposes any resources or pointers held by this instance to prepare for
- * garbage collection
- */
- public void dispose()
- {
- }
-
/**
* Listens for configuration identifiers and adds them to this decoder state's identifier collection.
* @return
diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java b/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java
index 2edb5ff5..e824d19d 100644
--- a/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java
+++ b/src/main/java/io/github/dsheirer/channel/state/DecoderStateEvent.java
@@ -22,16 +22,12 @@
package io.github.dsheirer.channel.state;
+/**
+ * State and Status request and notification events used for communication between Decoder States and Channel states
+ * and by peripheral entities like the ChannelRotationMonitor.
+ */
public class DecoderStateEvent
{
- private static final DecoderStateEvent ACTIVE_STATE_TIMESLOT_0 =
- new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_ACTIVE_STATE, State.ACTIVE, 0);
- private static final DecoderStateEvent ACTIVE_STATE_TIMESLOT_1 =
- new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_ACTIVE_STATE, State.ACTIVE, 1);
- private static final DecoderStateEvent INACTIVE_STATE_TIMESLOT_0 =
- new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_INACTIVE_STATE, State.ACTIVE, 0);
- private static final DecoderStateEvent INACTIVE_STATE_TIMESLOT_1 =
- new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_INACTIVE_STATE, State.ACTIVE, 1);
private Object mSource;
private Event mEvent;
private State mState;
@@ -63,65 +59,14 @@ public class DecoderStateEvent
}
/**
- * Creates a channel state active event for the specified timeslot
- * @param timeslot that the state applies to
- * @return new event
+ * Creates a decoder state notification event for a null source with the specified state and timeslot.
+ * @param state (current) of the decoder
+ * @param timeslot that is reporting state
+ * @return new decoder state event
*/
- public static DecoderStateEvent activeState(int timeslot)
+ public static DecoderStateEvent stateNotification(State state, int timeslot)
{
- if(timeslot == 0)
- {
- return ACTIVE_STATE_TIMESLOT_0;
- }
- else if(timeslot == 1)
- {
- return ACTIVE_STATE_TIMESLOT_1;
- }
- else
- {
- throw new IllegalArgumentException("Only timeslots 0 and 1 are currently supported");
- }
- }
-
- /**
- * Creates a channel state active event for the specified timeslot.
- * @param state currently
- * @return new event
- */
- public static DecoderStateEvent activeState()
- {
- return activeState(0);
- }
-
- /**
- * Creates a channel state inactive event for the specified timeslot
- * @param timeslot that the state applies to
- * @return new event
- */
- public static DecoderStateEvent inactiveState(int timeslot)
- {
- if(timeslot == 0)
- {
- return INACTIVE_STATE_TIMESLOT_0;
- }
- else if(timeslot == 1)
- {
- return INACTIVE_STATE_TIMESLOT_1;
- }
- else
- {
- throw new IllegalArgumentException("Only timeslots 0 and 1 are currently supported");
- }
- }
-
- /**
- * Creates a channel state active event for the specified timeslot.
- * @param state currently
- * @return new event
- */
- public static DecoderStateEvent inactiveState()
- {
- return inactiveState(0);
+ return new DecoderStateEvent(null, Event.NOTIFICATION_CHANNEL_STATE, state, timeslot);
}
public String toString()
@@ -164,15 +109,23 @@ public class DecoderStateEvent
public enum Event
{
- ALWAYS_UNSQUELCH,
- CHANGE_CALL_TIMEOUT,
+ //Decode state discrete events
CONTINUATION,
DECODE,
END,
+ START,
+
+ NOTIFICATION_CHANNEL_STATE,
NOTIFICATION_CHANNEL_ACTIVE_STATE,
NOTIFICATION_CHANNEL_INACTIVE_STATE,
- RESET,
- SOURCE_FREQUENCY,
- START;
+ NOTIFICATION_SOURCE_FREQUENCY,
+
+
+ REQUEST_ALWAYS_UNSQUELCH,
+ REQUEST_CHANGE_CALL_TIMEOUT,
+ REQUEST_RESET,
+
+ //Request to convert the current channel to a traffic channel (used by DMR Cap+)
+ REQUEST_CONVERT_TO_TRAFFIC_CHANNEL;
}
}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/channel/state/DecoderStateNotificationEventCache.java b/src/main/java/io/github/dsheirer/channel/state/DecoderStateNotificationEventCache.java
new file mode 100644
index 00000000..50433244
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/channel/state/DecoderStateNotificationEventCache.java
@@ -0,0 +1,62 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.channel.state;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Cache for frequently reused decoder state notification events and timeslots
+ */
+public class DecoderStateNotificationEventCache
+{
+ private Map> mTimeslotMap = new HashMap<>();
+
+ public DecoderStateNotificationEventCache()
+ {
+ }
+
+ /**
+ * Creates a decoder state notification event for the state and timeslot, or reuses a previously created event.
+ * @param state for the decoder
+ * @param timeslot of the decoder
+ * @return state notification event
+ */
+ public DecoderStateEvent getStateNotificationEvent(State state, int timeslot)
+ {
+ Map timeslotStateMap = mTimeslotMap.get(timeslot);
+
+ if(timeslotStateMap == null)
+ {
+ timeslotStateMap = new HashMap<>();
+ mTimeslotMap.put(timeslot, timeslotStateMap);
+ }
+
+ DecoderStateEvent event = timeslotStateMap.get(state);
+
+ if(event == null)
+ {
+ event = DecoderStateEvent.stateNotification(state, timeslot);
+ timeslotStateMap.put(state, event);
+ }
+
+ return event;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java
index 1377dede..b9dd869c 100644
--- a/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/MultiChannelState.java
@@ -21,12 +21,13 @@
*/
package io.github.dsheirer.channel.state;
+import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.channel.metadata.ChannelMetadata;
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
import io.github.dsheirer.controller.channel.Channel;
-import io.github.dsheirer.controller.channel.Channel.ChannelType;
+import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierUpdateListener;
@@ -39,7 +40,6 @@ import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentif
import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
-import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
@@ -50,11 +50,28 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Collection;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
+/**
+ * Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
+ * and provides squelch control and decoder state reset events.
+ *
+ * Uses a state enumeration that defines allowable channel state transitions in order to track a call or data decode
+ * event from start to finish. Uses a timer to monitor for inactivity and to provide a FADE period that indicates
+ * to the user that the activity has stopped while continuing to provide details about the call, before the state is
+ * reset to IDLE.
+ *
+ * State Descriptions:
+ * IDLE: Normal state. No voice or data call activity
+ * CALL/DATA/ENCRYPTED/CONTROL: Decoding states.
+ * FADE: The phase after a voice or data call when either an explicit call end has been received, or when no new
+ * signalling updates have been received, and the fade timer has expired. This phase allows for gui updates to
+ * signal to the user that the call is ended, while continuing to display the call details for the user
+ * TEARDOWN: Indicates a traffic channel that will be torn down for reuse.
+ */
public class MultiChannelState extends AbstractChannelState implements IDecoderStateEventListener, ISourceEventListener,
IdentifierUpdateListener, IStateMachineListener
{
@@ -70,33 +87,23 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
private Map mIdentifierCollectionMap = new TreeMap<>();
private Map mStateMachineMap = new TreeMap<>();
private Map mSquelchControllerMap = new TreeMap<>();
- private int mTimeslotCount;
+ private int[] mTimeslots;
+ private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache();
private Listener mIdentifierUpdateListener = new IdentifierUpdateListenerProxy();
/**
- * Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
- * and provides squelch control and decoder state reset events.
- *
- * Uses a state enumeration that defines allowable channel state transitions in order to track a call or data decode
- * event from start to finish. Uses a timer to monitor for inactivity and to provide a FADE period that indicates
- * to the user that the activity has stopped while continuing to provide details about the call, before the state is
- * reset to IDLE.
- *
- * State Descriptions:
- * IDLE: Normal state. No voice or data call activity
- * CALL/DATA/ENCRYPTED/CONTROL: Decoding states.
- * FADE: The phase after a voice or data call when either an explicit call end has been received, or when no new
- * signalling updates have been received, and the fade timer has expired. This phase allows for gui updates to
- * signal to the user that the call is ended, while continuing to display the call details for the user
- * TEARDOWN: Indicates a traffic channel that will be torn down for reuse.
+ * Constructs an instance
+ * @param channel configuration
+ * @param aliasModel for channel metadata and identifiers
+ * @param timeslots array of timeslot numbers to use
*/
- public MultiChannelState(Channel channel, AliasModel aliasModel, int timeslots)
+ public MultiChannelState(Channel channel, AliasModel aliasModel, int[] timeslots)
{
super(channel);
- mTimeslotCount = timeslots;
+ mTimeslots = timeslots;
- for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
+ for(int timeslot: timeslots)
{
mChannelMetadataMap.put(timeslot, new ChannelMetadata(aliasModel, timeslot));
MutableIdentifierCollection mutableIdentifierCollection = new MutableIdentifierCollection(timeslot);
@@ -113,36 +120,45 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
mSquelchControllerMap.put(timeslot, squelchController);
stateMachine.addListener(squelchController);
- stateMachine.setChannelType(mChannel.getChannelType());
stateMachine.setIdentifierUpdateListener(mutableIdentifierCollection);
stateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
- if(channel.getChannelType() == ChannelType.STANDARD)
- {
- stateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY);
- }
- else
- {
- stateMachine.setFadeTimeoutBufferMilliseconds(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
- }
+ stateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY);
}
+ configureChannelType(channel);
+
createConfigurationIdentifiers(channel);
}
+ /**
+ * Configure items according to channel type
+ * @param channel configuration
+ */
+ private void configureChannelType(Channel channel)
+ {
+ for(int timeslot: mTimeslots)
+ {
+ StateMachine stateMachine = mStateMachineMap.get(timeslot);
+ stateMachine.setChannelType(channel.getChannelType());
+ }
+ }
+
+ /**
+ * Receive notification that the underlying channel configuration has changed.
+ * @param notification
+ */
+ @Subscribe
+ public void channelConfigurationChanged(ChannelConfigurationChangeNotification notification)
+ {
+ updateChannelConfiguration(notification.getChannel());
+ configureChannelType(notification.getChannel());
+ createConfigurationIdentifiers(notification.getChannel());
+ }
+
@Override
public void stateChanged(State state, int timeslot)
{
- //Broadcast current channel state so that channel rotation monitor can track
- if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state))
- {
- broadcast(DecoderStateEvent.activeState(timeslot));
- }
- else
- {
- broadcast(DecoderStateEvent.inactiveState(timeslot));
- }
-
- ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.create(state);
+ ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.get(state);
mIdentifierCollectionMap.get(timeslot).update(stateIdentifier);
mChannelMetadataMap.get(timeslot).receive(new IdentifierUpdateNotification(stateIdentifier, IdentifierUpdateNotification.Operation.ADD, timeslot));
@@ -153,7 +169,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
mStateMachineMap.get(timeslot).setState(State.IDLE);
break;
case TEARDOWN:
- if(mChannel.isTrafficChannel())
+ if(getChannel().isTrafficChannel())
{
checkTeardown();
}
@@ -191,9 +207,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
if(teardown && !active)
{
- if(mChannel.isTrafficChannel())
+ if(getChannel().isTrafficChannel())
{
- broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
+ broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
}
else
{
@@ -230,7 +246,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
*/
private void createConfigurationIdentifiers(Channel channel)
{
- for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
+ for(int timeslot: mTimeslots)
{
MutableIdentifierCollection identifierCollection = mIdentifierCollectionMap.get(timeslot);
@@ -334,9 +350,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
/**
* Channel metadata for this channel.
*/
- public Collection getChannelMetadata()
+ public List getChannelMetadata()
{
- return mChannelMetadataMap.values();
+ return new ArrayList<>(mChannelMetadataMap.values());
}
/**
@@ -345,7 +361,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
@Override
public void reset()
{
- for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
+ for(int timeslot: mTimeslots)
{
reset(timeslot);
}
@@ -356,7 +372,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
private void reset(int timeslot)
{
mStateMachineMap.get(timeslot).setState(State.RESET);
- broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE, timeslot));
+ broadcast(new DecoderStateEvent(this, Event.REQUEST_RESET, State.IDLE, timeslot));
MutableIdentifierCollection identifierCollection = mIdentifierCollectionMap.get(timeslot);
identifierCollection.remove(IdentifierClass.USER);
}
@@ -364,7 +380,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
@Override
public void start()
{
- for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
+ for(int timeslot: mTimeslots)
{
mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers();
mStateMachineMap.get(timeslot).setState(State.RESET);
@@ -380,12 +396,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
}
}
- public void dispose()
- {
- mDecodeEventListener = null;
- mDecoderStateListener = null;
- }
-
@Override
public Listener getSourceEventListener()
{
@@ -480,9 +490,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
//Rebroadcast source frequency change events for the decoder(s) to process
long frequency = sourceEvent.getValue().longValue();
- for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
+ for(int timeslot: mTimeslots)
{
- broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY,
+ broadcast(new DecoderStateEvent(this, Event.NOTIFICATION_SOURCE_FREQUENCY,
mStateMachineMap.get(timeslot).getState(), frequency));
//Create a new frequency configuration identifier so that downstream consumers receive the change
@@ -497,12 +507,13 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//Rebroadcast frequency error measurements to external listener if we're currently
//in an active (ie sync locked) state.
- for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
+ for(int timeslot: mTimeslots)
{
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState()))
{
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
- mChannel.getChannelType().name()));
+ getChannel().getChannelType().name()));
+ return;
}
}
break;
@@ -522,10 +533,10 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
{
switch(event.getEvent())
{
- case ALWAYS_UNSQUELCH:
+ case REQUEST_ALWAYS_UNSQUELCH:
mSquelchControllerMap.get(event.getTimeslot()).setSquelchLock(true);
break;
- case CHANGE_CALL_TIMEOUT:
+ case REQUEST_CHANGE_CALL_TIMEOUT:
if(event instanceof ChangeChannelTimeoutEvent)
{
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
@@ -538,19 +549,18 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(event.getState()))
{
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
+
+ //Broadcast current channel/timeslot state so that channel rotation monitor can track
+ broadcast(mStateNotificationCache.getStateNotificationEvent(event.getState(), event.getTimeslot()));
}
break;
case END:
- if(mChannel.isTrafficChannel())
- {
- mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
- }
- else
- {
- mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
- }
+ mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
+
+ //Broadcast current channel/timeslot state so that channel rotation monitor can track
+ broadcast(mStateNotificationCache.getStateNotificationEvent(event.getState(), event.getTimeslot()));
break;
- case RESET:
+ case REQUEST_RESET:
/* Channel State does not respond to reset events */
break;
default:
diff --git a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java
index 6b8da41b..c532eef2 100644
--- a/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java
+++ b/src/main/java/io/github/dsheirer/channel/state/SingleChannelState.java
@@ -21,12 +21,13 @@
*/
package io.github.dsheirer.channel.state;
+import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.channel.metadata.ChannelMetadata;
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
import io.github.dsheirer.controller.channel.Channel;
-import io.github.dsheirer.controller.channel.Channel.ChannelType;
+import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierUpdateListener;
@@ -51,7 +52,6 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -97,6 +97,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
private ChannelMetadata mChannelMetadata;
private StateMachine mStateMachine = new StateMachine(0, State.SINGLE_CHANNEL_ACTIVE_STATES);
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
+ private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache();
public SingleChannelState(Channel channel, AliasModel aliasModel)
{
@@ -107,10 +108,21 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mStateMachine.addListener(this);
mStateMachine.addListener(mSquelchController);
- mStateMachine.setChannelType(mChannel.getChannelType());
mStateMachine.setIdentifierUpdateListener(mIdentifierCollection);
mStateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
- if(channel.getChannelType() == ChannelType.STANDARD)
+
+ configureChannelType(channel);
+ }
+
+ /**
+ * Configure items according to channel type
+ * @param channel configuration
+ */
+ private void configureChannelType(Channel channel)
+ {
+ mStateMachine.setChannelType(getChannel().getChannelType());
+
+ if(channel.isStandardChannel())
{
mStateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY);
}
@@ -125,36 +137,38 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
}
}
+ /**
+ * Receive notification that the underlying channel configuration has changed.
+ * @param notification
+ */
+ @Subscribe
+ public void channelConfigurationChanged(ChannelConfigurationChangeNotification notification)
+ {
+ updateChannelConfiguration(notification.getChannel());
+ configureChannelType(notification.getChannel());
+ createConfigurationIdentifiers(notification.getChannel());
+ }
+
@Override
public void stateChanged(State state, int timeslot)
{
- //Broadcast current channel state so that channel rotation monitor can track
- if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(state))
- {
- broadcast(DecoderStateEvent.activeState(timeslot));
- }
- else
- {
- broadcast(DecoderStateEvent.inactiveState(timeslot));
- }
-
- ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.create(state);
+ ChannelStateIdentifier stateIdentifier = ChannelStateIdentifier.get(state);
mIdentifierCollection.update(stateIdentifier);
mChannelMetadata.receive(new IdentifierUpdateNotification(stateIdentifier, IdentifierUpdateNotification.Operation.ADD, timeslot));
switch(state)
{
case IDLE:
- broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE));
+ broadcast(new DecoderStateEvent(this, Event.REQUEST_RESET, State.IDLE));
break;
case RESET:
reset();
mStateMachine.setState(State.IDLE);
break;
case TEARDOWN:
- if(mChannel.isTrafficChannel())
+ if(getChannel().isTrafficChannel())
{
- broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
+ broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
}
else
{
@@ -258,7 +272,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
/**
* Channel metadata for this channel.
*/
- public Collection getChannelMetadata()
+ public List getChannelMetadata()
{
return Collections.singletonList(mChannelMetadata);
}
@@ -270,7 +284,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
public void reset()
{
mStateMachine.setState(State.RESET);
- broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE));
+ broadcast(new DecoderStateEvent(this, Event.REQUEST_RESET, State.IDLE));
mIdentifierCollection.remove(IdentifierClass.USER);
sourceOverflow(false);
}
@@ -280,7 +294,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
{
mIdentifierCollection.broadcastIdentifiers();
- if(mChannel.getChannelType() == ChannelType.TRAFFIC)
+ if(getChannel().isTrafficChannel())
{
mStateMachine.setState(State.ACTIVE);
}
@@ -292,12 +306,6 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mSquelchController.setSquelchLock(false);
}
- public void dispose()
- {
- mDecodeEventListener = null;
- mDecoderStateListener = null;
- }
-
@Override
public Listener getSourceEventListener()
{
@@ -372,7 +380,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
case NOTIFICATION_FREQUENCY_CHANGE:
//Rebroadcast source frequency change events for the decoder(s) to process
long frequency = sourceEvent.getValue().longValue();
- broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, mStateMachine.getState(), frequency));
+ broadcast(new DecoderStateEvent(this, Event.NOTIFICATION_SOURCE_FREQUENCY, mStateMachine.getState(), frequency));
//Create a new frequency configuration identifier so that downstream consumers receive the change
//via channel metadata and audio packet updates - this is a silent add that is sent as a notification
@@ -386,7 +394,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState()))
{
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
- mChannel.getChannelType().name()));
+ getChannel().getChannelType().name()));
}
break;
}
@@ -405,10 +413,10 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
{
switch(event.getEvent())
{
- case ALWAYS_UNSQUELCH:
+ case REQUEST_ALWAYS_UNSQUELCH:
mSquelchController.setSquelchLock(true);
break;
- case CHANGE_CALL_TIMEOUT:
+ case REQUEST_CHANGE_CALL_TIMEOUT:
if(event instanceof ChangeChannelTimeoutEvent)
{
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
@@ -420,19 +428,28 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(event.getState()))
{
mStateMachine.setState(event.getState());
+
+ //Broadcast state so channel rotation manager can track
+ broadcast(mStateNotificationCache.getStateNotificationEvent(event.getState(), event.getTimeslot()));
}
break;
case END:
- if(mChannel.isTrafficChannel())
+ if(getChannel().isTrafficChannel())
{
mStateMachine.setState(State.TEARDOWN);
+
+ //Broadcast state so channel rotation manager can track
+ broadcast(mStateNotificationCache.getStateNotificationEvent(State.TEARDOWN, event.getTimeslot()));
}
else
{
mStateMachine.setState(State.FADE);
+
+ //Broadcast state so channel rotation manager can track
+ broadcast(mStateNotificationCache.getStateNotificationEvent(State.FADE, event.getTimeslot()));
}
break;
- case RESET:
+ case REQUEST_RESET:
/* Channel State does not respond to reset events */
break;
default:
diff --git a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java
index ef5b694f..f3f86e68 100644
--- a/src/main/java/io/github/dsheirer/controller/ControllerPanel.java
+++ b/src/main/java/io/github/dsheirer/controller/ControllerPanel.java
@@ -83,7 +83,7 @@ public class ControllerPanel extends JPanel
{
if(index == mSettingsTabIndex)
{
- MyEventBus.getEventBus().post(new ViewPlaylistRequest());
+ MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest());
}
else
{
diff --git a/src/main/java/io/github/dsheirer/controller/channel/Channel.java b/src/main/java/io/github/dsheirer/controller/channel/Channel.java
index aa00a7d8..fb54c1e7 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/Channel.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/Channel.java
@@ -390,6 +390,8 @@ public class Channel extends Configuration implements Listener
sb.append(hasSite() ? getSite() : "SITE");
sb.append("_");
sb.append(getName());
+ sb.append("_");
+ sb.append(getChannelID());
return sb.toString();
}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelConfigurationChangeNotification.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelConfigurationChangeNotification.java
new file mode 100644
index 00000000..ab179b34
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelConfigurationChangeNotification.java
@@ -0,0 +1,47 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.controller.channel;
+
+import io.github.dsheirer.module.ModuleEventBusMessage;
+
+/**
+ * Notification that a channel configuration has changed.
+ */
+public class ChannelConfigurationChangeNotification extends ModuleEventBusMessage
+{
+ private Channel mChannel;
+
+ /**
+ * Constructs an instance
+ * @param channel current
+ */
+ public ChannelConfigurationChangeNotification(Channel channel)
+ {
+ mChannel = channel;
+ }
+
+ /**
+ * New channel configuration
+ */
+ public Channel getChannel()
+ {
+ return mChannel;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelConversionRequest.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelConversionRequest.java
new file mode 100644
index 00000000..60b3078b
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelConversionRequest.java
@@ -0,0 +1,59 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.controller.channel;
+
+import io.github.dsheirer.module.ModuleEventBusMessage;
+
+/**
+ * Request to convert a currently processing standard channel type to a traffic channel type. This request will
+ * normally be handled by the ChannelProcessingManager instance.
+ */
+public class ChannelConversionRequest extends ModuleEventBusMessage
+{
+ private Channel mCurrentChannel;
+ private Channel mTrafficChannel;
+
+ /**
+ * Constructs an instance.
+ * @param currentChannel that is actively processing
+ * @param trafficChannel to convert to
+ */
+ public ChannelConversionRequest(Channel currentChannel, Channel trafficChannel)
+ {
+ mCurrentChannel = currentChannel;
+ mTrafficChannel = trafficChannel;
+ }
+
+ /**
+ * Channel configuration for the channel that is currently processing
+ */
+ public Channel getCurrentChannel()
+ {
+ return mCurrentChannel;
+ }
+
+ /**
+ * Channel configuration for the traffic channel to convert to
+ */
+ public Channel getTrafficChannel()
+ {
+ return mTrafficChannel;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelGrantEvent.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelGrantEvent.java
deleted file mode 100644
index 1666df2d..00000000
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelGrantEvent.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * ******************************************************************************
- * sdrtrunk
- * Copyright (C) 2014-2018 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.controller.channel;
-
-import io.github.dsheirer.channel.IChannelDescriptor;
-import io.github.dsheirer.identifier.IdentifierCollection;
-
-/**
- * Channel grant event with a channel descriptor that identifies the channel that is being created
- */
-public class ChannelGrantEvent extends ChannelEvent
-{
- private IChannelDescriptor mChannelDescriptor;
- private IdentifierCollection mIdentifierCollection;
-
- /**
- * Constructs a channel grant event
- * @param channel with setup/configuration details
- * @param event to convey for the channel
- * @param channelDescriptor for the channel that is to be created
- * @param identifierCollection containing identifiers to preload into the channel
- */
- public ChannelGrantEvent(Channel channel, Event event, IChannelDescriptor channelDescriptor,
- IdentifierCollection identifierCollection)
- {
- super(channel, event);
- mChannelDescriptor = channelDescriptor;
- mIdentifierCollection = identifierCollection;
- }
-
- /**
- * Channel descriptor that contains uplink and downlink information
- */
- public IChannelDescriptor getChannelDescriptor()
- {
- return mChannelDescriptor;
- }
-
- /**
- * Identifier collection to use in preloading the channel state for the allocated channel
- */
- public IdentifierCollection getIdentifierCollection()
- {
- return mIdentifierCollection;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java
index 283e9ddf..bd68cc6a 100644
--- a/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java
+++ b/src/main/java/io/github/dsheirer/controller/channel/ChannelProcessingManager.java
@@ -21,25 +21,24 @@
*/
package io.github.dsheirer.controller.channel;
+import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioSegment;
-import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.channel.metadata.ChannelAndMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
+import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
+import io.github.dsheirer.controller.channel.event.PreloadDataContent;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
-import io.github.dsheirer.filter.FilterSet;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
-import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
-import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.module.decode.DecoderFactory;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
-import io.github.dsheirer.module.decode.event.MessageActivityModel;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderFactory;
@@ -51,6 +50,7 @@ import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
+import io.github.dsheirer.util.ThreadPool;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,6 +60,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
/**
* Channel processing manager handles all starting and stopping of channel decoding. A processing chain is created
@@ -83,7 +85,8 @@ public class ChannelProcessingManager implements Listener
private SourceManager mSourceManager;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
- private List mUnTunableFrequencies = new ArrayList<>();
+ private List mLoggedFrequencies = new ArrayList<>();
+ private List> mDelayedChannelStartTasks = new ArrayList<>();
/**
* Constructs the channel processing manager
@@ -167,6 +170,7 @@ public class ChannelProcessingManager implements Listener
*
* @param event that requests either enable/start or disable/stop a channel.
*/
+ @Deprecated //TODO: convert all usage to start(ChannelStartProcessingRequest request) method
@Override
public synchronized void receive(ChannelEvent event)
{
@@ -179,7 +183,7 @@ public class ChannelProcessingManager implements Listener
{
try
{
- startProcessing(event);
+ startProcessing(new ChannelStartProcessingRequest(event.getChannel()));
}
catch(ChannelException ce)
{
@@ -187,9 +191,9 @@ public class ChannelProcessingManager implements Listener
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
- if(!mUnTunableFrequencies.contains(frequency))
+ if(!mLoggedFrequencies.contains(frequency))
{
- mUnTunableFrequencies.add(frequency);
+ mLoggedFrequencies.add(frequency);
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequency +
"] - " + ce.getMessage());
}
@@ -199,9 +203,9 @@ public class ChannelProcessingManager implements Listener
List frequencies = ((SourceConfigTunerMultipleFrequency)channel
.getSourceConfiguration()).getFrequencies();
- if(frequencies.size() > 0 && !mUnTunableFrequencies.contains(frequencies.get(0)))
+ if(frequencies.size() > 0 && !mLoggedFrequencies.contains(frequencies.get(0)))
{
- mUnTunableFrequencies.add(frequencies.get(0));
+ mLoggedFrequencies.add(frequencies.get(0));
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequencies +
"] - " + ce.getMessage());
}
@@ -214,40 +218,16 @@ public class ChannelProcessingManager implements Listener
}
break;
case REQUEST_DISABLE:
- if(channel.isProcessing())
- {
- try
- {
- switch(channel.getChannelType())
- {
- case STANDARD:
- stopProcessing(channel, true);
- break;
- case TRAFFIC:
- //Don't remove traffic channel processing chains
- //until explicitly deleted, so that we can reuse them
- stopProcessing(channel, false);
- break;
- default:
- break;
- }
- }
- catch(ChannelException ce)
- {
- mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage());
- }
- }
- break;
case NOTIFICATION_DELETE:
if(channel.isProcessing())
{
try
{
- stopProcessing(channel, true);
+ stopProcessing(channel);
}
catch(ChannelException ce)
{
- mLog.error("Error stopping deleted channel [" + channel.getName() + "] - " + ce.getMessage());
+ mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage());
}
}
break;
@@ -263,7 +243,33 @@ public class ChannelProcessingManager implements Listener
*/
public void start(Channel channel) throws ChannelException
{
- startProcessing(new ChannelEvent(channel, ChannelEvent.Event.REQUEST_ENABLE));
+ startProcessing(new ChannelStartProcessingRequest(channel));
+ }
+
+ /**
+ * Request to start processing a channel received over the Guava event bus.
+ *
+ * Note: since this is received over the event bus, we handle any channel exceptions inside this method.
+ */
+ @Subscribe
+ public void startChannelRequest(ChannelStartProcessingRequest request)
+ {
+ if(!isProcessing(request.getChannel()))
+ {
+ try
+ {
+ startProcessing(request);
+ }
+ catch(ChannelException ce)
+ {
+ if(request.isPersistentAttempt())
+ {
+ ScheduledFuture> future = ThreadPool.SCHEDULED
+ .schedule(new DelayedChannelStartTask(request), 500, TimeUnit.MILLISECONDS);
+ mDelayedChannelStartTasks.add(future);
+ }
+ }
+ }
}
/**
@@ -272,24 +278,21 @@ public class ChannelProcessingManager implements Listener
*/
public void stop(Channel channel) throws ChannelException
{
- stopProcessing(channel, !channel.isTrafficChannel());
+ stopProcessing(channel);
}
/**
- * Starts a channel/processing chain
- *
- * @param event that requested the channel start
+ * Starts a channel processing
+ * @param request containing channel and other details
+ * @throws ChannelException if a source is not available for the channel
*/
- private void startProcessing(ChannelEvent event) throws ChannelException
+ private void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
{
- Channel channel = event.getChannel();
+ Channel channel = request.getChannel();
- ProcessingChain processingChain = mProcessingChains.get(channel);
-
- //If we're already processing, ignore the request
- if(processingChain != null && processingChain.isProcessing())
+ if(isProcessing(channel))
{
- throw new ChannelException("Channel is already playing");
+ return;
}
//Ensure that we can get a source before we construct a new processing chain
@@ -316,74 +319,90 @@ public class ChannelProcessingManager implements Listener
throw new ChannelException("No Tuner Available");
}
- if(processingChain == null)
+ ProcessingChain processingChain = new ProcessingChain(channel, mAliasModel);
+
+ //Certain decoders aggregate the decode events in the parent channel that also includes any events produced
+ //by the traffic channels. Establish listener registration depending on if this channel is a traffic channel
+ //and the request contains the parent event history, or if this is a parent channel and the request contains
+ //the traffic channel event history.
+ if(request.hasParentDecodeEventHistory())
{
- processingChain = new ProcessingChain(channel, mAliasModel);
- mChannelEventBroadcaster.addListener(processingChain);
-
- /* Register global listeners */
- for(Listener listener : mAudioSegmentListeners)
- {
- processingChain.addAudioSegmentListener(listener);
- }
-
- for(Listener listener : mDecodeEventListeners)
- {
- processingChain.addDecodeEventListener(listener);
- }
-
- //Add a listener to detect source error state that indicates the channel should be shutdown
- processingChain.addSourceEventListener(sourceEvent ->
- {
- if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE && sourceEvent.getSource() != null)
- {
- Channel toShutdown = null;
-
- for(Map.Entry entry: mProcessingChains.entrySet())
- {
- if(entry.getValue().hasSource(sourceEvent.getSource()))
- {
- toShutdown = entry.getKey();
- break;
- }
- }
-
- if(toShutdown != null)
- {
- mLog.warn("Channel source error detected - stopping channel [" + toShutdown.getName() + "]");
-
- try
- {
- stopProcessing(toShutdown, true);
- }
- catch(ChannelException ce)
- {
- mLog.error("Error stopping channel [" + channel.getName() + "] with source error - " +
- ce.getMessage());
- }
- }
- }
- });
-
- //Register this manager to receive channel events from traffic channel manager modules within
- //the processing chain
- processingChain.addChannelEventListener(this);
-
- //Register channel to receive frequency correction events to show in the spectral display (hack!)
- processingChain.addFrequencyChangeListener(channel);
-
- /* Processing Modules */
- List modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences);
- processingChain.addModules(modules);
-
- /* Setup message activity model with filtering */
- FilterSet messageFilter = DecoderFactory.getMessageFilters(modules);
- MessageActivityModel messageModel = new MessageActivityModel(messageFilter);
- processingChain.setMessageActivityModel(messageModel);
-
+ processingChain.getDecodeEventHistory().addListener(request.getParentDecodeEventHistory());
+ }
+ else if(request.hasChildDecodeEventHistory())
+ {
+ request.getChildDecodeEventHistory().addListener(processingChain.getDecodeEventHistory());
}
- /* Setup event logging */
+ //Register to receive event bus requests/notifications
+ processingChain.getEventBus().register(ChannelProcessingManager.this);
+
+ mChannelEventBroadcaster.addListener(processingChain);
+
+ /* Register global listeners */
+ for(Listener listener : mAudioSegmentListeners)
+ {
+ processingChain.addAudioSegmentListener(listener);
+ }
+
+ for(Listener listener : mDecodeEventListeners)
+ {
+ processingChain.addDecodeEventListener(listener);
+ }
+
+ //Add a listener to detect source error state that indicates the channel should be shutdown
+ processingChain.addSourceEventListener(sourceEvent ->
+ {
+ if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE && sourceEvent.getSource() != null)
+ {
+ Channel toShutdown = null;
+
+ for(Map.Entry entry: mProcessingChains.entrySet())
+ {
+ if(entry.getValue().hasSource(sourceEvent.getSource()))
+ {
+ toShutdown = entry.getKey();
+ break;
+ }
+ }
+
+ if(toShutdown != null)
+ {
+ mLog.warn("Channel source error detected - stopping channel [" + toShutdown.getName() + "]");
+
+ try
+ {
+ stopProcessing(toShutdown);
+ }
+ catch(ChannelException ce)
+ {
+ mLog.error("Error stopping channel [" + channel.getName() + "] with source error - " +
+ ce.getMessage());
+ }
+ }
+ }
+ });
+
+ //Register this manager to receive channel events from traffic channel manager modules within
+ //the processing chain
+ processingChain.addChannelEventListener(this);
+
+ //Register channel to receive frequency correction events to show in the spectral display (hack!)
+ processingChain.addFrequencyChangeListener(channel);
+
+ /* Processing Modules */
+ List modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences,
+ request.getTrafficChannelManager());
+ processingChain.addModules(modules);
+
+ //Post preload data from the request to the event bus. Modules that can handle preload data will annotate
+ //their processor method with @Subscribe to receive each specific preload data content class.
+ for(PreloadDataContent preloadDataContent: request.getPreloadDataContents())
+ {
+ processingChain.getEventBus().post(preloadDataContent);
+ }
+
+ //Setup event logging
List loggers = mEventLogManager.getLoggers(channel);
if(!loggers.isEmpty())
@@ -398,25 +417,22 @@ public class ChannelProcessingManager implements Listener
processingChain.setSource(source);
//Inject the channel identifier for traffic channels and preload user identifiers
- if(channel.isTrafficChannel() && event instanceof ChannelGrantEvent)
+ if(channel.isTrafficChannel())
{
- ChannelGrantEvent channelGrantEvent = (ChannelGrantEvent)event;
- IChannelDescriptor channelDescriptor = channelGrantEvent.getChannelDescriptor();
-
- IdentifierCollection identifierCollection = channelGrantEvent.getIdentifierCollection();
-
- if(channelDescriptor != null)
+ if(request.hasChannelDescriptor() && request.hasIdentifierCollection())
{
- for(int timeslot = 0; timeslot < channelDescriptor.getTimeslotCount(); timeslot++)
+ for(int timeslot = 0; timeslot < request.getChannelDescriptor().getTimeslotCount(); timeslot++)
{
DecoderLogicalChannelNameIdentifier identifier =
- DecoderLogicalChannelNameIdentifier.create(channelDescriptor.toString(), channelDescriptor.getProtocol());
+ DecoderLogicalChannelNameIdentifier.create(request.getChannelDescriptor().toString(),
+ request.getChannelDescriptor().getProtocol());
IdentifierUpdateNotification notification = new IdentifierUpdateNotification(identifier,
IdentifierUpdateNotification.Operation.ADD, timeslot);
processingChain.getChannelState().updateChannelStateIdentifiers(notification);
//Inject scramble parameters
- for(Identifier scrambleParameters: identifierCollection.getIdentifiers(Form.SCRAMBLE_PARAMETERS))
+ for(Identifier scrambleParameters: request.getIdentifierCollection()
+ .getIdentifiers(Form.SCRAMBLE_PARAMETERS))
{
//Broadcast scramble parameters to both timeslots
IdentifierUpdateNotification scrambleNotification = new IdentifierUpdateNotification(scrambleParameters,
@@ -426,13 +442,13 @@ public class ChannelProcessingManager implements Listener
}
}
- for(Identifier userIdentifier : identifierCollection.getIdentifiers(IdentifierClass.USER))
+ for(Identifier userIdentifier : request.getIdentifierCollection().getIdentifiers(IdentifierClass.USER))
{
- if(channelDescriptor.getTimeslotCount() > 1)
+ if(request.getChannelDescriptor().getTimeslotCount() > 1)
{
//Only broadcast an identifier update for the timeslot specified in the originating collection
IdentifierUpdateNotification notification = new IdentifierUpdateNotification(userIdentifier,
- IdentifierUpdateNotification.Operation.ADD, channelGrantEvent.getIdentifierCollection().getTimeslot());
+ IdentifierUpdateNotification.Operation.ADD, request.getIdentifierCollection().getTimeslot());
processingChain.getChannelState().updateChannelStateIdentifiers(notification);
}
else
@@ -446,13 +462,13 @@ public class ChannelProcessingManager implements Listener
}
+ mProcessingChains.put(channel, processingChain);
processingChain.start();
+
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(true));
- getChannelMetadataModel().add(processingChain.getChannelState().getChannelMetadata(), channel);
-
- mProcessingChains.put(channel, processingChain);
+ getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_START));
}
@@ -461,16 +477,15 @@ public class ChannelProcessingManager implements Listener
* Stops the channel/processing chain.
*
* @param channel to stop
- * @param remove set to true to remove the associated processing chain.
*/
- private void stopProcessing(Channel channel, boolean remove) throws ChannelException
+ private void stopProcessing(Channel channel) throws ChannelException
{
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(false));
if(mProcessingChains.containsKey(channel))
{
- ProcessingChain processingChain = mProcessingChains.get(channel);
+ ProcessingChain processingChain = mProcessingChains.remove(channel);
for(ChannelMetadata channelMetadata: processingChain.getChannelState().getChannelMetadata())
{
@@ -480,7 +495,6 @@ public class ChannelProcessingManager implements Listener
processingChain.stop();
processingChain.removeEventLoggingModules();
-
processingChain.removeRecordingModules();
//Deregister channel from receive frequency correction events to show in the spectral display (hack!)
@@ -488,13 +502,11 @@ public class ChannelProcessingManager implements Listener
channel.resetFrequencyCorrection();
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
+ mChannelEventBroadcaster.removeListener(processingChain);
- if(remove)
- {
- mChannelEventBroadcaster.removeListener(processingChain);
- mProcessingChains.remove(channel);
- processingChain.dispose();
- }
+ //Unregister for event bus requests and notifications
+ processingChain.getEventBus().unregister(ChannelProcessingManager.this);
+ processingChain.dispose();
}
else
{
@@ -507,13 +519,21 @@ public class ChannelProcessingManager implements Listener
*/
public void shutdown()
{
+ List> delayedTasks = new ArrayList<>(mDelayedChannelStartTasks);
+
+ for(ScheduledFuture> delayedTask: delayedTasks)
+ {
+ delayedTask.cancel(true);
+ mDelayedChannelStartTasks.remove(delayedTask);
+ }
+
List channelsToStop = new ArrayList<>(mProcessingChains.keySet());
for(Channel channel : channelsToStop)
{
try
{
- stopProcessing(channel, true);
+ stopProcessing(channel);
}
catch(ChannelException ce)
{
@@ -522,6 +542,41 @@ public class ChannelProcessingManager implements Listener
}
}
+ /**
+ * Process a request to convert a currently processing standard channel type to a traffic channel type.
+ * @param request from the currently processing channel's processing chain event bus
+ */
+ @Subscribe
+ public void convertToTrafficChannel(ChannelConversionRequest request)
+ {
+ //Update the channel to processing chain map.
+ ProcessingChain processingChain = mProcessingChains.remove(request.getCurrentChannel());
+
+ if(processingChain != null)
+ {
+ //Remove the traffic channel manager from this processing chain. Reuse or reinsertion of the traffic
+ //channel manager to another processing chain is handled separately.
+ processingChain.removeTrafficChannelManager();
+
+ //Update processing flag for each configuration.
+ Platform.runLater(() -> {
+ request.getCurrentChannel().setProcessing(false);
+ request.getTrafficChannel().setProcessing(true);
+ });
+
+ mProcessingChains.put(request.getTrafficChannel(), processingChain);
+ mChannelMetadataModel.updateChannelMetadataToChannelMap(processingChain.getChannelState().getChannelMetadata(),
+ request.getTrafficChannel());
+
+ //Post a change notification so that processing chain modules can reconfigure
+ processingChain.channelConfigurationChanged(new ChannelConfigurationChangeNotification(request.getTrafficChannel()));
+ }
+ else
+ {
+ mLog.warn("Request to convert to traffic channel ignored - no processing chain was found");
+ }
+ }
+
/**
* Adds a message listener that will be added to all channels to receive
* any messages.
@@ -571,4 +626,31 @@ public class ChannelProcessingManager implements Listener
{
mChannelEventBroadcaster.removeListener(listener);
}
+
+ /**
+ * Task to scheduling attempt to start a channel after previous attempts failed for lack of tuner channel
+ */
+ public class DelayedChannelStartTask implements Runnable
+ {
+ private ChannelStartProcessingRequest mRequest;
+
+ public DelayedChannelStartTask(ChannelStartProcessingRequest request)
+ {
+ mRequest = request;
+ }
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ mDelayedChannelStartTasks.remove(this);
+ startChannelRequest(mRequest);
+ }
+ catch(Throwable t)
+ {
+ mLog.error("Error executing persistent channel start task");
+ }
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java
new file mode 100644
index 00000000..eecd22d1
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/controller/channel/event/ChannelStartProcessingRequest.java
@@ -0,0 +1,228 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.controller.channel.event;
+
+import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.identifier.IdentifierCollection;
+import io.github.dsheirer.module.ModuleEventBusMessage;
+import io.github.dsheirer.module.decode.event.DecodeEventHistory;
+import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Request to start/enable processing for the specified channel
+ */
+public class ChannelStartProcessingRequest extends ModuleEventBusMessage
+{
+ private final Channel mChannel;
+ private final IChannelDescriptor mChannelDescriptor;
+ private final IdentifierCollection mIdentifierCollection;
+ private final TrafficChannelManager mTrafficChannelManager;
+ private final List> mPreloadDataContents = new ArrayList<>();
+ private DecodeEventHistory mParentDecodeEventHistory;
+ private DecodeEventHistory mChildDecodeEventHistory;
+ private boolean mPersistentAttempt;
+
+ /**
+ * Constructs an instance
+ * @param channel to start
+ * @param channelDescriptor that identifies which channel in a multi-channel config to start
+ * @param identifierCollection to use for the new channel
+ * @param trafficChannelManager (optional) to use for the new channel
+ */
+ public ChannelStartProcessingRequest(Channel channel, IChannelDescriptor channelDescriptor,
+ IdentifierCollection identifierCollection,
+ TrafficChannelManager trafficChannelManager)
+ {
+ mChannel = channel;
+ mChannelDescriptor = channelDescriptor;
+ mIdentifierCollection = identifierCollection;
+ mTrafficChannelManager = trafficChannelManager;
+ }
+
+ /**
+ * Constructs an instance
+ * @param channel to start
+ * @param channelDescriptor that identifies which channel in a multi-channel config to start
+ * @param identifierCollection to use for the new channel
+ */
+ public ChannelStartProcessingRequest(Channel channel, IChannelDescriptor channelDescriptor,
+ IdentifierCollection identifierCollection)
+ {
+ this(channel, channelDescriptor, identifierCollection, null);
+ }
+
+ /**
+ * Constructs an instance
+ * @param channel to start processing
+ * @param trafficChannelManager optional to use in the processing chain
+ */
+ public ChannelStartProcessingRequest(Channel channel, TrafficChannelManager trafficChannelManager)
+ {
+ this(channel, null, null, trafficChannelManager);
+ }
+
+ /**
+ * Constructs an instance
+ * @param channel to start processing
+ */
+ public ChannelStartProcessingRequest(Channel channel)
+ {
+ this(channel, null, null, null);
+ }
+
+ /**
+ * Sets a flag to persistently attempt to start this channel. When set to true, the channel processing manager
+ * will repeatedly try to (re)start this channel while the application continues to run.
+ * @param persistentAttempt if this start request should be continuously attempted until successful.
+ */
+ public void setPersistentAttempt(boolean persistentAttempt)
+ {
+ mPersistentAttempt = persistentAttempt;
+ }
+
+ /**
+ * Indicates if this request should be persistently attempted until the channel processing is started.
+ */
+ public boolean isPersistentAttempt()
+ {
+ return mPersistentAttempt;
+ }
+
+ /**
+ * Channel to start processing
+ * @return channel to start
+ */
+ public Channel getChannel()
+ {
+ return mChannel;
+ }
+
+ /**
+ * Optional traffic channel manager to (re)use.
+ * @return traffic channel manager or null
+ */
+ public TrafficChannelManager getTrafficChannelManager()
+ {
+ return mTrafficChannelManager;
+ }
+
+ /**
+ * Optional parent decode event history module to be added as a decode event listener
+ */
+ public DecodeEventHistory getParentDecodeEventHistory()
+ {
+ return mParentDecodeEventHistory;
+ }
+
+ /**
+ * Indicates if this request has a parent decode event history
+ */
+ public boolean hasParentDecodeEventHistory()
+ {
+ return mParentDecodeEventHistory != null;
+ }
+
+ /**
+ * Sets the parent decode event history
+ * @param parentHistory
+ */
+ public void setParentDecodeEventHistory(DecodeEventHistory parentHistory)
+ {
+ mParentDecodeEventHistory = parentHistory;
+ }
+
+ /**
+ * Optional child decode event history module
+ */
+ public DecodeEventHistory getChildDecodeEventHistory()
+ {
+ return mChildDecodeEventHistory;
+ }
+
+ /**
+ * Indicates if this request has a child decode event history
+ */
+ public boolean hasChildDecodeEventHistory()
+ {
+ return mChildDecodeEventHistory != null;
+ }
+
+ /**
+ * Sets the child decode event history
+ * @param childHistory
+ */
+ public void setChildDecodeEventHistory(DecodeEventHistory childHistory)
+ {
+ mChildDecodeEventHistory = childHistory;
+ }
+
+ /**
+ * Channel descriptor to use for the started channel
+ */
+ public IChannelDescriptor getChannelDescriptor()
+ {
+ return mChannelDescriptor;
+ }
+
+ /**
+ * Indicates if this request has a non-null channel descriptor
+ */
+ public boolean hasChannelDescriptor()
+ {
+ return mChannelDescriptor != null;
+ }
+
+ /**
+ * Identifier collection to use for preloading the channel with identifiers
+ */
+ public IdentifierCollection getIdentifierCollection()
+ {
+ return mIdentifierCollection;
+ }
+
+ /**
+ * Indicates if this request has a non-null identifier collection
+ */
+ public boolean hasIdentifierCollection()
+ {
+ return mIdentifierCollection != null;
+ }
+
+ /**
+ * Adds data content to be preloaded onto the event bus at startup.
+ * @param content to broadcast.
+ */
+ public void addPreloadDataContent(PreloadDataContent> content)
+ {
+ mPreloadDataContents.add(content);
+ }
+
+ /**
+ * Data content that should be broadcast on the processing chain event bus at startup.
+ */
+ public List> getPreloadDataContents()
+ {
+ return mPreloadDataContents;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/controller/channel/event/PreloadDataContent.java b/src/main/java/io/github/dsheirer/controller/channel/event/PreloadDataContent.java
new file mode 100644
index 00000000..e9d635a0
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/controller/channel/event/PreloadDataContent.java
@@ -0,0 +1,58 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.controller.channel.event;
+
+import io.github.dsheirer.module.ModuleEventBusMessage;
+
+/**
+ * Data that should be preloaded into a processing chain by broadcasting the data onto the processing chain so that
+ * any module can subscribe to it.
+ * @param data generic that should be loaded into modules in the processing chain.
+ */
+public abstract class PreloadDataContent extends ModuleEventBusMessage
+{
+ private final T mData;
+
+ /**
+ * Constructs an instance
+ * @param data to preload
+ */
+ public PreloadDataContent(T data)
+ {
+ mData = data;
+ }
+
+ /**
+ * Data to preload
+ * @return
+ */
+ public T getData()
+ {
+ return mData;
+ }
+
+ /**
+ * Indicates if the data payload is non-null
+ */
+ public boolean hasData()
+ {
+ return mData != null;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
index a3897c40..235f8581 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
@@ -599,9 +599,9 @@ public class FilterFactory
* @param alpha - roll-off factor.
* @return - filter coefficients
*/
- public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha)
+ public static float[] getRootRaisedCosine(double samplesPerSymbol, int symbolCount, float alpha)
{
- int taps = samplesPerSymbol * symbolCount;
+ int taps = (int)(samplesPerSymbol * symbolCount);
float scale = 0;
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
index eaaf131b..6af2eea3 100644
--- a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
+++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
@@ -22,8 +22,8 @@
package io.github.dsheirer.dsp.filter.design;
+import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
-import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
@@ -83,41 +83,52 @@ public class FilterViewer extends Application
// .passBandRipple(0.01)
// .build();
- FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
- .sampleRate(50000.0)
- .passBandCutoff(6500)
- .passBandAmplitude(1.0)
- .passBandRipple(0.005)
- .stopBandAmplitude(0.0)
- .stopBandStart(7200)
- .stopBandRipple(0.01)
- .build();
+// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
+// .sampleRate(50000.0)
+// .passBandCutoff(6500)
+// .passBandAmplitude(1.0)
+// .passBandRipple(0.005)
+// .stopBandAmplitude(0.0)
+// .stopBandStart(7200)
+// .stopBandRipple(0.01)
+// .build();
float[] taps = null;
-// try
-// {
-// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19);
-// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
-// }
-// catch(FilterDesignException fde)
-// {
-// mLog.error("Error");
-// }
+ boolean rrc = false;
- try
+ if(rrc)
{
- RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
+ double sampleRate = 50000.0;
+ int symbolRate = 4800;
+ double samplesPerSymbol = sampleRate / symbolRate / 2;
+ float alpha = 0.25f;
+ int symbolCount = 20;
- if(designer.isValid())
+ taps = FilterFactory.getRootRaisedCosine(samplesPerSymbol, symbolCount, alpha);
+ }
+ else
+ {
+ FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
+ .sampleRate(50000)
+ .passBandCutoff(4500)
+ .passBandAmplitude(1.0)
+ .passBandRipple(0.1)
+ .stopBandAmplitude(0.0)
+ .stopBandStart(7000)
+ .stopBandRipple(0.01)
+ .build();
+
+ try
{
- taps = designer.getImpulseResponse();
+ taps = FilterFactory.getTaps(specification);//
+ }
+ catch(Exception fde) //FilterDesignException
+ {
+ mLog.error("Couldn't design low pass baseband filter for sample rate");
}
}
- catch(FilterDesignException fde)
- {
- mLog.error("Filter design error", fde);
- }
+
if(taps == null)
{
diff --git a/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewerLauncher.java b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewerLauncher.java
new file mode 100644
index 00000000..36bb653c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewerLauncher.java
@@ -0,0 +1,35 @@
+package io.github.dsheirer.dsp.filter.design;
+
+import javafx.application.Platform;
+import javafx.embed.swing.JFXPanel;
+import javafx.stage.Stage;
+
+public class FilterViewerLauncher
+{
+ private JFXPanel mJFXPanel;
+
+ public FilterViewerLauncher()
+ {
+ mJFXPanel = new JFXPanel();
+
+ Platform.runLater(() ->
+ {
+ FilterViewer filterViewer = new FilterViewer();
+ Stage stage = new Stage();
+
+ try
+ {
+ filterViewer.start(stage);
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ }
+ });
+ }
+
+ public static void main(String[] args)
+ {
+ FilterViewerLauncher filterViewerLauncher = new FilterViewerLauncher();
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java b/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java
index 9a1ef20a..8c13b300 100644
--- a/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java
+++ b/src/main/java/io/github/dsheirer/dsp/symbol/Dibit.java
@@ -24,22 +24,32 @@ package io.github.dsheirer.dsp.symbol;
public enum Dibit
{
- D01_PLUS_3(false, true, 1, 4),
- D00_PLUS_1(false, false, 0, 0),
- D10_MINUS_1(true, false, 2, 8),
- D11_MINUS_3(true, true, 3, 12);
+ D01_PLUS_3(false, true, 1, 4, 1),
+ D00_PLUS_1(false, false, 0, 0, 0),
+ D10_MINUS_1(true, false, 2, 8, 2),
+ D11_MINUS_3(true, true, 3, 12, 3);
private boolean mBit1;
private boolean mBit2;
private int mLowValue;
private int mHighValue;
+ private int mValue;
- private Dibit(boolean bit1, boolean bit2, int lowValue, int highValue)
+ private Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value)
{
mBit1 = bit1;
mBit2 = bit2;
mLowValue = lowValue;
mHighValue = highValue;
+ mValue = value;
+ }
+
+ /**
+ * Binary value of the symbol
+ */
+ public int getValue()
+ {
+ return mValue;
}
/**
diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayBuffer.java b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayBuffer.java
new file mode 100644
index 00000000..25131e03
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayBuffer.java
@@ -0,0 +1,260 @@
+/*
+ *
+ * * ******************************************************************************
+ * * 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
+ * * *****************************************************************************
+ *
+ *
+ */
+
+package io.github.dsheirer.dsp.symbol;
+
+import io.github.dsheirer.bits.BitSetFullException;
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import org.apache.commons.lang3.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Circular buffer for storing and accessing dibits.
+ */
+public class DibitDelayBuffer
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DibitDelayBuffer.class);
+
+ protected Dibit[] mBuffer;
+ protected int mPointer;
+
+ /**
+ * Constructs a dibit delay buffer of the specified length
+ */
+ public DibitDelayBuffer(int length)
+ {
+ Validate.isTrue(length > 0, "Dibit buffer length must be greater than 0");
+ mBuffer = new Dibit[length];
+ reset();
+ }
+
+ /**
+ * Resets the buffer and preloads the buffer with all zeros
+ */
+ public void reset()
+ {
+ for(int x = 0; x < mBuffer.length; x++)
+ {
+ mBuffer[x] = Dibit.D00_PLUS_1;
+ }
+ }
+
+ /**
+ * Length of this circular buffer
+ */
+ public int length()
+ {
+ return mBuffer.length;
+ }
+
+ /**
+ * Returns the dibit at the specified index
+ * @param index to get
+ * @return dibit at the specified index
+ */
+ public Dibit get(int index)
+ {
+ if(index < 0 || index >= mBuffer.length)
+ {
+ throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]");
+ }
+
+ return mBuffer[index];
+ }
+
+ /**
+ * Sets the dibit at the specified index
+ * @param index of the dibit
+ * @param dibit to load at the specified index
+ */
+ public void set(int index, Dibit dibit)
+ {
+ if(index < 0 || index >= mBuffer.length)
+ {
+ throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]");
+ }
+
+ mBuffer[index] = dibit;
+ }
+
+ /**
+ * Returns an ordered view of the internal circular buffer contents.
+ */
+ public Dibit[] getBuffer()
+ {
+ Dibit[] transferBuffer = new Dibit[mBuffer.length];
+
+ int transferBufferPointer = 0;
+ int bufferPointer = mPointer;
+
+ while(transferBufferPointer < transferBuffer.length)
+ {
+ transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++];
+
+ if(bufferPointer >= mBuffer.length)
+ {
+ bufferPointer = 0;
+ }
+ }
+
+ return transferBuffer;
+ }
+
+ /**
+ * Retrieves a view of the buffer from the starting index to the specified length. Note: if the requested
+ * length exceeds the length of this buffer, the contents will be unwrapped from the circular buffer and repeated.
+ * @param start of the view where 0 is the first element.
+ * @param length of the dibit view
+ * @return an array of dibits
+ */
+ public Dibit[] getBuffer(int start, int length)
+ {
+ Dibit[] transferBuffer = new Dibit[length];
+
+ int transferBufferPointer = 0;
+ int bufferPointer = (mPointer + start) % mBuffer.length;
+
+ while(transferBufferPointer < transferBuffer.length)
+ {
+ transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++];
+
+ if(bufferPointer >= mBuffer.length)
+ {
+ bufferPointer = 0;
+ }
+ }
+
+ return transferBuffer;
+ }
+
+ /**
+ * Extracts a corrected binary message from the dibit buffer
+ *
+ * @param start dibit index where 0 is the oldest dibit and the newest dibit is buffer length - 1
+ * @param dibitLength number of dibits to include in the message, making the message length = dibitLength * 2
+ * @return corrected binary message
+ */
+ public CorrectedBinaryMessage getMessage(int start, int dibitLength)
+ {
+ CorrectedBinaryMessage message = new CorrectedBinaryMessage(dibitLength * 2);
+
+ int dibitCount = 0;
+ int bufferPointer = (mPointer + start) % mBuffer.length;
+
+ try
+ {
+ while(dibitCount < dibitLength)
+ {
+ Dibit dibit = mBuffer[bufferPointer++];
+ message.add(dibit.getBit1());
+ message.add(dibit.getBit2());
+ dibitCount++;
+
+ if(bufferPointer >= mBuffer.length)
+ {
+ bufferPointer = 0;
+ }
+ }
+ }
+ catch(BitSetFullException e)
+ {
+ //This shouldn't happen
+ mLog.error("Bit set full ??", e);
+ }
+
+ return message;
+ }
+
+ /**
+ * Utility method to view the buffer contents with the dibits decoded as an array of zeros and ones.
+ */
+ public int[] getBufferAsArray()
+ {
+ Dibit[] dibits = getBuffer();
+
+ int[] bits = new int[dibits.length * 2];
+
+ for(int x = 0; x < dibits.length; x++)
+ {
+ if(dibits[x].getBit1())
+ {
+ bits[x * 2] = 1;
+ }
+ if(dibits[x].getBit2())
+ {
+ bits[x * 2 + 1] = 1;
+ }
+ }
+
+ return bits;
+ }
+
+ /**
+ * Places the dibit into the internal circular buffer, overwriting the oldest dibit.
+ */
+ public void put(Dibit dibit)
+ {
+ mBuffer[mPointer++] = dibit;
+
+ if(mPointer >= mBuffer.length)
+ {
+ mPointer = 0;
+ }
+ }
+
+ /**
+ * Places the dibit into the internal circular buffer, overwriting and returning the
+ * oldest dibit.
+ */
+ public Dibit getAndPut(Dibit dibit)
+ {
+ Dibit toReturn = mBuffer[mPointer];
+ put(dibit);
+ return toReturn;
+ }
+
+ /**
+ * Logs the contents of the buffer
+ */
+ public void log()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ int counter = 0;
+ int pointer = mPointer;
+
+ while(counter < mBuffer.length)
+ {
+ sb.append(mBuffer[pointer].getBit1() ? "1" : "0");
+ sb.append(mBuffer[pointer++].getBit2() ? "1" : "0");
+
+ if(pointer >= mBuffer.length)
+ {
+ pointer = 0;
+ }
+
+ counter++;
+ }
+ mLog.debug("BUFFER: " + sb + " Length:" + mBuffer.length);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/dsp/symbol/QPSKCarrierLock.java b/src/main/java/io/github/dsheirer/dsp/symbol/QPSKCarrierLock.java
new file mode 100644
index 00000000..1da8c892
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/dsp/symbol/QPSKCarrierLock.java
@@ -0,0 +1,120 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.dsp.symbol;
+
+/**
+ * QPSK carrier lock elements that indicate if a PLL is locked normally, or if it has locked at +/- 90 degrees,
+ * or inverted 180 degrees. Abnormal PLL lock is normally detected by rotated sync patterns.
+ *
+ * Each enumeration entry provides a correct(dibit) method that supports correcting dibits that were captured with a
+ * misaligned PLL lock to what they should be if the PLL were locked correctly. This allows detected (yet misaligned)
+ * messages to become usable through dibit correction even though the PLL was misaligned.
+ */
+public enum QPSKCarrierLock
+{
+ /**
+ * Normal PLL lock indication
+ */
+ NORMAL
+ {
+ @Override
+ public Dibit correct(Dibit dibit)
+ {
+ //No correction needed
+ return dibit;
+ }
+ },
+
+ /**
+ * PLL lock at plus 90 degrees to the carrier, requiring a PLL adjustment of -90 degrees or -PI/2.
+ */
+ PLUS_90
+ {
+ @Override
+ public Dibit correct(Dibit dibit)
+ {
+ switch(dibit)
+ {
+ case D00_PLUS_1:
+ return Dibit.D10_MINUS_1;
+ case D01_PLUS_3:
+ return Dibit.D00_PLUS_1;
+ case D10_MINUS_1:
+ return Dibit.D11_MINUS_3;
+ case D11_MINUS_3:
+ default:
+ return Dibit.D01_PLUS_3;
+ }
+ }
+ },
+
+ /**
+ * PLL lock at minus 90 degrees to the carrier, requiring a PLL adjustment of +90 degrees, or +PI/2.
+ */
+ MINUS_90
+ {
+ @Override
+ public Dibit correct(Dibit dibit)
+ {
+ switch(dibit)
+ {
+ case D00_PLUS_1:
+ return Dibit.D01_PLUS_3;
+ case D01_PLUS_3:
+ return Dibit.D11_MINUS_3;
+ case D10_MINUS_1:
+ return Dibit.D00_PLUS_1;
+ case D11_MINUS_3:
+ default:
+ return Dibit.D10_MINUS_1;
+ }
+ }
+ },
+
+ /**
+ * PLL lock at 180 degrees or inverted to the carrier, requiring a PLL adjustment of 180 degrees or PI.
+ */
+ INVERTED
+ {
+ @Override
+ public Dibit correct(Dibit dibit)
+ {
+ switch(dibit)
+ {
+ case D00_PLUS_1:
+ return Dibit.D11_MINUS_3;
+ case D01_PLUS_3:
+ return Dibit.D10_MINUS_1;
+ case D10_MINUS_1:
+ return Dibit.D01_PLUS_3;
+ case D11_MINUS_3:
+ default:
+ return Dibit.D00_PLUS_1;
+ }
+ }
+ };
+
+ /**
+ * Method for each entry to provide dibit correction solutions
+ * @param dibit that was generated by a misaligned QPSK Phase locked loop
+ * @return dibit that is corrected to the dibit for a correctly aligned QPSK Phase Locked Loop
+ */
+ public abstract Dibit correct(Dibit dibit);
+}
diff --git a/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java b/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java
index e6eb9e5a..aac7e24a 100644
--- a/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java
+++ b/src/main/java/io/github/dsheirer/edac/BCH_63_16_11.java
@@ -1,106 +1,106 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.BinaryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/*******************************************************************************
- * SDR Trunk
- * Copyright (C) 2014 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
- ******************************************************************************/
-
-public class BCH_63_16_11 extends BerlekempMassey_63
+public class BCH_63_16_11 extends ReedSolomon_63_P25
{
- private final static Logger mLog = LoggerFactory.getLogger( BCH_63_16_11.class );
+ private final static Logger mLog = LoggerFactory.getLogger(BCH_63_16_11.class);
- /**
- * BCH( 63,16,11) decoder
- */
- public BCH_63_16_11()
+ /**
+ * BCH( 63,16,11) decoder
+ */
+ public BCH_63_16_11()
{
- /* TT = 11 = maximum correctable bit errors */
- super( 11 );
+ super(63, 41);
}
-
- /**
- * Performs error detection and correction on the first 63 bits of the
- * message argument. If the message is correctable, only the first 16 bits
- * (information bits) are corrected.
- *
- * @return - true = success, false = failure
- */
- public BinaryMessage correctNID( BinaryMessage message )
- {
- CRC status = CRC.PASSED;
-
- int[] original = message.toReverseIntegerArray( 0, 62 );
- int[] corrected = new int[ 63 ];
- boolean irrecoverableErrors = decode( original, corrected );
+ /**
+ * Performs error detection and correction on the first 63 bits of the
+ * message argument. If the message is correctable, only the first 16 bits
+ * (information bits) are corrected.
+ *
+ * @return - true = success, false = failure
+ */
+ public BinaryMessage correctNID(BinaryMessage message)
+ {
+ CRC status = CRC.PASSED;
- if( irrecoverableErrors )
- {
- message.setCRC( CRC.FAILED_CRC );
-
- return message;
- }
- else
- {
- for( int x = 0; x < 16; x++ )
- {
- int index = 63 - x - 1;
-
- if( corrected[ index ] != original[ index ] )
- {
- status = CRC.CORRECTED;
-
- if( corrected[ index ] == 1 )
- {
- message.set( x );
- }
- else
- {
- message.clear( x );
- }
- }
- }
- }
-
- message.setCRC( status );
-
- return message;
- }
-
- public static void main( String[] args )
- {
- String orig = "0010011000000011010010100000000110000111110011101010001010110000";
- String error = "0001010100000011010010100000000110000111100011001010001010110000";
+ int[] original = message.toReverseIntegerArray(0, 62);
+ int[] corrected = new int[63];
- BinaryMessage errorMessage = BinaryMessage.load( error );
+ boolean irrecoverableErrors = decode(original, corrected);
- BCH_63_16_11 bch = new BCH_63_16_11();
+ if(irrecoverableErrors)
+ {
+ message.setCRC(CRC.FAILED_CRC);
- mLog.debug( "ORIG:" + orig );
- mLog.debug( " ERR:" + errorMessage.toString() );
- mLog.debug( " " );
+ return message;
+ }
+ else
+ {
+ for(int x = 0; x < 16; x++)
+ {
+ int index = 63 - x - 1;
- errorMessage = bch.correctNID( errorMessage );
-
- mLog.debug( " CRC: " + errorMessage.getCRC().name() );
- mLog.debug( "CORR:" + errorMessage.toString() );
- mLog.debug( "ORIG:" + orig );
- }
+ if(corrected[index] != original[index])
+ {
+ status = CRC.CORRECTED;
+
+ if(corrected[index] == 1)
+ {
+ message.set(x);
+ }
+ else
+ {
+ message.clear(x);
+ }
+ }
+ }
+ }
+
+ message.setCRC(status);
+
+ return message;
+ }
+
+ public static void main(String[] args)
+ {
+ String orig = "0010011000000011010010100000000110000111110011101010001010110000";
+ String error = "0001010100000011010010100000000110000111100011001010001010110000";
+
+ BinaryMessage errorMessage = BinaryMessage.load(error);
+
+ BCH_63_16_11 bch = new BCH_63_16_11();
+
+ mLog.debug("ORIG:" + orig);
+ mLog.debug(" ERR:" + errorMessage.toString());
+ mLog.debug(" ");
+
+ errorMessage = bch.correctNID(errorMessage);
+
+ mLog.debug(" CRC: " + errorMessage.getCRC().name());
+ mLog.debug("CORR:" + errorMessage.toString());
+ mLog.debug("ORIG:" + orig);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/edac/BPTC_17_12_3.java b/src/main/java/io/github/dsheirer/edac/BPTC_17_12_3.java
new file mode 100644
index 00000000..c405a3c6
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/BPTC_17_12_3.java
@@ -0,0 +1,62 @@
+package io.github.dsheirer.edac;
+
+import io.github.dsheirer.bits.BinaryMessage;
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+
+/*
+ Adopted from https://github.com/boatbod/op25/blob/e5e8fe457240fb358a8163bcf6fe7dda5e4d8392/op25/gr-op25_repeater/lib/hamming.cc
+ */
+public class BPTC_17_12_3 {
+
+ public static boolean decode17123(BinaryMessage d, int offset)
+ {
+ if(d==null) {
+ return false;
+ }
+
+ // Calculate the checksum this column should have
+ boolean c0 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(6 + offset) ^ d.get(7 + offset) ^ d.get(9 + offset);
+ boolean c1 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(7 + offset) ^ d.get(8 + offset) ^ d.get(10 + offset);
+ boolean c2 = d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(8 + offset) ^ d.get(9 + offset) ^ d.get(11 + offset);
+ boolean c3 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(7 + offset) ^ d.get(10 + offset);
+ boolean c4 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(5 + offset) ^ d.get(6 + offset) ^ d.get(8 + offset) ^ d.get(11 + offset);
+
+ // Compare these with the actual bits
+ byte n = 0x00;
+ n |= (c0 != d.get(12 + offset)) ? 0x01 : 0x00;
+ n |= (c1 != d.get(13 + offset)) ? 0x02 : 0x00;
+ n |= (c2 != d.get(14 + offset)) ? 0x04 : 0x00;
+ n |= (c3 != d.get(15 + offset)) ? 0x08 : 0x00;
+ n |= (c4 != d.get(16 + offset)) ? 0x10 : 0x00;
+
+ switch (n) {
+ // Parity bit errors
+ case 0x01: d.set(12 + offset, !d.get(12 + offset)); return true;
+ case 0x02: d.set(13 + offset, !d.get(13 + offset)); return true;
+ case 0x04: d.set(14 + offset, !d.get(14 + offset)); return true;
+ case 0x08: d.set(15 + offset, !d.get(15 + offset)); return true;
+ case 0x10: d.set(16 + offset, !d.get(16 + offset)); return true;
+
+ // Data bit errors
+ case 0x1B: d.set(0 + offset, !d.get(0 + offset)); return true;
+ case 0x1F: d.set(1 + offset, !d.get(1 + offset)); return true;
+ case 0x17: d.set(2 + offset, !d.get(2 + offset)); return true;
+ case 0x07: d.set(3 + offset, !d.get(3 + offset)); return true;
+ case 0x0E: d.set(4 + offset, !d.get(4 + offset)); return true;
+ case 0x1C: d.set(5 + offset, !d.get(5 + offset)); return true;
+ case 0x11: d.set(6 + offset, !d.get(6 + offset)); return true;
+ case 0x0B: d.set(7 + offset, !d.get(7 + offset)); return true;
+ case 0x16: d.set(8 + offset, !d.get(8 + offset)); return true;
+ case 0x05: d.set(9 + offset, !d.get(9 + offset)); return true;
+ case 0x0A: d.set(10 + offset,!d.get(10 + offset)); return true;
+ case 0x14: d.set(11 + offset,!d.get(11 + offset)); return true;
+
+ // No bit errors
+ case 0x00: return true;
+
+ // Unrecoverable errors
+ default: return false;
+ }
+ }
+
+}
diff --git a/src/main/java/io/github/dsheirer/edac/BPTC_196_96.java b/src/main/java/io/github/dsheirer/edac/BPTC_196_96.java
new file mode 100644
index 00000000..cd01f677
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/BPTC_196_96.java
@@ -0,0 +1,284 @@
+package io.github.dsheirer.edac;
+
+import io.github.dsheirer.bits.BinaryMessage;
+import io.github.dsheirer.bits.BitSetFullException;
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Block Product Turbo Code (BPTC) 196/96 Support
+ */
+public class BPTC_196_96
+{
+ public static final int BPTC_LENGTH = 196;
+ public static final int EXTRACTED_LENGTH = 96;
+ public static final int MAX_ORIGINAL_INDEX = 136;
+ public static final int COLUMN_COUNT = 15;
+ public static final int MESSAGE_COLUMN_COUNT = 12; //Should be 11, but adjusted for the first pad bit
+ public static final int CHECKSUM_COLUMN_COUNT = 4;
+ public static final int MESSAGE_START_INDEX = 4;
+
+ /**
+ * DMR De-interleaving indices
+ */
+ public static int[] BPTC_DEINTERLEAVE = new int[]{0, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 16, 1, 182,
+ 167, 152, 137, 122, 107, 92, 77, 62, 47, 32, 17, 2, 183, 168, 153, 138, 123, 108, 93, 78, 63, 48, 33, 18, 3,
+ 184, 169, 154, 139, 124, 109, 94, 79, 64, 49, 34, 19, 4, 185, 170, 155, 140, 125, 110, 95, 80, 65, 50, 35, 20,
+ 5, 186, 171, 156, 141, 126, 111, 96, 81, 66, 51, 36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82, 67, 52, 37,
+ 22, 7, 188, 173, 158, 143, 128, 113, 98, 83, 68, 53, 38, 23, 8, 189, 174, 159, 144, 129, 114, 99, 84, 69, 54,
+ 39, 24, 9, 190, 175, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 191, 176, 161, 146, 131, 116, 101, 86, 71,
+ 56, 41, 26, 11, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 193, 178, 163, 148, 133, 118, 103,
+ 88, 73, 58, 43, 28, 13, 194, 179, 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 195, 180, 165, 150, 135, 120,
+ 105, 90, 75, 60, 45, 30, 15};
+
+ /**
+ * DMR Column Indices
+ */
+ public static int[][] COLUMN_INDEXES = new int[][]{
+ {1, 16, 31, 46, 61, 76, 91, 106, 121, 136, 151, 166, 181},
+ {2, 17, 32, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182},
+ {3, 18, 33, 48, 63, 78, 93, 108, 123, 138, 153, 168, 183},
+ {4, 19, 34, 49, 64, 79, 94, 109, 124, 139, 154, 169, 184},
+ {5, 20, 35, 50, 65, 80, 95, 110, 125, 140, 155, 170, 185},
+ {6, 21, 36, 51, 66, 81, 96, 111, 126, 141, 156, 171, 186},
+ {7, 22, 37, 52, 67, 82, 97, 112, 127, 142, 157, 172, 187},
+ {8, 23, 38, 53, 68, 83, 98, 113, 128, 143, 158, 173, 188},
+ {9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 159, 174, 189},
+ {10, 25, 40, 55, 70, 85, 100, 115, 130, 145, 160, 175, 190},
+ {11, 26, 41, 56, 71, 86, 101, 116, 131, 146, 161, 176, 191},
+ {12, 27, 42, 57, 72, 87, 102, 117, 132, 147, 162, 177, 192},
+ {13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193},
+ {14, 29, 44, 59, 74, 89, 104, 119, 134, 149, 164, 179, 194},
+ {15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195}};
+
+ /**
+ * Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
+ * protected message
+ * @param original uncorrected BPTC(196,96) message
+ * @return error corrected and extracted 96-bit message
+ */
+ public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original)
+ {
+ //De-Interleave
+ CorrectedBinaryMessage message = deinterleave(original);
+
+ //Perform error detection and correction
+ correct(message);
+
+ //Extract the 96-bit message from the corrected 196 bits
+ CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH);
+
+ //Transfer the corrected bit count
+ extracted.setCorrectedBitCount(message.getCorrectedBitCount());
+
+ int index = MESSAGE_START_INDEX;
+
+ while(index < MAX_ORIGINAL_INDEX)
+ {
+ if((index % COLUMN_COUNT) < MESSAGE_COLUMN_COUNT)
+ {
+ try
+ {
+ extracted.add(message.get(index));
+ }
+ catch(BitSetFullException bsfe)
+ {
+ //Should never get to here
+ bsfe.printStackTrace();
+ return extracted;
+ }
+
+ index++;
+ }
+ else
+ {
+ index += CHECKSUM_COLUMN_COUNT;
+ }
+ }
+
+ return extracted;
+ }
+
+ /**
+ * De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message
+ * @param message that contains interleaved (transmitted) message
+ * @return de-interleaved message
+ */
+ private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage message)
+ {
+ CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(BPTC_LENGTH);
+ for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++)
+ {
+ deinterleaved.set(x, message.get(BPTC_DEINTERLEAVE[x]));
+ }
+
+ return deinterleaved;
+ }
+
+ /**
+ * Identifies the error indices from the row level Hamming(15,11,3) protected words
+ * @param message containing protected words in rows
+ * @return list of zero or more error indices
+ */
+ private static List getRowErrors(CorrectedBinaryMessage message)
+ {
+ List rowErrors = new ArrayList<>();
+
+ int offset = 0;
+ int index = -1;
+
+ for(int row = 0; row < 13; row++)
+ {
+ //The first bit of the message is a pad/reserved bit and is ignored.
+ offset = (row * 15) + 1;
+ index = Hamming15.getErrorIndex(message, offset);
+
+ if(index >= 0)
+ {
+ rowErrors.add(index);
+ }
+ }
+
+ //Note: row errors should already be in order and don't need sorted
+ return rowErrors;
+ }
+
+ /**
+ * Identifies the error indices from the column level Hamming(13,9,3) protected words
+ * @param message containing protected words in columns
+ * @return list of zero or more error indices
+ */
+ private static List getColumnErrors(CorrectedBinaryMessage message)
+ {
+ List columnErrors = new ArrayList<>();
+
+ int index = -1;
+
+ for(int column = 0; column < 15; column++)
+ {
+ index = Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
+
+ if(index >= 0)
+ {
+ columnErrors.add(index);
+ }
+ }
+
+ Collections.sort(columnErrors);
+ return columnErrors;
+ }
+
+ /**
+ * Performs error detection and correction on the DMR BPTC(196,96) protected message
+ *
+ * @param message to process and extract
+ */
+ private static void correct(CorrectedBinaryMessage message)
+ {
+ List rowErrors = getRowErrors(message);
+
+ if(!rowErrors.isEmpty())
+ {
+ List columnErrors = getColumnErrors(message);
+
+ if(matches(rowErrors, columnErrors))
+ {
+ for(Integer index: rowErrors)
+ {
+ message.flip(index);
+ }
+
+ message.setCorrectedBitCount(rowErrors.size());
+ }
+ else
+ {
+ //Step 1: correct row errors and recalculate column errors (should be zero)
+ for(Integer index: rowErrors)
+ {
+ message.flip(index);
+ }
+
+ List columnErrorsPart2 = getColumnErrors(message);
+
+ if(columnErrorsPart2.isEmpty())
+ {
+ //Success
+ message.setCorrectedBitCount(rowErrors.size());
+ }
+ else
+ {
+ //Reverse the row error correction
+ for(Integer index: rowErrors)
+ {
+ message.flip(index);
+ }
+
+ //Step 2: correct column errors and recalculate row errors (should be zero)
+ for(Integer index: columnErrors)
+ {
+ message.flip(index);
+ }
+
+ List rowErrorsPart2 = getRowErrors(message);
+
+ if(rowErrorsPart2.isEmpty())
+ {
+ //Success
+ message.setCorrectedBitCount(columnErrors.size());
+ }
+ else
+ {
+ //Reverse the column error correction
+ for(Integer index: columnErrors)
+ {
+ message.flip(index);
+ }
+
+ //Set the error count greater than the maximum correctable column errors (16)
+ message.setCorrectedBitCount(16);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Indicates if the errors detected by the row protected words match the errors detected by the column protected words
+ * @param rowErrors bit positions
+ * @param columnErrors bit positions
+ * @return true if they have the same quantity of bit position errors and the bit positions match
+ */
+ private static boolean matches(List rowErrors, List columnErrors)
+ {
+ if(rowErrors.size() == columnErrors.size())
+ {
+ for(int x = 0; x < rowErrors.size(); x++)
+ {
+ if(!rowErrors.get(x).equals(columnErrors.get(0)))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ String raw = "0000000001100010100101100011100100110100110101000100000011101001110001001000100001001010010100001000011111011000000011110000111000111111100011000011100001100000000101001010000100010111010100111001";
+ System.out.println("RAW:" + raw);
+
+ CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.load(raw));
+ System.out.println("LOD:" + message.toString());
+ CorrectedBinaryMessage extracted = extract(message);
+ System.out.println("EXT:" + extracted.toString());
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java b/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java
new file mode 100644
index 00000000..1865f403
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/BerlekempMassey.java
@@ -0,0 +1,559 @@
+/*
+ *
+ * * ******************************************************************************
+ * * 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
+ * * *****************************************************************************
+ *
+ *
+ */
+
+package io.github.dsheirer.edac;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.commons.math3.util.FastMath;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Berlekemp Massey decoder for primitive RS/BCH block codes
+ *
+ * Original C implementation by Simon Rockliff, 26 June 1991
+ */
+public class BerlekempMassey
+{
+ public final static Logger mLog = LoggerFactory.getLogger(BerlekempMassey.class);
+
+ /* Golay field size GF( 2 ** MM ) */
+ private int MM;
+
+ /* Codeword Length: NN = 2 ** MM - 1 */
+ private int NN;
+ private int MAX_NN;
+
+ /* Hamming distance between codewords: NN - KK + 1 = 2 * TT + 1 */
+ private int KK;
+
+ /* Maximum number of errors that can be corrected */
+ int TT;
+
+ int[] alpha_to;
+ int[] index_of;
+ int[] gg;
+
+ /**
+ * Constructs an instance
+ *
+ * @param galoisFieldSize as a power of 2, e.g. for a GF(2^6) the parameter would be 6
+ * @param maxCorrectableErrors is Hamming Distance / 2. e.g. RS(24,16,9) would be (24 - 16) / 2, or floor(9/2).
+ * @param generatorPolynomial for the Galois Field represented in binary as 0/1 integers where the array is in
+ * big-endian format. For example: polynomial a6 + a1 + 1 is 1000011, as big endian: 1100001, and in array
+ * form as {1,1,0,0,0,0,1}
+ */
+ public BerlekempMassey(int galoisFieldSize, int maxCorrectableErrors, int[] generatorPolynomial)
+ {
+ //TODO: fix this
+ MM = galoisFieldSize;
+ NN = (int)FastMath.pow(2, MM) - 1;
+ TT = maxCorrectableErrors;
+ KK = NN - 2 * TT;
+
+ }
+
+ public BerlekempMassey(int bitSize, int n, int k, int[] generatorPolynomial)
+ {
+ Validate.isTrue(bitSize == generatorPolynomial.length - 1,
+ "Generator polynomial length must be: bitSize + 1");
+
+ MM = bitSize;
+ NN = n;
+ KK = k;
+ TT = (int)Math.floor((NN - KK) / 2);
+
+ //Since this may be a shortened code, setup arrays to the max size of NN
+ int maxNN = (int)Math.pow(2, MM) - 1;
+
+ alpha_to = new int[maxNN + 1];
+ index_of = new int[maxNN + 1];
+
+ gg = new int[NN - KK + 1];
+
+ generate_gf(generatorPolynomial);
+
+ gen_poly();
+ }
+
+ /**
+ * Generates the Golay Field.
+ *
+ * Generates a GF( 2**mm ) from the irreducible polynomial
+ * p(X) in pp[0]..pp[mm]
+ *
+ * Lookup tables:
+ * index_of[] = polynomial form
+ * alpha_to[] = contains j=alpha**i;
+ *
+ * Polynomial form -> Index form index_of[j=alpha**i] = i
+ *
+ * alpha_to = 2 is the primitive element of GF( 2**mm )
+ *
+ * @param generator_polynomial
+ */
+ private void generate_gf(int[] generator_polynomial)
+ {
+ int i;
+ int mask = 1;
+
+ alpha_to[MM] = 0;
+
+ for(i = 0; i < MM; i++)
+ {
+ alpha_to[i] = mask;
+ index_of[alpha_to[i]] = i;
+
+ if(generator_polynomial[i] != 0)
+ {
+ alpha_to[MM] ^= mask;
+ }
+
+ mask <<= 1;
+ }
+
+ index_of[alpha_to[MM]] = MM;
+
+ mask >>= 1;
+
+ for(i = MM + 1; i < NN; i++)
+ {
+ if(alpha_to[i - 1] >= mask)
+ {
+ alpha_to[i] = alpha_to[MM] ^ ((alpha_to[i - 1] ^ mask) << 1);
+ }
+ else
+ {
+ alpha_to[i] = alpha_to[i - 1] << 1;
+ }
+
+ index_of[alpha_to[i]] = i;
+ }
+
+ index_of[0] = -1;
+ }
+
+ /**
+ * Generates the polynomial for a TT-error correction code.
+ *
+ * Length NN = ( 2 ** MM -1 ) Reed Solomon code from the product of
+ * (X+alpha**i), i=1..2*tt
+ */
+ private void gen_poly()
+ {
+ int i, j;
+
+ gg[0] = 2; /* primitive element alpha = 2 for GF(2**mm) */
+ gg[1] = 1; /* g(x) = (X+alpha) initially */
+ for(i = 2; i <= (NN - KK); i++)
+ {
+ gg[i] = 1;
+
+ for(j = i - 1; j > 0; j--)
+ {
+ if(gg[j] != 0)
+ {
+ gg[j] = gg[j - 1] ^ alpha_to[(index_of[gg[j]] + i) % NN];
+ }
+ else
+ {
+ gg[j] = gg[j - 1];
+ }
+ }
+
+ /* gg[0] can never be zero */
+ gg[0] = alpha_to[(index_of[gg[0]] + i) % NN];
+ }
+
+ /* convert gg[] to index form for quicker encoding */
+ for(i = 0; i <= (NN - KK); i++)
+ {
+ gg[i] = index_of[gg[i]];
+ }
+ }
+
+ /**
+ * Decodes
+ *
+ * @param input
+ * @param output
+ * @return
+ */
+ /* assume we have received bits grouped into mm-bit symbols in recd[i],
+ i=0..(nn-1), and recd[i] is polynomial form.
+ We first compute the 2*tt syndromes by substituting alpha**i into rec(X) and
+ evaluating, storing the syndromes in s[i], i=1..2tt (leave s[0] zero) .
+ Then we use the Berlekamp iteration to find the error location polynomial
+ elp[i]. If the degree of the elp is >tt, we cannot correct all the errors
+ and hence just put out the information symbols uncorrected. If the degree of
+ elp is <=tt, we substitute alpha**i , i=1..n into the elp to get the roots,
+ hence the inverse roots, the error location numbers. If the number of errors
+ located does not equal the degree of the elp, we have more than tt errors
+ and cannot correct them. Otherwise, we then solve for the error value at
+ the error location and correct the error. The procedure is that found in
+ Lin and Costello. For the cases where the number of errors is known to be too
+ large to correct, the information symbols as received are output (the
+ advantage of systematic encoding is that hopefully some of the information
+ symbols will be okay and that if we are in luck, the errors are in the
+ parity part of the transmitted codeword). Of course, these insoluble cases
+ can be returned as error flags to the calling routine if desired. */
+ public boolean decode(final int[] input, int[] output) //input, output
+ {
+ int u, q;
+ int[][] elp = new int[NN - KK + 2][NN - KK];
+ int[] d = new int[NN - KK + 2];
+ int[] l = new int[NN - KK + 2];
+ int[] u_lu = new int[NN - KK + 2];
+ int[] s = new int[NN - KK + 1];
+ int count = 0;
+ boolean syn_error = false;
+ int[] root = new int[TT];
+ int[] loc = new int[TT];
+ int[] z = new int[TT + 1];
+ int[] err = new int[NN];
+ int[] reg = new int[TT + 1];
+
+ boolean irrecoverable_error = false;
+
+ /* put recd[i] into index form (ie as powers of alpha) */
+ for(int i = 0; i < NN; i++)
+ {
+ output[i] = index_of[input[i]];
+ }
+
+ /* first form the syndromes */
+ for(int i = 1; i <= NN - KK; i++)
+ {
+ s[i] = 0;
+
+ for(int j = 0; j < NN; j++)
+ {
+ if(output[j] != -1)
+ {
+ /* recd[j] in index form */
+ s[i] ^= alpha_to[(output[j] + i * j) % NN];
+ }
+ }
+
+ /* convert syndrome from polynomial form to index form */
+ if(s[i] != 0)
+ {
+ /* set flag if non-zero syndrome => error */
+ syn_error = true;
+ }
+
+ s[i] = index_of[s[i]];
+ }
+
+ if(syn_error) /* if errors, try and correct */
+ {
+ /* compute the error location polynomial via the Berlekamp iterative algorithm,
+ following the terminology of Lin and Costello : d[u] is the 'mu'th
+ discrepancy, where u='mu'+1 and 'mu' (the Greek letter!) is the step number
+ ranging from -1 to 2*tt (see L&C), l[u] is the
+ degree of the elp at that step, and u_l[u] is the difference between the
+ step number and the degree of the elp.
+ */
+
+ /* initialise table entries */
+ d[0] = 0; /* index form */
+ d[1] = s[1]; /* index form */
+ elp[0][0] = 0; /* index form */
+ elp[1][0] = 1; /* polynomial form */
+
+ for(int i = 1; i < NN - KK; i++)
+ {
+ elp[0][i] = -1; /* index form */
+ elp[1][i] = 0; /* polynomial form */
+ }
+
+ l[0] = 0;
+ l[1] = 0;
+ u_lu[0] = -1;
+ u_lu[1] = 0;
+ u = 0;
+
+ do
+ {
+ u++;
+
+ if(d[u] == -1)
+ {
+ l[u + 1] = l[u];
+
+ for(int i = 0; i <= l[u]; i++)
+ {
+ elp[u + 1][i] = elp[u][i];
+ elp[u][i] = index_of[elp[u][i]];
+ }
+ }
+ else
+ /* search for words with greatest u_lu[q] for which d[q]!=0 */
+ {
+ q = u - 1;
+
+ while((d[q] == -1) && (q > 0))
+ {
+ q--;
+ }
+
+ /* have found first non-zero d[q] */
+ if(q > 0)
+ {
+ int j = q;
+
+ do
+ {
+ j--;
+
+ if((d[j] != -1) && (u_lu[q] < u_lu[j]))
+ {
+ q = j;
+ }
+ }
+ while(j > 0);
+ }
+ ;
+
+ /* have now found q such that d[u]!=0 and u_lu[q] is maximum */
+ /* store degree of new elp polynomial */
+ l[u + 1] = FastMath.max(l[u], l[q] + u - q);
+
+ /* form new elp(x) */
+ for(int i = 0; i < NN - KK; i++)
+ {
+ elp[u + 1][i] = 0;
+ }
+
+ for(int i = 0; i <= l[q]; i++)
+ {
+ if(elp[q][i] != -1)
+ {
+ elp[u + 1][i + u - q] =
+ alpha_to[(d[u] + NN - d[q]
+ + elp[q][i]) % NN];
+ }
+ }
+ for(int i = 0; i <= l[u]; i++)
+ {
+ elp[u + 1][i] ^= elp[u][i];
+ elp[u][i] = index_of[elp[u][i]]; /*convert old elp value to index*/
+ }
+ }
+
+ u_lu[u + 1] = u - l[u + 1];
+
+ /* form (u+1)th discrepancy */
+ if(u < NN - KK) /* no discrepancy computed on last iteration */
+ {
+ if(s[u + 1] != -1)
+ {
+ d[u + 1] = alpha_to[s[u + 1]];
+ }
+ else
+ {
+ d[u + 1] = 0;
+ }
+ for(int i = 1; i <= l[u + 1]; i++)
+ {
+ if((s[u + 1 - i] != -1) && (elp[u + 1][i] != 0))
+ {
+ d[u + 1] ^= alpha_to[(s[u + 1 - i]
+ + index_of[elp[u + 1][i]]) % NN];
+ }
+ }
+
+ d[u + 1] = index_of[d[u + 1]]; /* put d[u+1] into index form */
+ }
+ }
+ while((u < NN - KK) && (l[u + 1] <= TT));
+
+ u++;
+
+ if(l[u] <= TT) /* can correct error */
+ {
+ /* put elp into index form */
+ for(int i = 0; i <= l[u]; i++)
+ {
+ elp[u][i] = index_of[elp[u][i]];
+ }
+
+ /* find roots of the error location polynomial */
+ if(l[u] >= 0)
+ {
+ System.arraycopy(elp[u], 1, reg, 1, l[u]);
+ }
+
+ count = 0;
+
+ for(int i = 1; i <= NN; i++)
+ {
+ q = 1;
+
+ for(int j = 1; j <= l[u]; j++)
+ {
+ if(reg[j] != -1)
+ {
+ reg[j] = (reg[j] + j) % NN;
+ q ^= alpha_to[reg[j]];
+ }
+ ;
+ }
+
+ if(q == 0) /* store root and error location number indices */
+ {
+ root[count] = i;
+ loc[count] = NN - i;
+ count++;
+ }
+ ;
+ }
+ ;
+
+ if(count == l[u]) /* no. roots = degree of elp hence <= tt errors */
+ {
+ /* form polynomial z(x) */
+ for(int i = 1; i <= l[u]; i++) /* Z[0] = 1 always - do not need */
+ {
+ if((s[i] != -1) && (elp[u][i] != -1))
+ {
+ z[i] = alpha_to[s[i]] ^ alpha_to[elp[u][i]];
+ }
+ else if((s[i] != -1) && (elp[u][i] == -1))
+ {
+ z[i] = alpha_to[s[i]];
+ }
+ else if((s[i] == -1) && (elp[u][i] != -1))
+ {
+ z[i] = alpha_to[elp[u][i]];
+ }
+ else
+ {
+ z[i] = 0;
+ }
+
+ for(int j = 1; j < i; j++)
+ {
+ if((s[j] != -1) && (elp[u][i - j] != -1))
+ {
+ z[i] ^= alpha_to[(elp[u][i - j] + s[j]) % NN];
+ }
+ }
+
+ z[i] = index_of[z[i]]; /* put into index form */
+ }
+ ;
+
+ /* evaluate errors at locations given by error location numbers loc[i] */
+ for(int i = 0; i < NN; i++)
+ {
+ err[i] = 0;
+
+ if(output[i] != -1) /* convert recd[] to polynomial form */
+ {
+ output[i] = alpha_to[output[i]];
+ }
+ else
+ {
+ output[i] = 0;
+ }
+ }
+
+ for(int i = 0; i < l[u]; i++) /* compute numerator of error term first */
+ {
+ err[loc[i]] = 1; /* accounts for z[0] */
+
+ for(int j = 1; j <= l[u]; j++)
+ {
+ if(z[j] != -1)
+ {
+ err[loc[i]] ^= alpha_to[(z[j] + j * root[i]) % NN];
+ }
+ }
+
+ if(err[loc[i]] != 0)
+ {
+ err[loc[i]] = index_of[err[loc[i]]];
+
+ q = 0; /* form denominator of error term */
+
+ for(int j = 0; j < l[u]; j++)
+ {
+ if(j != i)
+ {
+ q += index_of[1 ^ alpha_to[(loc[j] + root[i]) % NN]];
+ }
+ }
+
+ q = q % NN;
+ err[loc[i]] = alpha_to[(err[loc[i]] - q + NN) % NN];
+ output[loc[i]] ^= err[loc[i]]; /*recd[i] must be in polynomial form */
+ }
+ }
+ }
+ else
+ {
+ /* no. roots != degree of elp => >tt errors and cannot solve */
+ irrecoverable_error = true;
+ }
+
+ }
+ else
+ {
+ /* elp has degree >tt hence cannot solve */
+ irrecoverable_error = true;
+ }
+ }
+ else
+ {
+ /* no non-zero syndromes => no errors: output received codeword */
+ for(int i = 0; i < NN; i++)
+ {
+ if(output[i] != -1) /* convert recd[] to polynomial form */
+ {
+ output[i] = alpha_to[output[i]];
+ }
+ else
+ {
+ output[i] = 0;
+ }
+ }
+ }
+
+ if(irrecoverable_error)
+ {
+ for(int i = 0; i < NN; i++) /* could return error flag if desired */
+ {
+ if(output[i] != -1) /* convert recd[] to polynomial form */
+ {
+ output[i] = alpha_to[output[i]];
+ }
+ else
+ {
+ output[i] = 0; /* just output received codeword as is */
+ }
+ }
+ }
+
+ return irrecoverable_error;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/BerlekempMassey_63.java b/src/main/java/io/github/dsheirer/edac/BerlekempMassey_63.java
deleted file mode 100644
index 6ab364ea..00000000
--- a/src/main/java/io/github/dsheirer/edac/BerlekempMassey_63.java
+++ /dev/null
@@ -1,521 +0,0 @@
-/*
- *
- * * ******************************************************************************
- * * 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
- * * *****************************************************************************
- *
- *
- */
-
-package io.github.dsheirer.edac;
-
-import org.apache.commons.math3.util.FastMath;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Berlekemp Massey decoder for 63-bit primitive RS/BCH block codes
- */
-public class BerlekempMassey_63
-{
- public final static Logger mLog = LoggerFactory.getLogger( BerlekempMassey_63.class );
-
- /* Golay field size GF( 2 ** MM ) */
- private static final int MM = 6;
-
- /* Codeword Length: NN = 2 ** MM - 1 */
- private static final int NN = 63;
-
- /* Hamming distance between codewords: NN - KK + 1 = 2 * TT + 1 */
- private int KK;
-
- /* Maximum number of errors that can be corrected */
- int TT;
-
- int[] alpha_to;
- int[] index_of;
- int[] gg;
-
- public BerlekempMassey_63( int tt )
- {
- TT = tt;
- KK = NN - 2 * TT;
-
- alpha_to = new int[ NN + 1 ];
- index_of = new int[ NN + 1 ];
- gg = new int[ NN - KK + 1 ];
-
- /* P25 generator polynomial */
- int generator_polinomial[] = { 1, 1, 0, 0, 0, 0, 1 };
-
- generate_gf( generator_polinomial );
-
- gen_poly();
- }
-
- /**
- * Generates the Golay Field.
- *
- * Generates a GF( 2**mm ) from the irreducible polynomial
- * p(X) in pp[0]..pp[mm]
- *
- * Lookup tables:
- * index_of[] = polynomial form
- * alpha_to[] = contains j=alpha**i;
- *
- * Polynomial form -> Index form index_of[j=alpha**i] = i
- *
- * alpha_to = 2 is the primitive element of GF( 2**mm )
- *
- * @param generator_polynomial
- */
- private void generate_gf( int[] generator_polynomial )
- {
- int i;
- int mask = 1;
-
- alpha_to[ MM ] = 0;
-
- for( i = 0; i < MM; i++ )
- {
- alpha_to[ i ] = mask;
- index_of[ alpha_to[ i ] ] = i;
-
- if( generator_polynomial[ i ] != 0 )
- {
- alpha_to[ MM ] ^= mask;
- }
-
- mask <<= 1;
- }
-
- index_of[ alpha_to[ MM ] ] = MM;
-
- mask >>= 1;
-
- for ( i = MM + 1; i < NN; i++ )
- {
- if( alpha_to[ i - 1 ] >= mask )
- {
- alpha_to[ i ] = alpha_to[ MM ] ^ ( ( alpha_to[ i - 1 ] ^ mask ) << 1 );
- }
- else
- {
- alpha_to[ i ] = alpha_to[ i - 1 ] << 1;
- }
-
- index_of[ alpha_to[ i ] ] = i;
- }
-
- index_of[ 0 ] = -1;
- }
-
- /**
- * Generates the polynomial for a TT-error correction code.
- *
- * Length NN = ( 2 ** MM -1 ) Reed Solomon code from the product of
- * (X+alpha**i), i=1..2*tt
- */
- private void gen_poly()
- {
- int i, j;
-
- gg[ 0 ] = 2; /* primitive element alpha = 2 for GF(2**mm) */
- gg[ 1 ] = 1; /* g(x) = (X+alpha) initially */
- for( i = 2; i <= NN - KK; i++ )
- {
- gg[ i ] = 1;
-
- for( j = i - 1; j > 0; j-- )
- {
- if( gg[ j ] != 0 )
- {
- gg[ j ] = gg[ j - 1 ] ^ alpha_to[ ( index_of[ gg[ j ] ] + i ) % NN ];
- }
- else
- {
- gg[ j ] = gg[ j - 1 ];
- }
- }
-
- /* gg[0] can never be zero */
- gg[ 0 ] = alpha_to[ ( index_of[ gg[ 0 ] ] + i ) % NN ];
- }
-
- /* convert gg[] to index form for quicker encoding */
- for( i = 0; i <= NN - KK; i++ )
- {
- gg[ i ] = index_of[ gg[ i ] ];
- }
- }
-
- /**
- * Decodes
- * @param input
- * @param output
- * @return
- */
- /* assume we have received bits grouped into mm-bit symbols in recd[i],
- i=0..(nn-1), and recd[i] is polynomial form.
- We first compute the 2*tt syndromes by substituting alpha**i into rec(X) and
- evaluating, storing the syndromes in s[i], i=1..2tt (leave s[0] zero) .
- Then we use the Berlekamp iteration to find the error location polynomial
- elp[i]. If the degree of the elp is >tt, we cannot correct all the errors
- and hence just put out the information symbols uncorrected. If the degree of
- elp is <=tt, we substitute alpha**i , i=1..n into the elp to get the roots,
- hence the inverse roots, the error location numbers. If the number of errors
- located does not equal the degree of the elp, we have more than tt errors
- and cannot correct them. Otherwise, we then solve for the error value at
- the error location and correct the error. The procedure is that found in
- Lin and Costello. For the cases where the number of errors is known to be too
- large to correct, the information symbols as received are output (the
- advantage of systematic encoding is that hopefully some of the information
- symbols will be okay and that if we are in luck, the errors are in the
- parity part of the transmitted codeword). Of course, these insoluble cases
- can be returned as error flags to the calling routine if desired. */
- public boolean decode( final int[] input, int[] output ) //input, output
- {
- int u, q;
- int[][] elp = new int[ NN - KK + 2 ][ NN - KK ];
- int[] d = new int[ NN - KK + 2 ];
- int[] l = new int[ NN - KK + 2 ];
- int[] u_lu = new int[ NN - KK + 2 ];
- int[] s = new int[ NN - KK + 1 ];
- int count = 0;
- boolean syn_error = false;
- int[] root = new int[ TT ];
- int[] loc = new int[ TT ];
- int[] z = new int[ TT + 1 ];
- int[] err = new int[ NN ];
- int[] reg = new int[ TT + 1 ];
-
- boolean irrecoverable_error = false;
-
- /* put recd[i] into index form (ie as powers of alpha) */
- for( int i = 0; i < NN; i++ )
- {
- output[ i ] = index_of[ input[ i ] ];
- }
-
- /* first form the syndromes */
- for( int i = 1; i <= NN - KK; i++ )
- {
- s[ i ] = 0;
-
- for( int j = 0; j < NN; j++ )
- {
- if( output[ j ] != -1 )
- {
- /* recd[j] in index form */
- s[ i ] ^= alpha_to[ ( output[ j ] + i * j ) % NN ];
- }
- }
-
- /* convert syndrome from polynomial form to index form */
- if( s[ i ] != 0 )
- {
- /* set flag if non-zero syndrome => error */
- syn_error = true;
- }
-
- s[ i ] = index_of[ s[ i ] ];
- }
-
- if( syn_error ) /* if errors, try and correct */
- {
- /* compute the error location polynomial via the Berlekamp iterative algorithm,
- following the terminology of Lin and Costello : d[u] is the 'mu'th
- discrepancy, where u='mu'+1 and 'mu' (the Greek letter!) is the step number
- ranging from -1 to 2*tt (see L&C), l[u] is the
- degree of the elp at that step, and u_l[u] is the difference between the
- step number and the degree of the elp.
- */
-
- /* initialise table entries */
- d[ 0 ] = 0; /* index form */
- d[ 1 ] = s[ 1 ]; /* index form */
- elp[ 0 ][ 0 ] = 0; /* index form */
- elp[ 1 ][ 0 ] = 1; /* polynomial form */
-
- for( int i = 1; i < NN - KK; i++ )
- {
- elp[ 0 ][ i ] = -1; /* index form */
- elp[ 1 ][ i ] = 0; /* polynomial form */
- }
-
- l[ 0 ] = 0;
- l[ 1 ] = 0;
- u_lu[ 0 ] = -1;
- u_lu[ 1 ] = 0;
- u = 0;
-
- do
- {
- u++;
-
- if( d[ u ] == -1 )
- {
- l[ u + 1 ] = l[ u ];
-
- for( int i = 0; i <= l[ u ]; i++ )
- {
- elp[ u + 1 ][ i ] = elp[ u ][ i ];
- elp[ u ][ i ] = index_of[ elp[ u ][ i ] ];
- }
- }
- else
- /* search for words with greatest u_lu[q] for which d[q]!=0 */
- {
- q = u - 1;
-
- while( ( d[ q ] == -1 ) && ( q > 0 ) )
- {
- q--;
- }
-
- /* have found first non-zero d[q] */
- if( q > 0 )
- {
- int j = q;
-
- do
- {
- j--;
-
- if( ( d[ j ] != -1 ) && ( u_lu[ q ] < u_lu[ j ] ) )
- {
- q = j;
- }
- }
- while( j > 0 );
- };
-
- /* have now found q such that d[u]!=0 and u_lu[q] is maximum */
- /* store degree of new elp polynomial */
- l[u + 1] = FastMath.max(l[u], l[q] + u - q);
-
- /* form new elp(x) */
- for( int i = 0; i < NN - KK; i++ )
- {
- elp[ u + 1 ][ i ] = 0;
- }
-
- for( int i = 0; i <= l[q]; i++ )
- {
- if( elp[ q ][ i ] != -1 )
- {
- elp[ u + 1 ][ i + u - q ] =
- alpha_to[ ( d[ u ] + NN - d[ q ]
- + elp[ q ][ i ]) % NN ];
- }
- }
- for( int i = 0; i <= l[u]; i++ )
- {
- elp[ u + 1 ][ i ] ^= elp[ u ][ i ];
- elp[ u ][ i ] = index_of[ elp[ u ][ i ] ]; /*convert old elp value to index*/
- }
- }
-
- u_lu[ u + 1 ] = u - l[ u + 1 ];
-
- /* form (u+1)th discrepancy */
- if( u < NN - KK ) /* no discrepancy computed on last iteration */
- {
- if ( s[ u + 1 ] != -1 )
- {
- d[ u + 1 ] = alpha_to[ s[ u + 1 ] ];
- }
- else
- {
- d[ u + 1 ] = 0;
- }
- for( int i = 1; i <= l[ u + 1 ]; i++ )
- {
- if( ( s[ u + 1 - i ] != -1 ) && ( elp[ u + 1 ][ i] != 0 ) )
- {
- d[ u + 1 ] ^= alpha_to[ ( s[ u + 1 - i ]
- + index_of[ elp[ u + 1 ][ i ] ] ) % NN ];
- }
- }
-
- d[ u + 1 ] = index_of[ d[ u + 1 ] ]; /* put d[u+1] into index form */
- }
- }
- while( ( u < NN - KK ) && ( l[ u + 1 ] <= TT) );
-
- u++;
-
- if( l[ u ] <= TT ) /* can correct error */
- {
- /* put elp into index form */
- for( int i = 0; i <= l[u]; i++ )
- {
- elp[ u ][ i ] = index_of[ elp[ u ][ i ] ];
- }
-
- /* find roots of the error location polynomial */
- if (l[u] >= 0) System.arraycopy(elp[u], 1, reg, 1, l[u]);
-
- count = 0;
-
- for( int i = 1; i <= NN; i++ )
- {
- q = 1;
-
- for( int j = 1; j <= l[u]; j++ )
- {
- if( reg[ j ] != -1 )
- {
- reg[ j ] = ( reg[ j ] + j ) % NN;
- q ^= alpha_to[ reg[ j ] ];
- };
- }
-
- if( q == 0 ) /* store root and error location number indices */
- {
- root[ count ] = i;
- loc[ count ] = NN - i;
- count++;
- };
- };
-
- if( count == l[ u ] ) /* no. roots = degree of elp hence <= tt errors */
- {
- /* form polynomial z(x) */
- for( int i = 1; i <= l[ u ]; i++ ) /* Z[0] = 1 always - do not need */
- {
- if( ( s[ i ] != -1 ) && ( elp[ u ][ i ] != -1 ) )
- {
- z[ i ] = alpha_to[ s[ i ] ] ^ alpha_to[ elp[ u ][ i ] ];
- }
- else if( ( s[ i ] != -1 ) && ( elp[ u ][ i ] == -1 ) )
- {
- z[ i ] = alpha_to[ s[ i ] ];
- }
- else if( ( s[ i ] == -1 ) && ( elp[ u ][ i ] != -1 ) )
- {
- z[ i ] = alpha_to[ elp[ u ][ i ] ];
- }
- else
- {
- z[ i ] = 0;
- }
-
- for( int j = 1; j < i; j++ )
- {
- if( ( s[ j ] != -1 ) && ( elp[ u ][ i - j ] != -1 ) )
- {
- z[ i ] ^= alpha_to[ ( elp[ u ][ i - j ] + s[ j ] ) % NN ];
- }
- }
-
- z[ i ] = index_of[ z[ i ] ]; /* put into index form */
- };
-
- /* evaluate errors at locations given by error location numbers loc[i] */
- for( int i = 0; i < NN; i++ )
- {
- err[ i ] = 0;
-
- if( output[ i ] != -1 ) /* convert recd[] to polynomial form */
- {
- output[ i ] = alpha_to[ output[ i ] ];
- }
- else
- {
- output[ i ] = 0;
- }
- }
-
- for( int i = 0; i < l[ u ]; i++ ) /* compute numerator of error term first */
- {
- err[ loc[ i ] ] = 1; /* accounts for z[0] */
-
- for( int j = 1; j <= l[ u ]; j++ )
- {
- if( z[ j ] != -1 )
- {
- err[ loc[ i ] ] ^= alpha_to[ ( z[ j ] + j * root[ i ] ) % NN ];
- }
- }
-
- if( err[ loc[ i ] ] != 0 )
- {
- err[ loc[ i ] ] = index_of[ err[ loc[ i ] ] ];
-
- q = 0; /* form denominator of error term */
-
- for (int j = 0; j < l[u]; j++)
- {
- if (j != i)
- {
- q += index_of[1 ^ alpha_to[(loc[j] + root[i]) % NN]];
- }
- }
-
- q = q % NN;
- err[loc[i]] = alpha_to[(err[loc[i]] - q + NN) % NN];
- output[loc[i]] ^= err[loc[i]]; /*recd[i] must be in polynomial form */
- }
- }
- }
- else
- {
- /* no. roots != degree of elp => >tt errors and cannot solve */
- irrecoverable_error = true;
- }
-
- }
- else
- {
- /* elp has degree >tt hence cannot solve */
- irrecoverable_error = true;
- }
- }
- else
- {
- /* no non-zero syndromes => no errors: output received codeword */
- for (int i = 0; i < NN; i++)
- {
- if (output[i] != -1) /* convert recd[] to polynomial form */
- {
- output[i] = alpha_to[output[i]];
- }
- else
- {
- output[i] = 0;
- }
- }
- }
-
- if( irrecoverable_error )
- {
- for (int i = 0; i < NN; i++) /* could return error flag if desired */
- {
- if (output[i] != -1) /* convert recd[] to polynomial form */
- {
- output[i] = alpha_to[output[i]];
- }
- else
- {
- output[i] = 0; /* just output received codeword as is */
- }
- }
- }
-
- return irrecoverable_error;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/edac/CRCDMR.java b/src/main/java/io/github/dsheirer/edac/CRCDMR.java
new file mode 100644
index 00000000..0112995b
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/CRCDMR.java
@@ -0,0 +1,290 @@
+/*******************************************************************************
+ * sdr-trunk
+ * Copyright (C) 2014-2018 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.BinaryMessage;
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * DMR CRC check/correction methods
+ */
+public class CRCDMR
+{
+ private final static Logger mLog = LoggerFactory.getLogger(CRCDMR.class);
+
+ /**
+ * CRC-CCITT 16-bit checksums for a message length of 80 bits plus 16
+ * additional checksums representing CRC checksum bit errors
+ *
+ * Generated by:
+ * CRCUtil.generate( 80, 16, 0x11021l, 0xFFFFl, true );
+ */
+ public static final int[] CCITT_80_CHECKSUMS = new int[]
+ {
+ 0xE434, 0x721A, 0x390D, 0x9496, 0x4A4B, 0xAD35, 0xDE8A, 0x6F45, 0xBFB2,
+ 0x5FD9, 0xA7FC, 0x53FE, 0x29FF, 0x9CEF, 0xC667, 0xEB23, 0xFD81, 0xF6D0,
+ 0x7B68, 0x3DB4, 0x1EDA, 0xF6D, 0x8FA6, 0x47D3, 0xABF9, 0xDDEC, 0x6EF6,
+ 0x377B, 0x93AD, 0xC1C6, 0x60E3, 0xB861, 0xD420, 0x6A10, 0x3508, 0x1A84,
+ 0xD42, 0x6A1, 0x8B40, 0x45A0, 0x22D0, 0x1168, 0x8B4, 0x45A, 0x22D, 0x8906,
+ 0x4483, 0xAA51, 0xDD38, 0x6E9C, 0x374E, 0x1BA7, 0x85C3, 0xCAF1, 0xED68,
+ 0x76B4, 0x3B5A, 0x1DAD, 0x86C6, 0x4363, 0xA9A1, 0xDCC0, 0x6E60, 0x3730,
+ 0x1B98, 0xDCC, 0x6E6, 0x373, 0x89A9, 0xCCC4, 0x6662, 0x3331, 0x9188,
+ 0x48C4, 0x2462, 0x1231, 0x8108, 0x4084, 0x2042, 0x1021, 0x1, 0x2, 0x4, 0x8,
+ 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, 0x2000, 0x4000,
+ 0x8000,
+ };
+
+ /**
+ * Confirmed Packet Data Unit CRC-9 checksums, generated by:
+ *
+ * long[] table = CRCUtil.generate( 135, 9, 0x259l, 0x1FF, false, Parity.NONE );
+ */
+ public static final int[] CRC9_CHECKSUMS = new int[]
+ {
+ 0x1E7, 0x1F3, 0x1F9, 0x1FC, 0x0D2, 0x045, 0x122, 0x0BD, 0x15E, 0x083,
+ 0x141, 0x1A0, 0x0FC, 0x052, 0x005, 0x102, 0x0AD, 0x156, 0x087, 0x143,
+ 0x1A1, 0x1D0, 0x0C4, 0x04E, 0x00B, 0x105, 0x182, 0x0ED, 0x176, 0x097,
+ 0x14B, 0x1A5, 0x1D2, 0x0C5, 0x162, 0x09D, 0x14E, 0x08B, 0x145, 0x1A2,
+ 0x0FD, 0x17E, 0x093, 0x149, 0x1A4, 0x0FE, 0x053, 0x129, 0x194, 0x0E6,
+ 0x05F, 0x12F, 0x197, 0x1CB, 0x1E5, 0x1F2, 0x0D5, 0x16A, 0x099, 0x14C,
+ 0x08A, 0x069, 0x134, 0x0B6, 0x077, 0x13B, 0x19D, 0x1CE, 0x0CB, 0x165,
+ 0x1B2, 0x0F5, 0x17A, 0x091, 0x148, 0x088, 0x068, 0x018, 0x020, 0x03C,
+ 0x032, 0x035, 0x11A, 0x0A1, 0x150, 0x084, 0x06E, 0x01B, 0x10D, 0x186,
+ 0x0EF, 0x177, 0x1BB, 0x1DD, 0x1EE, 0x0DB, 0x16D, 0x1B6, 0x0F7, 0x17B,
+ 0x1BD, 0x1DE, 0x0C3, 0x161, 0x1B0, 0x0F4, 0x056, 0x007, 0x103, 0x181,
+ 0x1C0, 0x0CC, 0x04A, 0x009, 0x104, 0x0AE, 0x07B, 0x13D, 0x19E, 0x0E3,
+ 0x171, 0x1B8, 0x0F0, 0x054, 0x006, 0x02F, 0x117, 0x18B, 0x1C5, 0x1E2,
+ 0x0DD, 0x16E, 0x09B, 0x14D, 0x1A6
+ };
+
+
+ /**
+ * Error detection and correction of single-bit errors for CCITT 16-bit CRC protected 80-bit messages.
+ */
+ public static BinaryMessage correctCCITT80(BinaryMessage message, int messageStart, int crcStart)
+ {
+ int calculated = 0; //Starting value
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(messageStart);
+ i >= messageStart && i < crcStart;
+ i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CCITT_80_CHECKSUMS[i - messageStart];
+ }
+
+ int checksum = getIntChecksum(message, crcStart, 16);
+
+ int residual = calculated ^ checksum;
+
+ if(residual == 0 || residual == 0xFFFF)
+ {
+ message.setCRC(CRC.PASSED);
+
+ return message;
+ }
+ else
+ {
+ int errorLocation = getBitError(residual, CCITT_80_CHECKSUMS);
+
+ if(errorLocation >= 0)
+ {
+ message.flip(errorLocation + messageStart);
+
+ message.setCRC(CRC.CORRECTED);
+
+ return message;
+ }
+ }
+
+ message.setCRC(CRC.FAILED_CRC);
+
+ return message;
+ }
+
+ /**
+ * Calculates the CRC checksum for the specified message
+ * @param message to check
+ * @param start of the message
+ * @param length of the message (assumes the CRC checksum follows)
+ * @param checksums to use in calculating the message checksum
+ * @return calculated checksum
+ */
+ public static int calculate(BinaryMessage message, int start, int length, long[] checksums)
+ {
+ int calculated = 0;
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(start); i >= start && i < length; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= checksums[i - start];
+ }
+
+ return calculated;
+ }
+
+ /**
+ * Error detection and correction of single-bit errors for CCITT 16-bit CRC protected 80-bit messages.
+ */
+ public static int correctCCITT80(CorrectedBinaryMessage message, int messageStart, int crcStart, int mask)
+ {
+ int calculated = mask; //Starting value
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(messageStart); i >= messageStart && i < crcStart; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CCITT_80_CHECKSUMS[i - messageStart];
+ }
+
+ int checksum = getIntChecksum(message, crcStart, 16);
+
+ int residual = calculated ^ checksum;
+
+ if(residual == 0 || residual == 0xFFFF)
+ {
+ return 0;
+ }
+ else
+ {
+ int errorLocation = getBitError(residual, CCITT_80_CHECKSUMS);
+
+ if(errorLocation >= 0)
+ {
+ message.flip(errorLocation + messageStart);
+ message.incrementCorrectedBitCount(1);
+ return 1;
+ }
+ }
+
+ //Message has at least 2 bit errors - ie uncorrectable
+ message.incrementCorrectedBitCount(2);
+
+ return 2;
+ }
+
+ /**
+ * Error detection for CRC-9 protected Confirmed Packet Data blocks. These
+ * data blocks have a slightly complicated structure because the checksum
+ * is located between bits 7-15, within a 144 bit block. The checksums
+ * were generated assuming that the message is contiguous from 0 - 134 bits.
+ * No data correction is performed.
+ */
+ public static CRC checkCRC9(BinaryMessage message, int messageStart)
+ {
+ int calculated = 0x0; //Initial fill of all ones
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(messageStart);
+ i >= messageStart && i < messageStart + 144;
+ i = message.nextSetBit(i + 1))
+ {
+ /* message bits before the CRC */
+ if(i < (messageStart + 7))
+ {
+ calculated ^= CRC9_CHECKSUMS[i - messageStart];
+ }
+ /* message bits after the CRC */
+ else if(i > (messageStart + 15))
+ {
+ calculated ^= CRC9_CHECKSUMS[i - messageStart - 9];
+ }
+ }
+
+ int checksum = message.getInt(messageStart + 7, messageStart + 15);
+
+ int residual = calculated ^ checksum;
+
+ if(residual == 0 || residual == 0x1FF)
+ {
+ return CRC.PASSED;
+ }
+
+ return CRC.FAILED_CRC;
+ }
+
+ /**
+ * Calculates the value of the message checksum as a long
+ */
+ public static long getLongChecksum(BinaryMessage message,
+ int crcStart, int crcLength)
+ {
+ return message.getLong(crcStart, crcStart + crcLength - 1);
+ }
+
+ /**
+ * Calculates the value of the message checksum as an integer
+ */
+ public static int getIntChecksum(BinaryMessage message,
+ int crcStart, int crcLength)
+ {
+ return message.getInt(crcStart, crcStart + crcLength - 1);
+ }
+
+ /**
+ * Identifies any single bit error position that matches the checksum error.
+ */
+ public static int getBitError(long checksumError, long[] checksums)
+ {
+ for(int x = 0; x < checksums.length; x++)
+ {
+ if(checksums[x] == checksumError)
+ {
+ return x;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Identifies any single bit error position that matches the checksum error.
+ */
+ public static int getBitError(int checksumError, int[] checksums)
+ {
+ for(int x = 0; x < checksums.length; x++)
+ {
+ if(checksums[x] == checksumError)
+ {
+ return x;
+ }
+ }
+
+ return -1;
+ }
+
+ public static int crc8(BinaryMessage bits, int len) {
+ int crc=0;
+ final int K = 8;
+ final boolean[] poly = new boolean[]{true,false,false,false,false,false,true,true,true}; // crc8 poly
+ boolean[] buf = new boolean[256];
+ if (len+K > 256) {
+ return 0;
+ }
+ for (int i=0; i
******************************************************************************/
@@ -23,217 +23,219 @@ import org.slf4j.LoggerFactory;
public class CRCUtil
{
- private final static Logger mLog = LoggerFactory.getLogger( CRCUtil.class );
+ private final static Logger mLog = LoggerFactory.getLogger(CRCUtil.class);
- public static long[] generate( int messageSize,
- int crcSize,
- long polynomial,
- long initialFill,
- boolean includeCRCBitErrors )
- {
- long[] crcTable = new long[ messageSize +
- ( includeCRCBitErrors ? crcSize : 0 ) ];
-
- int[] checksumIndexes = new int[ crcSize ];
-
- for( int x = 0; x < crcSize; x++ )
- {
- checksumIndexes[ x ] = messageSize + x;
- }
+ public static long[] generate(int messageSize,
+ int crcSize,
+ long polynomial,
+ long initialFill,
+ boolean includeCRCBitErrors)
+ {
+ long[] crcTable = new long[messageSize +
+ (includeCRCBitErrors ? crcSize : 0)];
- for( int x = 0; x < messageSize; x++ )
- {
- BinaryMessage message = new BinaryMessage( messageSize + crcSize );
-
- message.load( messageSize, crcSize, initialFill );
-
- message.set( x );
-
- message = decode( message, 0, messageSize, polynomial, crcSize );
-
- long checksum = message.getLong( checksumIndexes );
-
- crcTable[ x ] = checksum;
- }
-
- if( includeCRCBitErrors )
- {
- for( int x = 0; x < crcSize; x++ )
- {
- crcTable[ messageSize + x ] = Long.rotateLeft( 1, x );
- }
- }
-
- return crcTable;
- }
+ int[] checksumIndexes = new int[crcSize];
- /**
- * Generates the checksum table and treats the final bit of each check value
- * as a parity bit that complies with the request parity argument.
- */
- public static long[] generate( int messageSize,
- int crcSize,
- long polynomial,
- long initialFill,
- boolean includeCRCBitErrors,
- Parity parity )
- {
- long[] table = generate( messageSize, crcSize, polynomial, initialFill,
- includeCRCBitErrors );
+ for(int x = 0; x < crcSize; x++)
+ {
+ checksumIndexes[x] = messageSize + x;
+ }
- if( parity == Parity.NONE )
- {
- return table;
- }
- else
- {
- for( int x = 0; x < table.length; x++ )
- {
- table[ x ] <<= 1;
-
- if( parity( table[ x ] ) != parity )
- {
- table[ x ] ^= 0x1;
- }
- }
-
- return table;
- }
- }
-
- public enum Parity{ EVEN, ODD, NONE };
+ for(int x = 0; x < messageSize; x++)
+ {
+ BinaryMessage message = new BinaryMessage(messageSize + crcSize);
- /* Determines the parity (number of 1 bits) for the value */
- public static Parity parity( long value )
- {
- return Long.bitCount( value ) % 2 == 0 ? Parity.EVEN : Parity.ODD;
- }
-
+ message.load(messageSize, crcSize, initialFill);
- /**
- * Performs binary division of the polynomial into the message for bits
- * in index 0 to index messageLength - 1, leaving the results in the
- * checksum filed following the message.
- *
- * IN: MMMMMMMMMMMMMMMMCCCCCCCCCCC
- * OUT: 0000000000000000RRRRRRRRRRR
- *
- * M: Message
- * C: Checksum
- * R: Remainder
- *
- * @param message - message and crc
- * @param polynomial - crc polynomial - must be 1 bit longer than the crc width
- * @param messageSize - message length and start of the checksum
- *
- * @return message with all message bits zeroed out, and the remainder
- * placed in the crc field which starts at index messageLength
- */
- public static BinaryMessage decode( BinaryMessage message,
- int messageStart,
- int messageSize,
- long polynomial,
- int crcSize )
- {
- for (int i = message.nextSetBit( messageStart );
- i >= messageStart && i < messageSize;
- i = message.nextSetBit( i+1 ) )
- {
- BinaryMessage polySet = new BinaryMessage( crcSize + i + 1 );
-
- polySet.load( i, crcSize + 1, polynomial );
-
- message.xor( polySet );
- }
-
- return message;
- }
-
- public static void main( String[] args )
- {
- mLog.debug( "Starting" );
-
-// String raw = "000000001010001000000000100010100000000000000000010000000011001001011100000000000000000011111110001000101001011100101110000101000110011101101001";
-// BitSetBuffer message = BitSetBuffer.load( raw );
-//
-// long polynomial = 0x259l;
-//
-// decode( message, 0, 135, polynomial, 9 );
-//
-// mLog.debug( message.toString() );
+ message.set(x);
- long[] table = CRCUtil.generate( 135, 9, 0x259l, 0x1FF, false, Parity.NONE );
- mLog.debug( toCodeArray( table ) );
- mLog.debug( "Finished" );
- }
-
- public static String toCodeArray( long[] values )
- {
- boolean integerArray = true;
+ message = decode(message, 0, messageSize, polynomial, crcSize);
- /* Determine the correct primitive type: int or long */
- for( long value: values )
- {
- if( value > Integer.MAX_VALUE || value < Integer.MIN_VALUE )
- {
- integerArray = false;
- break;
- }
- }
+ long checksum = message.getLong(checksumIndexes);
- StringBuilder sb = new StringBuilder();
+ crcTable[x] = checksum;
+ }
- if( integerArray )
- {
- sb.append( "\npublic static final int[] CHECKSUMS = new int[]\n" );
- }
- else
- {
- sb.append( "\npublic static final long[] CHECKSUMS = new long[]\n" );
- }
- sb.append( "{\n" );
+ if(includeCRCBitErrors)
+ {
+ for(int x = 0; x < crcSize; x++)
+ {
+ crcTable[messageSize + x] = Long.rotateLeft(1, x);
+ }
+ }
- StringBuilder row = new StringBuilder();
- row.append( " " );
-
- for( long value: values )
- {
- StringBuilder element = new StringBuilder();
-
- element.append( "0x" );
- element.append( Long.toHexString( value ).toUpperCase() );
-
- if( !integerArray )
- {
- element.append( "l" );
- }
-
- element.append( ", " );
-
- if( row.length() + element.length() <= 80 )
- {
- row.append( element.toString() );
- }
- else
- {
- sb.append( row.toString() );
- sb.append( "\n" );
- row = new StringBuilder();
- row.append( " " );
-
- row.append( element.toString() );
- }
- }
-
- if( row.length() > 4 )
- {
- sb.append( row.toString() );
- sb.append( "\n" );
- }
-
- sb.append( "};\n" );
+ return crcTable;
+ }
- return sb.toString();
- }
+ /**
+ * Generates the checksum table and treats the final bit of each check value
+ * as a parity bit that complies with the request parity argument.
+ */
+ public static long[] generate(int messageSize,
+ int crcSize,
+ long polynomial,
+ long initialFill,
+ boolean includeCRCBitErrors,
+ Parity parity)
+ {
+ long[] table = generate(messageSize, crcSize, polynomial, initialFill,
+ includeCRCBitErrors);
+ if(parity == Parity.NONE)
+ {
+ return table;
+ }
+ else
+ {
+ for(int x = 0; x < table.length; x++)
+ {
+ table[x] <<= 1;
+
+ if(parity(table[x]) != parity)
+ {
+ table[x] ^= 0x1;
+ }
+ }
+
+ return table;
+ }
+ }
+
+ public enum Parity
+ {EVEN, ODD, NONE}
+
+ ;
+
+ /* Determines the parity (number of 1 bits) for the value */
+ public static Parity parity(long value)
+ {
+ return Long.bitCount(value) % 2 == 0 ? Parity.EVEN : Parity.ODD;
+ }
+
+
+ /**
+ * Performs binary division of the polynomial into the message for bits
+ * in index 0 to index messageLength - 1, leaving the results in the
+ * checksum filed following the message.
+ *
+ * IN: MMMMMMMMMMMMMMMMCCCCCCCCCCC
+ * OUT: 0000000000000000RRRRRRRRRRR
+ *
+ * M: Message
+ * C: Checksum
+ * R: Remainder
+ *
+ * @param message - message and crc
+ * @param polynomial - crc polynomial - must be 1 bit longer than the crc width
+ * @param messageSize - message length and start of the checksum
+ * @return message with all message bits zeroed out, and the remainder
+ * placed in the crc field which starts at index messageLength
+ */
+ public static BinaryMessage decode(BinaryMessage message,
+ int messageStart,
+ int messageSize,
+ long polynomial,
+ int crcSize)
+ {
+ for(int i = message.nextSetBit(messageStart);
+ i >= messageStart && i < messageSize;
+ i = message.nextSetBit(i + 1))
+ {
+ BinaryMessage polySet = new BinaryMessage(crcSize + i + 1);
+
+ polySet.load(i, crcSize + 1, polynomial);
+
+ message.xor(polySet);
+ System.out.println(message.toString());
+ }
+
+ return message;
+ }
+
+ public static String toCodeArray(long[] values)
+ {
+ boolean integerArray = true;
+
+ /* Determine the correct primitive type: int or long */
+ for(long value : values)
+ {
+ if(value > Integer.MAX_VALUE || value < Integer.MIN_VALUE)
+ {
+ integerArray = false;
+ break;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ if(integerArray)
+ {
+ sb.append("\npublic static final int[] CHECKSUMS = new int[]\n");
+ }
+ else
+ {
+ sb.append("\npublic static final long[] CHECKSUMS = new long[]\n");
+ }
+ sb.append("{\n");
+
+ StringBuilder row = new StringBuilder();
+ row.append(" ");
+
+ for(long value : values)
+ {
+ StringBuilder element = new StringBuilder();
+
+ element.append("0x");
+ element.append(Long.toHexString(value).toUpperCase());
+
+ if(!integerArray)
+ {
+ element.append("l");
+ }
+
+ element.append(", ");
+
+ if(row.length() + element.length() <= 80)
+ {
+ row.append(element.toString());
+ }
+ else
+ {
+ sb.append(row.toString());
+ sb.append("\n");
+ row = new StringBuilder();
+ row.append(" ");
+
+ row.append(element.toString());
+ }
+ }
+
+ if(row.length() > 4)
+ {
+ sb.append(row.toString());
+ sb.append("\n");
+ }
+
+ sb.append("};\n");
+
+ return sb.toString();
+ }
+
+ public static void main(String[] args)
+ {
+ mLog.debug("Starting");
+
+ //DMR message
+// String raw = "100110010001000000001001000000001111011011010000011001010000000000000000000000001100001000001011";
+ String raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101001001111010001110";
+ mLog.debug(raw);
+ raw = "101010000000000000110110100000101101011011010000011001111100000000000011100101000000000000000000";
+ BinaryMessage message = BinaryMessage.load(raw);
+ mLog.debug(message.toString());
+
+ long polynomial = 0x11021l;
+ decode(message, 0, 80, polynomial, 16);
+ mLog.debug(message.toString());
+ mLog.debug("Finished");
+ }
}
diff --git a/src/main/java/io/github/dsheirer/edac/Hamming13.java b/src/main/java/io/github/dsheirer/edac/Hamming13.java
new file mode 100644
index 00000000..bb2a1f78
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/Hamming13.java
@@ -0,0 +1,129 @@
+package io.github.dsheirer.edac;
+
+import io.github.dsheirer.bits.BinaryMessage;
+
+/**
+ * Implements Hamming(13,9,3) Error Detection algorithm
+ */
+public class Hamming13
+{
+ //DMR Checksums from generator matrix TS 102 361-1 Table B.14
+ private static int[] CHECKSUMS = new int[]{0xF, 0xE, 0x7, 0xA, 0x5, 0XB, 0xC, 0x6, 0x3};
+ private static int[] ERROR_INDEX = new int[]{-1, 12, 11, 8, 10, 4, 7, 2, 9, -1, 3, 5, 6, -1, 1, 0, -1};
+
+ /**
+ * Calculates the bit error index of the Hamming(13,9,3) protected word that is contained in the binary message
+ * starting at the specified offset.
+ *
+ * @param message containing a Hamming protected word
+ * @param offset to the start of the protected word
+ * @return message index for an error bit or -1 if no errors are detected.
+ */
+ public static int getErrorIndex(BinaryMessage message, int offset)
+ {
+ int syndrome = getSyndrome(message, offset);
+
+ if(syndrome > 0)
+ {
+ return offset + ERROR_INDEX[syndrome];
+ }
+
+ return -1;
+ }
+
+ /**
+ * Calculates the bit error index of the Hamming(13,9,3) protected word that is contained in the binary message
+ * at the specified indices.
+ *
+ * @param message containing the Hamming protected word
+ * @param indices to the word
+ * @return index of error bit or -1 if no errors are detected
+ */
+ public static int getErrorIndex(BinaryMessage message, int[] indices)
+ {
+ int syndrome = getSyndrome(message, indices);
+
+ if(syndrome > 0)
+ {
+ int errorIndex = ERROR_INDEX[syndrome];
+
+ if(errorIndex >= 0 && errorIndex < indices.length)
+ {
+ return indices[errorIndex];
+ }
+ else
+ {
+ return 10000 + errorIndex;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Calculates the parity checksum (Parity 8,4,2,1) for data (9 <> 1 ) bits.
+ *
+ * @param message containing Hamming(15) protected word
+ * @param offset to the Hamming protected word
+ * @return parity value, 0 - 15
+ */
+ private static int calculateChecksum(BinaryMessage message, int offset)
+ {
+ int calculated = 0; //Starting value
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(offset); i >= offset && i < offset + 9; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CHECKSUMS[i - offset];
+ }
+
+ return calculated;
+ }
+
+ private static int calculateChecksum(BinaryMessage message, int[] indices)
+ {
+ int calculated = 0; //Starting value
+
+ for(int x = 0; x < 9; x++)
+ {
+ if(message.get(indices[x]))
+ {
+ calculated ^= CHECKSUMS[x];
+ }
+ }
+
+ return calculated;
+ }
+
+ private static int getSyndrome(BinaryMessage message, int[] indices)
+ {
+ int calculated = calculateChecksum(message, indices);
+
+ int checksum = 0;
+ for(int x = 9; x < 13; x++)
+ {
+ checksum = Integer.rotateLeft(checksum, 1);
+
+ if(message.get(indices[x]))
+ {
+ checksum++;
+ }
+ }
+
+ return calculated ^ checksum;
+ }
+
+ /**
+ * Calculates the syndrome as the xor of the calculated checksum and the actual checksum.
+ *
+ * @param message containing a hamming(15,11,3) protected word
+ * @param offset to bit 0 of the hamming protected word
+ * @return syndrome that can be used with the ERROR_INDEX error to find the index of the bit position error
+ */
+ private static int getSyndrome(BinaryMessage message, int offset)
+ {
+ int calculated = calculateChecksum(message, offset);
+ int checksum = message.getInt(offset + 9, offset + 12);
+ return (checksum ^ calculated);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/Hamming15.java b/src/main/java/io/github/dsheirer/edac/Hamming15.java
index 22c1e6e2..f498bb72 100644
--- a/src/main/java/io/github/dsheirer/edac/Hamming15.java
+++ b/src/main/java/io/github/dsheirer/edac/Hamming15.java
@@ -2,116 +2,66 @@ package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage;
+/**
+ * Implements Hamming(15,11,3) Error Detection algorithm
+ */
public class Hamming15
{
- private static int[] CHECKSUMS = new int[] { 0xF,0xE,0xD,0xC,0xB,0XA,0x9,
- 0x7,0x6,0x5,0x3 };
+ //DMR Checksums from generator matrix TS 102 361-1 Table B.15
+ private static int[] CHECKSUMS = new int[]{0x9, 0xD, 0xF, 0xE, 0x7, 0XA, 0x5, 0xB, 0xC, 0x6, 0x3};
+ private static int[] ERROR_INDEX = new int[]{-1, 14, 13, 10, 12, 6, 9, 4, 11, 0, 5, 7, 8, 1, 3, 2};
- /**
- * Performs error detection and correction of any single-bit errors.
- *
- * @param frame - binary frame containing a hamming(15,11,4) protected field
- *
- * @param startIndex - offset to the first bit of the field
- *
- * @return - 0 = no errors
- * 1 = a single-bit error was corrected
- * 2 = two or more errors detected - no corrections made
- */
- public static int checkAndCorrect( BinaryMessage frame, int startIndex )
- {
- int syndrome = getSyndrome( frame, startIndex );
+ /**
+ * Calculates the bit error index of the Hamming(15,11,3) protected word that is contained in the binary message
+ * starting at the specified offset.
+ *
+ * @param message containing a Hamming protected word
+ * @param offset to the start of the protected word
+ * @return message index for an error bit or -1 if no errors are detected.
+ */
+ public static int getErrorIndex(BinaryMessage message, int offset)
+ {
+ int syndrome = getSyndrome(message, offset);
- switch( syndrome )
- {
- case 0:
- return 0;
- case 1:
- frame.flip( startIndex + 14 ); //Parity 1
- return 1;
- case 2:
- frame.flip( startIndex + 13 ); //Parity 2
- return 1;
- case 3:
- frame.flip( startIndex + 10 ); //Data 1
- return 1;
- case 4:
- frame.flip( startIndex + 12 ); //Parity 4
- return 1;
- case 5:
- frame.flip( startIndex + 9 ); //Data 2
- return 1;
- case 6:
- frame.flip( startIndex + 8 ); //Data 3
- return 1;
- case 7:
- frame.flip( startIndex + 7 ); //Data 4
- return 1;
- case 8:
- frame.flip( startIndex + 11 ); //Parity 8
- return 1;
- case 9:
- frame.flip( startIndex + 6 ); //Data 5
- return 1;
- case 10:
- frame.flip( startIndex + 5 ); //Data 6
- return 1;
- case 11:
- frame.flip( startIndex + 4 ); //Data 7
- return 1;
- case 12:
- frame.flip( startIndex + 3 ); //Data 8
- return 1;
- case 13:
- frame.flip( startIndex + 2 ); //Data 9
- return 1;
- case 14:
- frame.flip( startIndex + 1 ); //Data 10
- return 1;
- case 15:
- frame.flip( startIndex ); //Data 11
- return 1;
- }
+ if(syndrome > 0)
+ {
+ return offset + ERROR_INDEX[syndrome];
+ }
- /* We'll never get to here */
- return 2;
- }
+ return -1;
+ }
- /**
- * Calculates the checksum (Parity 8,4,2,1) for data (11 <> 1 ) bits.
- * @param frame - frame containing hamming(15) protected word
- * @param startIndex - start bit index of the hamming protected word
- * @return parity value, 0 - 15
- */
- private static int calculateChecksum( BinaryMessage frame, int startIndex )
- {
- int calculated = 0; //Starting value
+ /**
+ * Calculates the parity checksum (Parity 8,4,2,1) for data (11 <> 1 ) bits.
+ *
+ * @param message containing Hamming(15) protected word
+ * @param offset to the Hamming protected word
+ * @return parity value, 0 - 15
+ */
+ private static int calculateChecksum(BinaryMessage message, int offset)
+ {
+ int calculated = 0; //Starting value
- /* Iterate the set bits and XOR running checksum with lookup value */
- for (int i = frame.nextSetBit( startIndex );
- i >= startIndex && i < startIndex + 11;
- i = frame.nextSetBit( i+1 ) )
- {
- calculated ^= CHECKSUMS[ i - startIndex ];
- }
-
- return calculated;
- }
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = message.nextSetBit(offset); i >= offset && i < offset + 11; i = message.nextSetBit(i + 1))
+ {
+ calculated ^= CHECKSUMS[i - offset];
+ }
- /**
- * Calculates the syndrome - xor of the calculated checksum and the actual
- * checksum.
- *
- * @param frame - binary frame containing a hamming(15,11,4) protected word
- * @param startIndex - of bit 0 of the hamming protected word
- * @return - 0 (no errors) or 1 (single bit error corrected)
- */
- private static int getSyndrome( BinaryMessage frame, int startIndex )
- {
- int calculated = calculateChecksum( frame, startIndex );
-
- int checksum = frame.getInt( startIndex + 11, startIndex + 14 );
+ return calculated;
+ }
- return ( checksum ^ calculated );
- }
+ /**
+ * Calculates the syndrome as the xor of the calculated checksum and the actual checksum.
+ *
+ * @param message containing a hamming(15,11,3) protected word
+ * @param offset to bit 0 of the hamming protected word
+ * @return syndrome that can be used with the ERROR_INDEX error to find the index of the bit position error
+ */
+ private static int getSyndrome(BinaryMessage message, int offset)
+ {
+ int calculated = calculateChecksum(message, offset);
+ int checksum = message.getInt(offset + 11, offset + 14);
+ return (checksum ^ calculated);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/edac/Hamming16.java b/src/main/java/io/github/dsheirer/edac/Hamming16.java
new file mode 100644
index 00000000..951c4539
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/Hamming16.java
@@ -0,0 +1,120 @@
+package io.github.dsheirer.edac;
+
+import io.github.dsheirer.bits.BinaryMessage;
+
+/**
+ * Hamming(16,11,5) Error detection and correction utility.
+ */
+public class Hamming16
+{
+ private static int[] CHECKSUMS = new int[]{0x13, 0x1A, 0x1F, 0x1C, 0x0E, 0x15, 0x0B, 0x16, 0x19, 0x0D, 0x07};
+
+ /**
+ * Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED)
+ *
+ * @param frame - binary frame containing a hamming(16,11,5) protected field
+ * @param startIndex - offset to the first bit of the field
+ * @return - 0 = no errors
+ * 1 = a single-bit error was detected and corrected
+ * 2 = two or more errors detected - no corrections made
+ */
+ public static int checkAndCorrect(BinaryMessage frame, int startIndex)
+ {
+ int syndrome = getSyndrome(frame, startIndex);
+
+ switch(syndrome)
+ {
+ case 0:
+ return 0;
+ case 1:
+ frame.flip(startIndex + 15); //Parity 1
+ return 1;
+ case 2:
+ frame.flip(startIndex + 14); //Parity 2
+ return 1;
+ case 3:
+ frame.flip(startIndex + 10); //Data 1
+ return 1;
+ case 4:
+ frame.flip(startIndex + 13); //Parity 4
+ return 1;
+ case 5:
+ frame.flip(startIndex + 9); //Data 2
+ return 1;
+ case 6:
+ frame.flip(startIndex + 8); //Data 3
+ return 1;
+ case 7:
+ frame.flip(startIndex + 7); //Data 4
+ return 1;
+ case 8:
+ frame.flip(startIndex + 12); //Parity 8
+ return 1;
+ case 9:
+ frame.flip(startIndex + 6); //Data 5
+ return 1;
+ case 10:
+ frame.flip(startIndex + 5); //Data 6
+ return 1;
+ case 11:
+ frame.flip(startIndex + 4); //Data 7
+ return 1;
+ case 12:
+ frame.flip(startIndex + 3); //Data 8
+ return 1;
+ case 13:
+ frame.flip(startIndex + 2); //Data 9
+ return 1;
+ case 14:
+ frame.flip(startIndex + 1); //Data 10
+ return 1;
+ case 15:
+ frame.flip(startIndex); //Data 11
+ return 1;
+ case 16:
+ frame.flip(startIndex + 11); //Parity 16
+ return 1;
+ default:
+ return 2;
+ }
+ }
+
+ /**
+ * Calculates the checksum (Parity 16,8,4,2,1) for data (11 <> 1 ) bits.
+ *
+ * @param frame - frame containing hamming protected word
+ * @param startIndex - start bit index of the hamming protected word
+ * @return parity value, 0 - 16
+ */
+ private static int calculateChecksum(BinaryMessage frame, int startIndex)
+ {
+ int calculated = 0; //Starting value
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = frame.nextSetBit(startIndex);
+ i >= startIndex && i < startIndex + 11;
+ i = frame.nextSetBit(i + 1))
+ {
+ calculated ^= CHECKSUMS[i - startIndex];
+ }
+
+ return calculated;
+ }
+
+ /**
+ * Calculates the syndrome - xor of the calculated checksum and the actual
+ * checksum.
+ *
+ * @param frame - binary frame containing a hamming(16,11,5) protected word
+ * @param startIndex - of bit 0 of the hamming protected word
+ * @return - 0 (no errors) or 1 (single bit error corrected)
+ */
+ private static int getSyndrome(BinaryMessage frame, int startIndex)
+ {
+ int calculated = calculateChecksum(frame, startIndex);
+
+ int checksum = frame.getInt(startIndex + 11, startIndex + 15);
+
+ return (checksum ^ calculated);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/Hamming17.java b/src/main/java/io/github/dsheirer/edac/Hamming17.java
new file mode 100644
index 00000000..86b17997
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/Hamming17.java
@@ -0,0 +1,123 @@
+package io.github.dsheirer.edac;
+
+import io.github.dsheirer.bits.BinaryMessage;
+
+/**
+ * Hamming(17,12,3) Error detection and correction utility.
+ */
+public class Hamming17
+{
+ private static int[] CHECKSUMS = new int[]{0x1B, 0x1F, 0x1D, 0x1C, 0x0E, 0x07, 0x11, 0x1A, 0x0D, 0x14, 0x0A, 0x05};
+
+ /**
+ * Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED)
+ *
+ * @param frame - binary frame containing a hamming(17,12,5) protected field
+ * @param startIndex - offset to the first bit of the field
+ * @return - 0 = no errors
+ * 1 = a single-bit error was detected and corrected
+ * 2 = two or more errors detected - no corrections made
+ */
+ public static int checkAndCorrect(BinaryMessage frame, int startIndex)
+ {
+ int syndrome = getSyndrome(frame, startIndex);
+
+ switch(syndrome)
+ {
+ case 0:
+ return 0;
+ case 1:
+ frame.flip(startIndex + 16); //Parity 1
+ return 1;
+ case 2:
+ frame.flip(startIndex + 15); //Parity 2
+ return 1;
+ case 3:
+ frame.flip(startIndex + 11); //Data 1
+ return 1;
+ case 4:
+ frame.flip(startIndex + 14); //Parity 4
+ return 1;
+ case 5:
+ frame.flip(startIndex + 10); //Data 2
+ return 1;
+ case 6:
+ frame.flip(startIndex + 9); //Data 3
+ return 1;
+ case 7:
+ frame.flip(startIndex + 8); //Data 4
+ return 1;
+ case 8:
+ frame.flip(startIndex + 13); //Parity 8
+ return 1;
+ case 9:
+ frame.flip(startIndex + 7); //Data 5
+ return 1;
+ case 10:
+ frame.flip(startIndex + 6); //Data 6
+ return 1;
+ case 11:
+ frame.flip(startIndex + 5); //Data 7
+ return 1;
+ case 12:
+ frame.flip(startIndex + 4); //Data 8
+ return 1;
+ case 13:
+ frame.flip(startIndex + 3); //Data 9
+ return 1;
+ case 14:
+ frame.flip(startIndex + 2); //Data 10
+ return 1;
+ case 15:
+ frame.flip(startIndex + 1); //Data 11
+ return 1;
+ case 16:
+ frame.flip(startIndex + 12); //Parity 16
+ return 1;
+ case 17:
+ frame.flip(startIndex); //Data 12
+ return 1;
+ default:
+ return 2;
+ }
+ }
+
+ /**
+ * Calculates the checksum (Parity 16,8,4,2,1) for data (12 <> 1 ) bits.
+ *
+ * @param frame - frame containing hamming protected word
+ * @param startIndex - start bit index of the hamming protected word
+ * @return parity value, 0 - 16
+ */
+ private static int calculateChecksum(BinaryMessage frame, int startIndex)
+ {
+ int calculated = 0; //Starting value
+
+ /* Iterate the set bits and XOR running checksum with lookup value */
+ for(int i = frame.nextSetBit(startIndex);
+ i >= startIndex && i < startIndex + 12;
+ i = frame.nextSetBit(i + 1))
+ {
+ calculated ^= CHECKSUMS[i - startIndex];
+ }
+
+ return calculated;
+ }
+
+ /**
+ * Calculates the syndrome - xor of the calculated checksum and the actual
+ * checksum.
+ *
+ * @param frame - binary frame containing a hamming(17,12,5) protected word
+ * @param startIndex - of bit 0 of the hamming protected word
+ * @return - 0 (no errors) or 1 (single bit error corrected)
+ */
+ private static int getSyndrome(BinaryMessage frame, int startIndex)
+ {
+ int calculated = calculateChecksum(frame, startIndex);
+
+ int checksum = frame.getInt(startIndex + 12, startIndex + 16);
+
+ return (checksum ^ calculated);
+ }
+}
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
new file mode 100644
index 00000000..b0caa4f4
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_12_9_4_DMR.java
@@ -0,0 +1,109 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+/**
+ * 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 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};
+ private static final int[] FLC_CODEWORD_3 = {24, 25, 26, 27, 28, 29, 30, 31};
+ private static final int[] FLC_CODEWORD_4 = {32, 33, 34, 35, 36, 37, 38, 39};
+ private static final int[] FLC_CODEWORD_5 = {40, 41, 42, 43, 44, 45, 46, 47};
+ private static final int[] FLC_CODEWORD_6 = {48, 49, 50, 51, 52, 53, 54, 55};
+ private static final int[] FLC_CODEWORD_7 = {56, 57, 58, 59, 60, 61, 62, 63};
+ private static final int[] FLC_CODEWORD_8 = {64, 65, 66, 67, 68, 69, 70, 71};
+ private static final int[] FLC_PARITY_0 = {72, 73, 74, 75, 76, 77, 78, 79};
+ private static final int[] FLC_PARITY_1 = {80, 81, 82, 83, 84, 85, 86, 87};
+ private static final int[] FLC_PARITY_2 = {88, 89, 90, 91, 92, 93, 94, 95};
+
+ /**
+ * Constructs an instance
+ */
+ public ReedSolomon_12_9_4_DMR()
+ {
+ //Note: the RS(12,9,4) is a shortened form of RS(255,252,4) over GF(8)
+ super(255, 252);
+ }
+
+ /**
+ * Attempts to correct the Full Link Control message returning true if there were zero or one correctable errors
+ * and returning false if there were two or more errors detected.
+ *
+ * Note: if one bit error was detected and corrected, the corrected bit count is set to 1 in the message
+ *
+ * @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.
+ */
+ 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[3] = message.getInt(FLC_CODEWORD_8);
+ input[4] = message.getInt(FLC_CODEWORD_7);
+ input[5] = message.getInt(FLC_CODEWORD_6);
+ input[6] = message.getInt(FLC_CODEWORD_5);
+ input[7] = message.getInt(FLC_CODEWORD_4);
+ input[8] = message.getInt(FLC_CODEWORD_3);
+ input[9] = message.getInt(FLC_CODEWORD_2);
+ input[10] = message.getInt(FLC_CODEWORD_1);
+ input[11] = message.getInt(FLC_CODEWORD_0);
+
+ try
+ {
+ boolean irrecoverableErrors = decode(input, output);
+
+ if(irrecoverableErrors)
+ {
+ return false;
+ }
+ }
+ catch(Exception e)
+ {
+ return false;
+ }
+
+ int pointer = 0;
+
+ for(int x = 11; x >= 0; x--)
+ {
+ if(output[x] != -1 && (output[x] != input[x]))
+ {
+ message.load(pointer, 8, output[x]);
+ message.setCorrectedBitCount(1);
+ return true;
+ }
+
+ pointer += 8;
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_12_13_P25.java
similarity index 64%
rename from src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17.java
rename to src/main/java/io/github/dsheirer/edac/ReedSolomon_24_12_13_P25.java
index ad2189a8..291ac0a3 100644
--- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17.java
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_12_13_P25.java
@@ -20,21 +20,18 @@
package io.github.dsheirer.edac;
-public class ReedSolomon_63_47_17 extends BerlekempMassey_63
+/**
+ * Reed Solomon RS(24,12,13) decoder over GF(6)
+ */
+public class ReedSolomon_24_12_13_P25 extends ReedSolomon_63_P25
{
/**
- * Reed-Solomon RS(63,47,17) decoder. This can also be used for error detection
- * and correction of the following RS codes:
+ * Constructs an instance.
*
- * RS(36,20,17) - max 8 errors
- * RS(24,16,9) - max 4 errors
- * RS(24,12,13) - max 6 errors
- *
- * The maximum correctable errors is determined by (n-k)/2, or hamming
- * distance divided by 2.
+ * Note: this is a shortened form of RS(63,51,13)
*/
- public ReedSolomon_63_47_17(int maximumCorrectableErrors)
+ public ReedSolomon_24_12_13_P25()
{
- super(maximumCorrectableErrors);
+ super(63,51);
}
}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_16_9_P25.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_16_9_P25.java
new file mode 100644
index 00000000..9c05281a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_24_16_9_P25.java
@@ -0,0 +1,37 @@
+/*
+ * ******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2018 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;
+
+/**
+ * Reed Solomon RS(24,16,9) decoder over GF(6)
+ */
+public class ReedSolomon_24_16_9_P25 extends ReedSolomon_63_P25
+{
+ /**
+ * Constructs an instance.
+ *
+ * Note: this is a shortened form of RS(63,55,9)
+ */
+ public ReedSolomon_24_16_9_P25()
+ {
+ super(63,55);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java
new file mode 100644
index 00000000..2f2e05c9
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_255_DMR.java
@@ -0,0 +1,44 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+/**
+ * Berlekemp Massey decoder for GF(8) using DMR Generator Polynomial
+ */
+public class ReedSolomon_255_DMR extends BerlekempMassey
+{
+ /**
+ * DMR Reed-Solomon code is generated from a Galois Field(2^8) by the polynomial: a8 + a4 + a3 + a2 + a0.
+ * In binary, this is expressed as: 100011101 which is reversed to big-endian format for this algorithm
+ * See: TS 102-361-1 paragraph B.3.6 Reed-Solomon(12,9)
+ */
+ public static final int[] DMR_GENERATOR_POLYNOMIAL = {1, 0, 1, 1, 1, 0, 0, 0, 1};
+
+ /**
+ * Constructs an instance of RS(total symbols, message symbols)
+ *
+ * @param n total symbols
+ * @param k message symbols
+ */
+ public ReedSolomon_255_DMR(int n, int k)
+ {
+ super(8, n, k, DMR_GENERATOR_POLYNOMIAL);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java
similarity index 61%
rename from src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29.java
rename to src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java
index d0155ad8..0c8119a3 100644
--- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29.java
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29_P25.java
@@ -22,21 +22,17 @@
package io.github.dsheirer.edac;
-public class ReedSolomon_63_35_29 extends BerlekempMassey_63
+/**
+ * Reed Solomon Decoder for P25 RS(44,16,29) protected messages.
+ */
+public class ReedSolomon_44_16_29_P25 extends ReedSolomon_63_P25
{
/**
- * Reed-Solomon RS(63,35,29) decoder. This can also be used for error detection and correction of the following
- * RS codes:
- *
- * RS(46,26,21) - max 10 errors = P25P2 IEMI
- * RS(45,26,20) - max 9 errors = P25P2 SOEMI (FACCH)
- * RS(52,30,23) - max 11 errors = P25P2 IOEMI (SACCH)
- * RS(44,16,29) - max 14 errors = P25P2 ESS
- *
- * The maximum correctable errors is determined by (n-k)/2, or hamming distance divided by 2.
+ * Constructs an instance
*/
- public ReedSolomon_63_35_29(int maximumCorrectableErrors)
+ public ReedSolomon_44_16_29_P25()
{
- super(maximumCorrectableErrors);
+ //The RS(44,16,29) is shortened from RS(63,35,29)
+ super(63,35);
}
}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29_P25.java
similarity index 78%
rename from src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29.java
rename to src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29_P25.java
index 14c4eb01..5d9bae07 100644
--- a/src/main/java/io/github/dsheirer/edac/ReedSolomon_44_16_29.java
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_35_29_P25.java
@@ -22,13 +22,16 @@
package io.github.dsheirer.edac;
-public class ReedSolomon_44_16_29 extends BerlekempMassey_63
+/**
+ * Reed Solomon decoder for APCO-25 RS(63,35,29) over GF(6)
+ */
+public class ReedSolomon_63_35_29_P25 extends ReedSolomon_63_P25
{
- //Hamming distance = 29, so max correctable errors = 29 / 2 = 14
- private static final int MAXIMUM_CORRECTABLE_ERRORS = 14;
-
- public ReedSolomon_44_16_29()
+ /**
+ * Reed-Solomon RS(63,35,29) decoder.
+ */
+ public ReedSolomon_63_35_29_P25()
{
- super(MAXIMUM_CORRECTABLE_ERRORS);
+ super(63, 35);
}
}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17_P25.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17_P25.java
new file mode 100644
index 00000000..e94e9736
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_47_17_P25.java
@@ -0,0 +1,32 @@
+/*
+ * ******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2018 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;
+
+public class ReedSolomon_63_47_17_P25 extends ReedSolomon_63_P25
+{
+ /**
+ * Reed-Solomon RS(63,47,17) decoder.
+ */
+ public ReedSolomon_63_47_17_P25()
+ {
+ super(63,47);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_P25.java b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_P25.java
new file mode 100644
index 00000000..38f61142
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/edac/ReedSolomon_63_P25.java
@@ -0,0 +1,44 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+/**
+ * Berlekemp Massey decoder using APCO-25 Generator Polynomial over GF(6)
+ */
+public class ReedSolomon_63_P25 extends BerlekempMassey
+{
+ /**
+ * APCO-25 Reed-Solomon code is generated from a Galois Field(2^6) by the polynomial: a6 + a1 + a0.
+ * In binary, this is expressed as: 1000011 which is reversed to big-endian format for this algorithm
+ * See: TIA 102-BAAA paragraph 5.9 Reed-Solomon Code Generator Matrices
+ */
+ public static final int[] P25_GENERATOR_POLYNOMIAL = { 1, 1, 0, 0, 0, 0, 1 };
+
+ /**
+ * Constructs an instance
+ *
+ * @param n total symbols
+ * @param k message symbols
+ */
+ public ReedSolomon_63_P25(int n, int k)
+ {
+ super(6, n, k, P25_GENERATOR_POLYNOMIAL);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java b/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java
index b04f90c8..6a797784 100644
--- a/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java
+++ b/src/main/java/io/github/dsheirer/edac/trellis/DMR_3_4_Node.java
@@ -83,4 +83,10 @@ public class DMR_3_4_Node extends Node
{
return DMR_TRANSITION_MATRIX[precedingNode.getInputValue()][getInputValue()];
}
+
+ @Override
+ public String toString()
+ {
+ return "IN:" + getInputValue() + " OUT:" + getTransmittedOutputValue();
+ }
}
diff --git a/src/main/java/io/github/dsheirer/edac/trellis/Path.java b/src/main/java/io/github/dsheirer/edac/trellis/Path.java
index e4b7eb7e..01d3798e 100644
--- a/src/main/java/io/github/dsheirer/edac/trellis/Path.java
+++ b/src/main/java/io/github/dsheirer/edac/trellis/Path.java
@@ -18,6 +18,8 @@
******************************************************************************/
package io.github.dsheirer.edac.trellis;
+import com.google.common.base.Joiner;
+
import java.util.ArrayList;
import java.util.List;
@@ -128,4 +130,10 @@ public class Path
{
return new Path(this);
}
+
+ @Override
+ public String toString()
+ {
+ return "ERR:" + getError() + " " + Joiner.on(",").join(mNodes);
+ }
}
diff --git a/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java b/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java
index 28c87cfd..380cb230 100644
--- a/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java
+++ b/src/main/java/io/github/dsheirer/edac/trellis/ViterbiDecoder_3_4_DMR.java
@@ -18,21 +18,77 @@
******************************************************************************/
package io.github.dsheirer.edac.trellis;
+import com.google.common.base.Joiner;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+/**
+ * Viterbi decoder for 3/4 rate trellis coded modulation (TCM) data blocks
+ */
public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder
{
+ private static final int[] DEINTERLEAVE_DIBITS = new int[]{0, 1, 8, 9, 16, 17, 24, 25, 32, 33, 40, 41, 48, 49, 56, 57, 64, 65, 72, 73, 80, 81,
+ 88, 89, 96, 97, 2, 3, 10, 11, 18, 19, 26, 27, 34, 35, 42, 43, 50, 51, 58, 59, 66, 67, 74, 75, 82, 83, 90, 91, 4, 5, 12, 13, 20, 21, 28, 29, 36, 37, 44, 45,
+ 52, 53, 60, 61, 68, 69, 76, 77, 84, 85, 92, 93, 6, 7, 14, 15, 22, 23, 30, 31, 38, 39, 46, 47, 54, 55, 62, 63, 70, 71, 78, 79, 86, 87, 94, 95};
+
+ private static final int[] DEINTERLEAVE_INDEXES = new int[]{0, 1, 2, 3, 16, 17, 18, 19, 32, 33, 34, 35, 48, 49, 50,
+ 51, 64, 65, 66, 67, 80, 81, 82, 83, 96, 97, 98, 99, 112, 113, 114, 115, 128, 129, 130, 131, 144, 145, 146, 147,
+ 160, 161, 162, 163, 176, 177, 178, 179, 192, 193, 194, 195, 4, 5, 6, 7, 20, 21, 22, 23, 36, 37, 38, 39, 52, 53,
+ 54, 55, 68, 69, 70, 71, 84, 85, 86, 87, 100, 101, 102, 103, 116, 117, 118, 119, 132, 133, 134, 135, 148, 149,
+ 150, 151, 164, 165, 166, 167, 180, 181, 182, 183, 8, 9, 10, 11, 24, 25, 26, 27, 40, 41, 42, 43, 56, 57, 58, 59,
+ 72, 73, 74, 75, 88, 89, 90, 91, 104, 105, 106, 107, 120, 121, 122, 123, 136, 137, 138, 139, 152, 153, 154, 155,
+ 168, 169, 170, 171, 184, 185, 186, 187, 12, 13, 14, 15, 28, 29, 30, 31, 44, 45, 46, 47, 60, 61, 62, 63, 76, 77,
+ 78, 79, 92, 93, 94, 95, 108, 109, 110, 111, 124, 125, 126, 127, 140, 141, 142, 143, 156, 157, 158, 159, 172,
+ 173, 174, 175, 188, 189, 190, 191};
+
/**
* Viterbi decoder for Digital Mobile Radio (DMR) 3/4 rate Trellis Coded Modulation (TCM) encoded messages.
*/
public ViterbiDecoder_3_4_DMR()
{
- super(3,4);
+ super(3, 4);
}
+ public static void main(String[] args)
+ {
+ List indexes = new ArrayList<>();
+ for(int i : DEINTERLEAVE_DIBITS)
+ {
+ indexes.add(i * 2);
+ indexes.add(i * 2 + 1);
+ }
+
+ System.out.println(Joiner.on(",").join(indexes));
+
+ Collections.sort(indexes);
+ System.out.println(Joiner.on(",").join(indexes));
+ }
+
+ /**
+ * Performs deinterleave of transmitted 3/4 TCM encoded payload
+ * @param interleaved
+ * @return
+ */
+ private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage interleaved)
+ {
+ CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(196);
+
+ for(int x = 0; x < 196; x++)
+ {
+ if(interleaved.get(x))
+ {
+ deinterleaved.set(DEINTERLEAVE_INDEXES[x]);
+ }
+ }
+
+ return deinterleaved;
+ }
+
+
/**
* Decodes a 3/4 rate trellis coded modulation (TCM) encoded DMR binary message containing 196 bits that have
* already been deinterleaved.
@@ -40,12 +96,11 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder
* @param encodedMessage to decode that has already been deinterleaved.
* @return decoded message
*/
- public CorrectedBinaryMessage decode(BinaryMessage encodedMessage)
+ public CorrectedBinaryMessage decode(CorrectedBinaryMessage encodedMessage)
{
- int[] symbols = getSymbols(encodedMessage);
-
+ CorrectedBinaryMessage deinterleaved = deinterleave(encodedMessage);
+ int[] symbols = getSymbols(deinterleaved);
Path mostLikelyPath = decode(symbols);
-
return getMessage(mostLikelyPath);
}
@@ -104,7 +159,7 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder
//Ensure we have an integral number of transmitted symbols (nibbles) in the message
if(encodedMessage.size() % getOutputBitLength() != 0)
{
- throw new IllegalArgumentException("Encoded message must contain an integral number of 4-bit symbols -" +
+ throw new IllegalArgumentException("Encoded message must contain an integral number of 4-bit symbols -" +
" message size: " + encodedMessage.size());
}
@@ -125,6 +180,7 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder
/**
* Creates a DMR node
+ *
* @param inputValue for the specific trellis node
* @param transmittedOutputValue the actual transmitted output value
* @return created node
@@ -154,6 +210,6 @@ public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder
@Override
protected Node createStartingNode()
{
- return new DMR_3_4_Node(0,0);
+ return new DMR_3_4_Node(0, 0);
}
}
diff --git a/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java b/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java
index ea34d7f7..f9d67644 100644
--- a/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java
+++ b/src/main/java/io/github/dsheirer/eventbus/MyEventBus.java
@@ -27,10 +27,11 @@ import com.google.common.eventbus.EventBus;
*/
public class MyEventBus
{
- private static final EventBus EVENT_BUS = new EventBus();
+ private static final EventBus GLOBAL_EVENT_BUS = new EventBus();
- public static EventBus getEventBus()
+ //TODO: this is primarily used by the Preference Service ... move this event bus to the Preference Service
+ public static EventBus getGlobalEventBus()
{
- return EVENT_BUS;
+ return GLOBAL_EVENT_BUS;
}
}
diff --git a/src/main/java/io/github/dsheirer/filter/FilterSet.java b/src/main/java/io/github/dsheirer/filter/FilterSet.java
index 6475778a..e5ecbd9b 100644
--- a/src/main/java/io/github/dsheirer/filter/FilterSet.java
+++ b/src/main/java/io/github/dsheirer/filter/FilterSet.java
@@ -18,6 +18,11 @@ public class FilterSet implements IFilter
{
mName = "Message Filter";
}
+
+ public FilterSet(IFilter filter)
+ {
+ addFilter(filter);
+ }
public String getName()
{
diff --git a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java
index a2b16902..accf5a29 100644
--- a/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java
+++ b/src/main/java/io/github/dsheirer/gui/JavaFxWindowManager.java
@@ -120,7 +120,7 @@ public class JavaFxWindowManager extends Application
private void setup()
{
//Register this class to receive events via each method annotated with @Subscribe
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
//Register JavaFX icon fonts
IconFontFX.register(jiconfont.icons.font_awesome.FontAwesome.getIconFont());
@@ -162,6 +162,8 @@ public class JavaFxWindowManager extends Application
*/
public void shutdown()
{
+ MyEventBus.getGlobalEventBus().unregister(this);
+
if(mChannelMapStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mChannelMapStage);
diff --git a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
index 030d0d04..b2691b74 100644
--- a/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
+++ b/src/main/java/io/github/dsheirer/gui/SDRTrunk.java
@@ -337,15 +337,15 @@ public class SDRTrunk implements Listener
JMenu viewMenu = new JMenu("View");
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
- viewPlaylistItem.addActionListener(e -> MyEventBus.getEventBus().post(new ViewPlaylistRequest()));
+ viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
viewMenu.add(viewPlaylistItem);
JMenuItem preferencesItem = new JMenuItem("User Preferences");
- preferencesItem.addActionListener(e -> MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest()));
+ preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
viewMenu.add(preferencesItem);
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
- settingsMenu.addActionListener(arg0 -> MyEventBus.getEventBus().post(new ViewIconManagerRequest()));
+ settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
viewMenu.add(settingsMenu);
JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/DMRPane.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DMRPane.java
new file mode 100644
index 00000000..53338a3a
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DMRPane.java
@@ -0,0 +1,201 @@
+/*
+ *
+ * * ******************************************************************************
+ * * 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
+ * * *****************************************************************************
+ *
+ *
+ */
+package io.github.dsheirer.gui.instrument.decoder;
+
+import io.github.dsheirer.gui.instrument.chart.ComplexSampleLineChart;
+import io.github.dsheirer.gui.instrument.chart.DoubleLineChart;
+import io.github.dsheirer.gui.instrument.chart.EyeDiagramChart;
+import io.github.dsheirer.gui.instrument.chart.PhaseLineChart;
+import io.github.dsheirer.gui.instrument.chart.SymbolChart;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.dmr.DMRDecoderInstrumented;
+import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DMRPane extends ComplexDecoderPane
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRPane.class);
+
+ private HBox mSampleChartBox;
+ private ComplexSampleLineChart mSampleLineChart;
+ private EyeDiagramChart mEyeDiagramChart;
+ private HBox mDecoderChartBox;
+ private SymbolChart mSymbolChart;
+ private PhaseLineChart mPLLPhaseErrorLineChart;
+ private DoubleLineChart mPLLFrequencyLineChart;
+ private DoubleLineChart mSamplesPerSymbolLineChart;
+ private ReusableBufferBroadcaster mFilteredBufferBroadcaster = new ReusableBufferBroadcaster();
+ private DMRDecoderInstrumented mDecoder = new DMRDecoderInstrumented(new DecodeConfigDMR());
+
+ public DMRPane()
+ {
+ super(DecoderType.P25_PHASE1);
+ init();
+ }
+
+ private void init()
+ {
+ addListener(getDecoder());
+
+ getDecoder().setFilteredBufferListener(mFilteredBufferBroadcaster);
+ getDecoder().setComplexSymbolListener(getSymbolChart());
+ getDecoder().setPLLPhaseErrorListener(getPLLPhaseErrorLineChart());
+ getDecoder().setPLLFrequencyListener(getPLLFrequencyLineChart());
+ getDecoder().setSymbolDecisionDataListener(getEyeDiagramChart());
+ getDecoder().setSamplesPerSymbolListener(getSamplesPerSymbolLineChart());
+ mFilteredBufferBroadcaster.addListener(getSampleLineChart());
+
+ HBox.setHgrow(getSampleChartBox(), Priority.ALWAYS);
+ HBox.setHgrow(getDecoderChartBox(), Priority.ALWAYS);
+ getChildren().addAll(getSampleChartBox(), getDecoderChartBox());
+
+ mDecoder.setMessageListener(new Listener()
+ {
+ @Override
+ public void receive(IMessage message)
+ {
+ mLog.debug(message.toString());
+ }
+ });
+
+ }
+
+ @Override
+ public void setSampleRate(double sampleRate)
+ {
+ mLog.debug("Configuring for sample rate: " + sampleRate);
+
+ mDecoder.setSampleRate(sampleRate);
+ double samplesPerSymbol = sampleRate / 4800.0;
+
+ getSampleLineChart().setSamplesPerSymbol((int) samplesPerSymbol);
+ }
+
+ private DMRDecoderInstrumented getDecoder()
+ {
+ return mDecoder;
+ }
+
+ private SymbolChart getSymbolChart()
+ {
+ if(mSymbolChart == null)
+ {
+ mSymbolChart = new SymbolChart(10);
+ }
+
+ return mSymbolChart;
+ }
+
+ private HBox getDecoderChartBox()
+ {
+ if(mDecoderChartBox == null)
+ {
+ mDecoderChartBox = new HBox();
+ mDecoderChartBox.setMaxHeight(Double.MAX_VALUE);
+ getSymbolChart().setMaxWidth(Double.MAX_VALUE);
+ getPLLPhaseErrorLineChart().setMaxWidth(Double.MAX_VALUE);
+ getPLLFrequencyLineChart().setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(getSymbolChart(), Priority.ALWAYS);
+ HBox.setHgrow(getPLLPhaseErrorLineChart(), Priority.ALWAYS);
+ HBox.setHgrow(getPLLFrequencyLineChart(), Priority.ALWAYS);
+ mDecoderChartBox.getChildren().addAll(getSymbolChart(), getPLLPhaseErrorLineChart(),
+ getPLLFrequencyLineChart());
+ }
+
+ return mDecoderChartBox;
+ }
+
+ private HBox getSampleChartBox()
+ {
+ if(mSampleChartBox == null)
+ {
+ mSampleChartBox = new HBox();
+ mSampleChartBox.setMaxHeight(Double.MAX_VALUE);
+ getSampleLineChart().setMaxWidth(Double.MAX_VALUE);
+ getEyeDiagramChart().setMaxWidth(Double.MAX_VALUE);
+ getSamplesPerSymbolLineChart().setMaxWidth(Double.MAX_VALUE);
+ HBox.setHgrow(getSampleLineChart(), Priority.ALWAYS);
+ HBox.setHgrow(getEyeDiagramChart(), Priority.ALWAYS);
+ HBox.setHgrow(getSamplesPerSymbolLineChart(), Priority.ALWAYS);
+ mSampleChartBox.getChildren().addAll(getSampleLineChart(), getEyeDiagramChart(),
+ getSamplesPerSymbolLineChart());
+ }
+
+ return mSampleChartBox;
+ }
+
+ private ComplexSampleLineChart getSampleLineChart()
+ {
+ if(mSampleLineChart == null)
+ {
+ mSampleLineChart = new ComplexSampleLineChart("Raw Samples", 100, 10);
+ }
+
+ return mSampleLineChart;
+ }
+
+ private EyeDiagramChart getEyeDiagramChart()
+ {
+ if(mEyeDiagramChart == null)
+ {
+ mEyeDiagramChart = new EyeDiagramChart(10, "Symbol: 3-4");
+ }
+
+ return mEyeDiagramChart;
+ }
+
+ private PhaseLineChart getPLLPhaseErrorLineChart()
+ {
+ if(mPLLPhaseErrorLineChart == null)
+ {
+ mPLLPhaseErrorLineChart = new PhaseLineChart(40);
+ }
+
+ return mPLLPhaseErrorLineChart;
+ }
+
+ private DoubleLineChart getPLLFrequencyLineChart()
+ {
+ if(mPLLFrequencyLineChart == null)
+ {
+ mPLLFrequencyLineChart = new DoubleLineChart("PLL Frequency", -500, 500, 50, 40);
+ }
+
+ return mPLLFrequencyLineChart;
+ }
+
+ private DoubleLineChart getSamplesPerSymbolLineChart()
+ {
+ if(mSamplesPerSymbolLineChart == null)
+ {
+ mSamplesPerSymbolLineChart = new DoubleLineChart("Sample Point", 9.5, 11.5, 0.1, 40);
+ }
+
+ return mSamplesPerSymbolLineChart;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java
index 5702f4da..15d4a195 100644
--- a/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java
+++ b/src/main/java/io/github/dsheirer/gui/instrument/decoder/DecoderPaneFactory.java
@@ -34,7 +34,8 @@ public class DecoderPaneFactory
DecoderType.LTR_NET,
DecoderType.MDC1200,
DecoderType.MPT1327,
- DecoderType.TAIT_1200);
+ DecoderType.TAIT_1200,
+ DecoderType.DMR);
/**
* Creates a decoder pane for the decoder type
@@ -59,6 +60,8 @@ public class DecoderPaneFactory
throw new IllegalArgumentException("Use the getP25P1DecoderPane() method for P25 decoder type");
case P25_PHASE2:
return new P25Phase2HDQPSKPane();
+ case DMR:
+ return new DMRPane();
}
return getDefaultPane();
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java
index 67b501c6..cd93cd62 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/PlaylistEditor.java
@@ -151,12 +151,12 @@ public class PlaylistEditor extends BorderPane
MenuItem iconManagerItem = new MenuItem("_Icon Manager");
iconManagerItem.setAccelerator(new KeyCodeCombination(KeyCode.I, KeyCombination.ALT_ANY));
- iconManagerItem.setOnAction(event -> MyEventBus.getEventBus().post(new ViewIconManagerRequest()));
+ iconManagerItem.setOnAction(event -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
viewMenu.getItems().add(iconManagerItem);
MenuItem userPreferenceItem = new MenuItem("_User Preferences");
userPreferenceItem.setAccelerator(new KeyCodeCombination(KeyCode.U, KeyCombination.ALT_ANY));
- userPreferenceItem.setOnAction(event -> MyEventBus.getEventBus()
+ userPreferenceItem.setOnAction(event -> MyEventBus.getGlobalEventBus()
.post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.TALKGROUP_FORMAT)));
viewMenu.getItems().add(userPreferenceItem);
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
index ac4c855d..82b9cc74 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasItemEditor.java
@@ -172,7 +172,7 @@ public class AliasItemEditor extends Editor
mPlaylistManager.getBroadcastModel().getConfiguredBroadcasts()
.addListener((ListChangeListener)c -> updateStreamViews());
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
setMaxWidth(Double.MAX_VALUE);
@@ -410,6 +410,7 @@ public class AliasItemEditor extends Editor
@Override
public void dispose()
{
+ MyEventBus.getGlobalEventBus().unregister(this);
}
private VBox getTitledPanesBox()
@@ -659,7 +660,7 @@ public class AliasItemEditor extends Editor
if(selected != null)
{
- MyEventBus.getEventBus().post(new ViewAliasIdentifierRequest(selected));
+ MyEventBus.getGlobalEventBus().post(new ViewAliasIdentifierRequest(selected));
}
}
});
@@ -687,6 +688,12 @@ public class AliasItemEditor extends Editor
p25Menu.getItems().add(new SeparatorMenuItem());
p25Menu.getItems().add(new AddTonesItem("Audio Tones (Phase 2 Only)"));
+ Menu dmrMenu = new ProtocolMenu(Protocol.DMR);
+ dmrMenu.getItems().add(new AddTalkgroupItem(Protocol.DMR));
+ dmrMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.DMR));
+ dmrMenu.getItems().add(new AddRadioIdItem(Protocol.DMR));
+ dmrMenu.getItems().add(new AddRadioIdRangeItem(Protocol.DMR));
+
Menu fleetsyncMenu = new ProtocolMenu(Protocol.FLEETSYNC);
fleetsyncMenu.getItems().add(new AddTalkgroupItem(Protocol.FLEETSYNC));
fleetsyncMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.FLEETSYNC));
@@ -715,8 +722,8 @@ public class AliasItemEditor extends Editor
Menu lojackMenu = new ProtocolMenu(Protocol.LOJACK);
lojackMenu.getItems().add(new AddLojackItem());
- mAddIdentifierButton.getItems().addAll(p25Menu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu, passportMenu,
- taitMenu, new SeparatorMenuItem(), lojackMenu);
+ mAddIdentifierButton.getItems().addAll(p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu,
+ passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
}
return mAddIdentifierButton;
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java
index 8be9febc..6124aea7 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/AliasViewByIdentifierEditor.java
@@ -178,7 +178,7 @@ public class AliasViewByIdentifierEditor extends VBox
if(selected != null)
{
- MyEventBus.getEventBus().post(new ViewAliasRequest(selected.getAlias()));
+ MyEventBus.getGlobalEventBus().post(new ViewAliasRequest(selected.getAlias()));
}
});
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java
index fd3d8ddd..96503008 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/RadioIdEditor.java
@@ -237,6 +237,10 @@ public class RadioIdEditor extends IdentifierEditor
"Format: 0 - 16777215"));
mRadioDetails.add(new RadioDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
"Format: 0 - FFFFFF"));
+ mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFFFF),
+ "Format: 0 - 16777215"));
+ mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
+ "Format: 0 - FFFFFF"));
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
"Format: 0 - 8388607"));
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
index 25fb0e95..a1045631 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/alias/identifier/TalkgroupEditor.java
@@ -239,6 +239,10 @@ public class TalkgroupEditor extends IdentifierEditor
"Format: 0 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
"Format: 0 - FFFF"));
+ mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFFFF),
+ "Format: 0 - 16,777,215"));
+ mTalkgroupDetails.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
+ "Format: 0 - FFFFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), "Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.LTR, IntegerFormat.FORMATTED, new LtrFormatter(0,0x3FFF),
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java
index 74290349..2744cf88 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditor.java
@@ -358,7 +358,7 @@ public abstract class ChannelConfigurationEditor extends Editor
if(optionalButtonType.isPresent() && optionalButtonType.get() == ButtonType.YES)
{
- MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY));
+ MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY));
return;
}
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java
index 8f7a733d..e0924b65 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/ChannelConfigurationEditorFactory.java
@@ -52,6 +52,8 @@ public class ChannelConfigurationEditorFactory
{
case AM:
return new AMConfigurationEditor(playlistManager, userPreferences);
+ case DMR:
+ return new DMRConfigurationEditor(playlistManager, userPreferences);
case NBFM:
return new NBFMConfigurationEditor(playlistManager, userPreferences);
case LTR_NET:
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java
new file mode 100644
index 00000000..bd91d8e6
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/DMRConfigurationEditor.java
@@ -0,0 +1,726 @@
+/*
+ *
+ * * ******************************************************************************
+ * * Copyright (C) 2014-2020 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.playlist.channel;
+
+import io.github.dsheirer.gui.control.IntegerTextField;
+import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor;
+import io.github.dsheirer.gui.playlist.record.RecordConfigurationEditor;
+import io.github.dsheirer.gui.playlist.source.FrequencyEditor;
+import io.github.dsheirer.gui.playlist.source.FrequencyField;
+import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
+import io.github.dsheirer.module.decode.config.DecodeConfiguration;
+import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
+import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
+import io.github.dsheirer.module.log.EventLogType;
+import io.github.dsheirer.module.log.config.EventLogConfiguration;
+import io.github.dsheirer.playlist.PlaylistManager;
+import io.github.dsheirer.preference.UserPreferences;
+import io.github.dsheirer.record.RecorderType;
+import io.github.dsheirer.record.config.RecordConfiguration;
+import io.github.dsheirer.source.config.SourceConfiguration;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TitledPane;
+import javafx.scene.control.Tooltip;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.util.Callback;
+import org.controlsfx.control.ToggleSwitch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * DMR channel configuration editor
+ */
+public class DMRConfigurationEditor extends ChannelConfigurationEditor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRConfigurationEditor.class);
+ private TitledPane mDecoderPane;
+ private TitledPane mEventLogPane;
+ private TitledPane mRecordPane;
+ private TitledPane mSourcePane;
+ private SourceConfigurationEditor mSourceConfigurationEditor;
+ private EventLogConfigurationEditor mEventLogConfigurationEditor;
+ private RecordConfigurationEditor mRecordConfigurationEditor;
+ private ToggleSwitch mIgnoreDataCallsButton;
+ private Spinner mTrafficChannelPoolSizeSpinner;
+ private TableView mTimeslotFrequencyTable;
+ private IntegerTextField mLogicalSlotNumberField;
+ private FrequencyField mDownlinkFrequencyField;
+// private FrequencyField mUplinkFrequencyField;
+ private Button mAddTimeslotFrequencyButton;
+ private Button mDeleteTimeslotFrequencyButton;
+ private Spinner mChannelRotationDelaySpinner;
+
+ /**
+ * Constructs an instance
+ * @param playlistManager
+ */
+ public DMRConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
+ {
+ super(playlistManager, userPreferences);
+ getTitledPanesBox().getChildren().add(getSourcePane());
+ getTitledPanesBox().getChildren().add(getDecoderPane());
+ getTitledPanesBox().getChildren().add(getEventLogPane());
+ getTitledPanesBox().getChildren().add(getRecordPane());
+ }
+
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.DMR;
+ }
+
+ private TitledPane getSourcePane()
+ {
+ if(mSourcePane == null)
+ {
+ mSourcePane = new TitledPane("Source", getSourceConfigurationEditor());
+ mSourcePane.setExpanded(true);
+ }
+
+ return mSourcePane;
+ }
+
+ private TitledPane getDecoderPane()
+ {
+ if(mDecoderPane == null)
+ {
+ mDecoderPane = new TitledPane();
+ mDecoderPane.setText("Decoder: DMR");
+ mDecoderPane.setExpanded(true);
+
+ GridPane gridPane = new GridPane();
+ gridPane.setPadding(new Insets(10,10,10,10));
+ gridPane.setHgap(10);
+ gridPane.setVgap(10);
+
+ int row = 0;
+
+ Label poolSizeLabel = new Label("Max Traffic Channels");
+ GridPane.setHalignment(poolSizeLabel, HPos.RIGHT);
+ GridPane.setConstraints(poolSizeLabel, 0, row);
+ gridPane.getChildren().add(poolSizeLabel);
+
+ GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 1, row);
+ gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner());
+
+ GridPane.setConstraints(getIgnoreDataCallsButton(), 2, row);
+ gridPane.getChildren().add(getIgnoreDataCallsButton());
+
+ Label directionLabel = new Label("Ignore Data Calls");
+ GridPane.setHalignment(directionLabel, HPos.LEFT);
+ GridPane.setConstraints(directionLabel, 3, row);
+ gridPane.getChildren().add(directionLabel);
+
+ Label timeslotTableLabel = new Label("Logical Slot Number (LSN) to Frequency Map. Required for: Connect Plus and Tier-III systems that don't use absolute frequencies");
+ GridPane.setHalignment(timeslotTableLabel, HPos.LEFT);
+ GridPane.setConstraints(timeslotTableLabel, 0, ++row, 4, 1);
+ gridPane.getChildren().add(timeslotTableLabel);
+
+ GridPane.setConstraints(getTimeslotTable(), 0, ++row, 4, 3);
+ gridPane.getChildren().add(getTimeslotTable());
+
+ VBox buttonsBox = new VBox();
+ buttonsBox.setAlignment(Pos.CENTER);
+ buttonsBox.setSpacing(10);
+ buttonsBox.getChildren().addAll(getAddTimeslotFrequencyButton(), getDeleteTimeslotFrequencyButton());
+
+ GridPane.setConstraints(buttonsBox, 4, row, 1, 3);
+ gridPane.getChildren().addAll(buttonsBox);
+
+ row += 3;
+
+ HBox editorBox = new HBox();
+ editorBox.setAlignment(Pos.CENTER_LEFT);
+ editorBox.setSpacing(5);
+
+ Label lsnLabel = new Label("LSN");
+ editorBox.getChildren().addAll(lsnLabel,getLogicalSlotNumberField());
+
+ Label downlinkLabel = new Label("Frequency (MHz)");
+ downlinkLabel.setPadding(new Insets(0,0,0,5));
+ editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
+
+// Label uplinkLabel = new Label("Uplink");
+// uplinkLabel.setPadding(new Insets(0,0,0,5));
+// editorBox.getChildren().addAll(uplinkLabel,getUplinkFrequencyField());
+
+ GridPane.setConstraints(editorBox, 0, row, 4, 1);
+ gridPane.getChildren().add(editorBox);
+
+ mDecoderPane.setContent(gridPane);
+ }
+
+ return mDecoderPane;
+ }
+
+ private TitledPane getEventLogPane()
+ {
+ if(mEventLogPane == null)
+ {
+ mEventLogPane = new TitledPane("Logging", getEventLogConfigurationEditor());
+ mEventLogPane.setExpanded(false);
+ }
+
+ return mEventLogPane;
+ }
+
+ private TitledPane getRecordPane()
+ {
+ if(mRecordPane == null)
+ {
+ mRecordPane = new TitledPane();
+ mRecordPane.setText("Recording");
+ mRecordPane.setExpanded(false);
+
+ Label notice = new Label("Note: use aliases to control call audio recording");
+ notice.setPadding(new Insets(10, 10, 0, 10));
+
+ VBox vBox = new VBox();
+ vBox.getChildren().addAll(getRecordConfigurationEditor(), notice);
+
+ mRecordPane.setContent(vBox);
+ }
+
+ return mRecordPane;
+ }
+
+ private SourceConfigurationEditor getSourceConfigurationEditor()
+ {
+ if(mSourceConfigurationEditor == null)
+ {
+ mSourceConfigurationEditor = new FrequencyEditor(getTunerModel(), true);
+
+ //Add a listener so that we can push change notifications up to this editor
+ mSourceConfigurationEditor.modifiedProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mSourceConfigurationEditor;
+ }
+
+ private EventLogConfigurationEditor getEventLogConfigurationEditor()
+ {
+ if(mEventLogConfigurationEditor == null)
+ {
+ List types = new ArrayList<>();
+ types.add(EventLogType.CALL_EVENT);
+ types.add(EventLogType.DECODED_MESSAGE);
+ types.add(EventLogType.TRAFFIC_CALL_EVENT);
+ types.add(EventLogType.TRAFFIC_DECODED_MESSAGE);
+
+ mEventLogConfigurationEditor = new EventLogConfigurationEditor(types);
+ mEventLogConfigurationEditor.setPadding(new Insets(5,5,5,5));
+ mEventLogConfigurationEditor.modifiedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mEventLogConfigurationEditor;
+ }
+
+ private TableView getTimeslotTable()
+ {
+ if(mTimeslotFrequencyTable == null)
+ {
+ mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor()));
+ mTimeslotFrequencyTable.setPrefHeight(100.0);
+
+ TableColumn numberColumn = new TableColumn("LSN");
+ numberColumn.setPrefWidth(70);
+ numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty());
+ mTimeslotFrequencyTable.getColumns().addAll(numberColumn);
+ mTimeslotFrequencyTable.getSortOrder().add(numberColumn);
+
+ TableColumn downlinkColumn = new TableColumn("Frequency (MHz)");
+ downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz());
+ downlinkColumn.setPrefWidth(175);
+ mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn);
+
+// TableColumn uplinkColumn = new TableColumn("Uplink (MHz)");
+// uplinkColumn.setCellValueFactory(cellData -> cellData.getValue().getUplinkMHz());
+// mTimeslotFrequencyTable.getColumns().addAll(uplinkColumn);
+
+ mTimeslotFrequencyTable.getSelectionModel().selectedItemProperty()
+ .addListener((observable, oldValue, newValue) -> setTimeslot(newValue));
+ }
+
+ return mTimeslotFrequencyTable;
+ }
+
+ /**
+ * Sets the specified timeslot frequency into the editor
+ */
+ private void setTimeslot(TimeslotFrequency timeslot)
+ {
+ //Preserve the current modified flag state since setting values in the editor will change it.
+ boolean modified = modifiedProperty().get();
+
+ getLogicalSlotNumberField().setDisable(timeslot == null);
+ getDownlinkFrequencyField().setDisable(timeslot == null);
+// getUplinkFrequencyField().setDisable(timeslot == null);
+ getDeleteTimeslotFrequencyButton().setDisable(timeslot == null);
+
+ if(timeslot != null)
+ {
+ getLogicalSlotNumberField().set(timeslot.getNumber());
+ getDownlinkFrequencyField().set(timeslot.getDownlinkFrequency());
+// getUplinkFrequencyField().set(timeslot.getUplinkFrequency());
+ }
+ else
+ {
+ getLogicalSlotNumberField().set(0);
+ getDownlinkFrequencyField().set(0);
+// getUplinkFrequencyField().set(0);
+ }
+
+ modifiedProperty().set(modified);
+ }
+
+ private Button getAddTimeslotFrequencyButton()
+ {
+ if(mAddTimeslotFrequencyButton == null)
+ {
+ mAddTimeslotFrequencyButton = new Button("Add");
+ mAddTimeslotFrequencyButton.setMaxWidth(Double.MAX_VALUE);
+ mAddTimeslotFrequencyButton.setOnAction(event -> addTimeslot());
+ }
+
+ return mAddTimeslotFrequencyButton;
+ }
+
+ /**
+ * Adds a new timeslot frequency value and makes a best guess of the next sequential LSN number
+ */
+ private void addTimeslot()
+ {
+ int lsn = 1;
+
+ while(hasLSN(lsn) && lsn <= 64) //64 is an arbitrary value to keep it from going too high
+ {
+ lsn++;
+ }
+
+ TimeslotFrequency timeslotFrequency = new TimeslotFrequency();
+ timeslotFrequency.setNumber(lsn);
+ getTimeslotTable().getItems().add(timeslotFrequency);
+ getTimeslotTable().scrollTo(timeslotFrequency);
+ getTimeslotTable().getSelectionModel().select(timeslotFrequency);
+ modifiedProperty().set(true);
+ }
+
+ /**
+ * Searches the current timeslot frequency list to determine if the specified lsn is already listed
+ */
+ private boolean hasLSN(int lsn)
+ {
+ for(TimeslotFrequency timeslotFrequency: getTimeslotTable().getItems())
+ {
+ if(timeslotFrequency.getNumber() == lsn)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private Button getDeleteTimeslotFrequencyButton()
+ {
+ if(mDeleteTimeslotFrequencyButton == null)
+ {
+ mDeleteTimeslotFrequencyButton = new Button("Delete");
+ mDeleteTimeslotFrequencyButton.setDisable(true);
+ mDeleteTimeslotFrequencyButton.setMaxWidth(Double.MAX_VALUE);
+ mDeleteTimeslotFrequencyButton.setOnAction(new EventHandler()
+ {
+ @Override
+ public void handle(ActionEvent event)
+ {
+ TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
+
+ if(selected != null)
+ {
+ getTimeslotTable().getItems().remove(selected);
+ modifiedProperty().set(true);
+ }
+ }
+ });
+ }
+
+ return mDeleteTimeslotFrequencyButton;
+ }
+
+ private IntegerTextField getLogicalSlotNumberField()
+ {
+ if(mLogicalSlotNumberField == null)
+ {
+ mLogicalSlotNumberField = new IntegerTextField();
+ mLogicalSlotNumberField.setDisable(true);
+ mLogicalSlotNumberField.setPrefWidth(65);
+ mLogicalSlotNumberField.textProperty().addListener((observable, oldValue, newValue) -> {
+ TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
+
+ if(selected != null)
+ {
+ Integer value = mLogicalSlotNumberField.get();
+
+ if(value != null)
+ {
+ selected.setNumber(value);
+ }
+ }
+
+ modifiedProperty().set(true);
+ });
+ }
+
+ return mLogicalSlotNumberField;
+ }
+
+ private FrequencyField getDownlinkFrequencyField()
+ {
+ if(mDownlinkFrequencyField == null)
+ {
+ mDownlinkFrequencyField = new FrequencyField();
+ mDownlinkFrequencyField.setDisable(true);
+ mDownlinkFrequencyField.textProperty().addListener(new ChangeListener()
+ {
+ @Override
+ public void changed(ObservableValue extends String> observable, String oldValue, String newValue)
+ {
+ TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
+
+ if(selected != null)
+ {
+ selected.setDownlinkFrequency(mDownlinkFrequencyField.get());
+ }
+
+ modifiedProperty().set(true);
+ }
+ });
+ }
+
+ return mDownlinkFrequencyField;
+ }
+
+// private FrequencyField getUplinkFrequencyField()
+// {
+// if(mUplinkFrequencyField == null)
+// {
+// mUplinkFrequencyField = new FrequencyField();
+// mUplinkFrequencyField.setDisable(true);
+// mUplinkFrequencyField.textProperty().addListener(new ChangeListener()
+// {
+// @Override
+// public void changed(ObservableValue extends String> observable, String oldValue, String newValue)
+// {
+// TimeslotFrequency selected = getTimeslotTable().getSelectionModel().getSelectedItem();
+//
+// if(selected != null)
+// {
+// selected.setUplinkFrequency(mUplinkFrequencyField.get());
+// int lsn = selected.getNumber();
+// selected.setNumber(-1);
+// selected.setNumber(lsn);
+// }
+//
+// modifiedProperty().set(true);
+// }
+// });
+// }
+//
+// return mUplinkFrequencyField;
+// }
+
+ private ToggleSwitch getIgnoreDataCallsButton()
+ {
+ if(mIgnoreDataCallsButton == null)
+ {
+ mIgnoreDataCallsButton = new ToggleSwitch();
+ mIgnoreDataCallsButton.setDisable(true);
+ mIgnoreDataCallsButton.selectedProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mIgnoreDataCallsButton;
+ }
+
+ private Spinner getTrafficChannelPoolSizeSpinner()
+ {
+ if(mTrafficChannelPoolSizeSpinner == null)
+ {
+ mTrafficChannelPoolSizeSpinner = new Spinner();
+ mTrafficChannelPoolSizeSpinner.setDisable(true);
+ mTrafficChannelPoolSizeSpinner.setTooltip(
+ new Tooltip("Maximum number of traffic channels that can be created by the decoder"));
+ mTrafficChannelPoolSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
+ SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 50);
+ mTrafficChannelPoolSizeSpinner.setValueFactory(svf);
+ mTrafficChannelPoolSizeSpinner.getValueFactory().valueProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mTrafficChannelPoolSizeSpinner;
+ }
+
+ /**
+ * Channel rotation monitor delay value. This dictates how long the decoder will remain on each frequency before
+ * rotating to the next frequency in the list
+ * @return spinner
+ */
+ private Spinner getChannelRotationDelaySpinner()
+ {
+ if(mChannelRotationDelaySpinner == null)
+ {
+ mChannelRotationDelaySpinner = new Spinner();
+ mChannelRotationDelaySpinner.setDisable(true);
+ mChannelRotationDelaySpinner.setTooltip(
+ new Tooltip("Delay on each frequency before rotating to next when seeking to next active channel frequency"));
+ mChannelRotationDelaySpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
+ SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(200, 2000, 200, 50);
+ mChannelRotationDelaySpinner.setValueFactory(svf);
+ mChannelRotationDelaySpinner.getValueFactory().valueProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mChannelRotationDelaySpinner;
+ }
+
+ private RecordConfigurationEditor getRecordConfigurationEditor()
+ {
+ if(mRecordConfigurationEditor == null)
+ {
+ List types = new ArrayList<>();
+ types.add(RecorderType.BASEBAND);
+ types.add(RecorderType.DEMODULATED_BIT_STREAM);
+ types.add(RecorderType.MBE_CALL_SEQUENCE);
+ types.add(RecorderType.TRAFFIC_BASEBAND);
+ types.add(RecorderType.TRAFFIC_DEMODULATED_BIT_STREAM);
+ types.add(RecorderType.TRAFFIC_MBE_CALL_SEQUENCE);
+ mRecordConfigurationEditor = new RecordConfigurationEditor(types);
+ mRecordConfigurationEditor.setDisable(true);
+ mRecordConfigurationEditor.modifiedProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mRecordConfigurationEditor;
+ }
+
+ @Override
+ protected void setDecoderConfiguration(DecodeConfiguration config)
+ {
+ getIgnoreDataCallsButton().setDisable(config == null);
+ getTrafficChannelPoolSizeSpinner().setDisable(config == null);
+ getTimeslotTable().getItems().clear();
+ getTimeslotTable().setDisable(config == null);
+ getAddTimeslotFrequencyButton().setDisable(config == null);
+ getDeleteTimeslotFrequencyButton().setDisable(true);
+ getLogicalSlotNumberField().set(0);
+ getLogicalSlotNumberField().setDisable(true);
+ getDownlinkFrequencyField().set(0);
+ getDownlinkFrequencyField().setDisable(true);
+// getUplinkFrequencyField().set(0);
+// getUplinkFrequencyField().setDisable(true);
+ getChannelRotationDelaySpinner().setDisable(config == null);
+
+ if(config instanceof DecodeConfigDMR)
+ {
+ DecodeConfigDMR decodeConfig = (DecodeConfigDMR)config;
+
+ getIgnoreDataCallsButton().setSelected(decodeConfig.getIgnoreDataCalls());
+ getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(decodeConfig.getTrafficChannelPoolSize());
+
+ for(TimeslotFrequency timeslotFrequency: decodeConfig.getTimeslotMap())
+ {
+ getTimeslotTable().getItems().add(timeslotFrequency.copy());
+ }
+ }
+ else
+ {
+ getIgnoreDataCallsButton().setSelected(false);
+ getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(0);
+ getChannelRotationDelaySpinner().getValueFactory().setValue(200);
+ }
+ }
+
+ @Override
+ protected void saveDecoderConfiguration()
+ {
+ DecodeConfigDMR config;
+
+ if(getItem().getDecodeConfiguration() instanceof DecodeConfigDMR)
+ {
+ config = (DecodeConfigDMR)getItem().getDecodeConfiguration();
+ }
+ else
+ {
+ config = new DecodeConfigDMR();
+ }
+
+// config.setChannelRotationDelay(getChannelRotationDelaySpinner().getValue());
+ config.setIgnoreDataCalls(getIgnoreDataCallsButton().isSelected());
+ config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
+ config.setTimeslotMap(new ArrayList<>(getTimeslotTable().getItems()));
+ getItem().setDecodeConfiguration(config);
+ }
+
+ @Override
+ protected void setEventLogConfiguration(EventLogConfiguration config)
+ {
+ getEventLogConfigurationEditor().setItem(config);
+ }
+
+ @Override
+ protected void saveEventLogConfiguration()
+ {
+ getEventLogConfigurationEditor().save();
+
+ if(getEventLogConfigurationEditor().getItem().getLoggers().isEmpty())
+ {
+ getItem().setEventLogConfiguration(null);
+ }
+ else
+ {
+ getItem().setEventLogConfiguration(getEventLogConfigurationEditor().getItem());
+ }
+ }
+
+ @Override
+ protected void setAuxDecoderConfiguration(AuxDecodeConfiguration config)
+ {
+ //no-op
+ }
+
+ @Override
+ protected void saveAuxDecoderConfiguration()
+ {
+ //no-op
+ }
+
+ @Override
+ protected void setRecordConfiguration(RecordConfiguration config)
+ {
+ getRecordConfigurationEditor().setDisable(config == null);
+ getRecordConfigurationEditor().setItem(config);
+ }
+
+ @Override
+ protected void saveRecordConfiguration()
+ {
+ getRecordConfigurationEditor().save();
+ RecordConfiguration config = getRecordConfigurationEditor().getItem();
+ getItem().setRecordConfiguration(config);
+ }
+
+ @Override
+ protected void setSourceConfiguration(SourceConfiguration config)
+ {
+ getSourceConfigurationEditor().setSourceConfiguration(config);
+ }
+
+ @Override
+ protected void saveSourceConfiguration()
+ {
+ getSourceConfigurationEditor().save();
+ SourceConfiguration sourceConfiguration = getSourceConfigurationEditor().getSourceConfiguration();
+ getItem().setSourceConfiguration(sourceConfiguration);
+ }
+
+ /**
+ * Channel tuner channel source frequencies value factory
+ */
+ public class FrequencyCellValueFactory implements Callback,
+ ObservableValue>
+ {
+ private SimpleStringProperty mFrequency = new SimpleStringProperty();
+ private boolean mIsDownlink;
+
+ public FrequencyCellValueFactory(boolean isDownlink)
+ {
+ mIsDownlink = isDownlink;
+ }
+
+ @Override
+ public ObservableValue call(TableColumn.CellDataFeatures param)
+ {
+ if(param.getValue() != null)
+ {
+ long frequency = (mIsDownlink ? param.getValue().getDownlinkFrequency() : param.getValue().getUplinkFrequency());
+ mFrequency.set(String.valueOf(frequency / 1E6));
+ }
+ else
+ {
+ mFrequency.set(null);
+ }
+
+ return mFrequency;
+ }
+ }
+
+ public class DownlinkPropertyValueFactory extends PropertyValueFactory
+ {
+ private StringProperty mStringProperty = new SimpleStringProperty();
+
+ public DownlinkPropertyValueFactory()
+ {
+ super("downlinkFrequency");
+ }
+
+ @Override
+ public ObservableValue call(TableColumn.CellDataFeatures param)
+ {
+ if(param.getValue() != null)
+ {
+ mStringProperty.set(String.valueOf(param.getValue().getDownlinkFrequency() / 1E6));
+ }
+ else
+ {
+ mStringProperty.setValue(null);
+ }
+
+ return mStringProperty;
+ }
+ }
+
+}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java
index 2fbd8a33..800e2fab 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/channel/MPT1327ConfigurationEditor.java
@@ -276,7 +276,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
channelMapName = getChannelMapComboBox().getSelectionModel().getSelectedItem().getName();
}
- MyEventBus.getEventBus().post(new ViewChannelMapEditorRequest(channelMapName));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelMapEditorRequest(channelMapName));
}
});
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java
index 4fe29864..c7be9524 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/manager/PlaylistManagerEditor.java
@@ -94,7 +94,7 @@ public class PlaylistManagerEditor extends HBox
mUserPreferences = userPreferences;
//Register to receive preferences updates
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
setPadding(new Insets(5, 5, 5, 5));
HBox.setHgrow(getPlaylistTableView(), Priority.ALWAYS);
@@ -102,6 +102,11 @@ public class PlaylistManagerEditor extends HBox
updateButtons();
}
+ public void dispose()
+ {
+ MyEventBus.getGlobalEventBus().unregister(this);
+ }
+
/**
* Indicates if the path argument is the same as the current playlist path
*/
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java
index 2ec7bbd6..90b76992 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/FrequencyEditor.java
@@ -376,7 +376,7 @@ public class FrequencyEditor extends VBox
if(getShowCreatedChannelCheckBox().selectedProperty().get())
{
- MyEventBus.getEventBus().post(new ViewChannelRequest(channel));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel));
}
else
{
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java
index a04335e6..80723b79 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/ModeDecoderType.java
@@ -31,7 +31,7 @@ public enum ModeDecoderType
{
AM("am", DecoderType.AM),
APCO25("p25", DecoderType.P25_PHASE1),
- DMR("dmr", null),
+ DMR("dmr", DecoderType.DMR),
DSTAR("d-star", null),
FM("fm", DecoderType.NBFM),
FMN("fmn", DecoderType.NBFM),
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
index 4894fcb1..6477ea1c 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/RadioReferenceDecoder.java
@@ -24,6 +24,7 @@ import io.github.dsheirer.identifier.talkgroup.LTRTalkgroup;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.identifier.talkgroup.UnknownTalkgroupIdentifier;
import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.mpt1327.identifier.MPT1327Talkgroup;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup;
@@ -33,7 +34,9 @@ import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormat
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.rrapi.type.Flavor;
import io.github.dsheirer.rrapi.type.Site;
+import io.github.dsheirer.rrapi.type.SiteFrequency;
import io.github.dsheirer.rrapi.type.System;
+import io.github.dsheirer.rrapi.type.SystemInformation;
import io.github.dsheirer.rrapi.type.Tag;
import io.github.dsheirer.rrapi.type.Talkgroup;
import io.github.dsheirer.rrapi.type.Type;
@@ -42,6 +45,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -228,6 +232,19 @@ public class RadioReferenceDecoder
return null;
}
+ /**
+ * Provides the radio reference Type for the system information instance
+ */
+ public Type getType(SystemInformation systemInformation)
+ {
+ if(systemInformation != null)
+ {
+ return mTypeMap.get(systemInformation.getTypeId());
+ }
+
+ return null;
+ }
+
/**
* Provides the radio reference Flavor for the system
*/
@@ -241,6 +258,19 @@ public class RadioReferenceDecoder
return null;
}
+ /**
+ * Provides the radio reference Flavor for the system information instance
+ */
+ public Flavor getFlavor(SystemInformation systemInformation)
+ {
+ if(systemInformation != null)
+ {
+ return mFlavorMap.get(systemInformation.getFlavorId());
+ }
+
+ return null;
+ }
+
/**
* Provides the radio reference voice/traffic channel protocol for the system
*/
@@ -302,6 +332,66 @@ public class RadioReferenceDecoder
return false;
}
+ /**
+ * Indicates if the specified site is a DMR Connect Plus site that has site frequencies that can be converted
+ * to timeslot frequencies
+ * @param systemInformation to determine the system type
+ * @param site containing site frequencies
+ */
+ public boolean hasTimeslotFrequencies(SystemInformation systemInformation, Site site)
+ {
+ Type type = getType(systemInformation);
+ Flavor flavor = getFlavor(systemInformation);
+ return type != null && flavor != null && type.getName().contains("DMR") && !site.getSiteFrequencies().isEmpty();
+ }
+
+ /**
+ * Creates a list of timeslot to frequency mappings from the radio reference site's list of site frequencies for a
+ * MotoTRBO Connect Plus site
+ * @param systemInformation to detect the system type (DMR) and flavor (Connect Plus).
+ * @param site containing 1 or more site frequencies
+ * @return list of timeslot frequency mappings or an empty list.
+ */
+ public List getTimeslotFrequencies(SystemInformation systemInformation, Site site)
+ {
+ if(hasTimeslotFrequencies(systemInformation, site))
+ {
+ List frequencies = new ArrayList<>();
+
+ for(SiteFrequency siteFrequency: site.getSiteFrequencies())
+ {
+ int lcn = siteFrequency.getLogicalChannelNumber();
+
+ if(siteFrequency.getChannelId() != null)
+ {
+ try
+ {
+ lcn = Integer.parseInt(siteFrequency.getChannelId());
+ }
+ catch(Exception e)
+ {
+ //Do nothing, we couldn't parse the LSN from the channel ID value
+ }
+ }
+
+ int lsn = (lcn - 1) * 2 + 1;
+ TimeslotFrequency timeslot1Frequency = new TimeslotFrequency();
+ timeslot1Frequency.setNumber(lsn);
+ timeslot1Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
+ frequencies.add(timeslot1Frequency);
+
+ TimeslotFrequency timeslot2Frequency = new TimeslotFrequency();
+ timeslot2Frequency.setNumber(lsn + 1);
+ timeslot2Frequency.setDownlinkFrequency((long)(siteFrequency.getFrequency() * 1E6));
+ frequencies.add(timeslot2Frequency);
+ }
+
+ return frequencies;
+ }
+
+ return Collections.emptyList();
+ }
+
/**
* Identifies the sdrtrunk protocol used by the system.
* @param system to identify
@@ -320,6 +410,8 @@ public class RadioReferenceDecoder
switch(type.getName())
{
+ case "DMR":
+ return Protocol.DMR;
case "LTR":
if(flavor.getName().contentEquals("Standard") || flavor.getName().contentEquals("Net"))
{
@@ -341,7 +433,6 @@ public class RadioReferenceDecoder
return Protocol.APCO25;
}
break;
- case "DMR":
case "NXDN":
case "EDACS":
case "TETRA":
@@ -371,6 +462,8 @@ public class RadioReferenceDecoder
{
switch(type.getName())
{
+ case "DMR":
+ return DecoderType.DMR;
case "LTR":
if(flavor.getName().contentEquals("Net"))
{
@@ -403,7 +496,6 @@ public class RadioReferenceDecoder
return DecoderType.P25_PHASE1;
}
break;
- case "DMR":
case "NXDN":
case "EDACS":
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java
index c79f43d7..498d4bc9 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SiteEditor.java
@@ -27,6 +27,8 @@ import io.github.dsheirer.gui.playlist.channel.ViewChannelRequest;
import io.github.dsheirer.module.decode.DecoderFactory;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
+import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
+import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1;
import io.github.dsheirer.module.decode.p25.phase1.P25P1Decoder;
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
@@ -222,6 +224,12 @@ public class SiteEditor extends GridPane
switch(decoderType)
{
+ case DMR:
+ DecodeConfigDMR dmr = new DecodeConfigDMR();
+ List timeslotFrequencies = mRadioReferenceDecoder
+ .getTimeslotFrequencies(systemInformation, site);
+ dmr.setTimeslotMap(timeslotFrequencies);
+ return dmr;
case P25_PHASE1:
DecodeConfiguration p1config = DecoderFactory.getDecodeConfiguration(decoderType);
@@ -290,6 +298,7 @@ public class SiteEditor extends GridPane
}
}
+
public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
{
mCurrentSite = site;
@@ -473,7 +482,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected() && gotoChannel != null)
{
- MyEventBus.getEventBus().post(new ViewChannelRequest(gotoChannel));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(gotoChannel));
}
}
@@ -535,7 +544,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected())
{
- MyEventBus.getEventBus().post(new ViewChannelRequest(channel));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel));
}
}
}
@@ -609,7 +618,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected())
{
- MyEventBus.getEventBus().post(new ViewChannelRequest(channel));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel));
}
}
else
@@ -637,7 +646,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected() && gotoChannel != null)
{
- MyEventBus.getEventBus().post(new ViewChannelRequest(gotoChannel));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(gotoChannel));
}
}
}
@@ -701,7 +710,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected())
{
- MyEventBus.getEventBus().post(new ViewChannelRequest(channel));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel));
}
}
else
@@ -730,7 +739,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected() && gotoChannel != null)
{
- MyEventBus.getEventBus().post(new ViewChannelRequest(gotoChannel));
+ MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(gotoChannel));
}
}
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java
index eff5eb3e..15ecdc6c 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/SystemTalkgroupSelectionEditor.java
@@ -105,7 +105,7 @@ public class SystemTalkgroupSelectionEditor extends GridPane
public SystemTalkgroupSelectionEditor(UserPreferences userPreferences, PlaylistManager playlistManager)
{
//Register to receive flash alias box requests
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
mUserPreferences = userPreferences;
mPlaylistManager = playlistManager;
@@ -172,6 +172,11 @@ public class SystemTalkgroupSelectionEditor extends GridPane
getChildren().add(getTalkgroupEditor());
}
+ public void dispose()
+ {
+ MyEventBus.getGlobalEventBus().unregister(this);
+ }
+
public void clear()
{
mTalkgroupList.clear();
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java
index 02b48a2d..5f7ec8ef 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/radioreference/TalkgroupEditor.java
@@ -278,7 +278,7 @@ public class TalkgroupEditor extends GridPane
alert.setHeaderText("An alias list is required to create aliases");
alert.initOwner((getCreateAliasButton()).getScene().getWindow());
alert.showAndWait();
- MyEventBus.getEventBus().post(new FlashAliasListComboBoxRequest());
+ MyEventBus.getGlobalEventBus().post(new FlashAliasListComboBoxRequest());
}
else if(mRadioReferenceDecoder != null && mTalkgroup != null && mSystem != null)
{
@@ -309,7 +309,7 @@ public class TalkgroupEditor extends GridPane
mEditAliasButton.setOnAction(event -> {
if(mAlias != null)
{
- MyEventBus.getEventBus().post(new ViewAliasRequest(mAlias));
+ MyEventBus.getGlobalEventBus().post(new ViewAliasRequest(mAlias));
}
});
}
diff --git a/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java b/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java
index 92a0a216..dd879f67 100644
--- a/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/playlist/source/FrequencyEditor.java
@@ -26,8 +26,12 @@ import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.TunerModel;
+import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor;
+import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -36,6 +40,9 @@ import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.SpinnerValueFactory;
+import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.slf4j.Logger;
@@ -54,9 +61,10 @@ public class FrequencyEditor extends SourceConfigurationEditor mFrequencyBoxes = new ArrayList<>();
+ private ObservableList mFrequencyBoxes = FXCollections.observableArrayList();
private ComboBox mPreferredTunerComboBox;
private VBox mFrequencyBoxContainer;
+ private Spinner mChannelRotationDelaySpinner;
private boolean mAllowMultipleFrequencies;
/**
@@ -80,9 +88,24 @@ public class FrequencyEditor extends SourceConfigurationEditor getFrequencyRotationDelaySpinner()
+ {
+ if(mChannelRotationDelaySpinner == null)
+ {
+ mChannelRotationDelaySpinner = new Spinner();
+ mChannelRotationDelaySpinner.setTooltip(
+ new Tooltip("Delay on each frequency before rotating to next when seeking to next active channel frequency"));
+ mChannelRotationDelaySpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
+ int min = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM;
+ int max = ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MAXIMUM;
+ SpinnerValueFactory svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, min, 50);
+ mChannelRotationDelaySpinner.setValueFactory(svf);
+ mChannelRotationDelaySpinner.getValueFactory().valueProperty()
+ .addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
+ }
+
+ return mChannelRotationDelaySpinner;
+ }
+
+
/**
* Updates the preferred tuner's combo box to reflect the current set of tuners.
*/
diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java
index 074c2a4f..39da9e07 100644
--- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java
+++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorFactory.java
@@ -24,7 +24,6 @@ import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
import io.github.dsheirer.gui.preference.duplicate.DuplicateCallPreferenceEditor;
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
-import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
import io.github.dsheirer.preference.UserPreferences;
import javafx.scene.Node;
@@ -50,8 +49,6 @@ public class PreferenceEditorFactory
return new DirectoryPreferenceEditor(userPreferences);
case JMBE_LIBRARY:
return new JmbeLibraryPreferenceEditor(userPreferences);
- case SOURCE_CHANNEL_MULTIPLE_FREQUENCY:
- return new ChannelMultipleFrequencyPreferenceEditor(userPreferences);
case SOURCE_TUNER_CHANNELIZER:
return new TunerPreferenceEditor(userPreferences);
case TALKGROUP_FORMAT:
diff --git a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java
index 4b5eda57..de2263fd 100644
--- a/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java
+++ b/src/main/java/io/github/dsheirer/gui/preference/PreferenceEditorType.java
@@ -30,7 +30,6 @@ public enum PreferenceEditorType
AUDIO_RECORD("Record"),
AUDIO_OUTPUT("Output/Tones"),
AUDIO_DUPLICATE_CALL_DETECTION("Duplicate Calls"),
- SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
DEFAULT("Default");
diff --git a/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java b/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java
index 07f72310..69e7e3ad 100644
--- a/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/preference/UserPreferencesEditor.java
@@ -190,7 +190,6 @@ public class UserPreferencesEditor extends BorderPane
storageItem.setExpanded(true);
TreeItem sourceItem = new TreeItem<>("Source");
- sourceItem.getChildren().add(new TreeItem(PreferenceEditorType.SOURCE_CHANNEL_MULTIPLE_FREQUENCY));
sourceItem.getChildren().add(new TreeItem(PreferenceEditorType.SOURCE_TUNER_CHANNELIZER));
treeRoot.getChildren().add(sourceItem);
sourceItem.setExpanded(true);
@@ -247,7 +246,7 @@ public class UserPreferencesEditor extends BorderPane
Menu viewMenu = new Menu("View");
MenuItem playlistEditorItem = new MenuItem("Playlist Editor");
- playlistEditorItem.setOnAction(event -> MyEventBus.getEventBus().post(new ViewPlaylistRequest()));
+ playlistEditorItem.setOnAction(event -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
viewMenu.getItems().add(playlistEditorItem);
mMenuBar.getMenus().add(viewMenu);
}
diff --git a/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java
index ef4606fd..36f45629 100644
--- a/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/preference/decoder/JmbeLibraryPreferenceEditor.java
@@ -82,13 +82,18 @@ public class JmbeLibraryPreferenceEditor extends VBox
mUserPreferences = userPreferences;
//Register to receive directory preference update notifications so we can update the path labels
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
setPadding(new Insets(10,10,10,10));
setSpacing(10);
getChildren().addAll(getEditorPane(), getButtonsBox(), getAlertUserWhenMissingCheckBox());
}
+ public void dispose()
+ {
+ MyEventBus.getGlobalEventBus().unregister(this);
+ }
+
private CheckBox getAlertUserWhenMissingCheckBox()
{
if(mAlertUserWhenMissingCheckBox == null)
@@ -214,7 +219,7 @@ public class JmbeLibraryPreferenceEditor extends VBox
if(buttonType == ButtonType.YES)
{
mLog.info("Posting JMBE editor request to event bus");
- MyEventBus.getEventBus().post(new JmbeEditorRequest(release));
+ MyEventBus.getGlobalEventBus().post(new JmbeEditorRequest(release));
}
});
}
diff --git a/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java
index 60973547..2a5c7e7e 100644
--- a/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java
+++ b/src/main/java/io/github/dsheirer/gui/preference/directory/DirectoryPreferenceEditor.java
@@ -98,12 +98,17 @@ public class DirectoryPreferenceEditor extends HBox
mDirectoryPreference = userPreferences.getDirectoryPreference();
//Register to receive directory preference update notifications so we can update the path labels
- MyEventBus.getEventBus().register(this);
+ MyEventBus.getGlobalEventBus().register(this);
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
getChildren().add(getEditorPane());
}
+ public void dispose()
+ {
+ MyEventBus.getGlobalEventBus().unregister(this);
+ }
+
private GridPane getEditorPane()
{
if(mEditorPane == null)
diff --git a/src/main/java/io/github/dsheirer/gui/preference/tuner/ChannelMultipleFrequencyPreferenceEditor.java b/src/main/java/io/github/dsheirer/gui/preference/tuner/ChannelMultipleFrequencyPreferenceEditor.java
deleted file mode 100644
index 8350f209..00000000
--- a/src/main/java/io/github/dsheirer/gui/preference/tuner/ChannelMultipleFrequencyPreferenceEditor.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * ******************************************************************************
- * sdrtrunk
- * 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
- * *****************************************************************************
- */
-
-package io.github.dsheirer.gui.preference.tuner;
-
-import io.github.dsheirer.preference.UserPreferences;
-import io.github.dsheirer.preference.source.ChannelMultiFrequencyPreference;
-import javafx.geometry.Insets;
-import javafx.scene.control.Button;
-import javafx.scene.control.Label;
-import javafx.scene.control.Spinner;
-import javafx.scene.layout.GridPane;
-import javafx.scene.layout.HBox;
-
-
-/**
- * Preference settings for channel source configurations containing multiple frequencies
- */
-public class ChannelMultipleFrequencyPreferenceEditor extends HBox
-{
- private static final String HELP_TEXT = "Rotation delay determines how long a decoder will wait before rotating " +
- "to the next frequency in a list for a rotating control channel configuration.";
-
- private static final Insets MARGINS = new Insets(5,5,5,5);
- private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
- private GridPane mEditorPane;
- private Label mRotationDelayLabel;
- private Spinner mRotationDelaySpinner;
- private Button mResetButton;
- private Label mHelpText;
-
- public ChannelMultipleFrequencyPreferenceEditor(UserPreferences userPreferences)
- {
- mChannelMultiFrequencyPreference = userPreferences.getChannelMultiFrequencyPreference();
- getChildren().add(getEditorPane());
- }
-
- private GridPane getEditorPane()
- {
- if(mEditorPane == null)
- {
- mEditorPane = new GridPane();
- GridPane.setMargin(getRotationDelaySpinner(), new Insets(5, 5, 5, 5));
- mEditorPane.add(getRotationDelaySpinner(), 0, 0);
- GridPane.setMargin(getRotationDelayLabel(), new Insets(5, 5, 5, 5));
- mEditorPane.add(getRotationDelayLabel(), 1, 0);
- GridPane.setMargin(getResetButton(), MARGINS);
- mEditorPane.add(getResetButton(), 2, 0);
- GridPane.setMargin(getHelpText(), MARGINS);
- mEditorPane.add(getHelpText(), 0, 1, 3, 1);
- }
-
- return mEditorPane;
- }
-
- public Spinner getRotationDelaySpinner()
- {
- if(mRotationDelaySpinner == null)
- {
- int currentDelay = (int)(mChannelMultiFrequencyPreference.getRotationDelay() / 1000);
- mRotationDelaySpinner = new Spinner<>(1, 60, currentDelay, 1);
- mRotationDelaySpinner.setEditable(true);
- mRotationDelaySpinner.valueProperty().addListener((observable, oldValue, newValue) -> {
- mChannelMultiFrequencyPreference.setRotationDelay(newValue * 1000);
- });
- }
-
- return mRotationDelaySpinner;
- }
-
- public Label getRotationDelayLabel()
- {
- if(mRotationDelayLabel == null)
- {
- mRotationDelayLabel = new Label("Rotation Delay (seconds)");
- }
-
- return mRotationDelayLabel;
- }
-
- public Button getResetButton()
- {
- if(mResetButton == null)
- {
- mResetButton = new Button("Reset");
- mResetButton.setOnAction(event -> {
- mChannelMultiFrequencyPreference.resetRotationDelay();
- int currentDelay = (int)(mChannelMultiFrequencyPreference.getRotationDelay() / 1000);
- getRotationDelaySpinner().getValueFactory().setValue(currentDelay);
- });
- }
-
- return mResetButton;
- }
-
- public Label getHelpText()
- {
- if(mHelpText == null)
- {
- mHelpText = new Label(HELP_TEXT);
- mHelpText.setWrapText(true);
- }
-
- return mHelpText;
- }
-}
diff --git a/src/main/java/io/github/dsheirer/identifier/Form.java b/src/main/java/io/github/dsheirer/identifier/Form.java
index a2962a4b..17637796 100644
--- a/src/main/java/io/github/dsheirer/identifier/Form.java
+++ b/src/main/java/io/github/dsheirer/identifier/Form.java
@@ -27,6 +27,9 @@ package io.github.dsheirer.identifier;
public enum Form
{
ALIAS_LIST,
+ ARS_DEVICE,
+ ARS_USER,
+ ARS_PASSWORD,
CALL_PROGRESS_TONE,
CHANNEL,
CHANNEL_DESCRIPTOR,
@@ -39,9 +42,11 @@ public enum Form
FULLY_QUALIFIED_IDENTIFIER,
IPV4_ADDRESS,
KNOX_TONE,
+ LOCATION,
LOCATION_REGISTRATION_AREA,
LOJACK,
NEIGHBOR_SITE,
+ NETWORK,
NETWORK_ACCESS_CODE,
PATCH_GROUP,
RADIO,
@@ -54,6 +59,7 @@ public enum Form
TALKGROUP,
TELEPHONE_NUMBER,
TONE,
+ UDP_PORT,
UNIT_IDENTIFIER,
UNIT_STATUS,
USER_STATUS,
diff --git a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java
index 9d7a168e..3e981cbb 100644
--- a/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java
+++ b/src/main/java/io/github/dsheirer/identifier/IdentifierCollection.java
@@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* (Immutable) Collection of identifiers with convenient accessor methods
@@ -36,7 +37,7 @@ import java.util.List;
public class IdentifierCollection
{
private final static Logger mLog = LoggerFactory.getLogger(IdentifierCollection.class);
- protected List mIdentifiers = new ArrayList<>();
+ protected List mIdentifiers = new CopyOnWriteArrayList<>();
protected AliasListConfigurationIdentifier mAliasListConfigurationIdentifier;
private int mTimeslot = 0;
diff --git a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java
index 211cd4da..b2814c7b 100644
--- a/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java
+++ b/src/main/java/io/github/dsheirer/identifier/MutableIdentifierCollection.java
@@ -27,8 +27,9 @@ import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.util.ArrayList;
import java.util.Collection;
-import java.util.Iterator;
+import java.util.List;
/**
* Identifier collection with methods for changing or updating managed identifiers
@@ -260,12 +261,11 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
public void clear()
{
- Iterator it = mIdentifiers.iterator();
+ List identifiers = new ArrayList<>();
- while(it.hasNext())
+ for(Identifier identifier: identifiers)
{
- notifyRemove(it.next());
- it.remove();
+ remove(identifier);
}
}
@@ -274,18 +274,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
public void remove(IdentifierClass identifierClass)
{
- Iterator it = mIdentifiers.iterator();
+ List identifiers = new ArrayList<>(mIdentifiers);
- Identifier next = null;
-
- while(it.hasNext())
+ for(Identifier identifier: identifiers)
{
- next = it.next();
-
- if(next.getIdentifierClass() == identifierClass)
+ if(identifier.getIdentifierClass() == identifierClass)
{
- it.remove();
- notifyRemove(next);
+ remove(identifier);
}
}
}
@@ -295,18 +290,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
public void remove(Form form)
{
- Iterator it = mIdentifiers.iterator();
+ List identifiers = new ArrayList<>(mIdentifiers);
- Identifier next = null;
-
- while(it.hasNext())
+ for(Identifier identifier: identifiers)
{
- next = it.next();
-
- if(next.getForm() == form)
+ if(identifier.getForm() == form)
{
- it.remove();
- notifyRemove(next);
+ remove(identifier);
}
}
}
@@ -316,18 +306,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
public void remove(Role role)
{
- Iterator it = mIdentifiers.iterator();
+ List identifiers = new ArrayList<>(mIdentifiers);
- Identifier next = null;
-
- while(it.hasNext())
+ for(Identifier identifier: identifiers)
{
- next = it.next();
-
- if(next.getRole() == role)
+ if(identifier.getRole() == role)
{
- it.remove();
- notifyRemove(next);
+ remove(identifier);
}
}
}
@@ -337,18 +322,15 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
public void remove(IdentifierClass identifierClass, Form form, Role role)
{
- Iterator it = mIdentifiers.iterator();
+ List identifiers = new ArrayList<>(mIdentifiers);
- Identifier next = null;
-
- while(it.hasNext())
+ for(Identifier identifier: identifiers)
{
- next = it.next();
-
- if(next.getIdentifierClass() == identifierClass && next.getForm() == form && next.getRole() == role)
+ if(identifier.getIdentifierClass() == identifierClass &&
+ identifier.getForm() == form &&
+ identifier.getRole() == role)
{
- it.remove();
- notifyRemove(next);
+ remove(identifier);
}
}
}
@@ -358,18 +340,13 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
public void remove(IdentifierClass identifierClass, Role role)
{
- Iterator it = mIdentifiers.iterator();
+ List identifiers = new ArrayList<>(mIdentifiers);
- Identifier next = null;
-
- while(it.hasNext())
+ for(Identifier identifier: identifiers)
{
- next = it.next();
-
- if(next.getIdentifierClass() == identifierClass && next.getRole() == role)
+ if(identifier.getIdentifierClass() == identifierClass && identifier.getRole() == role)
{
- it.remove();
- notifyRemove(next);
+ remove(identifier);
}
}
}
diff --git a/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java b/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java
index 981d7071..7a0a9871 100644
--- a/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java
+++ b/src/main/java/io/github/dsheirer/identifier/decoder/ChannelStateIdentifier.java
@@ -26,18 +26,25 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.protocol.Protocol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+/**
+ * Channel state identifier. Reflects the current state of the channel.
+ */
public class ChannelStateIdentifier extends Identifier
{
- public static final ChannelStateIdentifier ACTIVE = ChannelStateIdentifier.create(State.ACTIVE);
- public static final ChannelStateIdentifier CALL = ChannelStateIdentifier.create(State.CALL);
- public static final ChannelStateIdentifier CONTROL = ChannelStateIdentifier.create(State.CONTROL);
- public static final ChannelStateIdentifier DATA = ChannelStateIdentifier.create(State.DATA);
- public static final ChannelStateIdentifier ENCRYPTED = ChannelStateIdentifier.create(State.ENCRYPTED);
- public static final ChannelStateIdentifier FADE = ChannelStateIdentifier.create(State.FADE);
- public static final ChannelStateIdentifier IDLE = ChannelStateIdentifier.create(State.IDLE);
- public static final ChannelStateIdentifier TEARDOWN = ChannelStateIdentifier.create(State.TEARDOWN);
- public static final ChannelStateIdentifier RESET = ChannelStateIdentifier.create(State.RESET);
+ private final static Logger mLog = LoggerFactory.getLogger(ChannelStateIdentifier.class);
+
+ public static final ChannelStateIdentifier ACTIVE = new ChannelStateIdentifier(State.ACTIVE);
+ public static final ChannelStateIdentifier CALL = new ChannelStateIdentifier(State.CALL);
+ public static final ChannelStateIdentifier CONTROL = new ChannelStateIdentifier(State.CONTROL);
+ public static final ChannelStateIdentifier DATA = new ChannelStateIdentifier(State.DATA);
+ public static final ChannelStateIdentifier ENCRYPTED = new ChannelStateIdentifier(State.ENCRYPTED);
+ public static final ChannelStateIdentifier FADE = new ChannelStateIdentifier(State.FADE);
+ public static final ChannelStateIdentifier IDLE = new ChannelStateIdentifier(State.IDLE);
+ public static final ChannelStateIdentifier TEARDOWN = new ChannelStateIdentifier(State.TEARDOWN);
+ public static final ChannelStateIdentifier RESET = new ChannelStateIdentifier(State.RESET);
public ChannelStateIdentifier(State value)
{
@@ -53,9 +60,32 @@ public class ChannelStateIdentifier extends Identifier
/**
* Creates a decoder state identifier from the decoder state value
*/
- public static ChannelStateIdentifier create(State state)
+ public static ChannelStateIdentifier get(State state)
{
- return new ChannelStateIdentifier(state);
+ switch(state)
+ {
+ case ACTIVE:
+ return ACTIVE;
+ case CALL:
+ return CALL;
+ case CONTROL:
+ return CONTROL;
+ case DATA:
+ return DATA;
+ case ENCRYPTED:
+ return ENCRYPTED;
+ case FADE:
+ return FADE;
+ case IDLE:
+ return IDLE;
+ case RESET:
+ return RESET;
+ case TEARDOWN:
+ return TEARDOWN;
+ default:
+ mLog.warn("Creating new channel state identifier for unrecognized state [" + state + "]");
+ return new ChannelStateIdentifier(state);
+ }
}
@Override
diff --git a/src/main/java/io/github/dsheirer/identifier/location/LocationIdentifier.java b/src/main/java/io/github/dsheirer/identifier/location/LocationIdentifier.java
new file mode 100644
index 00000000..4e4619a2
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/identifier/location/LocationIdentifier.java
@@ -0,0 +1,41 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.identifier.location;
+
+import io.github.dsheirer.identifier.Form;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierClass;
+import io.github.dsheirer.identifier.Role;
+
+/**
+ * Location (point) identifier
+ */
+public abstract class LocationIdentifier extends Identifier
+{
+ /**
+ * Constructs an instance
+ * @param value location
+ * @param role to or from
+ */
+ public LocationIdentifier(Point value, Role role)
+ {
+ super(value, IdentifierClass.USER, Form.LOCATION, role);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/identifier/location/Point.java b/src/main/java/io/github/dsheirer/identifier/location/Point.java
new file mode 100644
index 00000000..57aa1884
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/identifier/location/Point.java
@@ -0,0 +1,59 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 Dennis Sheirer
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * ****************************************************************************
+ */
+
+package io.github.dsheirer.identifier.location;
+
+/**
+ * Geospatial point (location)
+ */
+public class Point
+{
+ private double mLatitude;
+ private double mLongitude;
+
+ /**
+ * Constructs an instance
+ * @param latitude in decimal degrees
+ * @param longitude in decimal degrees
+ */
+ public Point(double latitude, double longitude)
+ {
+ mLatitude = latitude;
+ mLongitude = longitude;
+ }
+
+ public double getLatitude()
+ {
+ return mLatitude;
+ }
+
+ public double getLongitude()
+ {
+ return mLongitude;
+ }
+
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append(Math.abs(mLatitude)).append(mLatitude >= 0 ? "N " : "S ");
+ sb.append(Math.abs(mLongitude)).append(mLongitude >= 0 ? "E" : "W");
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/SiteIdentifier.java b/src/main/java/io/github/dsheirer/identifier/site/SiteIdentifier.java
similarity index 77%
rename from src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/SiteIdentifier.java
rename to src/main/java/io/github/dsheirer/identifier/site/SiteIdentifier.java
index 983cc6fb..ed4961c7 100644
--- a/src/main/java/io/github/dsheirer/module/decode/ltrnet/identifier/SiteIdentifier.java
+++ b/src/main/java/io/github/dsheirer/identifier/site/SiteIdentifier.java
@@ -18,29 +18,20 @@
* *****************************************************************************
*/
-package io.github.dsheirer.module.decode.ltrnet.identifier;
+package io.github.dsheirer.identifier.site;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
-import io.github.dsheirer.protocol.Protocol;
-public class SiteIdentifier extends IntegerIdentifier
+/**
+ * Radio site identifier
+ */
+public abstract class SiteIdentifier extends IntegerIdentifier
{
public SiteIdentifier(int site)
{
super(site, IdentifierClass.NETWORK, Form.SITE, Role.BROADCAST);
}
-
- @Override
- public Protocol getProtocol()
- {
- return Protocol.LTR_NET;
- }
-
- public static SiteIdentifier create(int site)
- {
- return new SiteIdentifier(site);
- }
}
diff --git a/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java b/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java
index 419dc77f..b215d971 100644
--- a/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java
+++ b/src/main/java/io/github/dsheirer/jmbe/JmbeEditor.java
@@ -164,7 +164,7 @@ public class JmbeEditor extends VBox
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait().ifPresent(buttonType -> {
jmbeCreator.completeProperty().unbind();
- MyEventBus.getEventBus().post(new JmbeEditorRequest(true));
+ MyEventBus.getGlobalEventBus().post(new JmbeEditorRequest(true));
});
});
jmbeCreator.execute();
diff --git a/src/main/java/io/github/dsheirer/log/ApplicationLog.java b/src/main/java/io/github/dsheirer/log/ApplicationLog.java
index 4d15bcbf..d04cf97a 100644
--- a/src/main/java/io/github/dsheirer/log/ApplicationLog.java
+++ b/src/main/java/io/github/dsheirer/log/ApplicationLog.java
@@ -62,7 +62,6 @@ public class ApplicationLog
public ApplicationLog(UserPreferences userPreferences)
{
mUserPreferences = userPreferences;
- MyEventBus.getEventBus().register(this);
}
@Subscribe
@@ -87,6 +86,8 @@ public class ApplicationLog
*/
public void start()
{
+ MyEventBus.getGlobalEventBus().register(this);
+
if(mRollingFileAppender == null)
{
mApplicationLogPath = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog();
@@ -169,6 +170,7 @@ public class ApplicationLog
*/
public void stop()
{
+ MyEventBus.getGlobalEventBus().unregister(this);
if(mRollingFileAppender != null)
{
mLog.info("Stopping application logging");
diff --git a/src/main/java/io/github/dsheirer/message/MessageHistory.java b/src/main/java/io/github/dsheirer/message/MessageHistory.java
new file mode 100644
index 00000000..49cb39ff
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/message/MessageHistory.java
@@ -0,0 +1,72 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.message;
+
+import com.google.common.eventbus.Subscribe;
+import io.github.dsheirer.module.HistoryModule;
+import io.github.dsheirer.sample.Listener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Decode event history module. Maintains a history of decode events and constrains the total history size.
+ */
+public class MessageHistory extends HistoryModule implements IMessageListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(MessageHistory.class);
+
+ /**
+ * Constructs an instance
+ */
+ public MessageHistory(int historySize)
+ {
+ super(historySize);
+ }
+
+ /**
+ * Implements the IDecodeEventListener interface - delegates to receive(event) method.
+ */
+ @Override
+ public Listener getMessageListener()
+ {
+ return this;
+ }
+
+ /**
+ * Process a request for message history and post the response to the module event bus
+ */
+ @Subscribe
+ public void process(MessageHistoryRequest request)
+ {
+ getInterModuleEventBus().post(new MessageHistoryResponse(getItems()));
+ }
+
+ /**
+ * Processes a request to preload message history
+ */
+ @Subscribe
+ public void process(MessageHistoryPreloadData preloadData)
+ {
+ for(IMessage message: preloadData.getData())
+ {
+ receive(message);
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/message/MessageHistoryPreloadData.java b/src/main/java/io/github/dsheirer/message/MessageHistoryPreloadData.java
new file mode 100644
index 00000000..af1c3903
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/message/MessageHistoryPreloadData.java
@@ -0,0 +1,39 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.message;
+
+import io.github.dsheirer.controller.channel.event.PreloadDataContent;
+
+import java.util.List;
+
+/**
+ * Message history to preload into the message history module.
+ */
+public class MessageHistoryPreloadData extends PreloadDataContent>
+{
+ /**
+ * Constructs an instance
+ * @param messages from the history
+ */
+ public MessageHistoryPreloadData(List messages)
+ {
+ super(messages);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/message/MessageHistoryRequest.java b/src/main/java/io/github/dsheirer/message/MessageHistoryRequest.java
new file mode 100644
index 00000000..aebfdaa8
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/message/MessageHistoryRequest.java
@@ -0,0 +1,29 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.message;
+
+import io.github.dsheirer.module.ModuleEventBusMessage;
+
+/**
+ * Request for message history from the message history module.
+ */
+public class MessageHistoryRequest extends ModuleEventBusMessage
+{
+}
diff --git a/src/main/java/io/github/dsheirer/message/MessageHistoryResponse.java b/src/main/java/io/github/dsheirer/message/MessageHistoryResponse.java
new file mode 100644
index 00000000..0b5fa3a4
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/message/MessageHistoryResponse.java
@@ -0,0 +1,49 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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.message;
+
+import io.github.dsheirer.module.ModuleEventBusMessage;
+
+import java.util.List;
+
+/**
+ * Response to message history request.
+ */
+public class MessageHistoryResponse extends ModuleEventBusMessage
+{
+ private List mMessages;
+
+ /**
+ * Constructs an instance
+ * @param messages from the history
+ */
+ public MessageHistoryResponse(List messages)
+ {
+ mMessages = messages;
+ }
+
+ /**
+ * Message history
+ */
+ public List getMessages()
+ {
+ return mMessages;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/message/MessageProviderModule.java b/src/main/java/io/github/dsheirer/message/MessageProviderModule.java
index 081139af..94172e98 100644
--- a/src/main/java/io/github/dsheirer/message/MessageProviderModule.java
+++ b/src/main/java/io/github/dsheirer/message/MessageProviderModule.java
@@ -55,12 +55,6 @@ public class MessageProviderModule extends Module implements IMessageProvider
}
- @Override
- public void dispose()
- {
-
- }
-
public void receive(IMessage message)
{
if(mMessageListener != null)
diff --git a/src/main/java/io/github/dsheirer/message/SyncLossMessage.java b/src/main/java/io/github/dsheirer/message/SyncLossMessage.java
index 9740b980..7584c26c 100644
--- a/src/main/java/io/github/dsheirer/message/SyncLossMessage.java
+++ b/src/main/java/io/github/dsheirer/message/SyncLossMessage.java
@@ -30,6 +30,22 @@ public class SyncLossMessage extends Message
{
private int mBitsProcessed;
private Protocol mProtocol;
+ private int mTimeslot = 0;
+
+ /**
+ * Constructs a sync loss message.
+ *
+ * @param timestamp of the message
+ * @param bitsProcessed without a sync pattern detection of message decode
+ * @param timeslot that lost the sync
+ */
+ public SyncLossMessage(long timestamp, int bitsProcessed, Protocol protocol, int timeslot)
+ {
+ super(timestamp);
+ mBitsProcessed = bitsProcessed;
+ mProtocol = protocol;
+ mTimeslot = timeslot;
+ }
/**
* Constructs a sync loss message.
@@ -39,13 +55,21 @@ public class SyncLossMessage extends Message
*/
public SyncLossMessage(long timestamp, int bitsProcessed, Protocol protocol)
{
- super(timestamp);
- mBitsProcessed = bitsProcessed;
- mProtocol = protocol;
+ this(timestamp, bitsProcessed, protocol, 0);
}
/**
- * Indicates the number of bits that were processed without a sync dectection or message decode
+ * Timeslot that lost the sync
+ * @return timeslot or a default of 0
+ */
+ @Override
+ public int getTimeslot()
+ {
+ return mTimeslot;
+ }
+
+ /**
+ * Indicates the number of bits that were processed without a sync detection or message decode
*/
public int getBitsProcessed()
{
diff --git a/src/main/java/io/github/dsheirer/module/HistoryModule.java b/src/main/java/io/github/dsheirer/module/HistoryModule.java
new file mode 100644
index 00000000..889b229c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/HistoryModule.java
@@ -0,0 +1,101 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+import io.github.dsheirer.sample.Broadcaster;
+import io.github.dsheirer.sample.Listener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Abstract base history module. Maintains a history of items and constrains the total history size. Adds support
+ * for registering a listener to receive a copy of new items as they arrive.
+ *
+ * Note: internal history items are de-duplicated. However, all items are passed through to the listener.
+ */
+public abstract class HistoryModule extends Module implements Listener
+{
+ private List mItems = new ArrayList<>();
+ private Broadcaster mBroadcaster = new Broadcaster<>();
+ private int mMaximumHistorySize;
+
+ /**
+ * Constructs an instance
+ */
+ public HistoryModule(int maximumHistorySize)
+ {
+ mMaximumHistorySize = maximumHistorySize;
+ }
+
+ /**
+ * Access a copy of the events from this event history
+ */
+ public List getItems()
+ {
+ return new ArrayList<>(mItems);
+ }
+
+ @Override
+ public void reset()
+ {
+ mItems.clear();
+ }
+
+ @Override
+ public void start()
+ {
+ }
+
+ @Override
+ public void stop()
+ {
+ mItems.clear();
+ mBroadcaster.clear();
+ }
+
+ /**
+ * Adds the listener to receive a copy of all items received by this history.
+ * @param listener to receive items, or pass null to clear existing listener.
+ */
+ public void addListener(Listener listener)
+ {
+ mBroadcaster.addListener(listener);
+ }
+
+ /**
+ * Primary item receiver method.
+ */
+ @Override
+ public void receive(T item)
+ {
+ while(mItems.size() > mMaximumHistorySize)
+ {
+ mItems.remove(0);
+ }
+
+ if(!mItems.contains(item))
+ {
+ mItems.add(item);
+ }
+
+ mBroadcaster.broadcast(item);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/Module.java b/src/main/java/io/github/dsheirer/module/Module.java
index 9137643b..e88a6434 100644
--- a/src/main/java/io/github/dsheirer/module/Module.java
+++ b/src/main/java/io/github/dsheirer/module/Module.java
@@ -18,32 +18,86 @@
package io.github.dsheirer.module;
+import com.google.common.eventbus.EventBus;
+
+/**
+ * Defines the basic component level class for all processing, demodulation and decoding components that can operate
+ * within a processing chain.
+ *
+ * Modules can optionally implement any of the following legacy interfaces:
+ *
+ * IAudioPacketListener consumes audio packets
+ * IAudioPacketProvider produces audio packets
+ * ICallEventListener consumes call events
+ * ICallEventProvider provides call events
+ * IChannelEventListener consumes channel events
+ * IChannelEventProvider provides channel events (reset, selection, start, etc)
+ * IFrequencyCorrectionController provides tuned frequency error corrections
+ * IMessageProvider produces messages
+ * IRealBufferListener consumes demodulated real sample buffers
+ * IRealBufferListener produces demodulated real sample buffers
+ * IReusableComplexBufferListener consumes complex samples and normally produces demodulated real sample buffers
+ * ISquelchStateListener consumes squelch states
+ * ISquelchStateProvider provides squelch states
+ *
+ * TODO: convert all pub/sub interfaces to use the inter-module event bus for event broadcast/subscribe and remove
+ * all of these interfaces.
+ */
public abstract class Module
{
+
/**
- * Defines the basic component level class for all processing, demodulation
- * and decoding components that can operate within a processing chain.
- *
- * Modules can optionally implement any of the following interfaces:
- *
- * IAudioPacketListener consumes audio packets
- * IAudioPacketProvider produces audio packets
- * ICallEventListener consumes call events
- * ICallEventProvider provides call events
- * IChannelEventListener consumes channel events
- * IChannelEventProvider provides channel events (reset, selection, start, etc)
- * IFrequencyCorrectionController provides tuned frequency error corrections
- * IMessageProvider produces messages
- * IRealBufferListener consumes demodulated real sample buffers
- * IRealBufferListener produces demodulated real sample buffers
- * IReusableComplexBufferListener consumes complex samples and normally produces demodulated real sample buffers
- * ISquelchStateListener consumes squelch states
- * ISquelchStateProvider provides squelch states
+ * Event bus for inter-module communication of processing chain events. Note: this is an externally provided
+ * resource, typically provided by the ProcessingChain parent for each module.
+ */
+ private EventBus mInterModuleEventBus;
+
+ /**
+ * Constructs an instance
*/
public Module()
{
}
+ /**
+ * Sets the event bus to be used for inter-module event broadcasting and subscribing.
+ * @param interModuleEventBus to use
+ */
+ public void setInterModuleEventBus(EventBus interModuleEventBus)
+ {
+ //Unregister from the current event bus (if one exists)
+ if(hasInterModuleEventBus())
+ {
+ getInterModuleEventBus().unregister(this);
+ }
+
+ mInterModuleEventBus = interModuleEventBus;
+
+ //Auto-register with the event bus
+ if(hasInterModuleEventBus())
+ {
+ getInterModuleEventBus().register(this);
+ }
+ }
+
+ /**
+ * Event bus for inter-module communication. Note: use hasEventBus() to check that the module is assigned a bus.
+ * @return event bus or null if one has not been established.
+ */
+ protected EventBus getInterModuleEventBus()
+ {
+ return mInterModuleEventBus;
+ }
+
+ /**
+ * Indicates if this module has an assigned event bus.
+ * @return true if event bus is non-null.
+ */
+ protected boolean hasInterModuleEventBus()
+ {
+ return mInterModuleEventBus != null;
+ }
+
/**
* Initialize or reset all internal states to default - prepare to start
* processing or resume processing, potentially with a different source.
@@ -68,6 +122,11 @@ public abstract class Module
/**
* Dispose of all resources and listeners and prepare for garbage collection
*/
- public abstract void dispose();
-
+ public void dispose()
+ {
+ if(hasInterModuleEventBus())
+ {
+ getInterModuleEventBus().unregister(this);
+ }
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/ModuleEventBusMessage.java b/src/main/java/io/github/dsheirer/module/ModuleEventBusMessage.java
new file mode 100644
index 00000000..cd2607b5
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/ModuleEventBusMessage.java
@@ -0,0 +1,28 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+/**
+ * Base module event bus message. All classes that are passed over a processing chain event bus should extend this
+ * base class.
+ */
+public abstract class ModuleEventBusMessage
+{
+}
diff --git a/src/main/java/io/github/dsheirer/module/ProcessingChain.java b/src/main/java/io/github/dsheirer/module/ProcessingChain.java
index efc8bc88..25b825a1 100644
--- a/src/main/java/io/github/dsheirer/module/ProcessingChain.java
+++ b/src/main/java/io/github/dsheirer/module/ProcessingChain.java
@@ -18,6 +18,7 @@
*/
package io.github.dsheirer.module;
+import com.google.common.eventbus.EventBus;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.audio.AudioSegmentBroadcaster;
@@ -35,6 +36,7 @@ import io.github.dsheirer.channel.state.IDecoderStateEventProvider;
import io.github.dsheirer.channel.state.MultiChannelState;
import io.github.dsheirer.channel.state.SingleChannelState;
import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.IChannelEventListener;
import io.github.dsheirer.controller.channel.IChannelEventProvider;
@@ -44,11 +46,12 @@ import io.github.dsheirer.identifier.IdentifierUpdateProvider;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.message.IMessageProvider;
-import io.github.dsheirer.module.decode.event.DecodeEventModel;
+import io.github.dsheirer.message.MessageHistory;
+import io.github.dsheirer.module.decode.event.DecodeEventHistory;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.module.decode.event.IDecodeEventListener;
import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
-import io.github.dsheirer.module.decode.event.MessageActivityModel;
+import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
import io.github.dsheirer.module.log.EventLogger;
import io.github.dsheirer.record.binary.BinaryRecorder;
import io.github.dsheirer.record.wave.ComplexBufferWaveRecorder;
@@ -76,6 +79,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -115,64 +119,105 @@ public class ProcessingChain implements Listener
private Broadcaster mSquelchStateEventBroadcaster = new Broadcaster<>();
private AtomicBoolean mRunning = new AtomicBoolean();
private List mModules = new ArrayList<>();
- private DecodeEventModel mDecodeEventModel;
+ private DecodeEventHistory mDecodeEventHistory = new DecodeEventHistory(500);
+ private MessageHistory mMessageHistory = new MessageHistory(500);
private AbstractChannelState mChannelState;
- private MessageActivityModel mMessageActivityModel;
+ private EventBus mEventBus;
protected Source mSource;
/**
* Creates a processing chain for managing a set of modules
*
- * @param channel
+ * @param channel with configuration details for this processing chain
+ * @param aliasModel for looking up aliases
*/
public ProcessingChain(Channel channel, AliasModel aliasModel)
{
+ mEventBus = new EventBus("Processing Chain Event Bus - Channel: " + channel.getName());
+
if(channel.getDecodeConfiguration().getTimeslotCount() == 1)
{
mChannelState = new SingleChannelState(channel, aliasModel);
}
else
{
- mChannelState = new MultiChannelState(channel, aliasModel, channel.getDecodeConfiguration().getTimeslotCount());
+ mChannelState = new MultiChannelState(channel, aliasModel, channel.getDecodeConfiguration().getTimeslots());
}
addModule(mChannelState);
- mDecodeEventModel = new DecodeEventModel();
- addDecodeEventListener(mDecodeEventModel);
+ addModule(mDecodeEventHistory);
+ addModule(mMessageHistory);
}
+ /**
+ * Event bus used for inter-module communication.
+ * @return event bus
+ */
+ public EventBus getEventBus()
+ {
+ return mEventBus;
+ }
+
+ /**
+ * Channel state
+ */
public AbstractChannelState getChannelState()
{
return mChannelState;
}
- public DecodeEventModel getDecodeEventModel()
+ /**
+ * Decode event history module.
+ */
+ public DecodeEventHistory getDecodeEventHistory()
{
- return mDecodeEventModel;
+ return mDecodeEventHistory;
}
- public MessageActivityModel getMessageActivityModel()
+ /**
+ * Message history module
+ */
+ public MessageHistory getMessageHistory()
{
- return mMessageActivityModel;
+ return mMessageHistory;
}
-
- //TODO: should we introduce the concept of getTimeslot() to messages and events and then use that to vector the
- //TODO: inbound stream to the appropriate message and event models?
-
- public void setMessageActivityModel(MessageActivityModel model)
+ /**
+ * Process a channel configuration change notification.
+ *
+ * Note: this will normally occur when a standard channel is converted to a traffic channel.
+ * @param notification of the change
+ */
+ public void channelConfigurationChanged(ChannelConfigurationChangeNotification notification)
{
- mMessageActivityModel = model;
+ getEventBus().post(notification);
+ }
- addMessageListener(mMessageActivityModel);
+ /**
+ * Removes any module that is an instance of a TrafficChannelManager
+ */
+ public void removeTrafficChannelManager()
+ {
+ Iterator it = mModules.iterator();
+
+ while(it.hasNext())
+ {
+ if(it.next() instanceof TrafficChannelManager)
+ {
+ it.remove();
+ }
+ }
}
public void dispose()
{
stop();
- for(Module module : mModules)
+ List modules = new ArrayList<>(mModules);
+
+ for(Module module : modules)
{
+ removeModule(module);
module.dispose();
}
@@ -280,7 +325,7 @@ public class ProcessingChain implements Listener
public void addModule(Module module)
{
mModules.add(module);
-
+ module.setInterModuleEventBus(getEventBus());
registerListeners(module);
registerProviders(module);
}
@@ -293,7 +338,7 @@ public class ProcessingChain implements Listener
{
unregisterListeners(module);
unregisterProviders(module);
-
+ module.setInterModuleEventBus(null);
mModules.remove(module);
}
@@ -777,25 +822,6 @@ public class ProcessingChain implements Listener
return mDecoderStateEventBroadcaster;
}
- /**
- * Adds the listener to receive decoded messages from all decoders.
- */
- public void addMessageListener(Listener listener)
- {
- mMessageBroadcaster.addListener(listener);
- }
-
- /**
- * Adds the list of listeners to receive decoded messages from all decoders.
- */
- public void addMessageListeners(List> listeners)
- {
- for(Listener listener : listeners)
- {
- mMessageBroadcaster.addListener(listener);
- }
- }
-
/**
* Adds a listener to receive source events from this processing chain
*/
@@ -804,14 +830,6 @@ public class ProcessingChain implements Listener
mSourceEventBroadcaster.addListener(listener);
}
- /**
- * Removes the listener from receiving decoded messages from all decoders.
- */
- public void removeMessageListener(Listener listener)
- {
- mMessageBroadcaster.removeListener(listener);
- }
-
/**
* Adds the listener to receive frequency change events from the processing chain
*
diff --git a/src/main/java/io/github/dsheirer/module/decode/Decoder.java b/src/main/java/io/github/dsheirer/module/decode/Decoder.java
index 9cedf003..f90acfe1 100644
--- a/src/main/java/io/github/dsheirer/module/decode/Decoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/Decoder.java
@@ -19,7 +19,6 @@ package io.github.dsheirer.module.decode;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageProvider;
-import io.github.dsheirer.message.Message;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener;
@@ -36,12 +35,6 @@ public abstract class Decoder extends Module implements IMessageProvider
{
}
- @Override
- public void dispose()
- {
- mMessageListener = null;
- }
-
public void start()
{
//no-op
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
index 2fed3147..9a71bdf0 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
@@ -21,6 +21,7 @@ package io.github.dsheirer.module.decode;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.alias.action.AliasActionManager;
+import io.github.dsheirer.audio.AbstractAudioModule;
import io.github.dsheirer.audio.AudioModule;
import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState;
import io.github.dsheirer.channel.state.State;
@@ -38,6 +39,11 @@ import io.github.dsheirer.module.decode.am.AMDecoder;
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
+import io.github.dsheirer.module.decode.dmr.DMRDecoder;
+import io.github.dsheirer.module.decode.dmr.DMRDecoderState;
+import io.github.dsheirer.module.decode.dmr.DMRTrafficChannelManager;
+import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
+import io.github.dsheirer.module.decode.dmr.message.voice.DMRAudioModule;
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2Decoder;
import io.github.dsheirer.module.decode.fleetsync2.Fleetsync2DecoderState;
import io.github.dsheirer.module.decode.fleetsync2.FleetsyncMessageFilter;
@@ -80,12 +86,13 @@ import io.github.dsheirer.module.decode.passport.PassportDecoderState;
import io.github.dsheirer.module.decode.passport.PassportMessageFilter;
import io.github.dsheirer.module.decode.tait.Tait1200Decoder;
import io.github.dsheirer.module.decode.tait.Tait1200DecoderState;
+import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule;
import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
-import io.github.dsheirer.source.tuner.channel.ChannelRotationMonitor;
+import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -107,18 +114,25 @@ public class DecoderFactory
* @return list of configured decoders
*/
public static List getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel,
- UserPreferences userPreferences)
+ UserPreferences userPreferences, TrafficChannelManager trafficChannelManager)
{
- List modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences);
+ List modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences, trafficChannelManager);
modules.addAll(getAuxiliaryDecoders(channel.getAuxDecodeConfiguration()));
return modules;
}
/**
* Constructs a primary decoder as specified in the decode configuration
+ *
+ * @param channelMapModel for channel map lookups
+ * @param channel configuration
+ * @param aliasModel for alias lookups
+ * @param userPreferences instance
+ * @param trafficChannelManager optional traffic channel manager to use
+ * @return list of modules to use for a processing chain
*/
- public static List getPrimaryModules(ChannelMapModel channelMapModel, Channel channel,
- AliasModel aliasModel, UserPreferences userPreferences)
+ public static List getPrimaryModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel,
+ UserPreferences userPreferences, TrafficChannelManager trafficChannelManager)
{
List modules = new ArrayList();
@@ -135,6 +149,10 @@ public class DecoderFactory
case AM:
processAM(channel, modules, aliasList, decodeConfig);
break;
+ case DMR:
+ processDMR(channel, userPreferences, modules, aliasList, (DecodeConfigDMR)decodeConfig,
+ trafficChannelManager);
+ break;
case NBFM:
processNBFM(channel, modules, aliasList, decodeConfig);
break;
@@ -145,7 +163,7 @@ public class DecoderFactory
processLTRNet(channel, modules, aliasList, (DecodeConfigLTRNet) decodeConfig);
break;
case MPT1327:
- processMPT1327(channelMapModel, channel, userPreferences, modules, aliasList, channelType, (DecodeConfigMPT1327) decodeConfig);
+ processMPT1327(channelMapModel, channel, modules, aliasList, channelType, (DecodeConfigMPT1327) decodeConfig);
break;
case PASSPORT:
processPassport(channel, modules, aliasList, decodeConfig);
@@ -207,7 +225,8 @@ public class DecoderFactory
{
List activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
- modules.add(new ChannelRotationMonitor(activeStates, userPreferences));
+ modules.add(new ChannelRotationMonitor(activeStates,
+ ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay()));
}
}
@@ -221,7 +240,7 @@ public class DecoderFactory
}
}
- private static void processMPT1327(ChannelMapModel channelMapModel, Channel channel, UserPreferences userPreferences, List modules, AliasList aliasList, ChannelType channelType, DecodeConfigMPT1327 decodeConfig) {
+ private static void processMPT1327(ChannelMapModel channelMapModel, Channel channel, List modules, AliasList aliasList, ChannelType channelType, DecodeConfigMPT1327 decodeConfig) {
DecodeConfigMPT1327 mptConfig = decodeConfig;
ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName());
Sync sync = mptConfig.getSync();
@@ -232,8 +251,8 @@ public class DecoderFactory
// Set max segment audio sample length slightly above call timeout to
// not create a new segment if the processing chain finishes a bit after
// actual call timeout.
- int maxAudioSegmentLengthMillis = (callTimeoutMilliseconds + 5000);
- modules.add(new AudioModule(aliasList, maxAudioSegmentLengthMillis));
+ long maxAudioSegmentLengthMillis = (callTimeoutMilliseconds + 5000);
+ modules.add(new AudioModule(aliasList, AbstractAudioModule.DEFAULT_TIMESLOT, maxAudioSegmentLengthMillis));
SourceType sourceType = channel.getSourceConfiguration().getSourceType();
if(sourceType == SourceType.TUNER || sourceType == SourceType.TUNER_MULTIPLE_FREQUENCIES)
@@ -258,7 +277,8 @@ public class DecoderFactory
{
List activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
- modules.add(new ChannelRotationMonitor(activeStates, userPreferences));
+ modules.add(new ChannelRotationMonitor(activeStates,
+ ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay()));
}
}
@@ -319,6 +339,57 @@ public class DecoderFactory
}
}
+ /**
+ * Creates modules for DMR decoder setup.
+ *
+ * Note: on some DMR systems (e.g. Capacity+) we convert standard channels to traffic channels (when rest channel
+ * changes) and so we reuse the traffic channel manager from the converted channel.
+ *
+ * @param channel for the DMR decoder
+ * @param userPreferences for access to JMBE audio library
+ * @param modules list to add to
+ * @param aliasList for the audio module
+ * @param decodeConfig for the DMR configuration
+ * @param trafficChannelManager optional traffic channel manager to re-use
+ */
+ private static void processDMR(Channel channel, UserPreferences userPreferences, List modules,
+ AliasList aliasList, DecodeConfigDMR decodeConfig,
+ TrafficChannelManager trafficChannelManager)
+ {
+ modules.add(new DMRDecoder(decodeConfig));
+
+ DMRTrafficChannelManager dmrTrafficChannelManager = null;
+
+ if(channel.isStandardChannel())
+ {
+ if(trafficChannelManager instanceof DMRTrafficChannelManager)
+ {
+ dmrTrafficChannelManager = (DMRTrafficChannelManager)trafficChannelManager;
+ }
+ else
+ {
+ dmrTrafficChannelManager = new DMRTrafficChannelManager(channel);
+ }
+
+ modules.add(dmrTrafficChannelManager);
+ }
+
+ modules.add(new DMRDecoderState(channel, 1, dmrTrafficChannelManager));
+ modules.add(new DMRDecoderState(channel, 2, dmrTrafficChannelManager));
+ modules.add(new DMRAudioModule(userPreferences, aliasList, 1));
+ modules.add(new DMRAudioModule(userPreferences, aliasList, 2));
+
+ //Add a channel rotation monitor when we have multiple control channel frequencies specified
+ if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency &&
+ ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies())
+ {
+ List activeStates = new ArrayList<>();
+ activeStates.add(State.CONTROL);
+ modules.add(new ChannelRotationMonitor(activeStates,
+ ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencyRotationDelay()));
+ }
+ }
+
/**
* Constructs a list of auxiliary decoders, as specified in the configuration
*
@@ -419,6 +490,9 @@ public class DecoderFactory
case PASSPORT:
filters.add(new PassportMessageFilter());
break;
+ case DMR:
+ //filters.add(new DMR) //todo: not finished
+ break;
default:
break;
}
@@ -439,10 +513,12 @@ public class DecoderFactory
{
case AM:
return new DecodeConfigAM();
- case LTR_NET:
- return new DecodeConfigLTRNet();
+ case DMR:
+ return new DecodeConfigDMR();
case LTR:
return new DecodeConfigLTRStandard();
+ case LTR_NET:
+ return new DecodeConfigLTRNet();
case MPT1327:
return new DecodeConfigMPT1327();
case NBFM:
@@ -472,6 +548,8 @@ public class DecoderFactory
DecodeConfigAM origAM = (DecodeConfigAM)config;
copyAM.setRecordAudio(origAM.getRecordAudio());
return copyAM;
+ case DMR:
+ return new DecodeConfigDMR();
case LTR_NET:
DecodeConfigLTRNet originalLTRNet = (DecodeConfigLTRNet)config;
DecodeConfigLTRNet copyLTRNet = new DecodeConfigLTRNet();
diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
index 1def3b1d..fb2dde3b 100644
--- a/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
+++ b/src/main/java/io/github/dsheirer/module/decode/DecoderType.java
@@ -32,6 +32,7 @@ public enum DecoderType
{
//Primary Decoders
AM("AM", "AM", Protocol.UNKNOWN),
+ DMR("DMR","DMR", Protocol.DMR),
LTR("LTR", "LTR", Protocol.LTR),
LTR_NET("LTR-Net", "LTR-Net", Protocol.LTR_NET),
MPT1327("MPT1327", "MPT1327", Protocol.MPT1327),
@@ -62,8 +63,9 @@ public enum DecoderType
*/
public static EnumSet PRIMARY_DECODERS =
EnumSet.of(DecoderType.AM,
- DecoderType.LTR_NET,
+ DecoderType.DMR,
DecoderType.LTR,
+ DecoderType.LTR_NET,
DecoderType.MPT1327,
DecoderType.NBFM,
DecoderType.P25_PHASE1,
@@ -82,17 +84,14 @@ public enum DecoderType
/**
* Decoders that produce a (recordable) bitstream
*/
- public static final EnumSet BITSTREAM_DECODERS =
- EnumSet.of(DecoderType.MPT1327,
- DecoderType.P25_PHASE1,
- DecoderType.P25_PHASE2);
+ public static final EnumSet BITSTREAM_DECODERS = EnumSet.of(DecoderType.DMR,
+ DecoderType.MPT1327, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
/**
* Decoders that produce (recordable) MBE audio codec frames
*/
public static final EnumSet MBE_AUDIO_CODEC_DECODERS =
- EnumSet.of(DecoderType.P25_PHASE1,
- DecoderType.P25_PHASE2);
+ EnumSet.of(DecoderType.DMR, DecoderType.P25_PHASE1, DecoderType.P25_PHASE2);
public Protocol getProtocol()
{
diff --git a/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java b/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java
index da441c30..b6d9702d 100644
--- a/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java
+++ b/src/main/java/io/github/dsheirer/module/decode/PrimaryDecoder.java
@@ -49,15 +49,6 @@ public abstract class PrimaryDecoder extends Decoder implements ISquelchStatePro
return mDecodeConfiguration;
}
- @Override
- public void dispose()
- {
- super.dispose();
-
- mSquelchStateListener = null;
- mDecodeConfiguration = null;
- }
-
public void broadcast(SquelchStateEvent state)
{
if(mSquelchStateListener != null)
diff --git a/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java b/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java
index 77432370..8249ae28 100644
--- a/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java
+++ b/src/main/java/io/github/dsheirer/module/decode/config/DecodeConfiguration.java
@@ -29,6 +29,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.controller.config.Configuration;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
+import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
@@ -48,7 +49,8 @@ import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
@JsonSubTypes.Type(value = DecodeConfigNBFM.class, name = "decodeConfigNBFM"),
@JsonSubTypes.Type(value = DecodeConfigP25Phase1.class, name = "decodeConfigP25Phase1"),
@JsonSubTypes.Type(value = DecodeConfigP25Phase2.class, name = "decodeConfigP25Phase2"),
- @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport")
+ @JsonSubTypes.Type(value = DecodeConfigPassport.class, name = "decodeConfigPassport"),
+ @JsonSubTypes.Type(value = DecodeConfigDMR.class, name = "decodeConfigDMR")
})
@JacksonXmlRootElement(localName = "decode_configuration")
public abstract class DecodeConfiguration extends Configuration
@@ -76,4 +78,10 @@ public abstract class DecodeConfiguration extends Configuration
{
return 1;
}
+
+ @JsonIgnore
+ public int[] getTimeslots()
+ {
+ return new int[0];
+ }
}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRBurstFramer.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRBurstFramer.java
new file mode 100644
index 00000000..7da5ff1e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRBurstFramer.java
@@ -0,0 +1,520 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.dsp.symbol.DibitDelayBuffer;
+import io.github.dsheirer.dsp.symbol.QPSKCarrierLock;
+import io.github.dsheirer.module.decode.dmr.message.CACH;
+import io.github.dsheirer.sample.Listener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Processes a stream of Dibit symbols and performs burst detection and timeslot framing. This framer
+ * also detects abnormal PLL phase locks and issues PLL phase lock corrections.
+ *
+ * Timeslot Map:
+ * 0: Common Association Channel (CACH) and Slow Link Control (SLC)
+ * 1. Timeslot 1
+ * 2. Timeslot 2
+ */
+public class DMRBurstFramer implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRBurstFramer.class);
+
+ private static final int DMR_SYMBOL_RATE = 4800;
+ private static final double PLL_PHASE_CORRECTION_90_DEGREES = (double)DMR_SYMBOL_RATE / 4.0;
+ private static final double PLL_PHASE_CORRECTION_180_DEGREES = (double)DMR_SYMBOL_RATE / 2.0;
+
+ private static final int BURST_DIBIT_START_TS1 = 0;
+ private static final int BURST_DIBIT_START_TS2 = 144;
+ private static final int BURST_DIBIT_LENGTH = 144;
+ private static final int TWO_TIMESLOT_DIBIT_LENGTH = BURST_DIBIT_LENGTH * 2;
+ private static final int SYNC_DIBIT_OFFSET_TS1 = 66;
+ private static final int SYNC_DIBIT_OFFSET_TS2 = 210;
+ private static final int SYNC_DIBIT_LENGTH = 24;
+ private static final int MAX_STREAMING_SYNC_DETECT_BIT_ERRORS = 4;
+ private static final int MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS = 6;
+
+ /**
+ * Threshold for issuing a sync loss message. This is set to trigger once the dibit count exceeds one second of
+ * dibits (4800 baud/dibits) plus two bursts dibit length. The two burst length padding is so that we don't reset
+ * the dibit counter below the burst sync check threshold.
+ */
+ private static final int SYNC_LOSS_MESSAGE_THRESHOLD = DMR_SYMBOL_RATE + TWO_TIMESLOT_DIBIT_LENGTH;
+
+ /**
+ * The message buffer is sized to hold two DMR bursts of 144 dibits (288 bits) each.
+ */
+ private DibitDelayBuffer mMessageBuffer = new DibitDelayBuffer(BURST_DIBIT_LENGTH * 2);
+
+ /**
+ * The sync delay buffer is sized to align sync detection with two bursts being fully loaded into the message and
+ * timeslot 1 region of the message buffer is aligned with the sync delay buffer.
+ *
+ * This delay equals the number of dibits in the second half of the burst payload: 54 dibits (108 bits).
+ */
+ private DibitDelayBuffer mSyncDelayBuffer = new DibitDelayBuffer(54);
+
+ /**
+ * Synchronized indicates that either the primary or secondary sync tracker is currently synchronized to
+ * one of the timeslot bursts. Once either sync tracker obtains a valid sync pattern, this flag is set
+ * to true. The synchronized state will remain true as long as at least one of the two sync trackers
+ * maintains sync state. Once both sync trackers lose sync, the framer will fallback into sync search/detection
+ * mode to inspect each dibit until a sync match can be (re)discovered.
+ */
+ private boolean mSynchronized = false;
+
+ /**
+ * Tracks the number of dibits received to trigger burst or sync loss message processing
+ */
+ private int mDibitCounter = 0;
+
+ /**
+ * The Sync detection is used to gain initial lock to a sync pattern or to regain sync if lost. The sync detector
+ * is used to align a DMR burst into the timeslot 1 region of the message dibit delay buffer so that it can be
+ * inspected to ensure the timeslots are aligned.
+ */
+ private DMRSyncDetector mSyncDetectorTimeslot1 = new DMRSyncDetector(MAX_STREAMING_SYNC_DETECT_BIT_ERRORS,
+ MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS);
+ private DMRSyncDetector mSyncDetectorTimeslot2 = new DMRSyncDetector(MAX_STREAMING_SYNC_DETECT_BIT_ERRORS,
+ MAX_EXPLICIT_SYNC_DETECT_BIT_ERRORS);
+
+ /**
+ * Timeslot sync trackers track the synchronization state for each of the two timeslots.
+ */
+ private SyncTracker mSyncTrackerTimeslot1 = new SyncTracker();
+ private SyncTracker mSyncTrackerTimeslot2 = new SyncTracker();
+
+ /**
+ * Timeslot alignment tracker tracks that the timeslots in the delay buffer are aligned.
+ */
+ private TimeslotAlignmentTracker mTimeslotAlignmentTracker = new TimeslotAlignmentTracker();
+
+ private IDMRBurstDetectListener mBurstDetectListener;
+ private IPhaseLockedLoop mPhaseLockedLoop;
+
+ /**
+ * Constructs an instance
+ * @param listener to be notified of framed burst detections and/or sync loss bits processed
+ * @param phaseLockedLoop to receive symbol alignment corrections
+ */
+ public DMRBurstFramer(IDMRBurstDetectListener listener, IPhaseLockedLoop phaseLockedLoop)
+ {
+ mBurstDetectListener = listener;
+ mPhaseLockedLoop = phaseLockedLoop;
+ }
+
+ /**
+ * Primary dibit symbol input method
+ * @param dibit to process
+ */
+ @Override
+ public void receive(Dibit dibit)
+ {
+ mDibitCounter++;
+
+ //Feed the message buffer first to ensure buffer contains two full bursts when a sync is detected
+ mMessageBuffer.put(dibit);
+
+ //Feed the sync delay buffer and reassign the delayed dibit to feed the sync detector if we're not synchronized
+ dibit = mSyncDelayBuffer.getAndPut(dibit);
+
+ if(mSynchronized)
+ {
+ if(mDibitCounter >= TWO_TIMESLOT_DIBIT_LENGTH)
+ {
+ if(mDibitCounter > TWO_TIMESLOT_DIBIT_LENGTH)
+ {
+ processSyncLossDibits(mDibitCounter - TWO_TIMESLOT_DIBIT_LENGTH, 0);
+ }
+
+ dispatch();
+ }
+ }
+ else
+ {
+ mSyncDetectorTimeslot2.add(dibit);
+
+ if(mSyncDetectorTimeslot2.hasSync())
+ {
+ dispatch();
+ }
+ else if(mDibitCounter >= SYNC_LOSS_MESSAGE_THRESHOLD)
+ {
+ processSyncLossDibits(DMR_SYMBOL_RATE, 0);
+ }
+ }
+ }
+
+ /**
+ * Dispatches burst 1 and/or burst 2 that is currently in the message buffer
+ */
+ private void dispatch()
+ {
+ if(mBurstDetectListener != null)
+ {
+ if(mDibitCounter > TWO_TIMESLOT_DIBIT_LENGTH)
+ {
+ processSyncLossDibits(mDibitCounter - TWO_TIMESLOT_DIBIT_LENGTH, 0);
+ }
+
+ mSyncDetectorTimeslot1.setCurrentSyncValue(getSyncValue(SYNC_DIBIT_OFFSET_TS1));
+ mSyncDetectorTimeslot2.setCurrentSyncValue(getSyncValue(SYNC_DIBIT_OFFSET_TS2));
+
+ mSyncTrackerTimeslot1.update(mSyncDetectorTimeslot1.getSyncPattern(), mSyncDetectorTimeslot1.getCarrierLock());
+ mSyncTrackerTimeslot2.update(mSyncDetectorTimeslot2.getSyncPattern(), mSyncDetectorTimeslot2.getCarrierLock());
+
+ CorrectedBinaryMessage burst1 = null;
+ CorrectedBinaryMessage burst2 = null;
+
+ CACH cach1 = null;
+ CACH cach2 = null;
+
+ //If we have valid sync but PLL tracker misalignment, correct both timeslots before we extract messages
+ if((mSyncTrackerTimeslot1.hasSync() && mSyncTrackerTimeslot1.hasCarrierMisAlignment()) ||
+ (mSyncTrackerTimeslot2.hasSync() && mSyncTrackerTimeslot2.hasCarrierMisAlignment()))
+ {
+// mLog.debug("Repairing Message Bits - PLL Mis-Alignment Detected");
+ if(mSyncTrackerTimeslot1.hasCarrierMisAlignment())
+ {
+ repairPLLMisalignment(BURST_DIBIT_START_TS1, mSyncTrackerTimeslot1.getCarrierLock());
+ repairPLLMisalignment(BURST_DIBIT_START_TS2, mSyncTrackerTimeslot1.getCarrierLock());
+ }
+ else
+ {
+ repairPLLMisalignment(BURST_DIBIT_START_TS1, mSyncTrackerTimeslot2.getCarrierLock());
+ repairPLLMisalignment(BURST_DIBIT_START_TS2, mSyncTrackerTimeslot2.getCarrierLock());
+ }
+ }
+
+ if(mSyncTrackerTimeslot1.hasSync())
+ {
+ burst1 = mMessageBuffer.getMessage(BURST_DIBIT_START_TS1, BURST_DIBIT_LENGTH);
+ burst1.incrementCorrectedBitCount(mSyncDetectorTimeslot1.getPatternMatchBitErrorCount());
+
+ if(mSyncTrackerTimeslot1.getSyncPattern().hasCACH())
+ {
+ cach1 = CACH.getCACH(burst1);
+ }
+ }
+
+ if(mSyncTrackerTimeslot2.hasSync())
+ {
+ burst2 = mMessageBuffer.getMessage(BURST_DIBIT_START_TS2, BURST_DIBIT_LENGTH);
+ burst2.incrementCorrectedBitCount(mSyncDetectorTimeslot2.getPatternMatchBitErrorCount());
+
+ if(mSyncTrackerTimeslot2.getSyncPattern().hasCACH())
+ {
+ cach2 = CACH.getCACH(burst2);
+ }
+ }
+
+ //Update the timeslot alignment tracker using the sync patterns and optional CACHes from each timeslot
+ mTimeslotAlignmentTracker.update(cach1, cach2, mSyncDetectorTimeslot1.getSyncPattern(),
+ mSyncDetectorTimeslot2.getSyncPattern());
+
+ if(mTimeslotAlignmentTracker.hasSufficientData())
+ {
+ if(mTimeslotAlignmentTracker.isAligned())
+ {
+ if(burst1 != null)
+ {
+ mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 1);
+ mDibitCounter -= BURST_DIBIT_LENGTH;
+ }
+ else
+ {
+ processSyncLossDibits(BURST_DIBIT_LENGTH, 1);
+ }
+
+ if(burst2 != null)
+ {
+ mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 2);
+ mDibitCounter -= BURST_DIBIT_LENGTH;
+ }
+ else
+ {
+ processSyncLossDibits(BURST_DIBIT_LENGTH, 2);
+ }
+
+ mSynchronized = mSyncTrackerTimeslot1.hasSync() || mSyncTrackerTimeslot2.hasSync();
+ }
+ else
+ {
+ if(burst1 != null)
+ {
+ mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 2);
+ mDibitCounter -= BURST_DIBIT_LENGTH;
+
+ //Transfer the sync pattern from tracker 1 to tracker 2 so we don't lose voice framing
+ mSyncTrackerTimeslot2.setSyncPattern(mSyncTrackerTimeslot1.getSyncPattern());
+ }
+ else
+ {
+ processSyncLossDibits(BURST_DIBIT_LENGTH, 2);
+ mSyncTrackerTimeslot2.reset();
+ }
+
+ mTimeslotAlignmentTracker.reset();
+ mSyncTrackerTimeslot1.reset();
+ mDibitCounter = BURST_DIBIT_LENGTH;
+ mSynchronized = true;
+ }
+ }
+ else
+ {
+ //When we don't have sufficient timeslot tracking data, dump the messages to timeslot 0 so that they
+ //don't corrupt the decoder states for either timeslot, until we positively regain timeslot tracking
+ if(burst1 != null)
+ {
+ mBurstDetectListener.burstDetected(burst1, mSyncTrackerTimeslot1.getSyncPattern(), 0);
+ mDibitCounter -= BURST_DIBIT_LENGTH;
+ }
+ else
+ {
+ processSyncLossDibits(BURST_DIBIT_LENGTH, 0);
+ }
+
+ if(burst2 != null)
+ {
+ mBurstDetectListener.burstDetected(burst2, mSyncTrackerTimeslot2.getSyncPattern(), 0);
+ mDibitCounter -= BURST_DIBIT_LENGTH;
+ }
+ else
+ {
+ processSyncLossDibits(BURST_DIBIT_LENGTH, 0);
+ }
+
+ mSynchronized = mSyncTrackerTimeslot1.hasSync() || mSyncTrackerTimeslot2.hasSync();
+ }
+
+ if(!mSynchronized)
+ {
+ mTimeslotAlignmentTracker.reset();
+ mSyncTrackerTimeslot1.reset();
+ mSyncTrackerTimeslot2.reset();
+ }
+
+ if(mDibitCounter < 0)
+ {
+ mDibitCounter = 0;
+ }
+ }
+ }
+
+ /**
+ * Repairs the captured dibits in the message buffer when a PLL lock misalignment is detected.
+ * @param offset to start of burst
+ * @param carrierLock that indicates the mis-alignment
+ */
+ private void repairPLLMisalignment(int offset, QPSKCarrierLock carrierLock)
+ {
+ int end = offset + BURST_DIBIT_LENGTH;
+
+ for(int x = offset; x < end; x++)
+ {
+ Dibit misalignedDibit = mMessageBuffer.get(x);
+ mMessageBuffer.set(x, carrierLock.correct(misalignedDibit));
+ }
+
+ if(mPhaseLockedLoop != null)
+ {
+// mLog.warn("Repairing PLL Phase Lock Mis-Alignment - Detected: " + carrierLock);
+ switch(carrierLock)
+ {
+ case PLUS_90:
+ mPhaseLockedLoop.correctInversion(PLL_PHASE_CORRECTION_90_DEGREES);
+ break;
+ case MINUS_90:
+ mPhaseLockedLoop.correctInversion(-PLL_PHASE_CORRECTION_90_DEGREES);
+ break;
+ case INVERTED:
+ mPhaseLockedLoop.correctInversion(PLL_PHASE_CORRECTION_180_DEGREES);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Resets the message and sync delay buffers and resets the dibit counter to zero.
+ */
+ public void reset()
+ {
+ mDibitCounter = 0;
+ mMessageBuffer.reset();
+ mSyncDelayBuffer.reset();
+ mTimeslotAlignmentTracker.reset();
+ }
+
+ /**
+ * Calculates the value of the sync field currently in the message buffer as specified by the offset value.
+ */
+ private long getSyncValue(int dibitOffset)
+ {
+ Dibit[] syncDibits = mMessageBuffer.getBuffer(dibitOffset, SYNC_DIBIT_LENGTH);
+
+ long value = 0;
+
+ for(Dibit dibit: syncDibits)
+ {
+ value = Long.rotateLeft(value, 2);
+ value += dibit.getValue();
+ }
+
+ return value;
+ }
+
+ /**
+ * Processes dibits received without a sync condition
+ * @param dibitCount with sync loss
+ */
+ private void processSyncLossDibits(int dibitCount, int timeslot)
+ {
+ mDibitCounter -= dibitCount;
+
+ if(mBurstDetectListener != null)
+ {
+ mBurstDetectListener.syncLost(dibitCount * 2, timeslot);
+ }
+ }
+
+ /**
+ * Tracks the sync state for a timeslot to account for voice framing that does not transmit a sync pattern in each
+ * frame.
+ */
+ public class SyncTracker
+ {
+ private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN;
+ private QPSKCarrierLock mCarrierLock = QPSKCarrierLock.NORMAL;
+
+ /**
+ * Updates the sync pattern for the most recently detected burst to the specified pattern. When the previous
+ * sync pattern is one of either base or mobile voice frame and the detected pattern is unknown, automatically
+ * advances the sync pattern to the next voice frame in the super frame sequence to account for voice
+ * frames B - F that do not have a sync pattern.
+ */
+ public void update(DMRSyncPattern pattern, QPSKCarrierLock carrierLock)
+ {
+ if(pattern == DMRSyncPattern.UNKNOWN)
+ {
+ switch(mSyncPattern)
+ {
+ case BASE_STATION_VOICE: //Voice frame A
+ mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_B;
+ break;
+ case BS_VOICE_FRAME_B:
+ mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_C;
+ break;
+ case BS_VOICE_FRAME_C:
+ mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_D;
+ break;
+ case BS_VOICE_FRAME_D:
+ mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_E;
+ break;
+ case BS_VOICE_FRAME_E:
+ mSyncPattern = DMRSyncPattern.BS_VOICE_FRAME_F;
+ break;
+
+ case MOBILE_STATION_VOICE: //Voice frame A
+ case DIRECT_MODE_VOICE_TIMESLOT_1:
+ case DIRECT_MODE_VOICE_TIMESLOT_2:
+ mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_B;
+ break;
+ case MS_VOICE_FRAME_B:
+ mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_C;
+ break;
+ case MS_VOICE_FRAME_C:
+ mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_D;
+ break;
+ case MS_VOICE_FRAME_D:
+ mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_E;
+ break;
+ case MS_VOICE_FRAME_E:
+ mSyncPattern = DMRSyncPattern.MS_VOICE_FRAME_F;
+ break;
+ default:
+ mSyncPattern = pattern;
+ mCarrierLock = carrierLock;
+ break;
+ }
+ }
+ else
+ {
+ mSyncPattern = pattern;
+ mCarrierLock = carrierLock;
+ }
+ }
+
+ /**
+ * Most recently detected sync pattern
+ */
+ public DMRSyncPattern getSyncPattern()
+ {
+ return mSyncPattern;
+ }
+
+ /**
+ * Explicitly sets the pattern for this tracker
+ */
+ public void setSyncPattern(DMRSyncPattern pattern)
+ {
+ mSyncPattern = pattern;
+ }
+
+ /**
+ * Carrier lock as indicated by the sync detector.
+ */
+ public QPSKCarrierLock getCarrierLock()
+ {
+ return mCarrierLock;
+ }
+
+ /**
+ * Indicates if the burst has a QPSK constellation mis-alignment that has to be corrected
+ */
+ public boolean hasCarrierMisAlignment()
+ {
+ return getCarrierLock() != QPSKCarrierLock.NORMAL;
+ }
+
+ /**
+ * Checks the contents of the message buffer to determine if the message has a valid sync pattern, or if the
+ * message is a continuation voice frame from a voice super frame. The identified sync pattern and number of
+ * bit errors for that pattern are stored in the sync detector so that they can be accessed for dispatching
+ * a message.
+ */
+ public boolean hasSync()
+ {
+ return getSyncPattern() != DMRSyncPattern.UNKNOWN;
+ }
+
+ /**
+ * Resets the sync pattern to unknown
+ */
+ public void reset()
+ {
+ mSyncPattern = DMRSyncPattern.UNKNOWN;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
new file mode 100644
index 00000000..0f1de917
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoder.java
@@ -0,0 +1,344 @@
+package io.github.dsheirer.module.decode.dmr;
+
+import io.github.dsheirer.dsp.filter.FilterFactory;
+import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
+import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
+import io.github.dsheirer.dsp.gain.ComplexFeedForwardGainControl;
+import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulator;
+import io.github.dsheirer.dsp.psk.InterpolatingSampleBuffer;
+import io.github.dsheirer.dsp.psk.pll.CostasLoop;
+import io.github.dsheirer.dsp.psk.pll.FrequencyCorrectionSyncMonitor;
+import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.dsp.symbol.DibitToByteBufferAssembler;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.FeedbackDecoder;
+import io.github.dsheirer.sample.Broadcaster;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.IReusableByteBufferProvider;
+import io.github.dsheirer.sample.buffer.IReusableComplexBufferListener;
+import io.github.dsheirer.sample.buffer.ReusableByteBuffer;
+import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
+import io.github.dsheirer.source.ISourceEventListener;
+import io.github.dsheirer.source.ISourceEventProvider;
+import io.github.dsheirer.source.SourceEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * DMR decoder module.
+ */
+public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener, ISourceEventProvider,
+ IReusableComplexBufferListener, Listener, IReusableByteBufferProvider
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRDecoder.class);
+ protected static final float SAMPLE_COUNTER_GAIN = 0.4f;
+ private static final double SYMBOL_RATE = 4800.0;
+ private double mSampleRate;
+ private Broadcaster mDibitBroadcaster = new Broadcaster<>();
+ private DibitToByteBufferAssembler mByteBufferAssembler = new DibitToByteBufferAssembler(300);
+ private DMRMessageProcessor mMessageProcessor;
+ private Listener mSourceEventListener;
+ private ComplexFeedForwardGainControl mAGC = new ComplexFeedForwardGainControl(32);
+ private Map mBasebandFilters = new HashMap<>();
+ private ComplexFIRFilter2 mBasebandFilter;
+ protected InterpolatingSampleBuffer mInterpolatingSampleBuffer;
+ protected DQPSKDecisionDirectedDemodulator mQPSKDemodulator;
+ protected CostasLoop mCostasLoop;
+ protected FrequencyCorrectionSyncMonitor mFrequencyCorrectionSyncMonitor;
+ protected DMRMessageFramer mMessageFramer;
+
+ /**
+ * Constructs an instance
+ */
+ public DMRDecoder(DecodeConfigDMR config)
+ {
+ mMessageProcessor = new DMRMessageProcessor(config);
+ mMessageProcessor.setMessageListener(getMessageListener());
+ getDibitBroadcaster().addListener(mByteBufferAssembler);
+ setSampleRate(25000.0);
+ }
+
+ /**
+ * DMR decoder type.
+ */
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.DMR;
+ }
+
+ /**
+ * Message processor
+ */
+ protected DMRMessageProcessor getMessageProcessor()
+ {
+ return mMessageProcessor;
+ }
+
+ /**
+ * Sets the sample rate and configures internal decoder components.
+ * @param sampleRate
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ if(sampleRate <= getSymbolRate() * 2)
+ {
+ throw new IllegalArgumentException("Sample rate [" + sampleRate + "] must be >9600 (2 * " +
+ getSymbolRate() + " symbol rate)");
+ }
+
+ mSampleRate = sampleRate;
+ mBasebandFilter = new ComplexFIRFilter2(getBasebandFilter());
+ mCostasLoop = new CostasLoop(getSampleRate(), getSymbolRate());
+ mCostasLoop.setPLLBandwidth(PLLBandwidth.BW_300);
+ mFrequencyCorrectionSyncMonitor = new FrequencyCorrectionSyncMonitor(mCostasLoop, this);
+ mInterpolatingSampleBuffer = new InterpolatingSampleBuffer(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
+
+ mQPSKDemodulator = new DQPSKDecisionDirectedDemodulator(mCostasLoop, mInterpolatingSampleBuffer);
+
+ if(mMessageFramer != null)
+ {
+ getDibitBroadcaster().removeListener(mMessageFramer);
+ }
+
+ //The Costas Loop receives symbol-inversion correction requests when detected.
+ //The PLL gain monitor receives sync detect/loss signals from the message framer
+ mMessageFramer = new DMRMessageFramer(mCostasLoop);
+ mMessageFramer.setSyncDetectListener(mFrequencyCorrectionSyncMonitor);
+ mMessageFramer.setListener(getMessageProcessor());
+
+ mQPSKDemodulator.setSymbolListener(getDibitBroadcaster());
+ getDibitBroadcaster().addListener(mMessageFramer);
+ }
+
+ /**
+ * Primary method for processing incoming complex sample buffers
+ * @param reusableComplexBuffer containing channelized complex samples
+ */
+ @Override
+ public void receive(ReusableComplexBuffer reusableComplexBuffer)
+ {
+ //User accounting of the incoming buffer is handled by the filter
+ ReusableComplexBuffer basebandFiltered = filter(reusableComplexBuffer);
+
+ //User accounting of the incoming buffer is handled by the gain filter
+ ReusableComplexBuffer gainApplied = mAGC.filter(basebandFiltered);
+
+ mMessageFramer.setCurrentTime(reusableComplexBuffer.getTimestamp());
+
+ //User accounting of the filtered buffer is handled by the demodulator
+ mQPSKDemodulator.receive(gainApplied);
+ }
+
+ /**
+ * Filters the complex buffer and returns a new reusable complex buffer with the filtered contents.
+ * @param reusableComplexBuffer to filter
+ * @return filtered complex buffer
+ */
+ protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
+ {
+ //User accounting of the incoming buffer is handled by the filter
+ return mBasebandFilter.filter(reusableComplexBuffer);
+ }
+
+ /**
+ * Constructs a baseband filter for this decoder using the current sample rate
+ */
+ private float[] getBasebandFilter()
+ {
+ //Attempt to reuse a cached (ie already-designed) filter if available
+ float[] filter = mBasebandFilters.get(getSampleRate());
+
+ if(filter == null)
+ {
+ FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
+ .sampleRate((int)getSampleRate())
+ .passBandCutoff(5100)
+ .passBandAmplitude(1.0)
+ .passBandRipple(0.01)
+ .stopBandAmplitude(0.0)
+ .stopBandStart(6500)
+ .stopBandRipple(0.01)
+ .build();
+
+ try
+ {
+ filter = FilterFactory.getTaps(specification);//
+ }
+ catch(Exception fde) //FilterDesignException
+ {
+ mLog.error("Couldn't design low pass baseband filter for sample rate: " + getSampleRate());
+ }
+
+ if(filter != null)
+ {
+ mBasebandFilters.put(getSampleRate(), filter);
+ }
+ else
+ {
+ throw new IllegalStateException("Couldn't design a DMR baseband filter for sample rate: " + getSampleRate());
+ }
+ }
+
+ return filter;
+ }
+
+ /**
+ * Process source events
+ */
+ protected void process(SourceEvent sourceEvent)
+ {
+ switch(sourceEvent.getEvent())
+ {
+ case NOTIFICATION_SAMPLE_RATE_CHANGE:
+ mCostasLoop.reset();
+ setSampleRate(sourceEvent.getValue().doubleValue());
+ break;
+ case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
+ //Reset the PLL if/when the tuner PPM changes so that we can re-lock
+ mCostasLoop.reset();
+ break;
+ }
+ }
+
+ /**
+ * Resets this decoder to prepare for processing a new channel
+ */
+ @Override
+ public void reset()
+ {
+ mCostasLoop.reset();
+ mFrequencyCorrectionSyncMonitor.reset();
+ }
+
+ /**
+ * Assembler for packaging Dibit stream into reusable byte buffers.
+ */
+ protected Broadcaster getDibitBroadcaster()
+ {
+ return mDibitBroadcaster;
+ }
+
+ /**
+ * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
+ */
+ @Override
+ public void setBufferListener(Listener listener)
+ {
+ mByteBufferAssembler.setBufferListener(listener);
+ }
+
+ /**
+ * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
+ */
+ @Override
+ public void removeBufferListener(Listener listener)
+ {
+ mByteBufferAssembler.removeBufferListener(listener);
+ }
+
+ /**
+ * Implements the IByteBufferProvider interface - delegates to the byte buffer assembler
+ */
+ @Override
+ public boolean hasBufferListeners()
+ {
+ return mByteBufferAssembler.hasBufferListeners();
+ }
+
+ protected double getSymbolRate()
+ {
+ return SYMBOL_RATE;
+ }
+
+ /**
+ * Current sample rate for this decoder
+ */
+ protected double getSampleRate()
+ {
+ return mSampleRate;
+ }
+
+ /**
+ * Samples per symbol based on current sample rate and symbol rate.
+ */
+ public float getSamplesPerSymbol()
+ {
+ return (float)(getSampleRate() / getSymbolRate());
+ }
+
+ /**
+ * Sets the source event listener to receive source events from this decoder.
+ */
+ @Override
+ public void setSourceEventListener(Listener listener)
+ {
+ mSourceEventListener = listener;
+ }
+
+ /**
+ * Removes a registered source event listener from receiving source events from this decoder
+ */
+ @Override
+ public void removeSourceEventListener()
+ {
+ mSourceEventListener = null;
+ }
+
+ @Override
+ public Listener getSourceEventListener()
+ {
+ return new Listener()
+ {
+ @Override
+ public void receive(SourceEvent sourceEvent)
+ {
+ process(sourceEvent);
+ }
+ };
+ }
+
+ /**
+ * Broadcasts the source event to an optional registered listener. This method should primarily be used to
+ * issue frequency correction requests to the channel source.
+ * @param sourceEvent to broadcast
+ */
+ public void broadcast(SourceEvent sourceEvent)
+ {
+ if(mSourceEventListener != null)
+ {
+ mSourceEventListener.receive(sourceEvent);
+ }
+ }
+
+ /**
+ * Listener interface to receive reusable complex buffers
+ */
+ @Override
+ public Listener getReusableComplexBufferListener()
+ {
+ return DMRDecoder.this;
+ }
+
+ /**
+ * Starts the decoder
+ */
+ @Override
+ public void start()
+ {
+ //No-op
+ }
+
+ /**
+ * Stops the decoder
+ */
+ @Override
+ public void stop()
+ {
+ //No-op
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderInstrumented.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderInstrumented.java
new file mode 100644
index 00000000..40849285
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderInstrumented.java
@@ -0,0 +1,123 @@
+/*
+ * ******************************************************************************
+ * sdrtrunk
+ * 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
+ * *****************************************************************************
+ */
+package io.github.dsheirer.module.decode.dmr;
+
+import io.github.dsheirer.dsp.psk.DQPSKDecisionDirectedDemodulatorInstrumented;
+import io.github.dsheirer.dsp.psk.InterpolatingSampleBufferInstrumented;
+import io.github.dsheirer.dsp.psk.SymbolDecisionData;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
+import io.github.dsheirer.sample.complex.Complex;
+
+/**
+ * Instrumented version of DMR decoder
+ */
+public class DMRDecoderInstrumented extends DMRDecoder
+{
+ private Listener mPLLPhaseErrorListener;
+ private Listener mPLLFrequencyListener;
+ private Listener mSamplesPerSymbolListener;
+ private Listener mComplexSymbolListener;
+ private Listener mFilteredSymbolListener;
+ private Listener mSymbolDecisionDataListener;
+
+ /**
+ * Instrumented version of the DMR decoder that supports registering listeners to provide access to data as
+ * it is being processed by the decoder.
+ */
+ public DMRDecoderInstrumented(DecodeConfigDMR config)
+ {
+ super(config);
+ }
+
+ /**
+ * Overrides the filter method so that we can capture the filtered samples for instrumentation
+ */
+ protected ReusableComplexBuffer filter(ReusableComplexBuffer reusableComplexBuffer)
+ {
+ ReusableComplexBuffer filtered = super.filter(reusableComplexBuffer);
+
+ if(mFilteredSymbolListener != null)
+ {
+ filtered.incrementUserCount();
+ mFilteredSymbolListener.receive(filtered);
+ }
+
+ return filtered;
+ }
+
+ /**
+ * Overrides this method so we can correctly configure for instrumented operations
+ */
+ public void setSampleRate(double sampleRate)
+ {
+ super.setSampleRate(sampleRate);
+
+ InterpolatingSampleBufferInstrumented instrumentedBuffer =
+ new InterpolatingSampleBufferInstrumented(getSamplesPerSymbol(), SAMPLE_COUNTER_GAIN);
+ mInterpolatingSampleBuffer = instrumentedBuffer;
+
+ DQPSKDecisionDirectedDemodulatorInstrumented instrumented = new DQPSKDecisionDirectedDemodulatorInstrumented(mCostasLoop, instrumentedBuffer, getSampleRate());
+ mQPSKDemodulator = instrumented;
+
+ instrumented.setComplexSymbolListener(mComplexSymbolListener);
+ instrumented.setPLLErrorListener(mPLLPhaseErrorListener);
+ instrumented.setPLLFrequencyListener(mPLLFrequencyListener);
+ instrumented.setSymbolDecisionDataListener(mSymbolDecisionDataListener);
+ instrumented.setSamplesPerSymbolListener(mSamplesPerSymbolListener);
+ instrumented.setSymbolListener(getDibitBroadcaster());
+ getDibitBroadcaster().addListener(mMessageFramer);
+ }
+
+ public void setComplexSymbolListener(Listener listener)
+ {
+ mComplexSymbolListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setComplexSymbolListener(listener);
+ }
+
+ public void setPLLPhaseErrorListener(Listener listener)
+ {
+ mPLLPhaseErrorListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLErrorListener(listener);
+ }
+
+ public void setPLLFrequencyListener(Listener listener)
+ {
+ mPLLFrequencyListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setPLLFrequencyListener(listener);
+ }
+
+ public void setFilteredBufferListener(Listener listener)
+ {
+ mFilteredSymbolListener = listener;
+ }
+
+ public void setSymbolDecisionDataListener(Listener listener)
+ {
+ mSymbolDecisionDataListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSymbolDecisionDataListener(listener);
+ }
+
+ public void setSamplesPerSymbolListener(Listener listener)
+ {
+ mSamplesPerSymbolListener = listener;
+ ((DQPSKDecisionDirectedDemodulatorInstrumented)mQPSKDemodulator).setSamplesPerSymbolListener(listener);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java
new file mode 100644
index 00000000..d3876b7c
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java
@@ -0,0 +1,1135 @@
+/*
+ *
+ * * ******************************************************************************
+ * * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao
+ * *
+ * * This program is free software: you can redistribute it and/or modify
+ * * it under the terms of the GNU General Public License as published by
+ * * the Free Software Foundation, either version 3 of the License, or
+ * * (at your option) any later version.
+ * *
+ * * This program is distributed in the hope that it will be useful,
+ * * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * * GNU General Public License for more details.
+ * *
+ * * You should have received a copy of the GNU General Public License
+ * * along with this program. If not, see
+ * * *****************************************************************************
+ *
+ *
+ */
+package io.github.dsheirer.module.decode.dmr;
+
+import com.google.common.eventbus.Subscribe;
+import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
+import io.github.dsheirer.channel.state.DecoderStateEvent;
+import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
+import io.github.dsheirer.channel.state.State;
+import io.github.dsheirer.channel.state.TimeslotDecoderState;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.controller.channel.Channel.ChannelType;
+import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierClass;
+import io.github.dsheirer.identifier.IdentifierCollection;
+import io.github.dsheirer.identifier.MutableIdentifierCollection;
+import io.github.dsheirer.identifier.Role;
+import io.github.dsheirer.identifier.integer.IntegerIdentifier;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.DecoderType;
+import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
+import io.github.dsheirer.module.decode.dmr.event.DMRDecodeEvent;
+import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSystemStatus;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataChannelGrant;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Protect;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.acknowledge.Acknowledge;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.Ahoy;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.ServiceRadioCheck;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.ahoy.StunReviveKill;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.Announcement;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant;
+import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
+import io.github.dsheirer.module.decode.dmr.message.data.packet.DMRPacketMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
+import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
+import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
+import io.github.dsheirer.module.decode.event.DecodeEvent;
+import io.github.dsheirer.module.decode.event.DecodeEventType;
+import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
+import org.apache.commons.math3.util.FastMath;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Decoder state for an DMR channel. Maintains the call/data/idle state of the channel and produces events by
+ * monitoring the decoded message stream.
+ */
+public class DMRDecoderState extends TimeslotDecoderState
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRDecoderState.class);
+ private static final long MAX_VALID_CALL_DURATION_MS = 30000;
+ private Channel mChannel;
+ private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor;
+ private DMRTrafficChannelManager mTrafficChannelManager;
+ private DecodeEvent mCurrentCallEvent;
+ private long mCurrentFrequency;
+ private Map mDetectedCallEventsMap = new TreeMap<>();
+ private static final AddChannelRotationActiveStateRequest CAPACITY_PLUS_ACTIVE_STATE_REQUEST =
+ new AddChannelRotationActiveStateRequest(State.ACTIVE);
+
+ /**
+ * Constructs an DMR decoder state with an optional traffic channel manager.
+ * @param channel with configuration details
+ * @param trafficChannelManager for handling traffic channel grants.
+ * @param configurationMonitor for tracking activity summary
+ */
+ public DMRDecoderState(Channel channel, int timeslot, DMRTrafficChannelManager trafficChannelManager)
+ {
+ super(timeslot);
+ mChannel = channel;
+ mTrafficChannelManager = trafficChannelManager;
+
+ //The decoder state passes all messages to the network configuration monitor, so we only contruct
+ //the monitor for timeslot 1.
+ if(timeslot == 1)
+ {
+ mNetworkConfigurationMonitor = new DMRNetworkConfigurationMonitor(channel);
+ }
+ }
+
+ /**
+ * Indicates if this decoder state has an (optional) traffic channel manager.
+ */
+ private boolean hasTrafficChannelManager()
+ {
+ return mTrafficChannelManager != null;
+ }
+
+ /**
+ * Processes channel configuration change notifications received over the processing chain event bus. This is
+ * primarily used for Capacity+ systems when the standard channel is converted to a traffic channel. In response,
+ * we nullify the traffic channel manager to ensure the traffic channel no longer behaves as a standard channel
+ * regarding channel conversions and allocations.
+ *
+ * @param notification of channel configuration change
+ */
+ @Subscribe
+ public void channelChanged(ChannelConfigurationChangeNotification notification)
+ {
+ if(notification.getChannel().isTrafficChannel())
+ {
+ mTrafficChannelManager = null;
+ }
+ }
+
+ /**
+ * Identifies the decoder type
+ */
+ @Override
+ public DecoderType getDecoderType()
+ {
+ return DecoderType.DMR;
+ }
+
+ /**
+ * Performs a full reset to prepare this object for reuse on a new channel
+ */
+ @Override
+ public void reset()
+ {
+ super.reset();
+ resetState();
+
+ mCurrentFrequency = 0;
+ mDetectedCallEventsMap.clear();
+ }
+
+ /**
+ * Resets any temporal state details
+ */
+ protected void resetState()
+ {
+ super.resetState();
+ closeCurrentCallEvent(System.currentTimeMillis());
+ }
+
+ /**
+ * Primary message processing method.
+ */
+ @Override
+ public void receive(IMessage message)
+ {
+ if(message.isValid() && message.getTimeslot() == getTimeslot())
+ {
+ if(message instanceof VoiceMessage)
+ {
+ processVoice((VoiceMessage)message);
+ }
+ else if(message instanceof DataMessage)
+ {
+ processData((DataMessage)message);
+ }
+ else if(message instanceof LCMessage)
+ {
+ processLinkControl((LCMessage)message, false);
+ }
+ else if(message instanceof DMRPacketMessage)
+ {
+ processPacket((DMRPacketMessage)message);
+ }
+ else if(message instanceof DMRMessage)
+ {
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
+ }
+ }
+ //SLCO messages on timeslot 0 to catch capacity plus rest channel events
+ else if(message.isValid() && message.getTimeslot() == 0 && message instanceof LCMessage)
+ {
+ processLinkControl((LCMessage)message, false);
+ }
+
+ //Pass the message to the network configuration monitor, if this decoder state has a non-null instance
+ if(mNetworkConfigurationMonitor != null && message.isValid() && message instanceof DMRMessage)
+ {
+ mNetworkConfigurationMonitor.process((DMRMessage)message);
+ }
+ }
+
+ /**
+ * Processes Capacity Plus rest channel notifications to detect when the rest channel has changed. When a new
+ * rest channel is specified that is different from the current channel, notify the traffic channel manager so that
+ * it can convert the current channel to a traffic channel and start a new channel for the rest channel, using the
+ * current channel's configuration details and the specified rest channel frequency. This approach ensures that
+ * there is no disruption to the currently processing channel and that the original channel configuration can be
+ * recreated to follow the rest channel.
+ *
+ * @param restChannel currently indicated
+ */
+ private void updateRestChannel(DMRChannel restChannel)
+ {
+ //Only respond if this is a standard/control channel (not a traffic channel).
+ if(mChannel.isStandardChannel() && mCurrentFrequency > 0 &&
+ restChannel.getDownlinkFrequency() > 0 &&
+ restChannel.getDownlinkFrequency() != mCurrentFrequency && mTrafficChannelManager != null)
+ {
+ mTrafficChannelManager.convertToTrafficChannel(mChannel, mCurrentFrequency, restChannel,
+ mNetworkConfigurationMonitor);
+ }
+ }
+
+ /**
+ * Preloads the DMR network configuration monitor that is optionally delivered as preload data. This is primarily
+ * used during a Capacity plus REST channel rotation to pass the monitor from the previous REST channel to the
+ * current REST channel to ensure data continuity.
+ *
+ * Note: the monitor is only assigned to the timeslot 1 decoder state since the decoder state passes all received
+ * messages (Timeslots 0, 1, and 2) to the monitor.
+ *
+ * Note: this method is invoked over the Guava event but by the ChannelProcessingManager.
+ *
+ * @param preloadData containing a DMR network configuration monitor.
+ */
+ @Subscribe
+ public void preload(DMRNetworkConfigurationPreloadData preloadData)
+ {
+ if(getTimeslot() == 1 && preloadData.hasData())
+ {
+ mNetworkConfigurationMonitor = preloadData.getData();
+ }
+ }
+
+ /**
+ * Processes a packet message
+ */
+ private void processPacket(DMRPacketMessage packet)
+ {
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot()));
+
+ DecodeEvent packetEvent = DMRDecodeEvent.builder(packet.getTimestamp())
+ .eventDescription(DecodeEventType.DATA_PACKET.name())
+ .identifiers(getMergedIdentifierCollection(packet.getIdentifiers()))
+ .timeslot(packet.getTimeslot())
+ .details(packet.toString())
+ .build();
+
+ broadcast(packetEvent);
+ }
+
+ /**
+ * Processes voice messages
+ */
+ private void processVoice(VoiceMessage message)
+ {
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
+ }
+
+ /**
+ * Processes a voice header message
+ */
+ private void processHeader(HeaderMessage header)
+ {
+ switch(header.getSlotType().getDataType())
+ {
+ case VOICE_HEADER:
+ broadcast(new DecoderStateEvent(this, Event.START, State.CALL, getTimeslot()));
+ break;
+ case PI_HEADER:
+ case MBC_HEADER:
+ case DATA_HEADER:
+ case USB_DATA:
+ case MBC_ENC_HEADER:
+ case DATA_ENC_HEADER:
+ case CHANNEL_CONTROL_ENC_HEADER:
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot()));
+ break;
+ }
+
+ //Process the link control message to get the identifiers
+ LCMessage lc = header.getLCMessage();
+
+ if(lc.isValid())
+ {
+ processLinkControl(lc, false);
+ }
+ }
+
+ /**
+ * Process Data Messages
+ */
+ private void processData(DataMessage message)
+ {
+ switch(message.getSlotType().getDataType())
+ {
+ case CSBK:
+ if(message instanceof CSBKMessage)
+ {
+ processCSBK((CSBKMessage)message);
+ }
+ break;
+ case VOICE_HEADER:
+ if(message instanceof HeaderMessage)
+ {
+ processVoiceHeader((HeaderMessage)message);
+ }
+ break;
+ case USB_DATA:
+ break;
+ case PI_HEADER:
+ case MBC_HEADER:
+ case DATA_HEADER:
+ case MBC_ENC_HEADER:
+ case DATA_ENC_HEADER:
+ case CHANNEL_CONTROL_ENC_HEADER:
+ if(message instanceof HeaderMessage)
+ {
+ processHeader((HeaderMessage)message);
+ }
+ break;
+ case SLOT_IDLE:
+ closeCurrentCallEvent(message.getTimestamp());
+ getIdentifierCollection().remove(IdentifierClass.USER);
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
+ break;
+ case TLC:
+ if(message instanceof Terminator)
+ {
+ processTerminator((Terminator)message);
+ }
+ break;
+ case MBC_BLOCK:
+ case RATE_1_OF_2_DATA:
+ case RATE_3_OF_4_DATA:
+ case RATE_1_DATA:
+ case RESERVED_15:
+ case UNKNOWN:
+ default:
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
+ break;
+ }
+ }
+
+ /**
+ * Process terminator with link control messages
+ */
+ private void processTerminator(Terminator terminator)
+ {
+ closeCurrentCallEvent(terminator.getTimestamp());
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
+
+ LCMessage lcMessage = terminator.getLCMessage();
+
+ if(lcMessage.isValid())
+ {
+ processLinkControl(lcMessage, true);
+ }
+ }
+
+ /**
+ * Process a voice header message
+ */
+ private void processVoiceHeader(HeaderMessage voiceHeader)
+ {
+ LCMessage lcMessage = voiceHeader.getLCMessage();
+
+ if(lcMessage.isValid())
+ {
+ processLinkControl(lcMessage, false);
+ }
+ }
+
+ private void processCSBK(CSBKMessage csbk)
+ {
+ switch(csbk.getOpcode())
+ {
+ case STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_PAYLOAD:
+ case STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_PAYLOAD:
+ if(csbk instanceof Acknowledge)
+ {
+ DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.RESPONSE.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details(((Acknowledge)csbk).getReason().toString())
+ .build();
+
+ broadcast(ackEvent);
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
+ break;
+ case STANDARD_ACKNOWLEDGE_RESPONSE_INBOUND_TSCC:
+ case STANDARD_ACKNOWLEDGE_RESPONSE_OUTBOUND_TSCC:
+ if(csbk instanceof Acknowledge)
+ {
+ DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.RESPONSE.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details(((Acknowledge)csbk).getReason().toString())
+ .build();
+
+ broadcast(ackEvent);
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case STANDARD_AHOY:
+ if(csbk instanceof Ahoy)
+ {
+ switch(((Ahoy)csbk).getServiceKind())
+ {
+ case AUTHENTICATE_REGISTER_RADIO_CHECK_SERVICE:
+ DecodeEvent registerEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.COMMAND.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details(DecodeEventType.REGISTER.toString())
+ .build();
+ broadcast(registerEvent);
+ break;
+ case CANCEL_CALL_SERVICE:
+ DecodeEvent cancelEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.COMMAND.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details("CANCEL CALL")
+ .build();
+ broadcast(cancelEvent);
+ break;
+ case SUPPLEMENTARY_SERVICE:
+ if(csbk instanceof StunReviveKill)
+ {
+ DecodeEvent stunEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.COMMAND.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details(((StunReviveKill)csbk).getCommand() + " RADIO")
+ .build();
+ broadcast(stunEvent);
+ }
+ break;
+ case FULL_DUPLEX_MS_TO_MS_PACKET_CALL_SERVICE:
+ case FULL_DUPLEX_MS_TO_MS_VOICE_CALL_SERVICE:
+ case INDIVIDUAL_VOICE_CALL_SERVICE:
+ case INDIVIDUAL_PACKET_CALL_SERVICE:
+ case INDIVIDUAL_UDT_SHORT_DATA_CALL_SERVICE:
+ case TALKGROUP_PACKET_CALL_SERVICE:
+ case TALKGROUP_UDT_SHORT_DATA_CALL_SERVICE:
+ case TALKGROUP_VOICE_CALL_SERVICE:
+ if(csbk instanceof ServiceRadioCheck)
+ {
+ ServiceRadioCheck src = (ServiceRadioCheck)csbk;
+
+ DecodeEvent checkEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.RADIO_CHECK.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details(src.getServiceDescription() + " SERVICE FOR " +
+ (src.isTalkgroupTarget() ? "TALKGROUP" : "RADIO"))
+ .build();
+ broadcast(checkEvent);
+ }
+ break;
+ }
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case STANDARD_ALOHA:
+ if(csbk instanceof Aloha)
+ {
+ Aloha aloha = (Aloha)csbk;
+
+ if(aloha.hasRadioIdentifier())
+ {
+ DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.RESPONSE.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details("Aloha Acknowledge")
+ .build();
+
+ broadcast(ackEvent);
+ resetState();
+ }
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case STANDARD_ANNOUNCEMENT:
+ if(csbk instanceof Announcement)
+ {
+ switch(((Announcement)csbk).getAnnouncementType())
+ {
+ case MASS_REGISTRATION:
+ DecodeEvent massEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.REGISTER.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details("MASS REGISTRATION")
+ .build();
+ broadcast(massEvent);
+ break;
+ case VOTE_NOW_ADVICE:
+ if(csbk instanceof VoteNowAdvice)
+ {
+ DecodeEvent voteEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.COMMAND.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details("VOTE NOW FOR " + ((VoteNowAdvice)csbk).getVotedSystemIdentityCode())
+ .build();
+ broadcast(voteEvent);
+ }
+ break;
+ }
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case STANDARD_CLEAR:
+ broadcast(new DecoderStateEvent(this, Event.END, State.CALL, getTimeslot()));
+ resetState();
+ break;
+ case STANDARD_PREAMBLE:
+ getIdentifierCollection().update(csbk.getIdentifiers());
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.DATA, getTimeslot()));
+ break;
+ case STANDARD_PROTECT:
+ if(csbk instanceof Protect)
+ {
+ DecodeEvent protectEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.COMMAND.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details("PROTECT: " + ((Protect)csbk).getProtectKind())
+ .build();
+ broadcast(protectEvent);
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
+ break;
+ case HYTERA_08_ANNOUNCEMENT:
+ case HYTERA_68_ANNOUNCEMENT:
+ case HYTERA_XPT_SITE_STATE:
+
+ case MOTOROLA_CAPPLUS_NEIGHBOR_REPORT:
+ if(csbk instanceof CapacityPlusNeighbors)
+ {
+ //Update state and rest channel
+ updateRestChannel(((CapacityPlusNeighbors)csbk).getRestChannel());
+ }
+ break;
+ case MOTOROLA_CAPPLUS_SYSTEM_STATUS:
+ if(csbk instanceof CapacityPlusSystemStatus)
+ {
+ CapacityPlusSystemStatus cpss = (CapacityPlusSystemStatus)csbk;
+
+ //Channel rotation monitor normally uses only CONTROL state, so when we detect that we're a
+ //Capacity plus system, add ACTIVE as an active state to the monitor. This can be requested repeatedly.
+ getInterModuleEventBus().post(CAPACITY_PLUS_ACTIVE_STATE_REQUEST);
+
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
+
+ //Update state and rest channel
+ updateRestChannel(cpss.getRestChannel());
+ }
+ break;
+ case MOTOROLA_CONPLUS_NEIGHBOR_REPORT:
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT:
+ case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM:
+ case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM:
+ case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM:
+ if(csbk instanceof ChannelGrant)
+ {
+ ChannelGrant dataGrant = (ChannelGrant)csbk;
+ DMRChannel channel = dataGrant.getChannel();
+ if(hasTrafficChannelManager())
+ {
+ mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()),
+ csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
+ }
+ processCallDetection(dataGrant.getChannel(), dataGrant.getIdentifiers(), dataGrant.getTimestamp(), DecodeEventType.DATA_CALL);
+ }
+ break;
+ case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT:
+ case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT:
+ if(csbk instanceof ChannelGrant)
+ {
+ ChannelGrant tgGrant = (ChannelGrant)csbk;
+ DMRChannel channel = tgGrant.getChannel();
+ if(hasTrafficChannelManager())
+ {
+ mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()),
+ csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
+ }
+ processCallDetection(tgGrant.getChannel(), tgGrant.getIdentifiers(), tgGrant.getTimestamp(), DecodeEventType.CALL_GROUP);
+ }
+ break;
+ case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT:
+ case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT:
+ if(csbk instanceof ChannelGrant)
+ {
+ ChannelGrant channelGrant = (ChannelGrant)csbk;
+ DMRChannel channel = channelGrant.getChannel();
+ if(hasTrafficChannelManager())
+ {
+ mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()),
+ csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
+ }
+ processCallDetection(channelGrant.getChannel(), channelGrant.getIdentifiers(),
+ channelGrant.getTimestamp(), DecodeEventType.CALL_UNIT_TO_UNIT);
+ }
+ break;
+ case MOTOROLA_CAPMAX_ALOHA:
+ if(csbk instanceof CapacityMaxAloha)
+ {
+ CapacityMaxAloha cmAloha = (CapacityMaxAloha)csbk;
+
+ if(cmAloha.hasRadioIdentifier())
+ {
+ DecodeEvent ackEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .eventDescription(DecodeEventType.RESPONSE.name())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .details("Aloha Acknowledge")
+ .build();
+
+ broadcast(ackEvent);
+
+ resetState();
+ }
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT:
+ if(csbk instanceof ConnectPlusDataChannelGrant)
+ {
+ ConnectPlusDataChannelGrant cpdcg = (ConnectPlusDataChannelGrant)csbk;
+ if(hasTrafficChannelManager())
+ {
+ mTrafficChannelManager.processChannelGrant(cpdcg.getChannel(),
+ getMergedIdentifierCollection(csbk.getIdentifiers()), csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
+ }
+ processCallDetection(cpdcg.getChannel(), cpdcg.getIdentifiers(), cpdcg.getTimestamp(),
+ DecodeEventType.DATA_CALL);
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case MOTOROLA_CONPLUS_REGISTRATION_REQUEST:
+ DecodeEvent event = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .details("Registration Request")
+ .eventDescription(DecodeEventType.REGISTER.toString())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .build();
+ broadcast(event);
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case MOTOROLA_CONPLUS_REGISTRATION_RESPONSE:
+ DecodeEvent regRespEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .details("Registration Response")
+ .eventDescription(DecodeEventType.REGISTER.toString())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .build();
+ broadcast(regRespEvent);
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER:
+ if(csbk instanceof ConnectPlusVoiceChannelUser)
+ {
+ DMRChannel channel = ((ConnectPlusVoiceChannelUser)csbk).getChannel();
+ if(hasTrafficChannelManager())
+ {
+ mTrafficChannelManager.processChannelGrant(channel, getMergedIdentifierCollection(csbk.getIdentifiers()),
+ csbk.getOpcode(), csbk.getTimestamp(), csbk.isEncrypted());
+ }
+ processCallDetection(channel, csbk.getIdentifiers(), csbk.getTimestamp(),
+ DecodeEventType.CALL_GROUP);
+ }
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case MOTOROLA_CONPLUS_TALKGROUP_AFFILIATION:
+ DecodeEvent affiliateEvent = DMRDecodeEvent.builder(csbk.getTimestamp())
+ .details("TALKGROUP AFFILIATION")
+ .eventDescription(DecodeEventType.AFFILIATE.toString())
+ .identifiers(getMergedIdentifierCollection(csbk.getIdentifiers()))
+ .timeslot(csbk.getTimeslot())
+ .build();
+ broadcast(affiliateEvent);
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CONTROL, getTimeslot()));
+ break;
+ case STANDARD_MOVE_TSCC:
+
+
+ default:
+ broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
+ break;
+ }
+ }
+
+ /**
+ * Creates a copy of the current identifier collection, removes any USER class identifiers and loads the identifiers
+ * argument values into the collection.
+ * @param identifiers to load into the collection.
+ * @return copy identifier collection.
+ */
+ private IdentifierCollection getMergedIdentifierCollection(List identifiers)
+ {
+ MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
+ mic.remove(IdentifierClass.USER);
+ mic.update(identifiers);
+ return mic;
+ }
+
+ /**
+ * Process Call Detections.
+ *
+ * Note: once full support for Connect+ trunk tracking is implemented with the DMRTrafficChannelManager, this will
+ * have to be modified to detect if the trunked channel was allocated or if the call was simply detected. This
+ * would be contingent on having a map of Logical Slot Numbers to frequency for the traffic channel manager to make
+ * an allocation.
+ *
+ * @param channel for the call event
+ * @param identifiers for the call event
+ * @param timestamp of the event or update
+ */
+ private void processCallDetection(DMRChannel channel, List identifiers, long timestamp,
+ DecodeEventType eventType)
+ {
+ //Check to see if there is a current call event to see if the detected call event is actually for this timeslot
+ //and we can then identify the LSN for this timeslot
+ if(mCurrentFrequency == 0 && mCurrentCallEvent != null &&
+ isSameCall(mCurrentCallEvent.getIdentifierCollection(), identifiers))
+ {
+ mCurrentFrequency = channel.getDownlinkFrequency();
+
+ if(mNetworkConfigurationMonitor != null)
+ {
+ mNetworkConfigurationMonitor.setCurrentChannel(channel);
+ }
+ }
+
+ DecodeEvent event = mDetectedCallEventsMap.get(channel.getLogicalSlotNumber());
+
+ if(event == null)
+ {
+ event = DMRDecodeEvent.builder(timestamp)
+ .timeslot(getTimeslot())
+ .identifiers(getMergedIdentifierCollection(identifiers))
+ .eventDescription(eventType.toString())
+ .build();
+// mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event);
+ }
+ else
+ {
+ if(event.getIdentifierCollection() != null &&
+ isSameCall(event.getIdentifierCollection(), identifiers) &&
+ FastMath.abs(timestamp - event.getTimeStart()) < MAX_VALID_CALL_DURATION_MS)
+ {
+ event.update(timestamp);
+ }
+ else
+ {
+ event = DMRDecodeEvent.builder(timestamp)
+ .timeslot(getTimeslot())
+ .identifiers(getMergedIdentifierCollection(identifiers))
+ .eventDescription(DecodeEventType.CALL_DETECT.toString())
+ .build();
+// mDetectedCallEventsMap.put(channel.getLogicalSlotNumber(), event);
+ }
+ }
+
+ //Only broadcast the call detect event if it doesn't match the current logical slot number
+// if(mCurrentLSN == null || mCurrentLSN != channel.getLogicalSlotNumber())
+// {
+// broadcast(event);
+// }
+ }
+
+ /**
+ * Indicates if the TO/FROM identifiers in the identifier collection match the TO/FROM identifiers in the list
+ * of identifiers.
+ * @param identifierCollection containing TO/FROM identifiers
+ * @param identifiers containing TO/FROM identifiers
+ * @return true if the TO/FROM identifiers in each collection match
+ */
+ private boolean isSameCall(IdentifierCollection identifierCollection, List identifiers)
+ {
+ IntegerIdentifier to = null;
+ IntegerIdentifier from = null;
+
+ for(Identifier identifier: identifierCollection.getIdentifiers(IdentifierClass.USER, Role.TO))
+ {
+ if(identifier instanceof IntegerIdentifier)
+ {
+ to = (IntegerIdentifier)identifier;
+ break;
+ }
+ }
+
+ for(Identifier identifier: identifierCollection.getIdentifiers(IdentifierClass.USER, Role.FROM))
+ {
+ if(identifier instanceof IntegerIdentifier)
+ {
+ from = (IntegerIdentifier)identifier;
+ break;
+ }
+ }
+
+ if(to == null || from == null)
+ {
+ return false;
+ }
+
+ boolean toMatch = false;
+ boolean fromMatch = false;
+
+ for(Identifier identifier: identifiers)
+ {
+ if(identifier.getRole() == Role.TO &&
+ identifier instanceof IntegerIdentifier &&
+ ((IntegerIdentifier)identifier).getValue() == to.getValue())
+ {
+ toMatch = true;
+ }
+ else if(identifier.getRole() == Role.FROM &&
+ identifier instanceof IntegerIdentifier &&
+ ((IntegerIdentifier)identifier).getValue() == from.getValue())
+ {
+ fromMatch = true;
+ }
+ }
+
+ return toMatch & fromMatch;
+ }
+
+ /**
+ * Processes Link Control Messages
+ * @param isTerminator set to true when the link control is carried by a terminator
+ */
+ private void processLinkControl(LCMessage message, boolean isTerminator)
+ {
+ switch(message.getOpcode())
+ {
+ case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION:
+ if(message instanceof CapacityPlusRestChannel)
+ {
+ updateRestChannel(((CapacityPlusRestChannel)message).getRestChannel());
+ }
+ break;
+ case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER:
+ if(message instanceof CapacityPlusGroupVoiceChannelUser)
+ {
+ CapacityPlusGroupVoiceChannelUser cpgvcu = (CapacityPlusGroupVoiceChannelUser)message;
+
+ //This is the current channel - what do we do with the voice channel number?
+// updateRestChannel(cpgvcu.getVoiceChannel());
+
+ if(isTerminator)
+ {
+ getIdentifierCollection().remove(Role.FROM);
+ getIdentifierCollection().update(cpgvcu.getTalkgroup());
+ }
+ else
+ {
+ getIdentifierCollection().update(message.getIdentifiers());
+ ServiceOptions serviceOptions = cpgvcu.getServiceOptions();
+ updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED :
+ DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp());
+ }
+ }
+ break;
+ case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER:
+ if(message instanceof CapacityPlusWideAreaVoiceChannelUser)
+ {
+ CapacityPlusWideAreaVoiceChannelUser cpuo4 = (CapacityPlusWideAreaVoiceChannelUser)message;
+
+ updateRestChannel(cpuo4.getRestChannel());
+
+ if(isTerminator)
+ {
+ getIdentifierCollection().remove(Role.FROM);
+ getIdentifierCollection().update(cpuo4.getTalkgroup());
+ }
+ else
+ {
+ getIdentifierCollection().update(message.getIdentifiers());
+ }
+ }
+ break;
+ case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER:
+ if(message instanceof HyteraGroupVoiceChannelUser)
+ {
+ HyteraGroupVoiceChannelUser hgvcu = (HyteraGroupVoiceChannelUser)message;
+
+ if(isTerminator)
+ {
+ getIdentifierCollection().remove(Role.FROM);
+ getIdentifierCollection().update(hgvcu.getTalkgroup());
+ }
+ else
+ {
+ getIdentifierCollection().update(message.getIdentifiers());
+ ServiceOptions serviceOptions = hgvcu.getServiceOptions();
+ updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED :
+ DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp());
+
+ }
+ }
+ break;
+ case FULL_HYTERA_TERMINATOR:
+ case FULL_STANDARD_TERMINATOR_DATA:
+ getIdentifierCollection().update(message.getIdentifiers());
+ break;
+ case FULL_HYTERA_UNIT_TO_UNIT_VOICE_CHANNEL_USER:
+ if(message instanceof HyteraUnitToUnitVoiceChannelUser)
+ {
+ HyteraUnitToUnitVoiceChannelUser huuvcu = (HyteraUnitToUnitVoiceChannelUser)message;
+
+ if(isTerminator)
+ {
+ getIdentifierCollection().remove(Role.FROM);
+ getIdentifierCollection().update(huuvcu.getTargetRadio());
+ }
+ else
+ {
+ getIdentifierCollection().update(message.getIdentifiers());
+ ServiceOptions serviceOptions = huuvcu.getServiceOptions();
+ updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED :
+ DecodeEventType.CALL_UNIT_TO_UNIT, serviceOptions.toString(), message.getTimestamp());
+ }
+ }
+ break;
+ case FULL_STANDARD_GROUP_VOICE_CHANNEL_USER:
+ if(message instanceof GroupVoiceChannelUser)
+ {
+ GroupVoiceChannelUser gvcu = (GroupVoiceChannelUser)message;
+
+ if(isTerminator)
+ {
+ getIdentifierCollection().remove(Role.FROM);
+ getIdentifierCollection().update(gvcu.getTalkgroup());
+ }
+ else
+ {
+ getIdentifierCollection().update(message.getIdentifiers());
+ ServiceOptions serviceOptions = gvcu.getServiceOptions();
+ updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED :
+ DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp());
+ }
+ }
+ break;
+ case FULL_STANDARD_UNIT_TO_UNIT_VOICE_CHANNEL_USER:
+ if(message instanceof UnitToUnitVoiceChannelUser)
+ {
+ UnitToUnitVoiceChannelUser uuvcu = (UnitToUnitVoiceChannelUser)message;
+
+ if(isTerminator)
+ {
+ getIdentifierCollection().remove(Role.FROM);
+ getIdentifierCollection().update(uuvcu.getTargetRadio());
+ }
+ else
+ {
+ getIdentifierCollection().update(message.getIdentifiers());
+ ServiceOptions serviceOptions = uuvcu.getServiceOptions();
+ updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED :
+ DecodeEventType.CALL_UNIT_TO_UNIT, serviceOptions.toString(), message.getTimestamp());
+ }
+ }
+ break;
+ case FULL_STANDARD_GPS_INFO:
+ if(message instanceof GPSInformation)
+ {
+ GPSInformation gpsInformation = (GPSInformation)message;
+ MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
+ ic.update(gpsInformation.getGPSLocation());
+
+ DecodeEvent gpsEvent = DMRDecodeEvent.builder(message.getTimestamp())
+ .eventDescription(DecodeEventType.GPS.name())
+ .identifiers(ic)
+ .timeslot(message.getTimeslot())
+ .details("LOCATION:" + gpsInformation.getGPSLocation())
+ .build();
+
+ broadcast(gpsEvent);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Updates or creates a current call event.
+ *
+ * @param type of call that will be used as an event description
+ * @param details of the call (optional)
+ * @param timestamp of the message indicating a call or continuation
+ */
+ private void updateCurrentCall(DecodeEventType type, String details, long timestamp)
+ {
+ Event event = (mCurrentCallEvent == null ? Event.START : Event.CONTINUATION);
+
+ if(mCurrentCallEvent == null)
+ {
+ mCurrentCallEvent = DMRDecodeEvent.builder(timestamp)
+ .channel(getCurrentChannel())
+ .eventDescription(type.toString())
+ .details(details)
+ .identifiers(getIdentifierCollection().copyOf())
+ .build();
+
+ broadcast(mCurrentCallEvent);
+ }
+ else
+ {
+ if(type != DecodeEventType.CALL)
+ {
+ mCurrentCallEvent.setEventDescription(type.toString());
+ mCurrentCallEvent.setDetails(details);
+ }
+
+ mCurrentCallEvent.setIdentifierCollection(getIdentifierCollection().copyOf());
+ mCurrentCallEvent.end(timestamp);
+ broadcast(mCurrentCallEvent);
+ }
+
+ if(type == DecodeEventType.CALL_GROUP_ENCRYPTED || type == DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED)
+ {
+ broadcast(new DecoderStateEvent(this, event, State.ENCRYPTED, getTimeslot()));
+ }
+ else
+ {
+ broadcast(new DecoderStateEvent(this, event, State.CALL, getTimeslot()));
+ }
+ }
+
+ /**
+ * Ends/closes the current call event.
+ *
+ * @param timestamp of the message that indicates the event has ended.
+ */
+ private void closeCurrentCallEvent(long timestamp)
+ {
+ if(mCurrentCallEvent != null)
+ {
+ mCurrentCallEvent.end(timestamp);
+ broadcast(mCurrentCallEvent);
+ mCurrentCallEvent = null;
+ }
+ }
+
+ @Override
+ public String getActivitySummary()
+ {
+ if(mNetworkConfigurationMonitor != null)
+ {
+ return mNetworkConfigurationMonitor.getActivitySummary();
+ }
+
+ return "";
+ }
+
+ @Override
+ public void receiveDecoderStateEvent(DecoderStateEvent event)
+ {
+ switch(event.getEvent())
+ {
+ case REQUEST_RESET:
+ resetState();
+ if(mNetworkConfigurationMonitor != null)
+ {
+ mNetworkConfigurationMonitor.reset();
+ }
+ break;
+ case NOTIFICATION_SOURCE_FREQUENCY:
+ mCurrentFrequency = event.getFrequency();
+ if(hasTrafficChannelManager())
+ {
+ mTrafficChannelManager.setCurrentControlFrequency(mCurrentFrequency);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void start()
+ {
+ //Change the default (45-second) traffic channel timeout to 1 second
+ if(mChannel.isTrafficChannel())
+ {
+ broadcast(new ChangeChannelTimeoutEvent(this, ChannelType.TRAFFIC, 1000, getTimeslot()));
+ }
+ }
+
+ @Override
+ public void init()
+ {
+ }
+
+ @Override
+ public void stop()
+ {
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java
new file mode 100644
index 00000000..32276186
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageFramer.java
@@ -0,0 +1,429 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+import io.github.dsheirer.bits.CorrectedBinaryMessage;
+import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.message.SyncLossMessage;
+import io.github.dsheirer.module.decode.dmr.message.CACH;
+import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
+import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
+import io.github.dsheirer.module.decode.dmr.message.DMRMessageFactory;
+import io.github.dsheirer.protocol.Protocol;
+import io.github.dsheirer.record.binary.BinaryReader;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.sample.buffer.ReusableByteBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.function.Consumer;
+
+/**
+ * DMR Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors
+ * and issue phase corrections.
+ */
+public class DMRMessageFramer implements Listener, IDMRBurstDetectListener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRMessageFramer.class);
+
+ private static final double DMR_BIT_RATE = 9600.0;
+
+ /**
+ * Provides sync detection and burst framing
+ */
+ private DMRBurstFramer mBurstFramer;
+
+ /**
+ * External listener for messages produced by this framer
+ */
+ private Listener mMessageListener;
+
+ /**
+ * External listener for sync state information
+ */
+ private ISyncDetectListener mSyncDetectListener;
+
+ /**
+ * Tracks current time for use with framed bursts/messages. This value is updated externally from sample buffer
+ * timestamps and internally as each framed message is received or sync loss is processed.
+ */
+ private long mCurrentTime = System.currentTimeMillis();
+
+ /**
+ * Constructs an instance
+ *
+ * @param phaseLockedLoop to receive PLL phase lock error corrections that are identified by the DMRBurstFramer
+ * when known phase misaligned sync patterns are detected.
+ */
+ public DMRMessageFramer(IPhaseLockedLoop phaseLockedLoop)
+ {
+ mBurstFramer = new DMRBurstFramer(this, phaseLockedLoop);
+ }
+
+ /**
+ * Constructs an instance.
+ */
+ public DMRMessageFramer()
+ {
+ this(null);
+ }
+
+ /**
+ * Registers a sync detect listener to be notified each time a sync pattern and NID are detected.
+ */
+ public void setSyncDetectListener(ISyncDetectListener syncDetectListener)
+ {
+ mSyncDetectListener = syncDetectListener;
+ }
+
+ /**
+ * Current timestamp or timestamp of incoming message buffers that is continuously updated to as
+ * close as possible to the bits processed for the expected baud rate.
+ *
+ * @return current time
+ */
+ private long getTimestamp()
+ {
+ return mCurrentTime;
+ }
+
+ /**
+ * Sets the current time. This should be invoked by an incoming message buffer stream.
+ *
+ * @param currentTime
+ */
+ public void setCurrentTime(long currentTime)
+ {
+ mCurrentTime = currentTime;
+ }
+
+ /**
+ * Updates the current timestamp based on the number of bits processed versus the bit rate per second
+ * in order to keep an accurate running timestamp to use for timestamped message creation.
+ *
+ * @param bitsProcessed thus far
+ */
+ private void updateBitsProcessed(int bitsProcessed)
+ {
+ if(bitsProcessed > 0)
+ {
+ mCurrentTime += (long)((double)bitsProcessed / DMR_BIT_RATE * 1000.0);
+ }
+ }
+
+ /**
+ * Registers the listener for messages produced by this message framer
+ *
+ * @param messageListener to receive framed and decoded messages
+ */
+ public void setListener(Listener messageListener)
+ {
+ mMessageListener = messageListener;
+ }
+
+ /**
+ * Primary method for streaming decoded symbol dibits for message framing.
+ *
+ * @param dibit to process
+ */
+ @Override
+ public void receive(Dibit dibit)
+ {
+ mBurstFramer.receive(dibit);
+ }
+
+ /**
+ * Primary method for streaming decoded symbol byte arrays.
+ *
+ * @param buffer to process into a stream of dibits for processing.
+ */
+ public void receive(ReusableByteBuffer buffer)
+ {
+ //Updates current timestamp to the timestamp from the incoming buffer
+ setCurrentTime(buffer.getTimestamp());
+
+ for(byte value : buffer.getBytes())
+ {
+ for(int x = 0; x <= 3; x++)
+ {
+ receive(Dibit.parse(value, x));
+ }
+ }
+
+ buffer.decrementUserCount();
+ }
+
+ /**
+ * DMR burst detection processing. Processes DMR burst/messages from external burst framer/producer.
+ *
+ * @param message bits for the burst/message and any bit errors detected in the sync pattern
+ * @param syncPattern that was detected for the burst
+ */
+ @Override
+ public void burstDetected(CorrectedBinaryMessage message, DMRSyncPattern syncPattern, int timeslot)
+ {
+ //Each burst is 288 bits
+ updateBitsProcessed(288);
+
+ //This allows the frequency error monitor to be notified when we're in sync
+ if(mSyncDetectListener != null)
+ {
+ mSyncDetectListener.syncDetected(0);
+ }
+
+ CACH cach = null;
+
+ if(syncPattern.hasCACH())
+ {
+ cach = CACH.getCACH(message);
+ }
+
+ if(mMessageListener != null)
+ {
+ DMRMessage dmrMessage = DMRMessageFactory.create(syncPattern, message, cach, getTimestamp(), timeslot);
+
+ if(dmrMessage != null)
+ {
+ mMessageListener.receive(dmrMessage);
+ }
+ }
+ }
+
+ /**
+ * Processes a sync loss to track how many bits were processed and update message listeners.
+ *
+ * @param bitsProcessed since the last sync detect
+ */
+ @Override
+ public void syncLost(int bitsProcessed, int timeslot)
+ {
+ updateBitsProcessed(bitsProcessed);
+
+ dispatchSyncLoss(bitsProcessed, timeslot);
+
+ if(mSyncDetectListener != null)
+ {
+ mSyncDetectListener.syncLost(bitsProcessed);
+ }
+ }
+
+ /**
+ * Creates a sync loss message and dispatches it to the message listener
+ *
+ * @param bitsProcessed without a sync detection
+ * @param timeslot that lost the sync
+ */
+ private void dispatchSyncLoss(int bitsProcessed, int timeslot)
+ {
+ if(bitsProcessed > 0 && mMessageListener != null)
+ {
+ mMessageListener.receive(new SyncLossMessage(getTimestamp(), bitsProcessed, Protocol.DMR, timeslot));
+ }
+ }
+
+ public static class MessageListener implements Listener
+ {
+ private boolean mHasDMRData = false;
+ public int mTS0Count = 0;
+ public int mTS1Count = 0;
+
+ @Override
+ public void receive(IMessage message)
+ {
+ if(message instanceof DMRBurst && ((DMRBurst)message).hasCACH())
+ {
+ CACH cach = ((DMRBurst)message).getCACH();
+ mLog.info(cach.toString() + " TS:" + message.getTimeslot() + " " + message.toString());
+ }
+ else
+ {
+ mLog.info(" TS:" + message.getTimeslot() + " " + message.toString());
+ }
+
+// if(message instanceof UDTHeader)
+// {
+// mLog.info("TS:" + message.getTimeslot() + " " + message.toString());
+// }
+
+// if(message instanceof UnknownCSBKMessage && message.isValid())
+// {
+// mLog.info("TS:" + ((DMRMessage)message).getTimeslot() + " " + message.toString());
+// }
+ }
+
+ public boolean hasData()
+ {
+ return mHasDMRData;
+ }
+
+ public void reset()
+ {
+ mHasDMRData = true;
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ String path = "/media/denny/Lexar/Recordings/DMR/";
+
+ //Con+ Traffic + Voice
+// String file = path + "20200513_143340_9600BPS_DMR_SaiaNet_Onondaga_Control.bits"; //Enh GPS Revert Window Annce
+// String file = path + "20200514_062135_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //Neighbor only
+// String file = path + "20200514_063507_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 2579
+// String file = path + "20200514_064224_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 5056035
+// String file = path + "20200514_131623_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Grant: 5074193
+// String file = path + "20200514_133947_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits"; //<<<<<<<------ Basic Encryption
+// String file = path + "20200514_142249_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits";
+// String file = path + "20200514_144534_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //Con+ Control w/GPS Window Announce
+
+ //Con+ Multi-Site with Network Frequency Map - This file is really General Motors Con+ - Tarrant County site
+// String file = path + "20200716_235004_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits";
+
+ //Cap-Max Tier III CC
+// String file = path + "20200710_053632_9600BPS_DMR_Niles_Radio_Coconino_Control.bits";
+
+ //Cap+ Multi-Site 1 - Traffic LCN 2
+// String file = path + "20200716_210133_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
+// String file = path + "20200716_212309_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
+
+ //Cap+ Multi-Site Enhanced GPS Channel
+// String file = path + "20200714_224018_9600BPS_DMR_Farmers_Electric_Cooperative_Hunt_LCN_3.bits"; //This may have PLL mis-align issues
+
+ //Hytera Short Data = Proprietary (encrypted)
+// String file = path + "20200716_222839_9600BPS_DMR_SystemUnk_SiteUnk_Unk.bits"; //Hytera short data packets
+// String file = path + "20200716_223551_9600BPS_DMR_SystemUnk_SiteUnk_Unk.bits";
+
+ //Motorola GPS Revert channel
+// String file = path + "20200730_085433_9600BPS_DMR_Otsego_County_Road_Commission_Alpine_Center_Road_Commission.bits";
+
+ //Hytera XPT (pseudo trunk) beaconing and voice
+// String file = path + "20200729_161450_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits"; //PLL lock oscillating
+// String file = path + "20200729_162200_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits";
+// String file = path + "Hytera XPT/20200816_161126_9600BPS_DMR_JPJ_Communications_(DMR)_Madison_Control.bits"; //analzing this <<<<<<<<<<<<<<<<<<<<<<<<<<
+
+ //Hytera Tier III - CSBKO 40
+// String file = path + "20200716_234510_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits";
+// String file = path + "20200812_061410_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits";
+// String file = path + "20200716_234510_9600BPS_DMR_Azle_Communications_(Tier_3)_Dallas_Control.bits"; //MBC-channel grant
+
+ //UDT Header And Blocks
+// String file = path + "20200730_090634_9600BPS_DMR_Otsego_County_Road_Commission_Alpine_Center_Road_Commission.bits";
+
+ //Cap+ BP Scrambling
+// String file = path + "20200829_065610_9600BPS_DMR_Albany_Medical_Center_Albany_LCN_3.bits";
+
+ String file = "/home/denny/SDRTrunk/recordings/20200927_054837_9600BPS_DMR_Blair_Communications_(Capacity_Plus)_Dallas_Control_9.bits";
+
+
+
+ MessageListener listener = new MessageListener();
+ DecodeConfigDMR config = new DecodeConfigDMR();
+
+ boolean multi = false;
+
+ if(multi)
+ {
+ try
+ {
+ DirectoryStream stream = Files.newDirectoryStream(Paths.get(path), "*.bits");
+
+ stream.forEach(new Consumer()
+ {
+ @Override
+ public void accept(Path path)
+ {
+ mLog.info("Processing: " + path.toString());
+ DMRMessageFramer messageFramer = new DMRMessageFramer(null);
+ DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
+ messageFramer.setListener(messageProcessor);
+ messageProcessor.setMessageListener(listener);
+
+ try(BinaryReader reader = new BinaryReader(path, 200))
+ {
+ while(reader.hasNext())
+ {
+ ReusableByteBuffer buffer = reader.next();
+ messageFramer.receive(buffer);
+ }
+ }
+ catch(Exception ioe)
+ {
+ ioe.printStackTrace();
+ }
+
+ if(!listener.hasData())
+ {
+// mLog.info("Has Data: " + listener.hasData() + " File:" + path.toString());
+// try
+// {
+// Files.delete(path);
+// }
+// catch(IOException ioe)
+// {
+// ioe.printStackTrace();
+// }
+ }
+
+ listener.reset();
+
+ System.out.println("TS0 VOICE:" + listener.mTS0Count + " TS1 VOICE:" + listener.mTS1Count);
+ listener.mTS0Count = 0;
+ listener.mTS1Count = 0;
+ }
+ });
+ }
+ catch(IOException ioe)
+ {
+ ioe.printStackTrace();
+ }
+ }
+ else
+ {
+ DMRMessageFramer messageFramer = new DMRMessageFramer(null);
+ DMRMessageProcessor messageProcessor = new DMRMessageProcessor(config);
+ messageFramer.setListener(messageProcessor);
+ messageProcessor.setMessageListener(listener);
+
+ try(BinaryReader reader = new BinaryReader(Path.of(file), 200))
+ {
+ while(reader.hasNext())
+ {
+ ReusableByteBuffer buffer = reader.next();
+ messageFramer.receive(buffer);
+ }
+ }
+ catch(Exception ioe)
+ {
+ ioe.printStackTrace();
+ }
+
+ System.out.println("TS0 VOICE:" + listener.mTS0Count + " TS1 VOICE:" + listener.mTS1Count);
+ listener.mTS0Count = 0;
+ listener.mTS1Count = 0;
+ }
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java
new file mode 100644
index 00000000..2eab6b6e
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRMessageProcessor.java
@@ -0,0 +1,218 @@
+/*
+ *
+ * * ******************************************************************************
+ * * 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
+ * * *****************************************************************************
+ *
+ *
+ */
+package io.github.dsheirer.module.decode.dmr;
+
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.module.decode.dmr.channel.ITimeslotFrequencyReceiver;
+import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
+import io.github.dsheirer.module.decode.dmr.message.CACH;
+import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
+import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
+import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader;
+import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
+import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FLCAssembler;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.SLCAssembler;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ShortLCMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCAssembler;
+import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
+import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequenceAssembler;
+import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
+import io.github.dsheirer.sample.Listener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Processes DMR messages and performs re-assembly of link control fragments
+ */
+public class DMRMessageProcessor implements Listener
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRMessageProcessor.class);
+
+ private FLCAssembler mFLCAssemblerTimeslot1 = new FLCAssembler(1);
+ private FLCAssembler mFLCAssemblerTimeslot2 = new FLCAssembler(2);
+ private MBCAssembler mMBCAssembler = new MBCAssembler();
+ private PacketSequenceAssembler mPacketSequenceAssembler;
+ private SLCAssembler mSLCAssembler = new SLCAssembler();
+ private Listener mMessageListener;
+ private Map mTimeslotFrequencyMap = new TreeMap<>();
+
+ /**
+ * Constructs an instance
+ */
+ public DMRMessageProcessor(DecodeConfigDMR config)
+ {
+ for(TimeslotFrequency timeslotFrequency: config.getTimeslotMap())
+ {
+ mTimeslotFrequencyMap.put(timeslotFrequency.getNumber(), timeslotFrequency);
+ }
+
+ mPacketSequenceAssembler = new PacketSequenceAssembler();
+ }
+
+ /**
+ * Primary message processing
+ */
+ @Override
+ public void receive(IMessage message)
+ {
+ //Enrich messages that carry DMR Logical Slot Number channels with LCN to frequency mappings
+ if(message instanceof ITimeslotFrequencyReceiver)
+ {
+ ITimeslotFrequencyReceiver receiver = (ITimeslotFrequencyReceiver)message;
+ int[] lsns = receiver.getLogicalTimeslotNumbers();
+
+ List timeslotFrequencies = new ArrayList<>();
+
+ for(int lsn: lsns)
+ {
+ if(mTimeslotFrequencyMap.containsKey(lsn))
+ {
+ timeslotFrequencies.add(mTimeslotFrequencyMap.get(lsn));
+ }
+ }
+
+ if(!timeslotFrequencies.isEmpty())
+ {
+ receiver.apply(timeslotFrequencies);
+ }
+ }
+
+ //Extract the Full Link Control message fragment from the Voice with embedded signalling message
+ if(message instanceof VoiceEMBMessage)
+ {
+ VoiceEMBMessage voice = (VoiceEMBMessage)message;
+
+ if(message.getTimeslot() == 1)
+ {
+ FullLCMessage flco = mFLCAssemblerTimeslot1.process(voice.getEMB().getLCSS(),
+ voice.getFLCFragment(), message.getTimestamp());
+ receive(flco);
+ }
+ else
+ {
+ FullLCMessage flco = mFLCAssemblerTimeslot2.process(voice.getEMB().getLCSS(),
+ voice.getFLCFragment(), message.getTimestamp());
+ receive(flco);
+ }
+
+ if(voice.hasCACH())
+ {
+ CACH cach = voice.getCACH();
+ ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp());
+ receive(slco);
+ }
+ }
+ //Extract the Short Link Control message fragment from the DMR burst message when it has one
+ else if(message instanceof DMRBurst)
+ {
+ DMRBurst dmrBurst = (DMRBurst)message;
+
+ if(dmrBurst.hasCACH())
+ {
+ CACH cach = dmrBurst.getCACH();
+ ShortLCMessage slco = mSLCAssembler.process(cach.getLCSS(), cach.getPayload(), message.getTimestamp());
+ receive(slco);
+ }
+
+ //Multi-Block CSBK Reassembly
+ if(message instanceof MBCHeader)
+ {
+ mMBCAssembler.process((MBCHeader)message);
+ }
+ else if(message instanceof MBCContinuationBlock)
+ {
+ //Returns either a fully reassembled MultiCSBK or null
+ receive(mMBCAssembler.process((MBCContinuationBlock)message));
+ }
+ else
+ {
+ mMBCAssembler.reset(message.getTimeslot());
+ }
+
+ //Packet Sequence Message Assembly ...
+ if(message instanceof Preamble)
+ {
+ mPacketSequenceAssembler.process((Preamble)message);
+ }
+ else if(message instanceof PacketSequenceHeader)
+ {
+ mPacketSequenceAssembler.process((PacketSequenceHeader)message);
+ }
+ else if(message instanceof ProprietaryDataHeader)
+ {
+ mPacketSequenceAssembler.process(((ProprietaryDataHeader)message));
+ }
+ else if(message instanceof DataBlock)
+ {
+ mPacketSequenceAssembler.process((DataBlock)message);
+ }
+ }
+
+ //Now that the message has been (potentially) enriched, dispatch it to the modules
+ dispatch(message);
+ }
+
+ /**
+ * Dispatches the non-null message to the registered listener
+ */
+ private void dispatch(IMessage message)
+ {
+ if(mMessageListener != null && message != null)
+ {
+ mMessageListener.receive(message);
+ }
+ }
+
+ /**
+ * Prepares for disposal
+ */
+ public void dispose()
+ {
+ mMessageListener = null;
+ }
+
+ /**
+ * Registers the listener to receive messages from this processor
+ */
+ public void setMessageListener(Listener listener)
+ {
+ mMessageListener = listener;
+ mPacketSequenceAssembler.setMessageListener(listener);
+ }
+
+ /**
+ * Removes the listener from receiving messages from this processor
+ */
+ public void removeMessageListener()
+ {
+ mMessageListener = null;
+ mPacketSequenceAssembler.setMessageListener(null);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java
new file mode 100644
index 00000000..6280bd62
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java
@@ -0,0 +1,369 @@
+/*
+ *
+ * * ******************************************************************************
+ * * Copyright (C) 2014-2020 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;
+
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.identifier.site.SiteIdentifier;
+import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
+import io.github.dsheirer.module.decode.dmr.identifier.DMRNetwork;
+import io.github.dsheirer.module.decode.dmr.identifier.DMRSite;
+import io.github.dsheirer.module.decode.dmr.message.DMRMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAdjacentSiteInformation;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.hytera.HyteraAnnouncement;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusNeighborReport;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusVoiceChannelUser;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.AdjacentSiteInformation;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusControlChannel;
+import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ConnectPlusTrafficChannel;
+import io.github.dsheirer.module.decode.dmr.message.type.Model;
+import io.github.dsheirer.module.decode.dmr.message.type.SystemIdentityCode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tracks the network configuration details of a DMR network from the broadcast messages
+ */
+public class DMRNetworkConfigurationMonitor
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRNetworkConfigurationMonitor.class);
+ private static final String BRAND_HYTERA = "Hytera";
+ private static final String BRAND_MOTOROLA_CAPACITY_PLUS = "Motorola Capacity+";
+ private static final String BRAND_MOTOROLA_CONNECT_PLUS = "Motorola Connect+";
+ private static final String BRAND_STANDARD = "Standard";
+ private static final String BRAND_TIER_3_TRUNKING = "Tier III Trunking";
+ private static final String BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING = "Capacity Max Tier III Trunking";
+ private static final String BRAND_HYTERA_TIER_3_TRUNKING = "Hytera Tier III Trunking";
+
+ private List mNeighborSites = new ArrayList<>();
+ private Map mTier3NeighborSites = new HashMap<>();
+ private Map mObservedChannelMap = new HashMap<>();
+ private DMRNetwork mDMRNetwork;
+ private DMRSite mDMRSite;
+ private Model mTier3Model;
+ private String mBrand;
+ private Integer mColorCode;
+ private DMRChannel mCurrentChannel;
+ private Channel mChannel;
+
+ /**
+ * Constructs an instance
+ * @param channel configuration
+ */
+ public DMRNetworkConfigurationMonitor(Channel channel)
+ {
+ mChannel = channel;
+ }
+
+ /**
+ * Sets the current channel
+ */
+ public void setCurrentChannel(DMRChannel channel)
+ {
+ mCurrentChannel = channel;
+ }
+
+ /**
+ * Process a DMR message
+ * @param message to process that has already been checked for isValid()
+ */
+ public void process(DMRMessage message)
+ {
+ if(message instanceof CSBKMessage)
+ {
+ process((CSBKMessage)message);
+ }
+ else if(message instanceof LCMessage)
+ {
+ process((LCMessage)message);
+ }
+ }
+
+ /**
+ * Processes link control messages
+ */
+ public void process(LCMessage linkControl)
+ {
+ switch(linkControl.getOpcode())
+ {
+ case SHORT_CONNECT_PLUS_CONTROL_CHANNEL:
+ if((mDMRNetwork == null || mDMRSite == null) && linkControl instanceof ConnectPlusControlChannel)
+ {
+ ConnectPlusControlChannel cpcc = (ConnectPlusControlChannel)linkControl;
+ mDMRNetwork = cpcc.getNetwork();
+ mDMRSite = cpcc.getSite();
+ }
+ if(mBrand == null)
+ {
+ mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
+ }
+ break;
+ case SHORT_CONNECT_PLUS_TRAFFIC_CHANNEL:
+ if((mDMRNetwork == null || mDMRSite == null) && linkControl instanceof ConnectPlusTrafficChannel)
+ {
+ ConnectPlusTrafficChannel cptc = (ConnectPlusTrafficChannel)linkControl;
+ mDMRNetwork = cptc.getNetwork();
+ mDMRSite = cptc.getSite();
+ }
+ if(mBrand == null)
+ {
+ mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
+ }
+ break;
+ }
+ }
+
+ /**
+ * Processes Control Signalling Blocks (CSBK)
+ */
+ public void process(CSBKMessage csbk)
+ {
+ switch(csbk.getOpcode())
+ {
+ case STANDARD_ALOHA:
+ if(csbk instanceof Aloha)
+ {
+ if(mDMRNetwork == null || mDMRSite == null)
+ {
+ SystemIdentityCode sic = ((Aloha)csbk).getSystemIdentityCode();
+
+ if(mDMRNetwork == null)
+ {
+ mDMRNetwork = sic.getNetwork();
+ }
+ if(mDMRSite == null)
+ {
+ mDMRSite = sic.getSite();
+ }
+ if(mTier3Model == null)
+ {
+ mTier3Model = sic.getModel();
+ }
+ }
+
+ if(mBrand == null)
+ {
+ mBrand = BRAND_TIER_3_TRUNKING;
+ }
+ }
+ break;
+ case HYTERA_08_ANNOUNCEMENT:
+ case HYTERA_68_ANNOUNCEMENT:
+ if(csbk instanceof HyteraAnnouncement)
+ {
+ HyteraAnnouncement ha = (HyteraAnnouncement)csbk;
+
+ if(mBrand == null)
+ {
+ mBrand = BRAND_HYTERA_TIER_3_TRUNKING;
+ }
+
+ if(mDMRNetwork == null)
+ {
+ mDMRNetwork = ha.getSystemIdentityCode().getNetwork();
+ }
+ if(mDMRSite == null)
+ {
+ mDMRSite = ha.getSystemIdentityCode().getSite();
+ }
+ if(mTier3Model == null)
+ {
+ mTier3Model = ha.getSystemIdentityCode().getModel();
+ }
+
+ if(mBrand == null || mBrand != BRAND_HYTERA_TIER_3_TRUNKING)
+ {
+ mBrand = BRAND_HYTERA_TIER_3_TRUNKING;
+ }
+ }
+ if(csbk instanceof HyteraAdjacentSiteInformation)
+ {
+ HyteraAdjacentSiteInformation hasi = (HyteraAdjacentSiteInformation)csbk;
+
+ int site = hasi.getNeighborSystemIdentityCode().getSite().getValue();
+
+ if(!mTier3NeighborSites.containsKey(site))
+ {
+ mTier3NeighborSites.put(site, hasi);
+ }
+ }
+ break;
+ case MOTOROLA_CAPMAX_ALOHA:
+ if(csbk instanceof CapacityMaxAloha)
+ {
+ if(mDMRNetwork == null || mDMRSite == null)
+ {
+ SystemIdentityCode sic = ((CapacityMaxAloha)csbk).getSystemIdentityCode();
+
+ if(mDMRNetwork == null)
+ {
+ mDMRNetwork = sic.getNetwork();
+ }
+ if(mDMRSite == null)
+ {
+ mDMRSite = sic.getSite();
+ }
+ if(mTier3Model == null)
+ {
+ mTier3Model = sic.getModel();
+ }
+ }
+
+ if(mBrand == null || mBrand != BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING)
+ {
+ mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING;
+ }
+ }
+ break;
+ case MOTOROLA_CONPLUS_NEIGHBOR_REPORT:
+ if(mNeighborSites.isEmpty() && csbk instanceof ConnectPlusNeighborReport)
+ {
+ ConnectPlusNeighborReport cpnr = (ConnectPlusNeighborReport)csbk;
+ mNeighborSites.addAll(cpnr.getNeighbors());
+ }
+ if(mBrand == null)
+ {
+ mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
+ }
+ break;
+ case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER:
+ if(csbk instanceof ConnectPlusVoiceChannelUser)
+ {
+ DMRChannel channel = ((ConnectPlusVoiceChannelUser)csbk).getChannel();
+ addDmrChannel(channel);
+ }
+ if(mBrand == null)
+ {
+ mBrand = BRAND_MOTOROLA_CONNECT_PLUS;
+ }
+ break;
+ }
+
+ if(mColorCode == null)
+ {
+ mColorCode = csbk.getSlotType().getColorCode();
+ }
+ }
+
+ /**
+ * Adds the DMR channel to the observed channel map
+ */
+ private void addDmrChannel(DMRChannel dmrChannel)
+ {
+ mObservedChannelMap.put(dmrChannel.getLogicalSlotNumber(), dmrChannel);
+ }
+
+ public void reset()
+ {
+ mColorCode = null;
+ mDMRNetwork = null;
+ mDMRSite = null;
+ mNeighborSites.clear();
+ mTier3NeighborSites.clear();
+ mObservedChannelMap.clear();
+ }
+
+ public String getActivitySummary()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("Activity Summary - Decoder: DMR ");
+
+ //DMR System Brand Name
+ sb.append("\n\nBrand:").append((mBrand == null ? BRAND_STANDARD : mBrand));
+
+ if(mDMRNetwork != null)
+ {
+ sb.append("\nNetwork:").append(mDMRNetwork);
+ }
+
+ if(mDMRSite != null)
+ {
+ sb.append("\nSite:").append(mDMRSite);
+ }
+
+ if(mTier3Model != null)
+ {
+ sb.append("\nNetwork Model").append(mTier3Model);
+ }
+
+ //Observed DMR Channels
+ if(!mObservedChannelMap.isEmpty())
+ {
+ sb.append("\nObserved Logical Slot Numbers (LSN):");
+
+ List lsns = new ArrayList<>(mObservedChannelMap.keySet());
+ Collections.sort(lsns);
+
+ for(Integer lsn: lsns)
+ {
+ DMRChannel channel = mObservedChannelMap.get(lsn);
+
+ if(channel != null)
+ {
+ sb.append("\n\t").append(channel);
+
+ double frequency = channel.getDownlinkFrequency();
+
+ if(frequency != 0)
+ {
+ frequency /= 1E6d;
+ }
+ sb.append(" ").append(frequency).append(" MHz");
+ }
+ }
+ }
+
+ if(!mNeighborSites.isEmpty())
+ {
+ sb.append("\nNeighbor Sites\n");
+
+ for(SiteIdentifier neighbor: mNeighborSites)
+ {
+ sb.append("\tNeighbor: ").append(neighbor).append("\n");
+ }
+
+ for(Map.Entry neighbor: mTier3NeighborSites.entrySet())
+ {
+ AdjacentSiteInformation site = neighbor.getValue();
+ sb.append("\tNeighbor: ").append("Network:").append(site.getNeighborSystemIdentityCode().getNetwork());
+ sb.append(" Site:").append(site.getNeighborSystemIdentityCode().getSite());
+ sb.append(" ").append(site.getNeighborSystemIdentityCode().getModel());
+ sb.append(" ").append(site.getNeighborChannel()).append("\n");
+ }
+ }
+
+ sb.append("\n\n");
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationPreloadData.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationPreloadData.java
new file mode 100644
index 00000000..0d77a2b5
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationPreloadData.java
@@ -0,0 +1,42 @@
+/*
+ * *****************************************************************************
+ * Copyright (C) 2014-2020 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;
+
+import io.github.dsheirer.controller.channel.event.PreloadDataContent;
+
+/**
+ * Preload data container for DMRNetworkConfigurationMonitor.
+ *
+ * Note: this is used to transfer the monitor during a Capacity plus REST channel rotation operation. The monitor
+ * is passed from the channel that is converting from REST to a TRAFFIC channel and passed to the new STANDARD channel
+ * that represents the REST channel.
+ */
+public class DMRNetworkConfigurationPreloadData extends PreloadDataContent
+{
+ /**
+ * Constructs an instance
+ *
+ * @param data to preload
+ */
+ public DMRNetworkConfigurationPreloadData(DMRNetworkConfigurationMonitor data)
+ {
+ super(data);
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncDetector.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncDetector.java
new file mode 100644
index 00000000..4bccc8f5
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncDetector.java
@@ -0,0 +1,178 @@
+package io.github.dsheirer.module.decode.dmr;
+
+import io.github.dsheirer.dsp.symbol.Dibit;
+import io.github.dsheirer.dsp.symbol.QPSKCarrierLock;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * Detector for DMR sync patterns that also includes support for detecting PLL phase lock errors. There are
+ * two primary use cases for this class:
+ *
+ * The first is to detect a sync pattern from a stream of dibits that are processed via the add(dibit) method. After
+ * each dibit is added, the sync pattern can be inspected by either the hasSync() method or the getSyncPattern()
+ * method. The maxStreamBitErrors construction parameter specifies how many bit errors can be present when matching
+ * patterns to the dibit stream.
+ *
+ * The second use case is to set a candidate sync pattern value using the setCurrentSyncValue() method. You can then
+ * use the hasSync() and getSyncPatter() methods to determine if the set value matches a valid sync pattern. The
+ * maxExplicitBitErrors specifies how many bit errors can be present when matching patterns to the explicitly set
+ * sync value.
+ *
+ * Note: sync pattern matching maximum bit errors uses a hard-coded value for PLL misalignment sync detection since
+ * this threshold should be relatively high to avoid false triggering and once detected, a PLL should be immediately
+ * commanded to correct the issue.
+ */
+public class DMRSyncDetector
+{
+ private static long SYNC_MASK = 0xFFFFFFFFFFFFl;
+ private static final int MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT = 1;
+ private int mMaxStreamBitErrors;
+ private int mMaxExplicitBitErrors;
+ private long mCurrentSyncValue;
+ private long mErrorPattern;
+ private int mPatternMatchBitErrorCount;
+ private DMRSyncPattern mSyncPattern = DMRSyncPattern.UNKNOWN;
+ private QPSKCarrierLock mCarrierLock = QPSKCarrierLock.NORMAL;
+
+ /**
+ * Constructs an instance
+ * @param maxStreamBitErrors allowed for matching sync patterns when using the dibit streaming use case.
+ * @param maxExplicitBitErrors allows for matching sync patterns when using the explicitly set sync value use case.
+ */
+ public DMRSyncDetector(int maxStreamBitErrors, int maxExplicitBitErrors)
+ {
+ Validate.inclusiveBetween(0, 24, maxStreamBitErrors,
+ "Max (allowable) stream bit errors for sync match must be between 0 and 24");
+ Validate.inclusiveBetween(0, 24, maxExplicitBitErrors,
+ "Max (allowable) explicit bit errors for sync match must be between 0 and 24");
+ mMaxStreamBitErrors = maxStreamBitErrors;
+ mMaxExplicitBitErrors = maxExplicitBitErrors;
+ }
+
+ /**
+ * Sync pattern for the current sync value.
+ */
+ public DMRSyncPattern getSyncPattern()
+ {
+ return mSyncPattern;
+ }
+
+ /**
+ * Indicates if there is a sync pattern in the currently set sync value that was either explicitly set or was
+ * aggregated from a stream of dibits. When this method returns true, the sync pattern can be accessed via the
+ * getSyncPattern() method.
+ */
+ public boolean hasSync()
+ {
+ return getSyncPattern() != DMRSyncPattern.UNKNOWN;
+ }
+
+ /**
+ * Carrier lock for the QPSK PLL to detect when the PLL has locked to the signal at +/- 90 degrees or 180 degrees.
+ * @return carrier lock value.
+ */
+ public QPSKCarrierLock getCarrierLock()
+ {
+ return mCarrierLock;
+ }
+
+ /**
+ * Indicates if the detected sync pattern has normal QPSK carrier lock.
+ */
+ public boolean hasNormalCarrierLock()
+ {
+ return getCarrierLock() == QPSKCarrierLock.NORMAL;
+ }
+
+ /**
+ * Number of bit errors that were present when matching the current sync value to the current sync pattern.
+ */
+ public int getPatternMatchBitErrorCount()
+ {
+ return mPatternMatchBitErrorCount;
+ }
+
+ /**
+ * Processes the streaming dibit by left shifting the current sync value and adding the dibit onto the end and then
+ * test for sync pattern match.
+ * @param dibit to process and test for sync pattern.
+ */
+ public void add(Dibit dibit)
+ {
+ mCurrentSyncValue = Long.rotateLeft(mCurrentSyncValue, 2);
+ mCurrentSyncValue &= SYNC_MASK;
+ mCurrentSyncValue += dibit.getValue();
+ checkSync(mMaxStreamBitErrors);
+ }
+
+ /**
+ * Explicitly sets the argument as the current sync value and tests for sync pattern match.
+ * @param value to load as the current sync value and test
+ */
+ public void setCurrentSyncValue(long value)
+ {
+ mCurrentSyncValue = value;
+ checkSync(mMaxExplicitBitErrors);
+ }
+
+ /**
+ * Checks the current sync value against each of the sync patterns to determine if the value matches a pattern.
+ */
+ private void checkSync(int maxBitErrors)
+ {
+ for(DMRSyncPattern pattern: DMRSyncPattern.SYNC_PATTERNS)
+ {
+ mErrorPattern = mCurrentSyncValue ^ pattern.getPattern();
+
+ if(mErrorPattern == 0)
+ {
+ mSyncPattern = pattern;
+ mCarrierLock = QPSKCarrierLock.NORMAL;
+ mPatternMatchBitErrorCount = 0;
+ return;
+ }
+
+ mPatternMatchBitErrorCount = Long.bitCount(mErrorPattern);
+
+ if(mPatternMatchBitErrorCount <= maxBitErrors)
+ {
+ mSyncPattern = pattern;
+ mCarrierLock = QPSKCarrierLock.NORMAL;
+ return;
+ }
+
+
+ //For PLL mis-aligned lock patterns, reduce the pattern bit error match threshold
+ mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getPlus90Pattern());
+
+ if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT)
+ {
+ mSyncPattern = pattern;
+ mCarrierLock = QPSKCarrierLock.PLUS_90;
+ return;
+ }
+
+ mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getMinus90Pattern());
+
+ if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT)
+ {
+ mSyncPattern = pattern;
+ mCarrierLock = QPSKCarrierLock.MINUS_90;
+ return;
+ }
+
+ mPatternMatchBitErrorCount = Long.bitCount(mCurrentSyncValue ^ pattern.getInvertedPattern());
+
+ if(mPatternMatchBitErrorCount <= MAX_PATTERN_BIT_ERROR_PLL_MISALIGNMENT)
+ {
+ mSyncPattern = pattern;
+ mCarrierLock = QPSKCarrierLock.INVERTED;
+ return;
+ }
+ }
+
+ mSyncPattern = DMRSyncPattern.UNKNOWN;
+ mCarrierLock = QPSKCarrierLock.NORMAL;
+ mPatternMatchBitErrorCount = 0;
+ }
+}
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncPattern.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncPattern.java
new file mode 100644
index 00000000..60d94792
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRSyncPattern.java
@@ -0,0 +1,154 @@
+/*
+ * ******************************************************************************
+ * sdrtrunk
+ * Copyright (C) 2014-2020 Dennis Sheirer, Zhenyu Mao
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * *****************************************************************************
+ */
+package io.github.dsheirer.module.decode.dmr;
+
+import java.util.EnumSet;
+
+/**
+ * DMR burst sync patterns enumeration. In addition to the ETSI defined sync patterns, each enumeration entry also
+ * contains derived sync patterns that match decoded bit streams where a QPSK Phase-Locked Loop has locked to the signal
+ * misaligned by +/- 90 degrees, or 180 degrees. These derived patterns can be used to detect PLL misalignment for
+ * PLL correction purposes.
+ */
+public enum DMRSyncPattern
+{
+ BASE_STATION_DATA(0xDFF57D75DF5Dl, 0xBAAFEBEFBAFBl, 0x455014104504l, 0x200A828A20A2l, "BS DATA"),
+ BASE_STATION_VOICE(0x755FD7DF75F7l, 0xEFFABEBAEFAEl, 0x100541451051l, 0x8AA028208A08l, "BS VOICE A"),
+
+ MOBILE_STATION_DATA(0xD5D7F77FD757l, 0xBFBEAEEABEFEl, 0x404151154101l, 0x2A28088028A8l, "MS DATA"),
+ MOBILE_STATION_VOICE(0x7F7D5DD57DFDl, 0xEAEBFBBFEBABl, 0x151404401454l, 0x8082A22A8202l, "MS VOICE A"),
+ DIRECT_MODE_DATA_TIMESLOT_1(0xF7FDD5DDFD55l, 0xAEABBFBBABFFl, 0x515440445400l, 0x08022A2202AAl, "DM DAT0"),
+ DIRECT_MODE_DATA_TIMESLOT_2(0xD7557F5FF7F5l, 0xBEFFEAFAAEAFl, 0x410015055150l, 0x28AA80A0080Al, "DM DAT1"),
+ DIRECT_MODE_VOICE_TIMESLOT_1(0x5D577F7757FFl, 0xFBFEEAEEFEAAl, 0x040115110155l, 0xA2A88088A800l, "DM VOX0"),
+ DIRECT_MODE_VOICE_TIMESLOT_2(0x7DFFD5F55D5Fl, 0xEBAABFAFFBFAl, 0x145540500405l, 0x82002A0AA2A0l, "DM VOX1"),
+ MOBILE_STATION_REVERSE_CHANNEL(0x77D55F7DFD77l, 0xEEBFFAEBABEEl, 0x114005145411l, 0x882AA0820288l, "MS RVRS"),
+
+ RESERVED(0xDD7FF5D757DDl, 0xBBEAAFBEFEBBl, 0x441550410144l, 0x22800A28A822l, "RESERVED"),
+
+ //These are used to identify the sync-less sub frames of a voice super frame
+ BS_VOICE_FRAME_B("BS VOICE B"),
+ BS_VOICE_FRAME_C("BS VOICE C"),
+ BS_VOICE_FRAME_D("BS VOICE D"),
+ BS_VOICE_FRAME_E("BS VOICE E"),
+ BS_VOICE_FRAME_F("BS VOICE F"),
+
+ MS_VOICE_FRAME_B("MS VOICE B"),
+ MS_VOICE_FRAME_C("MS VOICE C"),
+ MS_VOICE_FRAME_D("MS VOICE D"),
+ MS_VOICE_FRAME_E("MS VOICE E"),
+ MS_VOICE_FRAME_F("MS VOICE F"),
+
+ UNKNOWN("UNKNOWN");
+
+ private long mPattern;
+ private long mPlus90Pattern;
+ private long mMinus90Pattern;
+ private long mInvertedPattern;
+ private String mLabel;
+
+ /**
+ * DMR Sync Patterns. See TS 102-361-1, paragraph 9.1.1
+ */
+ DMRSyncPattern(long pattern, long plus90Pattern, long minus90Pattern, long invertedPattern, String label)
+ {
+ mPattern = pattern;
+ mPlus90Pattern = plus90Pattern;
+ mMinus90Pattern = minus90Pattern;
+ mInvertedPattern = invertedPattern;
+ mLabel = label;
+ }
+
+ /**
+ * Alternate constructor for syncs that don't have defined patterns.
+ */
+ DMRSyncPattern(String label)
+ {
+ mLabel = label;
+ }
+
+ //Valid sync patterns (excluding the sync-less voice patterns)
+ public static final EnumSet SYNC_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
+ MOBILE_STATION_DATA, MOBILE_STATION_VOICE, DIRECT_MODE_DATA_TIMESLOT_1, DIRECT_MODE_DATA_TIMESLOT_2,
+ DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2, MOBILE_STATION_REVERSE_CHANNEL);
+
+ //Sync patterns containing a Common Announcement Channel (CACH)
+ private static final EnumSet CACH_PATTERNS = EnumSet.of(BASE_STATION_DATA, BASE_STATION_VOICE,
+ BS_VOICE_FRAME_B, BS_VOICE_FRAME_C, BS_VOICE_FRAME_D, BS_VOICE_FRAME_E, BS_VOICE_FRAME_F);
+
+ //Direct Mode Sync Patterns
+ private static final EnumSet DIRECT_MODE_PATTERNS = EnumSet.of(DIRECT_MODE_DATA_TIMESLOT_1,
+ DIRECT_MODE_DATA_TIMESLOT_2, DIRECT_MODE_VOICE_TIMESLOT_1, DIRECT_MODE_VOICE_TIMESLOT_2);
+
+ /**
+ * Pattern that represents the enum entry
+ */
+ public long getPattern()
+ {
+ return mPattern;
+ }
+
+ /**
+ * Pattern that represents the PLL locking to the carrier misaligned by plus 90 degrees.
+ */
+ public long getPlus90Pattern()
+ {
+ return mPlus90Pattern;
+ }
+
+ /**
+ * Pattern that represents the PLL locking to the carrier misaligned by minus 90 degrees.
+ */
+ public long getMinus90Pattern()
+ {
+ return mMinus90Pattern;
+ }
+
+ /**
+ * Pattern that represents the PLL locking to the carrier misaligned by 180 degrees (inverted).
+ */
+ public long getInvertedPattern()
+ {
+ return mInvertedPattern;
+ }
+
+ /**
+ * Indicates if a message using this sync pattern contains a Common Announcement Channel (CACH).
+ */
+ public boolean hasCACH()
+ {
+ return CACH_PATTERNS.contains(this);
+ }
+
+ /**
+ * Indicates if this is a Direct Mode sync pattern
+ */
+ public boolean isDirectMode()
+ {
+ return DIRECT_MODE_PATTERNS.contains(this);
+ }
+
+ /**
+ * String representation of the sync pattern
+ */
+ public String toString()
+ {
+ return mLabel;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java
new file mode 100644
index 00000000..4a6bf168
--- /dev/null
+++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java
@@ -0,0 +1,696 @@
+/*
+ *
+ * * ******************************************************************************
+ * * 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
+ * * *****************************************************************************
+ *
+ *
+ */
+package io.github.dsheirer.module.decode.dmr;
+
+import com.google.common.eventbus.Subscribe;
+import io.github.dsheirer.channel.IChannelDescriptor;
+import io.github.dsheirer.controller.channel.Channel;
+import io.github.dsheirer.controller.channel.Channel.ChannelType;
+import io.github.dsheirer.controller.channel.ChannelConversionRequest;
+import io.github.dsheirer.controller.channel.ChannelEvent;
+import io.github.dsheirer.controller.channel.ChannelEvent.Event;
+import io.github.dsheirer.controller.channel.IChannelEventListener;
+import io.github.dsheirer.controller.channel.IChannelEventProvider;
+import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
+import io.github.dsheirer.identifier.Identifier;
+import io.github.dsheirer.identifier.IdentifierCollection;
+import io.github.dsheirer.identifier.Role;
+import io.github.dsheirer.message.IMessage;
+import io.github.dsheirer.message.MessageHistoryPreloadData;
+import io.github.dsheirer.message.MessageHistoryRequest;
+import io.github.dsheirer.message.MessageHistoryResponse;
+import io.github.dsheirer.module.decode.config.DecodeConfiguration;
+import io.github.dsheirer.module.decode.dmr.channel.DMRChannel;
+import io.github.dsheirer.module.decode.dmr.event.DMRChannelGrantEvent;
+import io.github.dsheirer.module.decode.dmr.message.data.csbk.Opcode;
+import io.github.dsheirer.module.decode.event.DecodeEvent;
+import io.github.dsheirer.module.decode.event.DecodeEventHistory;
+import io.github.dsheirer.module.decode.event.DecodeEventHistoryPreloadData;
+import io.github.dsheirer.module.decode.event.DecodeEventHistoryRequest;
+import io.github.dsheirer.module.decode.event.DecodeEventHistoryResponse;
+import io.github.dsheirer.module.decode.event.DecodeEventType;
+import io.github.dsheirer.module.decode.event.IDecodeEvent;
+import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
+import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
+import io.github.dsheirer.sample.Listener;
+import io.github.dsheirer.source.SourceType;
+import io.github.dsheirer.source.config.SourceConfigTuner;
+import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
+import io.github.dsheirer.source.tuner.channel.rotation.DisableChannelRotationMonitorRequest;
+import io.github.dsheirer.source.tuner.channel.rotation.FrequencyLockChangeRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * Monitors channel grant and channel grant update messages to allocate traffic channels to capture
+ * traffic channel activity.
+ *
+ * Creates and reuses a limited set of Channel instances each with a TRAFFIC channel type. Since each of
+ * the traffic channels will be using the same decoder type and configuration options, we reuse each of the
+ * channel instances to allow the ChannelProcessingManager to reuse a cached set of processing chains that
+ * are created as each channel is activated.
+ *
+ * Each traffic channel is activated by sending a ChannelEvent via the channel model. The channel processing
+ * manager receives the activation request. If successful, a processing chain is activated for the traffic
+ * channel. Otherwise, a channel event is broadcast indicating that the channel could not be activated. On
+ * teardown of an activated traffic channel, a channel event is broadcast to indicate the traffic channels
+ * is no longer active.
+ *
+ * This manager monitors channel events watching for events related to managed traffic channels.
+ */
+public class DMRTrafficChannelManager extends TrafficChannelManager implements IDecodeEventProvider, IChannelEventListener,
+ IChannelEventProvider
+{
+ private final static Logger mLog = LoggerFactory.getLogger(DMRTrafficChannelManager.class);
+ public static final String CHANNEL_START_REJECTED = "CHANNEL START REJECTED";
+ public static final String MAX_TRAFFIC_CHANNELS_EXCEEDED = "MAX TRAFFIC CHANNELS EXCEEDED";
+
+ private Queue mAvailableTrafficChannelQueue = new ConcurrentLinkedQueue<>();
+ private List mManagedTrafficChannels;
+
+ private Map mAllocatedTrafficChannelFrequencyMap = new ConcurrentHashMap<>();
+ private Map mLSNGrantEventMap = new ConcurrentHashMap<>();
+
+ private Listener mChannelEventListener;
+ private Listener mDecodeEventListener;
+
+ private TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor();
+ private Channel mParentChannel;
+
+ //Used as temporary storage for message and decode event history during Cap+ REST channel rotation
+ private DecodeEventHistory mTransientDecodeEventHistory;
+ private List mTransientMessageHistory;
+
+ private boolean mIgnoreDataCalls;
+ private long mCurrentControlFrequency;
+
+ /**
+ * Monitors call events and allocates traffic decoder channels in response
+ * to traffic channel allocation call events. Manages a pool of reusable
+ * traffic channel allocations.
+ *
+ * @param parentChannel that owns this traffic channel manager
+ */
+ public DMRTrafficChannelManager(Channel parentChannel)
+ {
+ mParentChannel = parentChannel;
+
+ if(parentChannel.getDecodeConfiguration() instanceof DecodeConfigDMR)
+ {
+ mIgnoreDataCalls = ((DecodeConfigDMR)parentChannel.getDecodeConfiguration()).getIgnoreDataCalls();
+ }
+
+ createTrafficChannels();
+ }
+
+ /**
+ * Sets the current parent control channel frequency so that channel grants for the current frequency do not
+ * produce an additional traffic channel allocation.
+ * @param currentControlFrequency for current control channel.
+ */
+ public void setCurrentControlFrequency(long currentControlFrequency)
+ {
+ mCurrentControlFrequency = currentControlFrequency;
+ }
+
+ /**
+ * Creates up to the maximum number of traffic channels for use in allocating traffic channels.
+ *
+ * Note: this method uses lazy initialization and will only create the channels once. Subsequent calls will be ignored.
+ */
+ private void createTrafficChannels()
+ {
+ if(mManagedTrafficChannels == null)
+ {
+ DecodeConfiguration decodeConfiguration = mParentChannel.getDecodeConfiguration();
+ List trafficChannelList = new ArrayList<>();
+
+ if(decodeConfiguration instanceof DecodeConfigDMR)
+ {
+ DecodeConfigDMR decodeConfig = (DecodeConfigDMR)decodeConfiguration;
+
+ int maxTrafficChannels = decodeConfig.getTrafficChannelPoolSize();
+
+ if(maxTrafficChannels > 0)
+ {
+ for(int x = 0; x < maxTrafficChannels; x++)
+ {
+ Channel trafficChannel = new Channel("TRAFFIC", ChannelType.TRAFFIC);
+ trafficChannel.setAliasListName(mParentChannel.getAliasListName());
+ trafficChannel.setSystem(mParentChannel.getSystem());
+ trafficChannel.setSite(mParentChannel.getSite());
+ trafficChannel.setDecodeConfiguration(decodeConfig);
+ trafficChannel.setEventLogConfiguration(mParentChannel.getEventLogConfiguration());
+ trafficChannel.setRecordConfiguration(mParentChannel.getRecordConfiguration());
+ trafficChannelList.add(trafficChannel);
+ }
+ }
+ }
+
+ mAvailableTrafficChannelQueue.addAll(trafficChannelList);
+ mManagedTrafficChannels = Collections.unmodifiableList(trafficChannelList);
+ }
+ }
+
+ /**
+ * Used with Capacity Plus systems to convert the existing standard channel to a traffic channel and then recreate
+ * the original standard channel with the frequency specified for the new rest channel.
+ * @param channel
+ * @param currentFrequency of the standard channel
+ * @param restChannel to identify the new channel frequency to start
+ */
+ public void convertToTrafficChannel(Channel channel, long currentFrequency, IChannelDescriptor restChannel,
+ DMRNetworkConfigurationMonitor networkConfigurationMonitor)
+ {
+ //Only do the conversion of the original channel has multiple frequencies defined and the rest channel is
+ //one of those frequencies
+ if(restChannel.getDownlinkFrequency() > 0 &&
+ !mAllocatedTrafficChannelFrequencyMap.containsKey(restChannel.getDownlinkFrequency()) &&
+ channel.getSourceConfiguration().getSourceType() == SourceType.TUNER_MULTIPLE_FREQUENCIES)
+ {
+ SourceConfigTunerMultipleFrequency originalSourceConfig = (SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration();
+
+ //Add the rest channel to the list of frequencies in the source configuration
+ if(!originalSourceConfig.getFrequencies().contains(restChannel.getDownlinkFrequency()))
+ {
+ originalSourceConfig.addFrequency(restChannel.getDownlinkFrequency());
+
+ //Note: the channel configuration modification is not persisted from here, to avoid unnecessary coupling
+ //to the channel model. The configuration will simply be adjusted at runtime.
+ }
+
+ //Convert current channel to a traffic channel if one is available
+ Channel trafficChannel = mAvailableTrafficChannelQueue.poll();
+
+ if(trafficChannel != null && restChannel.getDownlinkFrequency() > 0)
+ {
+ //Disable the channel rotation manager when we have multiple frequencies defined
+ getInterModuleEventBus().post(new DisableChannelRotationMonitorRequest());
+
+ //Set the frequency for the traffic channel configuration
+ SourceConfigTuner trafficSourceConfig = new SourceConfigTuner();
+ trafficSourceConfig.setFrequency(currentFrequency);
+ trafficChannel.setSourceConfiguration(trafficSourceConfig);
+
+ //Post a request for message and decode event history to transfer to the new REST channel. This has
+ // to be posted before the channel conversion request
+ getInterModuleEventBus().post(new DecodeEventHistoryRequest());
+ getInterModuleEventBus().post(new MessageHistoryRequest());
+
+ //Dispatch a request to convert this processing chain to the traffic channel. This will cause the
+ //processing chain to convert to a traffic channel and notify all of the modules, which will in-turn
+ //cause the decoder states (2 timeslots) to dereference this manager so that the existing channel can
+ //no longer allocate traffic channels.
+ getInterModuleEventBus().post(new ChannelConversionRequest(channel, trafficChannel));
+
+ mAllocatedTrafficChannelFrequencyMap.put(currentFrequency, trafficChannel);
+
+ //Set the preferred frequency to use when restarting the original channel
+ originalSourceConfig.setPreferredFrequency(restChannel.getDownlinkFrequency());
+
+ //Dispatch request to persistently start the original channel with the rest channel frequency and reuse
+ //this traffic channel manager in the new processing chain.
+ ChannelStartProcessingRequest request = new ChannelStartProcessingRequest(channel, this);
+ request.setPersistentAttempt(true);
+ request.setChildDecodeEventHistory(mTransientDecodeEventHistory);
+
+ //If we received an event history response, add it to the request as preload data content
+ if(mTransientDecodeEventHistory != null)
+ {
+ DecodeEventHistoryPreloadData eventHistory =
+ new DecodeEventHistoryPreloadData(mTransientDecodeEventHistory.getItems());
+ request.addPreloadDataContent(eventHistory);
+
+ mTransientDecodeEventHistory = null;
+ }
+
+ //If we received a message history response, add it to the request as preload data content
+ if(mTransientMessageHistory != null)
+ {
+ MessageHistoryPreloadData messageHistory = new MessageHistoryPreloadData(mTransientMessageHistory);
+ request.addPreloadDataContent(messageHistory);
+ mTransientMessageHistory = null;
+ }
+
+ //Add the DMR network configuration monitor as preload data
+ if(networkConfigurationMonitor != null)
+ {
+ request.addPreloadDataContent(new DMRNetworkConfigurationPreloadData(networkConfigurationMonitor));
+ }
+
+ getInterModuleEventBus().post(request);
+ }
+ }
+ }
+
+ /**
+ * Processes a decode event history response and temporarily stores the event history.
+ *
+ * Note: this is used for Cap+ REST channel rotation.
+ *
+ * @param response containing the current decode event history.
+ */
+ @Subscribe
+ public void process(DecodeEventHistoryResponse response)
+ {
+ mTransientDecodeEventHistory = response.getDecodeEventHistory();
+ }
+
+ /**
+ * Processes a message history response and temporarily stores the history.
+ *
+ * Note: this is used for Cap+ REST channel rotation.
+ *
+ * @param response containing the current message history.
+ */
+ @Subscribe
+ public void process(MessageHistoryResponse response)
+ {
+ mTransientMessageHistory = response.getMessages();
+ }
+
+ /**
+ * Broadcasts an initial or update decode event to any registered listener.
+ */
+ public void broadcast(DecodeEvent decodeEvent)
+ {
+ if(mDecodeEventListener != null)
+ {
+ mDecodeEventListener.receive(decodeEvent);
+ }
+ }
+
+ public void processEndChannelGrant()
+ {
+ //TODO: process the CSBK opcode that indicates traffic channel teardown
+ }
+
+ /**
+ * Processes channel grants to allocate traffic channels and track overall channel usage. Generates
+ * decode events for each new channel that is allocated.
+ *
+ */
+ public void processChannelGrant(DMRChannel channel, IdentifierCollection identifierCollection,
+ Opcode opcode, long timestamp, boolean encrypted)
+ {
+ int lsn = channel.getLogicalSlotNumber();
+
+ DMRChannelGrantEvent event = mLSNGrantEventMap.get(lsn);
+
+ if(event != null && isSameCall(identifierCollection, event.getIdentifierCollection()))
+ {
+ Identifier from = getIdentifier(identifierCollection, Role.FROM);
+
+ if(from != null)
+ {
+ Identifier currentFrom = getIdentifier(event.getIdentifierCollection(), Role.FROM);
+ if(currentFrom != null && !Objects.equals(from, currentFrom))
+ {
+ event.end(timestamp);
+
+ DMRChannelGrantEvent continuationGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp)
+ .channel(channel)
+ .eventDescription(getEventType(opcode, event.isEncrypted()).toString() + " - Continue")
+ .details("CHANNEL GRANT" + (encrypted ? " ENCRYPTED" : ""))
+ .identifiers(identifierCollection)
+ .build();
+
+ mLSNGrantEventMap.put(lsn, continuationGrantEvent);
+ broadcast(continuationGrantEvent);
+ }
+ }
+
+ //update the ending timestamp so that the duration value is correctly calculated
+ event.update(timestamp);
+ broadcast(event);
+
+ //Even though we have an event, the initial channel grant may have been rejected. Check to see if there
+ //is a traffic channel allocated. If not, allocate one and update the event description.
+ long frequency = channel.getDownlinkFrequency();
+
+ if(frequency > 0 && frequency != mCurrentControlFrequency &&
+ !mAllocatedTrafficChannelFrequencyMap.containsKey(channel.getDownlinkFrequency()) &&
+ !(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode()))
+ {
+ Channel trafficChannel = mAvailableTrafficChannelQueue.poll();
+
+ if(trafficChannel != null)
+ {
+ event.setEventDescription(getEventType(opcode, event.isEncrypted()).toString());
+ event.setDetails("CHANNEL GRANT " + (encrypted ? " ENCRYPTED" : ""));
+ event.setChannelDescriptor(channel);
+ broadcast(event);
+
+ SourceConfigTuner sourceConfig = new SourceConfigTuner();
+ sourceConfig.setFrequency(frequency);
+ trafficChannel.setSourceConfiguration(sourceConfig);
+ mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel);
+ getInterModuleEventBus().post(new ChannelStartProcessingRequest(trafficChannel, channel, identifierCollection));
+ }
+ else
+ {
+ String description = (event.getEventDescription() != null ? event.getEventDescription() : "") +
+ " - MAX TRAFFIC CHANNELS EXCEEDED";
+ event.setEventDescription(description);
+ }
+ }
+
+ return;
+ }
+
+ if(mIgnoreDataCalls && opcode.isDataChannelGrantOpcode())
+ {
+ DMRChannelGrantEvent channelGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp)
+ .encrypted(encrypted)
+ .channel(channel)
+ .eventDescription(getEventType(opcode, encrypted).toString() + " - Ignored")
+ .details("DATA CALL IGNORED")
+ .identifiers(identifierCollection)
+ .build();
+
+ mLSNGrantEventMap.put(channel.getLogicalSlotNumber(), channelGrantEvent);
+ broadcast(channelGrantEvent);
+ return;
+ }
+
+ DMRChannelGrantEvent channelGrantEvent = DMRChannelGrantEvent.channelGrantBuilder(timestamp)
+ .encrypted(encrypted)
+ .channel(channel)
+ .eventDescription(getEventType(opcode, encrypted).toString())
+ .details("CHANNEL GRANT")
+ .identifiers(identifierCollection)
+ .build();
+
+ mLSNGrantEventMap.put(channel.getLogicalSlotNumber(), channelGrantEvent);
+
+ //Allocate a traffic channel for the downlink frequency if one isn't already allocated
+ //NOTE: we could also allocate a traffic channel for the uplink frequency here, in the future
+ long frequency = channel.getDownlinkFrequency();
+
+ if(frequency > 0 && frequency != mCurrentControlFrequency &&
+ !mAllocatedTrafficChannelFrequencyMap.containsKey(frequency))
+ {
+ Channel trafficChannel = mAvailableTrafficChannelQueue.poll();
+
+ if(trafficChannel == null)
+ {
+ String details = (channelGrantEvent.getDetails() != null ? channelGrantEvent.getDetails() : "") +
+ " " + MAX_TRAFFIC_CHANNELS_EXCEEDED;
+ channelGrantEvent.setDetails(details);
+ channelGrantEvent.setEventDescription(channelGrantEvent.getEventDescription() + " - Ignored");
+ }
+ else
+ {
+ SourceConfigTuner sourceConfig = new SourceConfigTuner();
+ sourceConfig.setFrequency(frequency);
+ trafficChannel.setSourceConfiguration(sourceConfig);
+ mAllocatedTrafficChannelFrequencyMap.put(frequency, trafficChannel);
+ broadcast(ChannelEvent.requestEnable(trafficChannel));
+ }
+ }
+
+ broadcast(channelGrantEvent);
+ }
+
+
+ /**
+ * Creates a call event type description for the specified opcode and service options
+ */
+ private DecodeEventType getEventType(Opcode opcode, boolean encrypted)
+ {
+ DecodeEventType type = null;
+
+ switch(opcode)
+ {
+ case STANDARD_TALKGROUP_VOICE_CHANNEL_GRANT:
+ case STANDARD_BROADCAST_TALKGROUP_VOICE_CHANNEL_GRANT:
+ case MOTOROLA_CONPLUS_VOICE_CHANNEL_USER:
+ type = encrypted ? DecodeEventType.CALL_GROUP_ENCRYPTED : DecodeEventType.CALL_GROUP;
+ break;
+
+ case STANDARD_PRIVATE_VOICE_CHANNEL_GRANT:
+ case STANDARD_DUPLEX_PRIVATE_VOICE_CHANNEL_GRANT:
+ type = encrypted ? DecodeEventType.CALL_UNIT_TO_UNIT_ENCRYPTED : DecodeEventType.CALL_UNIT_TO_UNIT;
+ break;
+
+ case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_SINGLE_ITEM:
+ case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_SINGLE_ITEM:
+ case STANDARD_DUPLEX_PRIVATE_DATA_CHANNEL_GRANT:
+ case STANDARD_PRIVATE_DATA_CHANNEL_GRANT_MULTI_ITEM:
+ case STANDARD_TALKGROUP_DATA_CHANNEL_GRANT_MULTI_ITEM:
+ case MOTOROLA_CONPLUS_DATA_CHANNEL_GRANT:
+ type = encrypted ? DecodeEventType.DATA_CALL_ENCRYPTED : DecodeEventType.DATA_CALL;
+ break;
+ }
+
+ if(type == null)
+ {
+ mLog.debug("Unrecognized opcode for determining decode event type: " + opcode.name());
+ type = DecodeEventType.CALL;
+ }
+
+ return type;
+ }
+
+ /**
+ * Channel event listener to receive notifications that a traffic channel has ended processing and we
+ * can reclaim the traffic channel for reuse.
+ *
+ * @return listener for processing channel events.
+ */
+ @Override
+ public Listener getChannelEventListener()
+ {
+ return mTrafficChannelTeardownMonitor;
+ }
+
+ /**
+ * Broadcasts a channel event to a registered external listener (for action).
+ */
+ private void broadcast(ChannelEvent channelEvent)
+ {
+ if(mChannelEventListener != null)
+ {
+ mChannelEventListener.receive(channelEvent);
+ }
+ }
+
+ /**
+ * Sets the external channel event listener to receive channel events from this traffic channel manager
+ */
+ @Override
+ public void setChannelEventListener(Listener