Compare commits

...

19 Commits

Author SHA1 Message Date
Denny Sheirer
49e7087b9b #952 Resolves issue with concurrent access to processing chain map. (#953)
Co-authored-by: Denny <denny@denny-desktop>
2020-10-12 09:33:49 -04:00
Denny Sheirer
c397b5a601 #950 Resolves issue where message and event panels were aggregating across all selected now playing channels. (#951)
Co-authored-by: Denny <denny@denny-desktop>
2020-10-12 09:26:59 -04:00
Denny Sheirer
c809dcc609 #948 Updates build to 0.5.0 Alpha 4 and updates gradle script to embed JDK15 as the Java runtime. (#949)
Co-authored-by: Denny <denny@denny-desktop>
2020-10-12 09:18:53 -04:00
Denny Sheirer
7eec82deee #6 DMR Decoder (#947)
* #6 DMR decoder feature - work in progress

* #6 DMR Decoder.

* #6 DMR Decoder.  Code review updates.

Co-authored-by: Zhenyu Mao <maozhenyu@foxmail.com>
Co-authored-by: Denny <denny@denny-desktop>
2020-10-12 04:57:59 -04:00
Denny Sheirer
b061db1189 #903 updates rrapi library to resolve radio reference web service deserialization issue. (#923)
Co-authored-by: Denny <denny@denny-desktop>
2020-09-04 04:46:08 -04:00
Denny Sheirer
8715dd2d6b #908 Add AMBE tone info to audio recording file names. (#921)
Co-authored-by: Denny <denny@denny-desktop>
2020-09-03 05:02:52 -04:00
Denny Sheirer
63b0aaaa7f Resolves #909 MPT1327 call events don't show talkgroup aliases. (#920)
Co-authored-by: Denny <denny@denny-desktop>
2020-09-03 04:21:28 -04:00
Denny Sheirer
d20dd1af8c Resolves #910 MPT-1327 talkgroup editor tooltip. (#919)
Co-authored-by: Denny <denny@denny-desktop>
2020-09-03 04:14:24 -04:00
Denny Sheirer
e1f32d3d0b Resolves #905. Adds try/catch block to duplicate call detection processing block to ensure that any error generated during duplicate call processing doesn't kill the processing thread and cause a queue-backup memory leak. (#918)
Co-authored-by: Denny <denny@denny-desktop>
2020-09-03 04:07:30 -04:00
Denny Sheirer
eaa3433034 #895 Cleanup and Testing of FCD Pro and Pro Plus tuners. (#896)
Co-authored-by: Denny <denny@denny-desktop>
2020-07-25 16:27:04 -04:00
Denny Sheirer
838016c89f #893 Updates UDP packet to construct a Cellocator packet payload when the source UDP port is 231 and any time the packet contains the correct start packet identifier. Updates the decode event type label. (#894)
Co-authored-by: Denny <denny@denny-desktop>
2020-07-25 09:01:44 -04:00
Denny
030a88f5a3 Updates build to 0.5.0 alpha 3 2020-07-25 08:28:27 -04:00
Denny Sheirer
de3ba5529a #890 Resolves issue where radio reference channel import only imports 1 of multiple listed control channels and adds improved detection for P25P2 simulcast sites to set the decoder type to LSM. (#892)
Co-authored-by: Denny <denny@denny-desktop>
2020-07-25 08:26:19 -04:00
Denny Sheirer
7962758e05 #866 Cellocator Packet Support (#875)
Resolves #866 Implements cellocator packet support.
2020-07-25 07:55:12 -04:00
Denny Sheirer
0f9d1c0792 #886 Resolves issue with audio recording when duplicate call detection is enabled. (#891)
Co-authored-by: Denny <denny@denny-desktop>
2020-07-25 07:24:12 -04:00
Noah Lynch
48be38a2d8 Wrap setAliases() in Platform.runLater() (#885)
Fixes issue #884 (see issue for more info).

AliasItemEditor.save() modifies objects that are observed by the
TableView's underlying SortedList.  Thus, the TableView's selection can
change by calling save(), which means the ListChangeListener can be
fired.

This is a problem for two reasons.
1. The alert dialogue that prompts the user to save a modified alias
will show when the user presses the "save" button, because saving
modifies the selected alias and causes the list to update.

2. setAliases() calls Alert.showAndWait().  From the JavaDoc:
"It must not be called during animation or layout processing."
This statement is likely there because it can cause issues like the one
here: TableView's layout processing (and the list updating) is paused
and the list is further modified (by calling save()).  Thus, when the
processing is resumed the list is in a different and unexpected state.

Calling setAliases() from Platform.runLater() will allow the SortedList,
TableView, and any other involved objects to finish processing changes
before more modifications are made.  Additionally, it allows save(),
when called by pressing the save button, to complete and set the
modifiedProperty to false, so setAliases() does not display an
additional save prompt.
2020-07-25 07:08:39 -04:00
Noah Lynch
207893e5a8 Move BinaryMessage isFull() check into try block to prevent NPE. (#881)
From issue 879:
When a BitSetFullException is caught and reset(0) is called, the
BinaryMessage is set to null.

This should resolve issue 879.
2020-07-25 06:59:13 -04:00
CurtisHx
e07d6fa854 Log build information on startup (#878) 2020-07-25 06:51:24 -04:00
Denny Sheirer
e26698d0d8 #839 Updates logging for tuner unavailable to allocate traffic channel frequencies. Logs the frequency once and then suppresses further logging. (#870)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-16 05:17:05 -04:00
456 changed files with 38834 additions and 2707 deletions

View File

@@ -4,6 +4,8 @@
<w>bahroo</w>
<w>broadcastify</w>
<w>correlator</w>
<w>deinterleave</w>
<w>descrambled</w>
<w>dinf</w>
<w>esnid</w>
<w>fleetsync</w>

2
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_13" default="false" project-jdk-name="13.0.2" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_13" default="false" project-jdk-name="15" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

10
.idea/modules.xml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdr-trunk.iml" filepath="$PROJECT_DIR$/.idea/modules/sdr-trunk.iml" group="sdr-trunk" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdr-trunk_main.iml" filepath="$PROJECT_DIR$/.idea/modules/sdr-trunk_main.iml" group="sdr-trunk" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdr-trunk_test.iml" filepath="$PROJECT_DIR$/.idea/modules/sdr-trunk_test.iml" group="sdr-trunk" />
</modules>
</component>
</project>

View File

@@ -24,7 +24,7 @@ plugins {
id 'java'
id 'idea'
id 'org.openjfx.javafxplugin' version '0.0.8'
id 'org.beryx.runtime' version '1.8.1'
id 'org.beryx.runtime' version '1.11.4'
}
repositories {
@@ -33,7 +33,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.5.0-alpha2'
version = '0.5.0-alpha4'
sourceCompatibility = '13'
sourceSets {
@@ -65,7 +65,7 @@ dependencies {
implementation 'com.miglayout:miglayout-swing:5.2'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:radio-reference-api:15.1.6'
implementation 'io.github.dsheirer:radio-reference-api:15.1.8'
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.4.0'
implementation 'org.apache.commons:commons-compress:1.20'
@@ -74,7 +74,7 @@ dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.mina:mina-core:2.1.3'
implementation 'org.apache.mina:mina-http:2.1.3'
implementation 'org.controlsfx:controlsfx:11.0.1'
implementation 'org.controlsfx:controlsfx:11.0.2'
implementation 'org.rauschig:jarchivelib:1.0.0'
implementation 'org.slf4j:slf4j-api:1.7.25'
@@ -84,18 +84,18 @@ dependencies {
implementation 'pl.edu.icm:JLargeArrays:1.6'
//These must remain as compile options to ensure they are included in the build to produce multi-OS images
compile "org.openjfx:javafx-base:13.0.2:win"
compile "org.openjfx:javafx-base:13.0.2:linux"
compile "org.openjfx:javafx-base:13.0.2:mac"
compile "org.openjfx:javafx-controls:13.0.2:win"
compile "org.openjfx:javafx-controls:13.0.2:linux"
compile "org.openjfx:javafx-controls:13.0.2:mac"
compile "org.openjfx:javafx-graphics:13.0.2:win"
compile "org.openjfx:javafx-graphics:13.0.2:linux"
compile "org.openjfx:javafx-graphics:13.0.2:mac"
compile "org.openjfx:javafx-swing:13.0.2:win"
compile "org.openjfx:javafx-swing:13.0.2:linux"
compile "org.openjfx:javafx-swing:13.0.2:mac"
compile "org.openjfx:javafx-base:15:win"
compile "org.openjfx:javafx-base:15:linux"
compile "org.openjfx:javafx-base:15:mac"
compile "org.openjfx:javafx-controls:15:win"
compile "org.openjfx:javafx-controls:15:linux"
compile "org.openjfx:javafx-controls:15:mac"
compile "org.openjfx:javafx-graphics:15:win"
compile "org.openjfx:javafx-graphics:15:linux"
compile "org.openjfx:javafx-graphics:15:mac"
compile "org.openjfx:javafx-swing:15:win"
compile "org.openjfx:javafx-swing:15:linux"
compile "org.openjfx:javafx-swing:15:mac"
}
application {
@@ -110,6 +110,7 @@ idea {
}
javafx {
version = "15"
modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing']
}
@@ -118,7 +119,6 @@ jar {
attributes (
'Implementation-Title' : 'sdrtrunk project',
'Implementation-Version' : archiveVersion,
'Class-Path' : 'jmbe-1.0.4.jar',
'Created-By' : "Gradle ${gradle.gradleVersion}",
'Build-Timestamp' : new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
@@ -133,48 +133,58 @@ jar {
* tasks to produce platform-specific builds. If none of these paths exists, then an image will be created for the
* host OS using the installed JDK.
*/
def jdk_linux_x86_64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-14'
def jdk_linux_x86_32 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-14'
def jdk_osx_x86_64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-14.jdk'
def jdk_windows_x86_64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-14'
def jdk_windows_x86_32 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-14'
def jdk_base = '/media/denny/WD250GB/java/bellsoft/'
def jdk_linux_arm64 = jdk_base + 'linux-arm64/jdk-15'
def jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-15'
def jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-15.jdk'
def jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-15'
def file_jdk_linux_arm64 = new File(jdk_linux_arm64)
def file_jdk_linux_x86_64 = new File(jdk_linux_x86_64)
def file_jdk_linux_x86_32 = new File(jdk_linux_x86_32)
def file_jdk_osx_x86_64 = new File(jdk_osx_x86_64)
def file_jdk_windows_x86_64 = new File(jdk_windows_x86_64)
def file_jdk_windows_x86_32 = new File(jdk_windows_x86_32)
def hasTargetJdk = file_jdk_linux_x86_32.exists() ||
file_jdk_linux_x86_64.exists() ||
def hasTargetJdk = file_jdk_linux_x86_64.exists() ||
file_jdk_linux_arm64 ||
file_jdk_osx_x86_64.exists() ||
file_jdk_windows_x86_32.exists() ||
file_jdk_windows_x86_64.exists()
runtime {
if(file_jdk_linux_arm64.exists())
{
targetPlatform('linux-arm64-v' + version, jdk_linux_arm64)
}
else
{
println("Skipping OS Image - Linux ARM 64-bit JDK was not found at " + jdk_linux_arm64)
}
if(file_jdk_linux_x86_64.exists())
{
targetPlatform('linux-x86_64-v' + version, jdk_linux_x86_64)
}
if(file_jdk_linux_x86_32.exists())
else
{
targetPlatform('linux-x86_32-v' + version, jdk_linux_x86_32)
println("Skipping OS Image - Linux x86 64-bit JDK was not found at " + jdk_linux_x86_64)
}
if(file_jdk_osx_x86_64.exists())
{
targetPlatform('osx-x86_64-v' + version, jdk_osx_x86_64)
}
else
{
println("Skipping OS Image - OSX x86 64-bit JDK was not found at " + jdk_osx_x86_64);
}
if(file_jdk_windows_x86_64.exists())
{
targetPlatform('windows-x86_64-v' + version, jdk_windows_x86_64)
}
if(file_jdk_windows_x86_32.exists())
else
{
targetPlatform('windows-x86_32-v' + version, jdk_windows_x86_32)
println("Skipping OS Image - Windows x86 64-bit JDK was not found at " + jdk_windows_x86_64)
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
@@ -184,50 +194,18 @@ runtime {
}
/**
* Check for existence of JDK folders for each supported platform before creating runtime images
* Copy JavaFX OS-specific libraries from each JDK to each platform target directory
*/
tasks.runtime.doFirst {
if(!file_jdk_linux_x86_64.exists())
{
println("Skipping OS Image - Linux x86 64-bit JDK was not found at " + jdk_linux_x86_64)
}
if(!file_jdk_linux_x86_32.exists())
{
println("Skipping OS Image - Linux x86 32-bit JDK was not found at " + jdk_linux_x86_32)
}
if(!file_jdk_osx_x86_64.exists())
{
println("Skipping OS Image - OSX x86 64-bit JDK was not found at " + jdk_osx_x86_64);
}
if(!file_jdk_windows_x86_64.exists())
{
println("Skipping OS Image - Windows x86 64-bit JDK was not found at " + jdk_windows_x86_64)
}
if(!file_jdk_windows_x86_32.exists())
{
println("Skipping OS Image - Windows x86 32-bit JDK was not found at " + jdk_windows_x86_32)
}
}
/**
* Remove unused JavaFX OS-specific libraries from each platform target directory
*/
tasks.runtime.doLast {
tasks.runtime.doLast
{
delete(fileTree('build/image/sdr-trunk-linux-arm64-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_32-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-osx-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|linux)\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_32-v' + version + '/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
//JavaFX web and media modules are transitive dependencies of ControlsFX but are not required for this application
delete(fileTree('build/image/sdr-trunk-linux-arm64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_32-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-osx-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_32-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
}

View File

@@ -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<Message>
*/

View File

@@ -36,6 +36,8 @@ public enum RadioFormat
{
APCO25("********", 0, 0xFFFFFF, "0 to 16,777,215",
"<html>APCO25 valid range is 0 to 16,777,215"),
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
"<html>DMR unit id valid range is 0 to 16,777,215"),
PASSPORT("********", 0, 0x7FFFFF, "0 to 8,388,607",
"<html>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:

View File

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

View File

@@ -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",
"<html>APCO25 talkgroup valid range is 0 to 65,535"),
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
"<html>DMR talkgroup valid range is 0 to 16,777,215"),
FLEETSYNC("###-####", 0, 0x7FFFFF, "001-0001 to 127-8192",
"<html>Fleetsync valid ranges are 1-127(prefix)<br>and 1-8192(ident) (ie. 001-0001 to 127-8192)"),
LTR("##-###", 0x101, 0x3FFF, "01-001 to 20-255",

View File

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

View File

@@ -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<AudioSegment> mAudioSegmentListener;
protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
protected MutableIdentifierCollection mIdentifierCollection;
private Broadcaster<IdentifierUpdateNotification> 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

View File

@@ -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()
{

View File

@@ -231,80 +231,88 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
*/
private void process()
{
//Transfer in newly arrived audio segments
mAudioSegmentQueue.drainTo(mAudioSegments);
//Remove any completed audio segments.
mAudioSegments.removeIf(audioSegment -> {
boolean complete = audioSegment.completeProperty().get();
if(complete)
{
audioSegment.decrementConsumerCount();
}
return complete;
});
//Only check for duplicates if there is more than one call
if(mAudioSegments.size() > 1)
try
{
List<AudioSegment> duplicates = new ArrayList<>();
//Transfer in newly arrived audio segments
mAudioSegmentQueue.drainTo(mAudioSegments);
int currentIndex = 0;
while(currentIndex < mAudioSegments.size() - 1)
{
AudioSegment current = mAudioSegments.get(currentIndex);
//Remove any completed audio segments.
mAudioSegments.removeIf(audioSegment -> {
boolean complete = audioSegment.completeProperty().get();
if(!current.isDuplicate())
if(complete)
{
int checkIndex = currentIndex + 1;
audioSegment.decrementConsumerCount();
}
while(checkIndex < mAudioSegments.size())
return complete;
});
//Only check for duplicates if there is more than one call
if(mAudioSegments.size() > 1)
{
List<AudioSegment> duplicates = new ArrayList<>();
int currentIndex = 0;
while(currentIndex < mAudioSegments.size() - 1)
{
AudioSegment current = mAudioSegments.get(currentIndex);
if(!current.isDuplicate())
{
AudioSegment toCheck = mAudioSegments.get(checkIndex);
int checkIndex = currentIndex + 1;
if(!toCheck.isDuplicate())
while(checkIndex < mAudioSegments.size())
{
if(isDuplicate(current, toCheck))
AudioSegment toCheck = mAudioSegments.get(checkIndex);
if(!toCheck.isDuplicate())
{
toCheck.setDuplicate(true);
toCheck.decrementConsumerCount();
duplicates.add(toCheck);
if(isDuplicate(current, toCheck))
{
toCheck.setDuplicate(true);
toCheck.decrementConsumerCount();
duplicates.add(toCheck);
}
}
checkIndex++;
}
checkIndex++;
}
currentIndex++;
}
currentIndex++;
mAudioSegments.removeAll(duplicates);
}
mAudioSegments.removeAll(duplicates);
}
//Finally, if the audio segment queue is empty, shutdown montitoring until a new segment arrives
if(mAudioSegments.isEmpty())
{
//Block on the audio segment queue so that we can shutdown before any new segments are added, and
//allow the add(segment) to restart monitoring as soon as needed.
synchronized(mAudioSegmentQueue)
//Finally, if the audio segment queue is empty, shutdown montitoring until a new segment arrives
if(mAudioSegments.isEmpty())
{
if(mAudioSegmentQueue.isEmpty())
//Block on the audio segment queue so that we can shutdown before any new segments are added, and
//allow the add(segment) to restart monitoring as soon as needed.
synchronized(mAudioSegmentQueue)
{
try
if(mAudioSegmentQueue.isEmpty())
{
stopMonitoring();
}
catch(Exception e)
{
mLog.error("Unexpected error during duplicate audio segment monitoring shutdown", e);
//Do nothing, we got interrupted
try
{
stopMonitoring();
}
catch(Exception e)
{
mLog.error("Unexpected error during duplicate audio segment monitoring shutdown", e);
//Do nothing, we got interrupted
}
}
}
}
}
catch(Throwable t)
{
mLog.error("Unknown error while processing audio segments for duplicate call detection. Please report " +
"this to the developer.", t);
}
}
}
}

View File

@@ -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)
{

View File

@@ -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)
{

View File

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

View File

@@ -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

View File

@@ -129,7 +129,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, 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<AudioEvent>, 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]", ""));

View File

@@ -168,7 +168,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
updateToneInsertionAudioClips();
//Register to receive directory preference update notifications so we can update the preference items
MyEventBus.getEventBus().register(this);
MyEventBus.getGlobalEventBus().register(this);
}
/**
@@ -514,6 +514,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
mCanProcessAudio = false;
if(mProcessorFuture != null)

View File

@@ -183,7 +183,7 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
@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);

View File

@@ -73,7 +73,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, 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<AudioSegment>, IAudioContr
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
if(mProcessingTask != null)
{
mProcessingTask.cancel(true);

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<ChannelMetadata> mChannelMetadata;
/**
* Connstructs an instance
* @param channel
* @param metadata
*/
public ChannelAndMetadata(Channel channel, List<ChannelMetadata> metadata)
{
mChannel = channel;
mChannelMetadata = metadata;
}
/**
* Channel
*/
public Channel getChannel()
{
return mChannel;
}
/**
* Channel Metadata
*/
public List<ChannelMetadata> getChannelMetadata()
{
return mChannelMetadata;
}
}

View File

@@ -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<ChannelMetadata> mChannelMetadata = new ArrayList();
private Map<ChannelMetadata,Channel> mMetadataChannelMap = new HashMap();
private Listener<ChannelAndMetadata> 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<ChannelMetadata> 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<ChannelAndMetadata> 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<ChannelMetadata> 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

View File

@@ -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<State,Color> mBackgroundColors = new EnumMap<>(State.class);
private Map<State,Color> 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<ChannelAndMetadata>
{
@Override
public void receive(ChannelAndMetadata channelAndMetadata)
{
if(mUserSelectedChannel != null &&
mUserSelectedChannel.getChannelID() == channelAndMetadata.getChannel().getChannelID())
{
List<ChannelMetadata> 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);
}
}
}
}
}
}

View File

@@ -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<IDecodeEvent> mDecodeEventListener;
protected Listener<DecoderStateEvent> mDecoderStateListener;
protected Listener<SourceEvent> 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<ChannelMetadata> getChannelMetadata();
public abstract List<ChannelMetadata> getChannelMetadata();
public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification);

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Integer,Map<State,DecoderStateEvent>> 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<State,DecoderStateEvent> 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;
}
}

View File

@@ -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<Integer,MutableIdentifierCollection> mIdentifierCollectionMap = new TreeMap<>();
private Map<Integer,StateMachine> mStateMachineMap = new TreeMap<>();
private Map<Integer,StateMonitoringSquelchController> mSquelchControllerMap = new TreeMap<>();
private int mTimeslotCount;
private int[] mTimeslots;
private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache();
private Listener<IdentifierUpdateNotification> 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<ChannelMetadata> getChannelMetadata()
public List<ChannelMetadata> 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<SourceEvent> 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:

View File

@@ -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<ChannelMetadata> getChannelMetadata()
public List<ChannelMetadata> 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<SourceEvent> 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:

View File

@@ -83,7 +83,7 @@ public class ControllerPanel extends JPanel
{
if(index == mSettingsTabIndex)
{
MyEventBus.getEventBus().post(new ViewPlaylistRequest());
MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest());
}
else
{

View File

@@ -390,6 +390,8 @@ public class Channel extends Configuration implements Listener<SourceEvent>
sb.append(hasSite() ? getSite() : "SITE");
sb.append("_");
sb.append(getName());
sb.append("_");
sb.append(getChannelID());
return sb.toString();
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
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;
}
}

View File

@@ -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,15 +50,18 @@ 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;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
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
@@ -71,7 +73,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelProcessingManager.class);
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
private Map<Channel,ProcessingChain> mProcessingChains = new HashMap<>();
private Map<Channel,ProcessingChain> mProcessingChains = new ConcurrentHashMap<>();
private List<Listener<AudioSegment>> mAudioSegmentListeners = new CopyOnWriteArrayList<>();
private List<Listener<IDecodeEvent>> mDecodeEventListeners = new CopyOnWriteArrayList<>();
@@ -83,6 +85,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
private SourceManager mSourceManager;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private List<Long> mLoggedFrequencies = new ArrayList<>();
private List<ScheduledFuture<?>> mDelayedChannelStartTasks = new ArrayList<>();
/**
* Constructs the channel processing manager
@@ -166,6 +170,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*
* @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)
{
@@ -178,22 +183,32 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
{
try
{
startProcessing(event);
startProcessing(new ChannelStartProcessingRequest(event.getChannel()));
}
catch(ChannelException ce)
{
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequency +
"] - " + ce.getMessage());
if(!mLoggedFrequencies.contains(frequency))
{
mLoggedFrequencies.add(frequency);
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequency +
"] - " + ce.getMessage());
}
}
else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency)
{
List<Long> frequencies = ((SourceConfigTunerMultipleFrequency)channel
.getSourceConfiguration()).getFrequencies();
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequencies +
"] - " + ce.getMessage());
if(frequencies.size() > 0 && !mLoggedFrequencies.contains(frequencies.get(0)))
{
mLoggedFrequencies.add(frequencies.get(0));
mLog.error("Error starting requested channel [" + channel.getName() + ":" + frequencies +
"] - " + ce.getMessage());
}
}
else
{
@@ -203,40 +218,16 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
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;
@@ -252,7 +243,33 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
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);
}
}
}
}
/**
@@ -261,24 +278,21 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
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
@@ -305,74 +319,90 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
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<AudioSegment> listener : mAudioSegmentListeners)
{
processingChain.addAudioSegmentListener(listener);
}
for(Listener<IDecodeEvent> 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<Channel,ProcessingChain> 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<Module> modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences);
processingChain.addModules(modules);
/* Setup message activity model with filtering */
FilterSet<IMessage> 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<AudioSegment> listener : mAudioSegmentListeners)
{
processingChain.addAudioSegmentListener(listener);
}
for(Listener<IDecodeEvent> 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<Channel,ProcessingChain> 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<Module> 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<Module> loggers = mEventLogManager.getLoggers(channel);
if(!loggers.isEmpty())
@@ -387,25 +417,22 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
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,
@@ -415,13 +442,13 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
}
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
@@ -435,13 +462,13 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
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));
}
@@ -450,16 +477,15 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
* 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())
{
@@ -469,7 +495,6 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
processingChain.stop();
processingChain.removeEventLoggingModules();
processingChain.removeRecordingModules();
//Deregister channel from receive frequency correction events to show in the spectral display (hack!)
@@ -477,13 +502,11 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
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
{
@@ -496,13 +519,21 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
public void shutdown()
{
List<ScheduledFuture<?>> delayedTasks = new ArrayList<>(mDelayedChannelStartTasks);
for(ScheduledFuture<?> delayedTask: delayedTasks)
{
delayedTask.cancel(true);
mDelayedChannelStartTasks.remove(delayedTask);
}
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
for(Channel channel : channelsToStop)
{
try
{
stopProcessing(channel, true);
stopProcessing(channel);
}
catch(ChannelException ce)
{
@@ -511,6 +542,41 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
}
/**
* 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.
@@ -560,4 +626,31 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
{
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");
}
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<PreloadDataContent<?>> 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<PreloadDataContent<?>> getPreloadDataContents()
{
return mPreloadDataContents;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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 <T> data generic that should be loaded into modules in the processing chain.
*/
public abstract class PreloadDataContent<T> 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;
}
}

View File

@@ -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;

View File

@@ -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)
{

View File

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

View File

@@ -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;
}
/**

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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);
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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 <http://www.gnu.org/licenses/>
******************************************************************************/
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);
}
}

