Compare commits

...

38 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
Denny
a8837c8e2a Updates version to 0.5.0-alpha2 2020-06-13 09:35:11 -04:00
Denny Sheirer
7395f02fb8 #864 Updates radio reference service library to resolve issue where the library wasn't parsing the system information object correctly and was assuming a singular bandplan object versus an array of bandplans. (#865)
Updating the service library prompted some minor changes with the way that county info objects are handled for each site.

Co-authored-by: Denny <denny@denny-desktop>
2020-06-13 08:16:37 -04:00
Denny Sheirer
706ada2940 #862 Updates JavaFX libraries to use version 13.0.2 (#863)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-12 05:39:01 -04:00
Denny Sheirer
d957846ab0 #837 Sorted list ArrayIndexOutOfBoundsException during alias save. (#858)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-11 04:59:15 -04:00
Denny Sheirer
bbe90ca74b #856 Adds support for resetting alias color selection in the alias bulk editor. (#857)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-11 04:26:40 -04:00
Denny Sheirer
96611fa2c2 #854 Resolves issue with radio reference system view where site list is getting squeezed by height requirements in the site editor components. (#855)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-10 16:11:17 -04:00
Denny Sheirer
c94edf05ef #851 Resolves issue with residual alias list names after playlist change. (#852)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-10 05:48:45 -04:00
Denny Sheirer
74adb6f30c #849 Resolves issue with frequency table column not updating after saving new channel configuration. (#850)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-10 05:34:21 -04:00
Denny Sheirer
421abe5768 #724 Resolves issue with usb transfer buffer(s) memory leak. (#847)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-09 04:33:33 -04:00
Denny Sheirer
1d003c561e #845 File Not Found when setting initial directory on JMBE library select button. (#846)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-08 04:47:19 -04:00
Denny Sheirer
b22177ee17 #829 Integrates JMBE Creator with new user preferences editor for auto-downloading and compiling the JMBE library from within sdrtrunk. (#844)
Co-authored-by: Denny <denny@denny-desktop>
2020-06-07 08:04:57 -04:00
Denny Sheirer
a1e8c761ef #438 Adds feature for duplicate call audio detection and suppression for playback, recording and streaming. (#835)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-27 06:08:53 -04:00
Denny Sheirer
ad194d1bab #818 Resolves P25 duplicate traffic channel issue where duplicate traffic channels were being allocated for the same frequency. (#834)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-25 06:29:56 -04:00
Denny Sheirer
2bb5082943 #831 Resolves issue with playlist editor and channel map editor. (#832)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-24 08:11:39 -04:00
Denny Sheirer
2c3648f8c2 #817 Resolves issue with jittery audio playback and recording for MPT1327 channels. (#830)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-24 07:58:59 -04:00
Andrzej Pańkowski
3d816c3570 MPT1327: Fix decoding of calls to past talkgroups (#824)
* MPT1327: Fix decoding of calls to past talkgroups

* Fixed updating event in case of teardown. Other improvements.
2020-05-21 05:22:12 -04:00
Andrzej Pańkowski
e1194d26bd MPT1327: Fix for call timeout setting not being honored (#823)
* MPT1327: Fix for call timeout setting not being honored
2020-05-21 05:14:50 -04:00
Denny Sheirer
8167e9e9f2 #812 Alias Bulk Editor feature (#821)
Co-authored-by: Denny <denny@denny-desktop>
2020-05-18 05:11:48 -04:00
Denny Sheirer
3b357c4d40 Update README.md 2020-05-15 04:39:14 -04:00
493 changed files with 42906 additions and 3037 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

@@ -37,7 +37,7 @@ Once unzipped, open a command prompt to where you unzipped the release. Change
If you're using sdrtrunk with a P25 trunked radio system, the [JMBE](https://github.com/DSheirer/sdrtrunk/wiki/JMBE) wiki page contains instructions for downloading the JMBE audio library source code and compiling the JMBE library. Once you have compiled the library, launch the sdrtrunk application. From the menu bar, choose **View >> Preferences**. In the **JMBE Audio Codec** section, update the path to where your compiled JMBE library is located. Any channels that are started after you set the path will be able to produce P25 audio.
## Minimum System Requirements
* **Operating System:** Windows (32 or 64-bit), Linux (32 or 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **CPU:** 4-core
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.

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-alpha1'
version = '0.5.0-alpha4'
sourceCompatibility = '13'
sourceSets {
@@ -51,7 +51,6 @@ dependencies {
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
//mavenCentral/jcenter imports
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'ch.qos.logback:logback-core:1.2.3'
implementation 'ch.qos.logback:logback-classic:1.2.3'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
@@ -60,38 +59,43 @@ dependencies {
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation 'com.github.wendykierp:JTransforms:3.1'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.guava:guava:27.0.1-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
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.4'
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'
implementation 'commons-io:commons-io:2.7'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'org.apache.commons:commons-io:1.3.2'
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'
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'pl.edu.icm:JLargeArrays:1.6'
//The must remain as compile options to ensure they are included in the build to produce multi-OS images
compile "org.openjfx:javafx-base:13:win"
compile "org.openjfx:javafx-base:13:linux"
compile "org.openjfx:javafx-base:13:mac"
compile "org.openjfx:javafx-controls:13.0.1:win"
compile "org.openjfx:javafx-controls:13.0.1:linux"
compile "org.openjfx:javafx-controls:13.0.1:mac"
compile "org.openjfx:javafx-graphics:13.0.1:win"
compile "org.openjfx:javafx-graphics:13.0.1:linux"
compile "org.openjfx:javafx-graphics:13.0.1:mac"
compile "org.openjfx:javafx-swing:13.0.1:win"
compile "org.openjfx:javafx-swing:13.0.1:linux"
compile "org.openjfx:javafx-swing:13.0.1:mac"
//These must remain as compile options to ensure they are included in the build to produce multi-OS images
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 {
@@ -106,6 +110,7 @@ idea {
}
javafx {
version = "15"
modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing']
}
@@ -114,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']}",
@@ -129,100 +133,79 @@ 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())
{
targetPlatform('windows-x86_32-v' + version, jdk_windows_x86_32)
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop', 'java.net.http', 'java.sql']
imageZip = hasTargetJdk ? file("$buildDir/image/sdr-trunk.zip") : file("$buildDir/image/sdr-trunk-" + version + ".zip")
}
/**
* Check for existence of JDK folders for each supported platform before creating runtime images
*/
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())
else
{
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)
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop', 'java.net.http',
'java.sql', 'jdk.crypto.ec']
imageZip = hasTargetJdk ? file("$buildDir/image/sdr-trunk.zip") : file("$buildDir/image/sdr-trunk-" + version + ".zip")
}
/**
* Remove unused JavaFX OS-specific libraries from each platform target directory
* Copy JavaFX OS-specific libraries from each JDK to 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

@@ -86,6 +86,8 @@ public class AliasModel
{
removeAlias(alias);
}
mAliasListNames.clear();
}
/**

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,26 +36,48 @@ 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 MAX_SEGMENT_AUDIO_SAMPLE_LENGTH = 8000 * 60 * 1; //8 kHz - 1 minute
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
*
* @param aliasList for aliasing identifiers
* @param maxSegmentAudioSampleLength in milliseconds
*/
public AbstractAudioModule(AliasList aliasList)
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);
}
protected abstract int getTimeslot();
/**
* Constructs an abstract audio module with a default maximum audio segment length and a default timeslot 0.
*/
public AbstractAudioModule(AliasList aliasList)
{
this(aliasList, DEFAULT_TIMESLOT, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
}
/**
* Timeslot for this audio module
*/
protected int getTimeslot()
{
return mTimeslot;
}
/**
* Closes the current audio segment
@@ -68,6 +90,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
{
mAudioSegment.completeProperty().set(true);
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
mAudioSegment.decrementConsumerCount();
mAudioSegment = null;
}
}
@@ -90,6 +113,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
if(mAudioSegment == null)
{
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
mAudioSegment.incrementConsumerCount();
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
@@ -117,7 +141,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
//If the current segment exceeds the max samples length, close it so that a new segment gets generated
//and then link the segments together
if(mAudioSampleCount >= MAX_SEGMENT_AUDIO_SAMPLE_LENGTH)
if(mAudioSampleCount >= mMaxSegmentAudioSampleLength)
{
AudioSegment previous = getAudioSegment();
closeAudioSegment();

View File

@@ -77,6 +77,17 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
/**
* Creates an Audio Module.
*
* @param aliasList for aliasing identifiers
* @param maxAudioSegmentLength in milliseconds
*/
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength)
{
super(aliasList, timeslot, maxAudioSegmentLength);
}
/**
* Creates an Audio Module.
*/
@@ -91,13 +102,6 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
return 0;
}
@Override
public void dispose()
{
removeAudioSegmentListener();
mSquelchStateListener = null;
}
@Override
public void reset()
{
@@ -116,17 +120,17 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
}
@Override
public void receive(ReusableFloatBuffer reusableFloatBuffer)
public void receive(ReusableFloatBuffer audioBuffer)
{
if(mSquelchState == SquelchState.UNSQUELCH)
{
ReusableFloatBuffer buffer = mHighPassFilter.filter(reusableFloatBuffer);
addAudio(buffer.getSamples());
buffer.decrementUserCount();
ReusableFloatBuffer highPassFilteredAudio = mHighPassFilter.filter(audioBuffer);
addAudio(highPassFilteredAudio.getSamplesCopy());
highPassFilteredAudio.decrementUserCount();
}
else
{
reusableFloatBuffer.decrementUserCount();
audioBuffer.decrementUserCount();
}
}