View File

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

View File

@@ -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<Integer> getRowErrors(CorrectedBinaryMessage message)
{
List<Integer> 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<Integer> getColumnErrors(CorrectedBinaryMessage message)
{
List<Integer> 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<Integer> rowErrors = getRowErrors(message);
if(!rowErrors.isEmpty())
{
List<Integer> 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<Integer> 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<Integer> 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<Integer> rowErrors, List<Integer> 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());
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
*
******************************************************************************/
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<len; i++){
buf[i] = bits.get(i);
}
for (int i=0; i<len; i++)
if (buf[i])
for (int j=0; j<K+1; j++)
buf[i+j] ^= poly[j];
for (int i=0; i<K; i++){
crc = (crc << 1) + (buf[len + i] ? 1:0);
}
return crc;
}
}

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* 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 <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -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");
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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;
}
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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);
}
}

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,11 @@ public class FilterSet<T> implements IFilter<T>
{
mName = "Message Filter";
}
public FilterSet(IFilter<T> filter)
{
addFilter(filter);
}
public String getName()
{

View File

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

View File

@@ -337,15 +337,15 @@ public class SDRTrunk implements Listener<TunerEvent>
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");

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
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<IMessage>()
{
@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;
}
}

View File

@@ -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();

View File

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

View File

@@ -422,7 +422,7 @@ public class AliasConfigurationEditor extends SplitPane
mAliasTableView.setItems(getAliasSortedList());
mAliasTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Alias>)c -> {
setAliases(mAliasTableView.getSelectionModel().getSelectedItems());
Platform.runLater(() -> setAliases(mAliasTableView.getSelectionModel().getSelectedItems()));
});
}

View File

@@ -172,7 +172,7 @@ public class AliasItemEditor extends Editor<Alias>
mPlaylistManager.getBroadcastModel().getConfiguredBroadcasts()
.addListener((ListChangeListener<ConfiguredBroadcast>)c -> updateStreamViews());
MyEventBus.getEventBus().register(this);
MyEventBus.getGlobalEventBus().register(this);
setMaxWidth(Double.MAX_VALUE);
@@ -410,6 +410,7 @@ public class AliasItemEditor extends Editor<Alias>
@Override
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
}
private VBox getTitledPanesBox()
@@ -659,7 +660,7 @@ public class AliasItemEditor extends Editor<Alias>
if(selected != null)
{
MyEventBus.getEventBus().post(new ViewAliasIdentifierRequest(selected));
MyEventBus.getGlobalEventBus().post(new ViewAliasIdentifierRequest(selected));
}
}
});
@@ -687,6 +688,12 @@ public class AliasItemEditor extends Editor<Alias>
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<Alias>
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;

View File

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

View File

@@ -237,6 +237,10 @@ public class RadioIdEditor extends IdentifierEditor<Radio>
"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),

View File

@@ -239,6 +239,10 @@ public class TalkgroupEditor extends IdentifierEditor<Talkgroup>
"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),