View File

@@ -65,6 +65,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
{
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
private BooleanProperty mDuplicate = new SimpleBooleanProperty(false);
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
@@ -74,6 +75,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
private AtomicInteger mConsumerCount = new AtomicInteger();
private AliasList mAliasList;
private long mStartTimestamp = System.currentTimeMillis();
private long mSampleCount = 0;
private boolean mDisposing = false;
private AudioSegment mLinkedAudioSegment;
private int mTimeslot;
@@ -107,6 +109,24 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mStartTimestamp;
}
/**
* End timestamp as calculated from start timestamp and current sample count
*/
public long getEndTimestamp()
{
return mStartTimestamp + getDuration();
}
/**
* Current duration of this audio segment. Note: this is a dynamic value until the complete property is set to true.
*
* @return duration in milliseconds
*/
public long getDuration()
{
return (mSampleCount / 8); //8 kHz audio generates 8 samples per millisecond
}
/**
* The complete property is used by the audio segment producer to signal that the segment is complete and no
* additional audio or identifiers will be added to the segment.
@@ -118,6 +138,15 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mComplete;
}
/**
* Duplicate call audio property. This flag is set to true whenever a duplicate call detection function detects
* an audio segment is a duplicate.
*/
public BooleanProperty duplicateProperty()
{
return mDuplicate;
}
/**
* An observable list of broadcast channels for this audio segment. Broadcast channels are added to this segment
* across the life-cycle of the segment. As each new alias identifier is added to the segment, any broadcast
@@ -347,6 +376,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
}
mAudioBuffers.add(audioBuffer);
mSampleCount += audioBuffer.length;
}
/**
@@ -396,6 +426,23 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
}
}
/**
* Indicates if this audio segment has been flagged as a duplicate audio call
*/
public boolean isDuplicate()
{
return mDuplicate.get();
}
/**
* Sets the duplicate audio call flag for this audio segment
* @param duplicate true if this is a duplicate audio segment
*/
public void setDuplicate(boolean duplicate)
{
mDuplicate.set(duplicate);
}
/**
* Processes identifier update notifications and updates audio segment properties using the associated aliases
* from the alias list.

View File

@@ -0,0 +1,318 @@
/*
* *****************************************************************************
* 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.audio;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
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.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
*
* Note: system in this context refers to the system name value that is used in channel configurations. All decoder
* channels must share the same system name for call duplication detection.
*/
public class DuplicateCallDetector implements Listener<AudioSegment>
{
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
public DuplicateCallDetector(UserPreferences userPreferences)
{
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
}
@Override
public void receive(AudioSegment audioSegment)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
{
Identifier identifier = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
if(identifier instanceof SystemConfigurationIdentifier)
{
String system = ((SystemConfigurationIdentifier)identifier).getValue();
synchronized(mDetectorMap)
{
SystemDuplicateCallDetector detector = mDetectorMap.get(system);
if(detector == null)
{
detector = new SystemDuplicateCallDetector();
mDetectorMap.put(system, detector);
}
detector.add(audioSegment);
}
}
}
}
public class SystemDuplicateCallDetector
{
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private AtomicBoolean mMonitoring = new AtomicBoolean();
private ScheduledFuture<?> mProcessorFuture;
public SystemDuplicateCallDetector()
{
}
public void add(AudioSegment audioSegment)
{
//Block on audio segment queue so that we don't interfere with monitoring shutdown
synchronized(mAudioSegmentQueue)
{
mAudioSegmentQueue.add(audioSegment);
startMonitoring();
}
}
private void startMonitoring()
{
if(mMonitoring.compareAndSet(false, true))
{
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> process(),
0, 25, TimeUnit.MILLISECONDS);
}
}
private void stopMonitoring()
{
if(mMonitoring.compareAndSet(true, false))
{
if(mProcessorFuture != null)
{
mProcessorFuture.cancel(true);
}
}
}
/**
* Checks if either the TO or FROM integer identifiers for the audio segments are the same
* @param segment1 to compare
* @param segment2 to compare
* @return true if integer identifiers are the same
*/
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
{
//Step 1 check for duplicate TO values
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
List<Identifier> to2 = segment2.getIdentifierCollection().getIdentifiers(Role.TO);
if(isDuplicate(to1, to2))
{
return true;
}
}
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
{
//Step 2 check for duplicate FROM values
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
List<Identifier> from2 = segment2.getIdentifierCollection().getIdentifiers(Role.FROM);
return isDuplicate(from1, from2);
}
return false;
}
/**
* Checks both lists of identifiers to determine if there are talkgroups or radio identifiers that are
* the same in both lists. Note: talkgroup check compares both talkgroups and patch groups.
*
* @param identifiers1
* @param identifiers2
* @return
*/
private boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
{
for(Identifier identifier1: identifiers1)
{
if(identifier1 instanceof TalkgroupIdentifier)
{
int talkgroup1 = ((TalkgroupIdentifier)identifier1).getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof TalkgroupIdentifier &&
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
{
return true;
}
else if(identifier2 instanceof PatchGroupIdentifier &&
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
{
return true;
}
}
}
else if(identifier1 instanceof PatchGroupIdentifier)
{
int talkgroup1 = ((PatchGroupIdentifier)identifier1).getValue().getPatchGroup().getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof TalkgroupIdentifier &&
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
{
return true;
}
else if(identifier2 instanceof PatchGroupIdentifier &&
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
{
return true;
}
}
}
else if(identifier1 instanceof RadioIdentifier)
{
int radio1 = ((RadioIdentifier)identifier1).getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof RadioIdentifier && ((RadioIdentifier)identifier2).getValue() == radio1)
{
return true;
}
}
}
}
return false;
}
/**
* Processes audio segments to detect duplicates
*/
private void process()
{
try
{
//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)
{
List<AudioSegment> duplicates = new ArrayList<>();
int currentIndex = 0;
while(currentIndex < mAudioSegments.size() - 1)
{
AudioSegment current = mAudioSegments.get(currentIndex);
if(!current.isDuplicate())
{
int checkIndex = currentIndex + 1;
while(checkIndex < mAudioSegments.size())
{
AudioSegment toCheck = mAudioSegments.get(checkIndex);
if(!toCheck.isDuplicate())
{
if(isDuplicate(current, toCheck))
{
toCheck.setDuplicate(true);
toCheck.decrementConsumerCount();
duplicates.add(toCheck);
}
}
checkIndex++;
}
}
currentIndex++;
}
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)
{
if(mAudioSegmentQueue.isEmpty())
{
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

@@ -127,7 +127,12 @@ public class AudioStreamingManager implements Listener<AudioSegment>
{
audioSegment = it.next();
if(audioSegment.completeProperty().get())
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
{
it.remove();
audioSegment.decrementConsumerCount();
}
else if(audioSegment.completeProperty().get())
{
it.remove();

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);
}
/**
@@ -381,8 +381,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
try
{
//Remove an assigned audio segment that's subsequently flagged as do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor())
//Remove an assigned segment that's subsequently flagged as duplicate or do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor() || (mNextAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
mNextAudioSegment = null;
@@ -456,9 +457,11 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(mCurrentAudioSegment != null)
{
//Check for completed audio segment or a segment flagged as Do Not Monitor
if(mCurrentAudioSegment.isDoNotMonitor() || (mCurrentAudioSegment.completeProperty().get() &&
mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
//Check for completed audio segment or a segment flagged as duplicate or Do Not Monitor
if(mCurrentAudioSegment.isDoNotMonitor() ||
(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()) ||
(mCurrentAudioSegment.completeProperty().get() && mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
{
if(mCurrentAudioSegment.isDoNotMonitor())
{
@@ -490,7 +493,8 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
//segment is a linked segment, ignore it so that we can close out the current segment.
while(mCurrentAudioSegment != null && (mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
!mCurrentAudioSegment.isDoNotMonitor())
!mCurrentAudioSegment.isDoNotMonitor() && !(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
@@ -510,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

@@ -27,20 +27,15 @@ import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Mixer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -78,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();
@@ -123,7 +118,12 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
while(newSegment != null)
{
if(newSegment.hasAudio())
if(newSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
{
newSegment.decrementConsumerCount();
}
else if(newSegment.hasAudio())
{
mAudioSegments.add(newSegment);
}
@@ -146,7 +146,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
audioSegment = it.next();
if(audioSegment.hasAudio())
if(audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
{
it.remove();
audioSegment.decrementConsumerCount();
}
else if(audioSegment.hasAudio())
{
//Queue it up for replay
it.remove();
@@ -174,7 +180,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
audioSegment = it.next();
if(audioSegment.isDoNotMonitor())
if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.decrementConsumerCount();
@@ -235,7 +242,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
audioSegment = it.next();
if(audioSegment.completeProperty().get())
if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.decrementConsumerCount();
@@ -246,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

@@ -30,15 +30,28 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
public class ChangeChannelTimeoutEvent extends DecoderStateEvent
{
private ChannelType mChannelType;
private long mCallTimeout;
private long mCallTimeoutMilliseconds;
/**
* Constructs an instance
* @param source of the event
* @param channelType for the event
* @param timeout in milliseconds
* @param timeslot of the event
*/
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;
mCallTimeout = timeout;
mCallTimeoutMilliseconds = timeout;
}
/**
* Constructs an instance
* @param source of the event
* @param channelType for the event
* @param timeout value in milliseconds
*/
public ChangeChannelTimeoutEvent(Object source, ChannelType channelType, long timeout)
{
this(source, channelType, timeout, 0);
@@ -49,8 +62,11 @@ public class ChangeChannelTimeoutEvent extends DecoderStateEvent
return mChannelType;
}
public long getCallTimeout()
/**
* Call timeout value in milliseconds
*/
public long getCallTimeoutMilliseconds()
{
return mCallTimeout;
return mCallTimeoutMilliseconds;
}
}

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.setEndTimeoutBuffer(RESET_TIMEOUT_DELAY);
if(channel.getChannelType() == ChannelType.STANDARD)
{
stateMachine.setFadeTimeoutBuffer(FADE_TIMEOUT_DELAY);
}
else
{
stateMachine.setFadeTimeoutBuffer(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
}
stateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
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,14 +533,14 @@ 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;
mStateMachineMap.get(event.getTimeslot()).setFadeTimeoutBuffer(timeout.getCallTimeout());
mStateMachineMap.get(event.getTimeslot()).setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
}
break;
case CONTINUATION:
@@ -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;
@@ -40,6 +41,7 @@ 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.config.WithCallTimeout;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
@@ -50,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;
@@ -96,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)
{
@@ -106,49 +108,67 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mStateMachine.addListener(this);
mStateMachine.addListener(mSquelchController);
mStateMachine.setChannelType(mChannel.getChannelType());
mStateMachine.setIdentifierUpdateListener(mIdentifierCollection);
mStateMachine.setEndTimeoutBuffer(RESET_TIMEOUT_DELAY);
if(channel.getChannelType() == ChannelType.STANDARD)
mStateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
configureChannelType(channel);
}
/**
* Configure items according to channel type
* @param channel configuration
*/
private void configureChannelType(Channel channel)
{
mStateMachine.setChannelType(getChannel().getChannelType());
if(channel.isStandardChannel())
{
mStateMachine.setFadeTimeoutBuffer(FADE_TIMEOUT_DELAY);
mStateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY);
}
else
{
mStateMachine.setFadeTimeoutBuffer(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
final DecodeConfiguration decodeConfig = channel.getDecodeConfiguration();
final int fadeTimeoutSeconds = decodeConfig instanceof WithCallTimeout
? ((WithCallTimeout) decodeConfig).getCallTimeoutSeconds()
: DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
mStateMachine.setFadeTimeoutBufferMilliseconds(fadeTimeoutSeconds * 1000);
}
}
/**
* 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
{
@@ -252,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);
}
@@ -264,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);
}
@@ -274,7 +294,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
{
mIdentifierCollection.broadcastIdentifiers();
if(mChannel.getChannelType() == ChannelType.TRAFFIC)
if(getChannel().isTrafficChannel())
{
mStateMachine.setState(State.ACTIVE);
}
@@ -286,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()
{
@@ -366,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
@@ -380,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;
}
@@ -399,14 +413,14 @@ 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;
mStateMachine.setFadeTimeoutBuffer(timeout.getCallTimeout());
mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
}
case CONTINUATION:
case DECODE:
@@ -414,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

@@ -42,9 +42,9 @@ public class StateMachine
protected State mState = State.IDLE;
protected long mFadeTimeout;
protected long mFadeTimeoutBuffer = 0;
protected long mFadeTimeoutBufferMilliseconds = 0;
protected long mEndTimeout;
protected long mEndTimeoutBuffer = 0;
protected long mEndTimeoutBufferMilliseconds = 0;
private int mTimeslot;
private EnumSet<State> mActiveStates;
private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD;
@@ -63,26 +63,42 @@ public class StateMachine
mActiveStates = activeStates;
}
/**
* Adds a state change listener
* @param listener to receive state change events
*/
public void addListener(IStateMachineListener listener)
{
mStateMachineListeners.add(listener);
}
/**
* Removes the state change listener
*/
public void removeListener(IStateMachineListener listener)
{
mStateMachineListeners.remove(listener);
}
/**
* Sets the listener to receive identifier update notifications (ie state identifier updates)
*/
public void setIdentifierUpdateListener(Listener<IdentifierUpdateNotification> listener)
{
mIdentifierUpdateListener = listener;
}
/**
* Sets the channel type for this state machine
*/
public void setChannelType(Channel.ChannelType channelType)
{
mChannelType = channelType;
}
/**
* Checks the state and transitions to FADE or TEARDOWN if timers have expired
*/
public void checkState()
{
if(mActiveStates.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
@@ -95,11 +111,17 @@ public class StateMachine
}
}
/**
* Current state of the state machine
*/
public State getState()
{
return mState;
}
/**
* Sets the state
*/
public void setState(State state)
{
if(state == mState)
@@ -173,6 +195,9 @@ public class StateMachine
}
}
/**
* Broadcasts a channel state identifier update
*/
private void broadcast(ChannelStateIdentifier channelStateIdentifier)
{
if(mIdentifierUpdateListener != null)
@@ -182,43 +207,70 @@ public class StateMachine
}
}
/**
* Updates the fade timeout to current time plus the fade timeout buffer value.
*/
private void updateFadeTimeout()
{
mFadeTimeout = System.currentTimeMillis() + mFadeTimeoutBuffer;
mFadeTimeout = System.currentTimeMillis() + mFadeTimeoutBufferMilliseconds;
}
/**
* Manually sets the fade timeout
*/
public void setFadeTimeout(long timeout)
{
mFadeTimeout = timeout;
}
/**
* Fade timeout value
*/
public long getFadeTimeout()
{
return mFadeTimeout;
}
public void setFadeTimeoutBuffer(long buffer)
/**
* Sets the fade timeout buffer value. This value determines the maximum length of time the state machine
* can stay in any state (without an update) before automatically transitioning to FADE state.
* @param buffer in milliseconds
*/
public void setFadeTimeoutBufferMilliseconds(long buffer)
{
mFadeTimeoutBuffer = buffer;
mFadeTimeoutBufferMilliseconds = buffer;
updateFadeTimeout();
}
/**
* Updates the end timeout value to current system time plus the end timeout buffer value.
*/
private void updateEndTimeout()
{
mEndTimeout = System.currentTimeMillis() + mEndTimeoutBuffer;
mEndTimeout = System.currentTimeMillis() + mEndTimeoutBufferMilliseconds;
}
public void setEndTimeoutBuffer(long buffer)
/**
* Sets the end timeout buffer value. This value is the maximum length of time that the channel can
* stay in the FADE state (without an update) before transitioning to a TEARDOWN state.
*/
public void setEndTimeoutBufferMilliseconds(long buffer)
{
mEndTimeoutBuffer = buffer;
mEndTimeoutBufferMilliseconds = buffer;
updateEndTimeout();
}
/**
* Sets the end timeout value
*/
public void setEndTimeout(long endTimeout)
{
mEndTimeout = endTimeout;
}
/**
* Current end timeout value
*/
public long getEndTimeout()
{
return mEndTimeout;

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

@@ -39,6 +39,7 @@ import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigFactory;
import io.github.dsheirer.source.config.SourceConfigRecording;
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.channel.TunerChannel;
import javafx.beans.Observable;
@@ -48,10 +49,13 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.Transient;
import java.util.Objects;
@JacksonXmlRootElement(localName = "channel")
@@ -78,6 +82,7 @@ public class Channel extends Configuration implements Listener<SourceEvent>
private StringProperty mSystem = new SimpleStringProperty();
private StringProperty mSite = new SimpleStringProperty();
private StringProperty mName = new SimpleStringProperty();
private ObservableList<Long> mFrequencyList;
private BooleanProperty mProcessing = new SimpleBooleanProperty();
private BooleanProperty mAutoStart = new SimpleBooleanProperty();
@@ -178,6 +183,23 @@ public class Channel extends Configuration implements Listener<SourceEvent>
return channel;
}
/**
* Updates the frequencies property as the source configuration changes or is initialized.
*/
private void updateFrequencies()
{
getFrequencyList().clear();
if(mSourceConfiguration instanceof SourceConfigTuner)
{
getFrequencyList().add(((SourceConfigTuner)mSourceConfiguration).getFrequency());
}
else if(mSourceConfiguration instanceof SourceConfigTunerMultipleFrequency)
{
getFrequencyList().addAll(((SourceConfigTunerMultipleFrequency)mSourceConfiguration).getFrequencies());
}
}
/**
* Alias list name property
*/
@@ -243,6 +265,23 @@ public class Channel extends Configuration implements Listener<SourceEvent>
return mChannelID;
}
/**
* List of frequencies extracted from the source configuration. This method only exists to support JavaFX
* monitoring of frequency list changes. Use the source configuration to manage frequencies.
*/
@JsonIgnore
@Transient
public ObservableList<Long> getFrequencyList()
{
if(mFrequencyList == null)
{
mFrequencyList = FXCollections.observableArrayList();
updateFrequencies();
}
return mFrequencyList;
}
/**
* Indicates if this channel has been selected for prioritized audio output
* and display of processing chain products
@@ -351,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();
}
@@ -529,6 +570,8 @@ public class Channel extends Configuration implements Listener<SourceEvent>
mSourceConfiguration = config;
}
updateFrequencies();
//Clear the tune channel object so that it can be recreated if the
//source configuration changes
mTunerChannel = null;
@@ -676,6 +719,7 @@ public class Channel extends Configuration implements Listener<SourceEvent>
public static Callback<Channel,Observable[]> extractor()
{
return (Channel c) -> new Observable[] {c.processingProperty(), c.nameProperty(), c.aliasListNameProperty(),
c.autoStartOrderProperty(), c.autoStartProperty(), c.siteProperty(), c.systemProperty()};
c.autoStartOrderProperty(), c.autoStartProperty(), c.siteProperty(), c.systemProperty(),
c.getFrequencyList()};
}
}

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;
@@ -49,15 +48,20 @@ import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
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
@@ -69,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<>();
@@ -81,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
@@ -164,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)
{
@@ -176,49 +183,51 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
{
try
{
startProcessing(event);
startProcessing(new ChannelStartProcessingRequest(event.getChannel()));
}
catch(ChannelException ce)
{
mLog.error("Error starting requested channel [" + channel.getName() + "] - " + ce.getMessage());
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
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();
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
{
mLog.error("Error starting requested channel [" + channel.getName() + "] - " + ce.getMessage());
}
}
}
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;
@@ -234,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);
}
}
}
}
/**
@@ -243,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
@@ -287,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())
@@ -369,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,
@@ -397,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
@@ -417,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));
}
@@ -432,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())
{
@@ -451,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!)
@@ -459,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
{
@@ -478,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)
{
@@ -493,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.
@@ -542,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

@@ -38,6 +38,8 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor;
import io.github.dsheirer.jmbe.JmbeEditorRequest;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
@@ -68,12 +70,14 @@ public class JavaFxWindowManager extends Application
public static final String USER_PREFERENCES_EDITOR = "preferences";
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
public static final String STAGE_MONITOR_KEY_USER_PREFERENCES_EDITOR = "user.preferences";
private JFXPanel mJFXPanel;
private ChannelMapEditor mChannelMapEditor;
private IconManager mIconManager;
private JmbeEditor mJmbeEditor;
private PlaylistEditor mPlaylistEditor;
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
@@ -81,6 +85,7 @@ public class JavaFxWindowManager extends Application
private Stage mChannelMapStage;
private Stage mIconManagerStage;
private Stage mJmbeEditorStage;
private Stage mPlaylistStage;
private Stage mUserPreferencesStage;
@@ -115,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());
@@ -157,6 +162,8 @@ public class JavaFxWindowManager extends Application
*/
public void shutdown()
{
MyEventBus.getGlobalEventBus().unregister(this);
if(mChannelMapStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mChannelMapStage);
@@ -205,6 +212,56 @@ public class JavaFxWindowManager extends Application
return mIconManager;
}
/**
* Processes a JMBE s editor request
*/
@Subscribe
public void process(final JmbeEditorRequest request)
{
if(request.isCloseEditorRequest())
{
execute(() -> {
getJmbeEditorStage().hide();
mJmbeEditorStage = null;
mJmbeEditor = null;
});
}
else
{
execute(() -> {
getJmbeEditorStage().show();
getJmbeEditorStage().requestFocus();
getJmbeEditorStage().toFront();
getJmbeEditor().process(request);
});
}
}
public Stage getJmbeEditorStage()
{
if(mJmbeEditorStage == null)
{
createJFXPanel();
Scene scene = new Scene(getJmbeEditor(), 650, 650);
mJmbeEditorStage = new Stage();
mJmbeEditorStage.setTitle("sdrtrunk - JMBE Library Updater");
mJmbeEditorStage.setScene(scene);
mUserPreferences.getJavaFxPreferences().monitor(mJmbeEditorStage, STAGE_MONITOR_KEY_JMBE_EDITOR);
}
return mJmbeEditorStage;
}
public JmbeEditor getJmbeEditor()
{
if(mJmbeEditor == null)
{
mJmbeEditor = new JmbeEditor(mUserPreferences);
}
return mJmbeEditor;
}
/**
* Lazy construct and access the playlist editor
*/

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.gui;
import com.jidesoft.plaf.LookAndFeelFactory;
import com.jidesoft.swing.JideSplitPane;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.DuplicateCallDetector;
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
@@ -180,6 +181,9 @@ public class SDRTrunk implements Listener<TunerEvent>
mUserPreferences);
mAudioStreamingManager.start();
DuplicateCallDetector duplicateCallDetector = new DuplicateCallDetector(mUserPreferences);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(duplicateCallDetector);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(audioPlaybackManager);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);
@@ -333,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);
@@ -243,7 +243,7 @@ public class PlaylistEditor extends BorderPane
{
if(mChannelEditor == null)
{
mChannelEditor = new ChannelEditor(mPlaylistManager);
mChannelEditor = new ChannelEditor(mPlaylistManager, mUserPreferences);
}
return mChannelEditor;

View File

@@ -0,0 +1,374 @@
/*
* *****************************************************************************
* 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.alias;
import com.google.common.collect.Ordering;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.collections.transformation.SortedList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import javafx.util.Callback;
import org.controlsfx.control.ToggleSwitch;
import java.util.List;
/**
* Editor for multiple selected aliases providing limited options for changing attributes of multiple aliases
*/
public class AliasBulkEditor extends Editor<List<Alias>>
{
private PlaylistManager mPlaylistManager;
private Label mEditingLabel;
private ColorPicker mColorPicker;
private Button mApplyColorButton;
private Button mResetColorButton;
private ComboBox<Icon> mIconNodeComboBox;
private Button mApplyIconButton;
private ToggleSwitch mMonitorAudioToggleSwitch;
private ComboBox<Integer> mMonitorPriorityComboBox;
private Button mApplyMonitorButton;
/**
* Constructs an instance
* @param playlistManager for accessing icon manager
*/
public AliasBulkEditor(PlaylistManager playlistManager)
{
mPlaylistManager = playlistManager;
GridPane gridPane = new GridPane();
gridPane.setPadding(new Insets(10,10,10,10));
gridPane.setHgap(10);
gridPane.setVgap(10);
int row = 0;
Label editorLabel = new Label("Multiple Alias Editor");
GridPane.setConstraints(editorLabel, 0, row, 3, 1);
gridPane.getChildren().add(editorLabel);
GridPane.setConstraints(getEditingLabel(), 3, row, 3, 1);
GridPane.setHalignment(getEditingLabel(), HPos.RIGHT);
gridPane.getChildren().add(getEditingLabel());
Separator separator = new Separator();
separator.setMaxWidth(Double.MAX_VALUE);
GridPane.setConstraints(separator, 0, ++row, 6, 1);
gridPane.getChildren().add(separator);
Label colorLabel = new Label("Color");
GridPane.setHalignment(colorLabel, HPos.RIGHT);
GridPane.setConstraints(colorLabel, 0, ++row);
gridPane.getChildren().add(colorLabel);
GridPane.setConstraints(getColorPicker(), 1, row, 3, 1);
gridPane.getChildren().add(getColorPicker());
GridPane.setConstraints(getApplyColorButton(), 4, row);
gridPane.getChildren().add(getApplyColorButton());
GridPane.setConstraints(getResetColorButton(), 5, row);
gridPane.getChildren().add(getResetColorButton());
Label iconLabel = new Label("Icon");
GridPane.setHalignment(iconLabel, HPos.RIGHT);
GridPane.setConstraints(iconLabel, 0, ++row);
gridPane.getChildren().add(iconLabel);
GridPane.setConstraints(getIconNodeComboBox(), 1, row, 3, 1);
gridPane.getChildren().add(getIconNodeComboBox());
GridPane.setConstraints(getApplyIconButton(), 4, row);
gridPane.getChildren().add(getApplyIconButton());
Label listenLabel = new Label("Listen");
GridPane.setHalignment(listenLabel, HPos.RIGHT);
GridPane.setConstraints(listenLabel, 0, ++row);
gridPane.getChildren().add(listenLabel);
GridPane.setConstraints(getMonitorAudioToggleSwitch(), 1, row);
gridPane.getChildren().add(getMonitorAudioToggleSwitch());
Label priorityLabel = new Label("Priority");
GridPane.setHalignment(priorityLabel, HPos.RIGHT);
GridPane.setConstraints(priorityLabel, 2, row);
gridPane.getChildren().add(priorityLabel);
GridPane.setConstraints(getMonitorPriorityComboBox(), 3, row);
gridPane.getChildren().add(getMonitorPriorityComboBox());
GridPane.setConstraints(getApplyMonitorButton(), 4, row);
gridPane.getChildren().add(getApplyMonitorButton());
getChildren().add(gridPane);
}
@Override
public List<Alias> getItem()
{
return super.getItem();
}
@Override
public void setItem(List<Alias> item)
{
super.setItem(item);
getEditingLabel().setText("Editing " + item.size() + " Aliases");
}
@Override
public void save()
{
//no-op
}
@Override
public void dispose()
{
//no-op
}
private Label getEditingLabel()
{
if(mEditingLabel== null)
{
mEditingLabel = new Label("Editing 0 Aliases");
}
return mEditingLabel;
}
private ColorPicker getColorPicker()
{
if(mColorPicker == null)
{
mColorPicker = new ColorPicker(Color.BLACK);
mColorPicker.setEditable(true);
mColorPicker.setStyle("-fx-color-rect-width: 60px; -fx-color-label-visible: false;");
}
return mColorPicker;
}
private Button getApplyColorButton()
{
if(mApplyColorButton == null)
{
mApplyColorButton = new Button("Apply");
mApplyColorButton.setOnAction(event -> {
int colorValue = ColorUtil.toInteger(getColorPicker().getValue());
for(Alias alias: getItem())
{
alias.setColor(colorValue);
}
});
}
return mApplyColorButton;
}
private Button getResetColorButton()
{
if(mResetColorButton == null)
{
mResetColorButton = new Button("Reset Color");
mResetColorButton.setOnAction(event -> {
for(Alias alias: getItem())
{
alias.setColor(0);
}
});
}
return mResetColorButton;
}
private ComboBox<Icon> getIconNodeComboBox()
{
if(mIconNodeComboBox == null)
{
mIconNodeComboBox = new ComboBox<>();
mIconNodeComboBox.setMaxWidth(Double.MAX_VALUE);
mIconNodeComboBox.setItems(new SortedList(mPlaylistManager.getIconModel().iconsProperty(), Ordering.natural()));
mIconNodeComboBox.setCellFactory(new IconCellFactory());
mIconNodeComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) ->
{
getApplyIconButton().setDisable(newValue == null);
});
}
return mIconNodeComboBox;
}
private Button getApplyIconButton()
{
if(mApplyIconButton == null)
{
mApplyIconButton = new Button("Apply");
mApplyIconButton.setDisable(true);
mApplyIconButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
Icon icon = getIconNodeComboBox().getSelectionModel().getSelectedItem();
if(icon != null)
{
for(Alias alias: getItem())
{
alias.setIconName(icon.getName());
}
}
}
});
}
return mApplyIconButton;
}
private ToggleSwitch getMonitorAudioToggleSwitch()
{
if(mMonitorAudioToggleSwitch == null)
{
mMonitorAudioToggleSwitch = new ToggleSwitch();
mMonitorAudioToggleSwitch.setSelected(true);
mMonitorAudioToggleSwitch.selectedProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mMonitorAudioToggleSwitch;
}
private ComboBox<Integer> getMonitorPriorityComboBox()
{
if(mMonitorPriorityComboBox == null)
{
mMonitorPriorityComboBox = new ComboBox<>();
mMonitorPriorityComboBox.getItems().add(null);
for(int x = io.github.dsheirer.alias.id.priority.Priority.MIN_PRIORITY;
x < io.github.dsheirer.alias.id.priority.Priority.MAX_PRIORITY; x++)
{
mMonitorPriorityComboBox.getItems().add(x);
}
mMonitorPriorityComboBox.disableProperty().bind(getMonitorAudioToggleSwitch().selectedProperty().not());
mMonitorPriorityComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mMonitorPriorityComboBox;
}
private Button getApplyMonitorButton()
{
if(mApplyMonitorButton == null)
{
mApplyMonitorButton = new Button("Apply");
mApplyMonitorButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
boolean canMonitor = getMonitorAudioToggleSwitch().isSelected();
Integer priority = getMonitorPriorityComboBox().getSelectionModel().getSelectedItem();
if(canMonitor)
{
if(priority == null)
{
priority = io.github.dsheirer.alias.id.priority.Priority.DEFAULT_PRIORITY;
}
}
else
{
priority = io.github.dsheirer.alias.id.priority.Priority.DO_NOT_MONITOR;
}
for(Alias alias: getItem())
{
alias.setCallPriority(priority);
}
}
});
}
return mApplyMonitorButton;
}
/**
* Cell factory for combo box for dislaying icon name and graphic
*/
public class IconCellFactory implements Callback<ListView<Icon>, ListCell<Icon>>
{
@Override
public ListCell<Icon> call(ListView<Icon> param)
{
Label iconLabel = new Label();
Label textLabel = new Label();
GridPane gridPane = new GridPane();
gridPane.setHgap(5);
GridPane.setHalignment(iconLabel, HPos.RIGHT);
gridPane.getColumnConstraints().add(new ColumnConstraints(50));
gridPane.add(iconLabel, 0, 0);
gridPane.add(textLabel,1,0);
ListCell<Icon> cell = new ListCell<>()
{
@Override
protected void updateItem(Icon item, boolean empty)
{
super.updateItem(item, empty);
if(empty)
{
setText(null);
setGraphic(null);
}
else
{
textLabel.setText(item.getName());
iconLabel.setGraphic(new ImageView(item.getFxImage()));
setGraphic(gridPane);
}
}
};
return cell;
}
}
}