View File

@@ -303,7 +303,7 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
new HexFormatter(0,0xFFFF), "Format: 0 - FFFF"));
details.add(new TalkgroupDetail(Protocol.MPT1327, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
"Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
"Format: PPP-IIII = Prefix (0-127), Ident (1-8191)"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),

View File

@@ -358,7 +358,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
if(optionalButtonType.isPresent() && optionalButtonType.get() == ButtonType.YES)
{
MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY));
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY));
return;
}
}

View File

@@ -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:

View File

@@ -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 <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
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<Integer> mTrafficChannelPoolSizeSpinner;
private TableView<TimeslotFrequency> mTimeslotFrequencyTable;
private IntegerTextField mLogicalSlotNumberField;
private FrequencyField mDownlinkFrequencyField;
// private FrequencyField mUplinkFrequencyField;
private Button mAddTimeslotFrequencyButton;
private Button mDeleteTimeslotFrequencyButton;
private Spinner<Integer> 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<EventLogType> 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<TimeslotFrequency> getTimeslotTable()
{
if(mTimeslotFrequencyTable == null)
{
mTimeslotFrequencyTable = new TableView<>(FXCollections.observableArrayList(TimeslotFrequency.extractor()));
mTimeslotFrequencyTable.setPrefHeight(100.0);
TableColumn<TimeslotFrequency,Number> numberColumn = new TableColumn("LSN");
numberColumn.setPrefWidth(70);
numberColumn.setCellValueFactory(cellData -> cellData.getValue().getNumberProperty());
mTimeslotFrequencyTable.getColumns().addAll(numberColumn);
mTimeslotFrequencyTable.getSortOrder().add(numberColumn);
TableColumn<TimeslotFrequency,Number> downlinkColumn = new TableColumn("Frequency (MHz)");
downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().getDownlinkMHz());
downlinkColumn.setPrefWidth(175);
mTimeslotFrequencyTable.getColumns().addAll(downlinkColumn);
// TableColumn<TimeslotFrequency,Number> 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<ActionEvent>()
{
@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<String>()
{
@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<String>()
// {
// @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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<RecorderType> 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<TableColumn.CellDataFeatures<TimeslotFrequency, String>,
ObservableValue<String>>
{
private SimpleStringProperty mFrequency = new SimpleStringProperty();
private boolean mIsDownlink;
public FrequencyCellValueFactory(boolean isDownlink)
{
mIsDownlink = isDownlink;
}
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<TimeslotFrequency, String> 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<TimeslotFrequency,String>
{
private StringProperty mStringProperty = new SimpleStringProperty();
public DownlinkPropertyValueFactory()
{
super("downlinkFrequency");
}
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<TimeslotFrequency,String> param)
{
if(param.getValue() != null)
{
mStringProperty.set(String.valueOf(param.getValue().getDownlinkFrequency() / 1E6));
}
else
{
mStringProperty.setValue(null);
}
return mStringProperty;
}
}
}

View File

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

View File

@@ -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
*/

View File

@@ -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
{

View File

@@ -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),

View File

@@ -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
*/
@@ -287,7 +317,79 @@ public class RadioReferenceDecoder
*/
public boolean isLSM(Site site)
{
return site != null && site.getModulation() != null && site.getModulation().contentEquals("LSM");
if(site != null)
{
if(site.getModulation() != null && site.getModulation().contentEquals("LSM"))
{
return true;
}
else if(site.getDescription() != null && site.getDescription().contains("Simulcast"))
{
return true;
}
}
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<TimeslotFrequency> getTimeslotFrequencies(SystemInformation systemInformation, Site site)
{
if(hasTimeslotFrequencies(systemInformation, site))
{
List<TimeslotFrequency> 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();
}
/**
@@ -308,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"))
{
@@ -329,7 +433,6 @@ public class RadioReferenceDecoder
return Protocol.APCO25;
}
break;
case "DMR":
case "NXDN":
case "EDACS":
case "TETRA":
@@ -359,6 +462,8 @@ public class RadioReferenceDecoder
{
switch(type.getName())
{
case "DMR":
return DecoderType.DMR;
case "LTR":
if(flavor.getName().contentEquals("Net"))
{
@@ -391,7 +496,6 @@ public class RadioReferenceDecoder
return DecoderType.P25_PHASE1;
}
break;
case "DMR":
case "NXDN":
case "EDACS":

View File

@@ -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<TimeslotFrequency> 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,26 +482,25 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected() && gotoChannel != null)
{
MyEventBus.getEventBus().post(new ViewChannelRequest(gotoChannel));
MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(gotoChannel));
}
}
private void createControlChannel()
{
SiteFrequency control = null;
List<SiteFrequency> controls = new ArrayList<>();
for(SiteFrequency siteFrequency: getSiteFrequencyTableView().getItems())
{
if(isControl(siteFrequency) && siteFrequency.getFrequency() > 0.01)
{
control = siteFrequency;
continue;
controls.add(siteFrequency);
}
}
if(control == null)
if(controls.isEmpty())
{
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control Channel", ButtonType.OK);
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control Channel(s)", ButtonType.OK);
alert.setTitle("Create Channel Configuration");
alert.setHeaderText("Can't Create Channel Configuration");
alert.initOwner((getCreateChannelConfigurationButton()).getScene().getWindow());
@@ -514,14 +522,29 @@ public class SiteEditor extends GridPane
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(), mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency((long)(control.getFrequency() * 1E6));
channel.setSourceConfiguration(sourceConfigTuner);
if(controls.size() == 1)
{
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency((long)(controls.get(0).getFrequency() * 1E6));
channel.setSourceConfiguration(sourceConfigTuner);
}
else
{
SourceConfigTunerMultipleFrequency sourceConfig = new SourceConfigTunerMultipleFrequency();
for(SiteFrequency control: controls)
{
sourceConfig.addFrequency((long)(control.getFrequency() * 1E6));
}
channel.setSourceConfiguration(sourceConfig);
}
mPlaylistManager.getChannelModel().addChannel(channel);
if(getGoToChannelEditorCheckBox().isSelected())
{
MyEventBus.getEventBus().post(new ViewChannelRequest(channel));
MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel));
}
}
}
@@ -548,7 +571,7 @@ public class SiteEditor extends GridPane
if(siteFrequencies.isEmpty())
{
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control or Alternate Channels", ButtonType.OK);
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control or Alternate Channel(s)", ButtonType.OK);
alert.setTitle("Create Channel Configuration");
alert.setHeaderText("Can't Create Channel Configuration");
alert.initOwner((getCreateChannelConfigurationButton()).getScene().getWindow());
@@ -595,7 +618,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected())
{
MyEventBus.getEventBus().post(new ViewChannelRequest(channel));
MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel));
}
}
else
@@ -623,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));
}
}
}
@@ -687,7 +710,7 @@ public class SiteEditor extends GridPane
if(getGoToChannelEditorCheckBox().isSelected())
{
MyEventBus.getEventBus().post(new ViewChannelRequest(channel));
MyEventBus.getGlobalEventBus().post(new ViewChannelRequest(channel));
}
}
else
@@ -716,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));
}
}
}

View File

@@ -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();

View File

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

View File

@@ -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<SourceConfigurati
private static final String NONE = "(none) ";
private TunerModel mTunerModel;
private FrequencyBox mPrimaryFrequencyBox;
private List<FrequencyBox> mFrequencyBoxes = new ArrayList<>();
private ObservableList<FrequencyBox> mFrequencyBoxes = FXCollections.observableArrayList();
private ComboBox<String> mPreferredTunerComboBox;
private VBox mFrequencyBoxContainer;
private Spinner<Integer> mChannelRotationDelaySpinner;
private boolean mAllowMultipleFrequencies;
/**
@@ -80,9 +88,24 @@ public class FrequencyEditor extends SourceConfigurationEditor<SourceConfigurati
hBox.getChildren().addAll(label, getFrequencyBoxContainer());
Label preferredTunerLabel = new Label("Preferred Tuner");
preferredTunerLabel.setAlignment(Pos.BASELINE_RIGHT);
preferredTunerLabel.setPadding(new Insets(0,0,0,20));
hBox.getChildren().addAll(preferredTunerLabel, getPreferredTunerComboBox());
HBox tunerBox = new HBox();
tunerBox.setAlignment(Pos.CENTER_LEFT);
tunerBox.setSpacing(10);
tunerBox.getChildren().addAll(preferredTunerLabel, getPreferredTunerComboBox());
Label frequencyRotationLabel = new Label("Frequency Rotation Delay (ms)");
HBox frequencyBox = new HBox();
frequencyBox.setAlignment(Pos.CENTER_LEFT);
frequencyBox.setSpacing(10);
frequencyBox.getChildren().addAll(frequencyRotationLabel, getFrequencyRotationDelaySpinner());
getFrequencyRotationDelaySpinner().disableProperty().bind(Bindings.greaterThan(2, Bindings.size(mFrequencyBoxes)));
VBox vbox = new VBox();
vbox.setSpacing(10);
vbox.setPadding(new Insets(0,0,0,20));
vbox.getChildren().addAll(tunerBox, frequencyBox);
hBox.getChildren().add(vbox);
setAlignment(Pos.TOP_LEFT);
getChildren().add(hBox);
@@ -164,6 +187,7 @@ public class FrequencyEditor extends SourceConfigurationEditor<SourceConfigurati
sourceConfigMulti.setFrequencies(frequencies);
sourceConfigMulti.setPreferredTuner(preferredTuner);
sourceConfigMulti.setFrequencyRotationDelay(getFrequencyRotationDelaySpinner().getValue());
setSourceConfiguration(sourceConfigMulti);
}
}
@@ -181,6 +205,9 @@ public class FrequencyEditor extends SourceConfigurationEditor<SourceConfigurati
disable(sourceConfiguration == null);
getFrequencyRotationDelaySpinner().getValueFactory()
.setValue(ChannelRotationMonitor.CHANNEL_ROTATION_DELAY_MINIMUM);
if(sourceConfiguration == null)
{
setFrequencies(Collections.emptyList());
@@ -198,6 +225,7 @@ public class FrequencyEditor extends SourceConfigurationEditor<SourceConfigurati
SourceConfigTunerMultipleFrequency sourceMulti = (SourceConfigTunerMultipleFrequency)sourceConfiguration;
setFrequencies(sourceMulti.getFrequencies());
preferredTuner = sourceMulti.getPreferredTuner();
getFrequencyRotationDelaySpinner().getValueFactory().setValue(sourceMulti.getFrequencyRotationDelay());
}
else
{
@@ -226,6 +254,31 @@ public class FrequencyEditor extends SourceConfigurationEditor<SourceConfigurati
modifiedProperty().set(false);
}
/**
* 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<Integer> 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<Integer> 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.
*/

View File

@@ -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:

View File

@@ -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");

View File

@@ -190,7 +190,6 @@ public class UserPreferencesEditor extends BorderPane
storageItem.setExpanded(true);
TreeItem<String> 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);
}

View File

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

View File

@@ -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)

Some files were not shown because too many files have changed in this diff Show More