View File

@@ -27,12 +27,14 @@ import io.github.dsheirer.alias.AliasFactory;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
@@ -85,6 +87,8 @@ public class AliasConfigurationEditor extends SplitPane
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
private AliasItemEditor mAliasItemEditor;
private AliasBulkEditor mAliasBulkEditor;
private Editor mCurrentEditor;
private TableView<Alias> mAliasTableView;
private Label mPlaceholderLabel;
private Button mNewAliasButton;
@@ -114,6 +118,7 @@ public class AliasConfigurationEditor extends SplitPane
topBox.getChildren().addAll(leftBox, getButtonBox());
setOrientation(Orientation.VERTICAL);
mCurrentEditor = getAliasItemEditor();
getItems().addAll(topBox, getAliasItemEditor());
}
@@ -141,7 +146,20 @@ public class AliasConfigurationEditor extends SplitPane
}
}
private void setAlias(Alias alias)
/**
* Sets the editor as the bottom alias editor, either single alias or bulk alias editor.
*/
private void setEditor(Editor editor)
{
if(editor != mCurrentEditor)
{
getItems().remove(mCurrentEditor);
mCurrentEditor = editor;
getItems().add(mCurrentEditor);
}
}
private void setAliases(List<Alias> aliases)
{
//Prompt the user to save if the contents of the current channel editor have been modified
if(getAliasItemEditor().modifiedProperty().get())
@@ -168,18 +186,27 @@ public class AliasConfigurationEditor extends SplitPane
}
}
getCloneAliasButton().setDisable(alias == null || getAliasTableView().getSelectionModel().getSelectedItems().size() > 1);
getDeleteAliasButton().setDisable(alias == null);
getMoveToAliasButton().setDisable(alias == null);
if(getAliasTableView().getSelectionModel().getSelectedItems().size() <= 1)
if(aliases.size() <= 1)
{
getAliasItemEditor().setItem(alias);
setEditor(getAliasItemEditor());
if(aliases.size() == 1)
{
getAliasItemEditor().setItem(aliases.get(0));
}
else
{
getAliasItemEditor().setItem(null);
}
}
else
{
getAliasItemEditor().setItem(null);
setEditor(getAliasBulkEditor());
getAliasBulkEditor().setItem(aliases);
}
getCloneAliasButton().setDisable(aliases.isEmpty() || aliases.size() > 1);
getDeleteAliasButton().setDisable(aliases.isEmpty());
getMoveToAliasButton().setDisable(aliases.isEmpty());
}
private AliasItemEditor getAliasItemEditor()
@@ -192,6 +219,16 @@ public class AliasConfigurationEditor extends SplitPane
return mAliasItemEditor;
}
private AliasBulkEditor getAliasBulkEditor()
{
if(mAliasBulkEditor == null)
{
mAliasBulkEditor = new AliasBulkEditor(mPlaylistManager);
}
return mAliasBulkEditor;
}
private HBox getSearchAndListSelectionBox()
{
if(mSearchAndListSelectionBox == null)
@@ -384,10 +421,9 @@ public class AliasConfigurationEditor extends SplitPane
mAliasTableView.setPlaceholder(getPlaceholderLabel());
mAliasTableView.setItems(getAliasSortedList());
mAliasTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
mAliasTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> {
setAlias(newValue);
});
mAliasTableView.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Alias>)c -> {
Platform.runLater(() -> setAliases(mAliasTableView.getSelectionModel().getSelectedItems()));
});
}
return mAliasTableView;

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);
@@ -330,17 +330,14 @@ public class AliasItemEditor extends Editor<Alias>
if(alias != null)
{
alias.setName(getNameField().getText());
alias.setGroup(getGroupField().getText());
alias.setRecordable(getRecordAudioToggleSwitch().isSelected());
alias.setColor(ColorUtil.toInteger(getColorPicker().getValue()));
boolean canMonitor = getMonitorAudioToggleSwitch().isSelected();
Integer priority = getMonitorPriorityComboBox().getSelectionModel().getSelectedItem();
Icon icon = getIconNodeComboBox().getSelectionModel().getSelectedItem();
alias.setIconName(icon != null ? icon.getName() : null);
boolean canMonitor = getMonitorAudioToggleSwitch().isSelected();
Integer priority = getMonitorPriorityComboBox().getSelectionModel().getSelectedItem();
if(canMonitor)
{
if(priority == null)
@@ -377,6 +374,27 @@ public class AliasItemEditor extends Editor<Alias>
{
alias.addAliasAction(aliasAction);
}
//Hack: because we're using a sorted list for the alias editor, sometimes setting
//name and or group can cause list errors. So, we wrap each of these in a try/catch
//block to mitigate the error and effect the changes.
try
{
alias.setName(getNameField().getText());
}
catch(Exception e)
{
mLog.error("Error while updating alias name.", e);
}
try
{
alias.setGroup(getGroupField().getText());
}
catch(Exception e)
{
mLog.error("Error while updating alias group value", e);
}
}
AliasList aliasList = mPlaylistManager.getAliasModel().getAliasList(alias.getAliasListName());
@@ -392,6 +410,7 @@ public class AliasItemEditor extends Editor<Alias>
@Override
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
}
private VBox getTitledPanesBox()
@@ -641,7 +660,7 @@ public class AliasItemEditor extends Editor<Alias>
if(selected != null)
{
MyEventBus.getEventBus().post(new ViewAliasIdentifierRequest(selected));
MyEventBus.getGlobalEventBus().post(new ViewAliasIdentifierRequest(selected));
}
}
});
@@ -669,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));
@@ -697,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

@@ -30,6 +30,7 @@ import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
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;
@@ -60,9 +61,9 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
* Constructs an instance
* @param playlistManager
*/
public AMConfigurationEditor(PlaylistManager playlistManager)
public AMConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getRecordPane());

View File

@@ -25,13 +25,17 @@ package io.github.dsheirer.gui.playlist.channel;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelException;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
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.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
@@ -48,6 +52,7 @@ import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
@@ -78,6 +83,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
private final static Logger mLog = LoggerFactory.getLogger(ChannelConfigurationEditor.class);
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
protected EditorModificationListener mEditorModificationListener = new EditorModificationListener();
private Button mPlayButton;
private TextField mSystemField;
@@ -97,9 +103,10 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
private IconNode mStopGraphicNode;
private ChannelProcessingMonitor mChannelProcessingMonitor = new ChannelProcessingMonitor();
public ChannelConfigurationEditor(PlaylistManager playlistManager)
public ChannelConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mUserPreferences = userPreferences;
setMaxWidth(Double.MAX_VALUE);
@@ -317,6 +324,45 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
});
}
if(requiresJmbeLibrarySetup() &&
mUserPreferences.getJmbeLibraryPreference().getAlertIfMissingLibraryRequired() &&
!getItem().processingProperty().get())
{
String content = "The decoder for this channel configuration requires the (optional) JMBE " +
"library to produce audio and the JMBE library is not currently setup. Do you want to " +
"setup the JMBE library?";
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, content, ButtonType.YES, ButtonType.NO);
alert.setTitle("JMBE Library");
alert.setHeaderText("Setup JMBE Library?");
Label label = new Label(content);
label.setMaxWidth(Double.MAX_VALUE);
label.setMaxHeight(Double.MAX_VALUE);
label.getStyleClass().add("content");
label.setWrapText(true);
CheckBox checkBox = new CheckBox("Don't Show This Again");
checkBox.setOnAction(event1 -> {
boolean dontShowAgain = checkBox.isSelected();
mUserPreferences.getJmbeLibraryPreference().setAlertIfMissingLibraryRequired(!dontShowAgain);
});
VBox contentBox = new VBox();
contentBox.setPrefWidth(360);
contentBox.setSpacing(10);
contentBox.getChildren().addAll(label, checkBox);
alert.getDialogPane().setContent(contentBox);
alert.initOwner(getPlayButton().getScene().getWindow());
Optional<ButtonType> optionalButtonType = alert.showAndWait();
if(optionalButtonType.isPresent() && optionalButtonType.get() == ButtonType.YES)
{
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY));
return;
}
}
if(!getItem().processingProperty().get())
{
ThreadPool.SCHEDULED.execute(() -> {
@@ -366,6 +412,17 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
return mPlayButton;
}
/**
* Indicates if the decoder type for the channel configuration requires the JMBE library and if the
* application is not currently setup for the JMBE library.
*/
private boolean requiresJmbeLibrarySetup()
{
return getItem() != null &&
getItem().getDecodeConfiguration().getDecoderType().providesMBEAudioFrames() &&
!mUserPreferences.getJmbeLibraryPreference().hasJmbeLibraryPath();
}
/**
* Toggles the text and graphic of the channel play button to reflect the playing state of the channel
*/

View File

@@ -24,6 +24,7 @@ package io.github.dsheirer.gui.playlist.channel;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,26 +45,29 @@ public class ChannelConfigurationEditorFactory
* @param playlistManager for the editor
* @return constructed editor
*/
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager)
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
UserPreferences userPreferences)
{
switch(decoderType)
{
case AM:
return new AMConfigurationEditor(playlistManager);
return new AMConfigurationEditor(playlistManager, userPreferences);
case DMR:
return new DMRConfigurationEditor(playlistManager, userPreferences);
case NBFM:
return new NBFMConfigurationEditor(playlistManager);
return new NBFMConfigurationEditor(playlistManager, userPreferences);
case LTR_NET:
return new LTRNetConfigurationEditor(playlistManager);
return new LTRNetConfigurationEditor(playlistManager, userPreferences);
case LTR:
return new LTRConfigurationEditor(playlistManager);
return new LTRConfigurationEditor(playlistManager, userPreferences);
case MPT1327:
return new MPT1327ConfigurationEditor(playlistManager);
return new MPT1327ConfigurationEditor(playlistManager, userPreferences);
case PASSPORT:
return new PassportConfigurationEditor(playlistManager);
return new PassportConfigurationEditor(playlistManager, userPreferences);
case P25_PHASE1:
return new P25P1ConfigurationEditor(playlistManager);
return new P25P1ConfigurationEditor(playlistManager, userPreferences);
case P25_PHASE2:
return new P25P2ConfigurationEditor(playlistManager);
return new P25P2ConfigurationEditor(playlistManager, userPreferences);
default:
if(decoderType != null && !mLoggedUnrecognizedTypes.contains(decoderType))
{

View File

@@ -27,11 +27,11 @@ import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.module.decode.DecoderFactory;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.preference.UserPreferences;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
@@ -79,6 +79,7 @@ public class ChannelEditor extends SplitPane
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
private PlaylistManager mPlaylistManager;
private UserPreferences mUserPreferences;
private TableView<Channel> mChannelTableView;
private Label mPlaceholderLabel;
private MenuButton mNewButton;
@@ -101,10 +102,11 @@ public class ChannelEditor extends SplitPane
* Constructs an instance
* @param playlistManager containing playlists and channel configurations
*/
public ChannelEditor(PlaylistManager playlistManager)
public ChannelEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
mPlaylistManager = playlistManager;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager);
mUserPreferences = userPreferences;
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, userPreferences);
HBox channelsBox = new HBox();
channelsBox.setSpacing(10.0);
@@ -200,7 +202,8 @@ public class ChannelEditor extends SplitPane
if(editor == null)
{
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager);
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
mUserPreferences);
if(editor != null)
{
@@ -638,6 +641,7 @@ public class ChannelEditor extends SplitPane
}
}
/**
* Channel tuner channel source frequencies value factory
*/
@@ -649,26 +653,17 @@ public class ChannelEditor extends SplitPane
@Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<Channel, String> param)
{
Channel channel = param.getValue();
ObservableList<Long> frequencies = param.getValue().getFrequencyList();
if(channel != null)
if(frequencies != null)
{
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
List<String> freqsMHz = new ArrayList<>();
for(Long frequency: frequencies)
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
mFrequency.set(String.valueOf(frequency / 1E6));
freqsMHz.add(String.valueOf(frequency / 1E6));
}
else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency)
{
List<Long> frequencies = ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencies();
List<String> formatted = new ArrayList<>();
for(Long frequency: frequencies)
{
formatted.add(String.valueOf(frequency / 1E6));
}
mFrequency.set(Joiner.on(",").join(formatted));
}
mFrequency.set(Joiner.on(", ").join(freqsMHz));
}
else
{

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

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
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;
@@ -74,10 +75,11 @@ public class LTRConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public LTRConfigurationEditor(PlaylistManager playlistManager)
public LTRConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
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;
@@ -74,10 +75,11 @@ public class LTRNetConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public LTRNetConfigurationEditor(PlaylistManager playlistManager)
public LTRNetConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -36,6 +36,7 @@ import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
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;
@@ -60,6 +61,9 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MAXIMUM;
import static io.github.dsheirer.module.decode.config.DecodeConfiguration.CALL_TIMEOUT_MINIMUM;
/**
* MPT-1327 channel configuration editor
*/
@@ -81,10 +85,11 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public MPT1327ConfigurationEditor(PlaylistManager playlistManager)
public MPT1327ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());
@@ -244,7 +249,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
mChannelMapComboBox.setMaxWidth(Double.MAX_VALUE);
mChannelMapComboBox.setDisable(true);
mChannelMapComboBox.setTooltip(new Tooltip("Select a channel map to use for this system"));
mChannelMapComboBox.getItems().addAll(getPlaylistManager().getChannelMapModel().getChannelMaps());
mChannelMapComboBox.setItems(getPlaylistManager().getChannelMapModel().getChannelMaps());
mChannelMapComboBox.setDisable(true);
mChannelMapComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
@@ -271,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));
}
});
}
@@ -301,11 +306,11 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
{
if(mCallTimeoutSpinner == null)
{
mCallTimeoutSpinner = new Spinner();
mCallTimeoutSpinner = new Spinner<>();
mCallTimeoutSpinner.setDisable(true);
mCallTimeoutSpinner.setTooltip(new Tooltip("Maximum call limit in seconds"));
mCallTimeoutSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
SpinnerValueFactory<Integer> svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 120);
var svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(CALL_TIMEOUT_MINIMUM, CALL_TIMEOUT_MAXIMUM);
mCallTimeoutSpinner.setValueFactory(svf);
mCallTimeoutSpinner.getValueFactory().valueProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
@@ -340,7 +345,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
}
}
int callTimeout = decodeConfigMPT1327.getCallTimeout();
int callTimeout = decodeConfigMPT1327.getCallTimeoutSeconds();
getCallTimeoutSpinner().getValueFactory().setValue(callTimeout);
int channelPoolSize = decodeConfigMPT1327.getTrafficChannelPoolSize();
@@ -367,7 +372,7 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
config = new DecodeConfigMPT1327();
}
config.setCallTimeout(getCallTimeoutSpinner().getValue());
config.setCallTimeoutSeconds(getCallTimeoutSpinner().getValue());
config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
ChannelMap selected = getChannelMapComboBox().getSelectionModel().getSelectedItem();

View File

@@ -33,6 +33,7 @@ import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
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;
@@ -73,10 +74,11 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public NBFMConfigurationEditor(PlaylistManager playlistManager)
public NBFMConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -34,6 +34,7 @@ import io.github.dsheirer.module.decode.p25.phase1.P25P1Decoder;
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;
@@ -80,10 +81,11 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public P25P1ConfigurationEditor(PlaylistManager playlistManager)
public P25P1ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

View File

@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameter
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;
@@ -70,10 +71,11 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public P25P2ConfigurationEditor(PlaylistManager playlistManager)
public P25P2ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());

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