Compare commits

..

29 Commits

Author SHA1 Message Date
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
110 changed files with 6605 additions and 619 deletions

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

@@ -33,7 +33,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.5.0-alpha1'
version = '0.5.0-alpha3'
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.6'
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.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:13.0.2:win"
compile "org.openjfx:javafx-base:13.0.2:linux"
compile "org.openjfx:javafx-base:13.0.2:mac"
compile "org.openjfx:javafx-controls:13.0.2:win"
compile "org.openjfx:javafx-controls:13.0.2:linux"
compile "org.openjfx:javafx-controls:13.0.2:mac"
compile "org.openjfx:javafx-graphics:13.0.2:win"
compile "org.openjfx:javafx-graphics:13.0.2:linux"
compile "org.openjfx:javafx-graphics:13.0.2:mac"
compile "org.openjfx:javafx-swing:13.0.2:win"
compile "org.openjfx:javafx-swing:13.0.2:linux"
compile "org.openjfx:javafx-swing:13.0.2:mac"
}
application {
@@ -174,7 +178,8 @@ runtime {
}
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']
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")
}

View File

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

View File

@@ -36,8 +36,9 @@ 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
private static final int DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz
private final int mMaxSegmentAudioSampleLength;
private Listener<AudioSegment> mAudioSegmentListener;
protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
@@ -46,13 +47,25 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
private int mAudioSampleCount = 0;
private boolean mRecordAudioOverride;
/**
* Constructs an abstract audio module
*
* @param aliasList for aliasing identifiers
* @param maxSegmentAudioSampleLength in milliseconds
*/
public AbstractAudioModule(AliasList aliasList, long maxSegmentAudioSampleLength)
{
mAliasList = aliasList;
mMaxSegmentAudioSampleLength = (int)(maxSegmentAudioSampleLength * 8); //Convert milliseconds to samples
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
}
/**
* Constructs an abstract audio module
*/
public AbstractAudioModule(AliasList aliasList)
{
mAliasList = aliasList;
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
this(aliasList, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
}
protected abstract int getTimeslot();
@@ -68,6 +81,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
{
mAudioSegment.completeProperty().set(true);
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
mAudioSegment.decrementConsumerCount();
mAudioSegment = null;
}
}
@@ -90,6 +104,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 +132,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, long maxAudioSegmentLength)
{
super(aliasList, maxAudioSegmentLength);
}
/**
* Creates an Audio Module.
*/
@@ -116,17 +127,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,310 @@
/*
* *****************************************************************************
* 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()
{
//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
}
}
}
}
}
}
}

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

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

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

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

@@ -115,14 +115,14 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
stateMachine.setChannelType(mChannel.getChannelType());
stateMachine.setIdentifierUpdateListener(mutableIdentifierCollection);
stateMachine.setEndTimeoutBuffer(RESET_TIMEOUT_DELAY);
stateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
if(channel.getChannelType() == ChannelType.STANDARD)
{
stateMachine.setFadeTimeoutBuffer(FADE_TIMEOUT_DELAY);
stateMachine.setFadeTimeoutBufferMilliseconds(FADE_TIMEOUT_DELAY);
}
else
{
stateMachine.setFadeTimeoutBuffer(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
stateMachine.setFadeTimeoutBufferMilliseconds(DecodeConfiguration.DEFAULT_CALL_TIMEOUT_DELAY_SECONDS * 1000);
}
}
@@ -529,7 +529,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
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:

View File

@@ -40,6 +40,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;
@@ -108,14 +109,19 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mStateMachine.addListener(mSquelchController);
mStateMachine.setChannelType(mChannel.getChannelType());
mStateMachine.setIdentifierUpdateListener(mIdentifierCollection);
mStateMachine.setEndTimeoutBuffer(RESET_TIMEOUT_DELAY);
mStateMachine.setEndTimeoutBufferMilliseconds(RESET_TIMEOUT_DELAY);
if(channel.getChannelType() == ChannelType.STANDARD)
{
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);
}
}
@@ -406,7 +412,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
if(event instanceof ChangeChannelTimeoutEvent)
{
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
mStateMachine.setFadeTimeoutBuffer(timeout.getCallTimeout());
mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
}
case CONTINUATION:
case DECODE:

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

@@ -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
@@ -529,6 +568,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 +717,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

@@ -49,6 +49,8 @@ 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 javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -81,6 +83,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
private SourceManager mSourceManager;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private List<Long> mUnTunableFrequencies = new ArrayList<>();
/**
* Constructs the channel processing manager
@@ -180,7 +183,33 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
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(!mUnTunableFrequencies.contains(frequency))
{
mUnTunableFrequencies.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 && !mUnTunableFrequencies.contains(frequencies.get(0)))
{
mUnTunableFrequencies.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;

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;
@@ -205,6 +210,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);

View File

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

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

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.getEventBus().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,27 @@ 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 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

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

View File

@@ -34,6 +34,7 @@ import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
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;
@@ -66,10 +67,11 @@ public class PassportConfigurationEditor extends ChannelConfigurationEditor
/**
* Constructs an instance
* @param playlistManager
* @param userPreferences
*/
public PassportConfigurationEditor(PlaylistManager playlistManager)
public PassportConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getAuxDecoderPane());

View File

@@ -27,6 +27,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.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import javafx.scene.control.TitledPane;
@@ -45,9 +46,9 @@ public class UnknownConfigurationEditor extends ChannelConfigurationEditor
* Constructs an instance
* @param aliasModel
*/
public UnknownConfigurationEditor(PlaylistManager playlistManager)
public UnknownConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
{
super(playlistManager);
super(playlistManager, userPreferences);
getTitledPanesBox().getChildren().add(getDecoderPane());
}

View File

@@ -0,0 +1,131 @@
/*
* *****************************************************************************
* 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.radioreference;
import io.github.dsheirer.rrapi.type.CountyInfo;
import io.github.dsheirer.rrapi.type.Site;
/**
* Wrapper class to join a Site and a corresponding County Info
*/
public class EnrichedSite
{
private Site mSite;
private CountyInfo mCountyInfo;
/**
* Constructs an instance
* @param site object
* @param countyInfo optional
*/
public EnrichedSite(Site site, CountyInfo countyInfo)
{
mSite = site;
mCountyInfo = countyInfo;
}
/**
* Site instance
* @return site or null
*/
public Site getSite()
{
return mSite;
}
/**
* Sets the site instance
* @param site or null
*/
public void setSite(Site site)
{
mSite = site;
}
/**
* County information
* @return county info or null
*/
public CountyInfo getCountyInfo()
{
return mCountyInfo;
}
/**
* Sets the county info
* @param countyInfo or null
*/
public void setCountyInfo(CountyInfo countyInfo)
{
mCountyInfo = countyInfo;
}
/**
* Optional site number
*/
public Integer getSiteNumber()
{
if(mSite != null)
{
return mSite.getSiteNumber();
}
return null;
}
/**
* Optional site RFSS value
*/
public Integer getRfss()
{
if(mSite != null)
{
return mSite.getRfss();
}
return null;
}
/**
* Optional county name
*/
public String getCountyName()
{
if(mCountyInfo != null)
{
return mCountyInfo.getName();
}
return null;
}
/**
* Optional description of the site
*/
public String getDescription()
{
if(mSite != null)
{
return mSite.getDescription();
}
return null;
}
}

View File

@@ -204,7 +204,7 @@ public class RadioReferenceDecoder
{
List<Tag> tags = new ArrayList<>();
if(talkgroup.getTags() != null)
if(talkgroup != null && talkgroup.getTags() != null)
{
for(Tag tag: talkgroup.getTags())
{
@@ -220,7 +220,12 @@ public class RadioReferenceDecoder
*/
public Type getType(System system)
{
return mTypeMap.get(system.getTypeId());
if(system != null)
{
return mTypeMap.get(system.getTypeId());
}
return null;
}
/**
@@ -228,7 +233,12 @@ public class RadioReferenceDecoder
*/
public Flavor getFlavor(System system)
{
return mFlavorMap.get(system.getFlavorId());
if(system != null)
{
return mFlavorMap.get(system.getFlavorId());
}
return null;
}
/**
@@ -236,7 +246,12 @@ public class RadioReferenceDecoder
*/
public Voice getVoice(System system)
{
return mVoiceMap.get(system.getVoiceId());
if(system != null)
{
return mVoiceMap.get(system.getVoiceId());
}
return null;
}
/**
@@ -272,7 +287,19 @@ public class RadioReferenceDecoder
*/
public boolean isLSM(Site site)
{
return site != null && site.getModulation() != null && site.getModulation().contentEquals("LSM");
if(site != null)
{
if(site.getModulation() != null && site.getModulation().contentEquals("LSM"))
{
return true;
}
else if(site.getDescription() != null && site.getDescription().contains("Simulcast"))
{
return true;
}
}
return false;
}
/**
@@ -340,51 +367,54 @@ public class RadioReferenceDecoder
Flavor flavor = getFlavor(system);
Voice voice = getVoice(system);
switch(type.getName())
if(type != null && flavor != null && voice != null)
{
case "LTR":
if(flavor.getName().contentEquals("Net"))
{
return DecoderType.LTR_NET;
}
else if(flavor.getName().contentEquals("Passport"))
{
return DecoderType.PASSPORT;
}
else
{
return DecoderType.LTR;
}
case "MPT-1327":
return DecoderType.MPT1327;
case "Project 25":
if(flavor.getName().contentEquals("Phase II"))
{
return DecoderType.P25_PHASE2;
}
else if(flavor.getName().contentEquals("Phase I"))
{
return DecoderType.P25_PHASE1;
}
break;
case "Motorola":
if(voice.getName().contentEquals("Analog and APCO-25 Common Air Interface") ||
voice.getName().contentEquals("APCO-25 Common Air Interface Exclusive"))
{
return DecoderType.P25_PHASE1;
}
break;
case "DMR":
case "NXDN":
switch(type.getName())
{
case "LTR":
if(flavor.getName().contentEquals("Net"))
{
return DecoderType.LTR_NET;
}
else if(flavor.getName().contentEquals("Passport"))
{
return DecoderType.PASSPORT;
}
else
{
return DecoderType.LTR;
}
case "MPT-1327":
return DecoderType.MPT1327;
case "Project 25":
if(flavor.getName().contentEquals("Phase II"))
{
return DecoderType.P25_PHASE2;
}
else if(flavor.getName().contentEquals("Phase I"))
{
return DecoderType.P25_PHASE1;
}
break;
case "Motorola":
if(voice.getName().contentEquals("Analog and APCO-25 Common Air Interface") ||
voice.getName().contentEquals("APCO-25 Common Air Interface Exclusive"))
{
return DecoderType.P25_PHASE1;
}
break;
case "DMR":
case "NXDN":
case "EDACS":
case "TETRA":
case "Midland CMS":
case "OpenSky":
case "iDEN":
case "SmarTrunk":
case "Other":
default:
case "EDACS":
case "TETRA":
case "Midland CMS":
case "OpenSky":
case "iDEN":
case "SmarTrunk":
case "Other":
default:
}
}
return null;

View File

@@ -30,10 +30,13 @@ public class RadioReferenceUnavailableAlert extends Alert
{
public RadioReferenceUnavailableAlert(Node ownerNode)
{
super(AlertType.INFORMATION, "Check network connection or see details in sdrtrunk log.", ButtonType.OK);
super(AlertType.INFORMATION, "This may be caused by a network outage or there was an error " +
"accessing the service. If this problem occurs each time you attempt to access a " +
"specific system or agency, please notify the developer. Details for this issue " +
"are located in the sdrtrunk application log.", ButtonType.OK);
setTitle("Service Unavailable");
setHeaderText("Radio Reference Is Currently Unavailable");
setHeaderText("Radio Reference Error or Service Is Currently Unavailable");
initOwner(ownerNode.getScene().getWindow());
}
}

View File

@@ -108,7 +108,7 @@ public class SiteEditor extends GridPane
private CheckBox mGoToChannelEditorCheckBox;
private Label mProtocolNotSupportedLabel;
private RadioReferenceDecoder mRadioReferenceDecoder;
private Site mCurrentSite;
private EnrichedSite mCurrentSite;
private System mCurrentSystem;
private SystemInformation mCurrentSystemInformation;
private ComboBox mAliasListNameComboBox;
@@ -290,7 +290,7 @@ public class SiteEditor extends GridPane
}
}
public void setSite(Site site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
{
mCurrentSite = site;
mCurrentSystem = system;
@@ -299,7 +299,7 @@ public class SiteEditor extends GridPane
getSiteFrequencyTableView().getItems().clear();
boolean disable = site == null || site.getSiteFrequencies().isEmpty();
boolean disable = site == null || site.getSite().getSiteFrequencies().isEmpty();
boolean supported = decoder.hasSupportedProtocol(system);
getFrequenciesSegmentedButton().setDisable(disable || !supported);
@@ -318,7 +318,7 @@ public class SiteEditor extends GridPane
if(site != null)
{
List<SiteFrequency> siteFrequencies = site.getSiteFrequencies();
List<SiteFrequency> siteFrequencies = site.getSite().getSiteFrequencies();
for(SiteFrequency siteFrequency: siteFrequencies)
{
if(siteFrequency.getFrequency() > 0.01)
@@ -340,7 +340,7 @@ public class SiteEditor extends GridPane
getCreateChannelConfigurationButton().setVisible(true);
getGoToChannelEditorCheckBox().setVisible(true);
getSystemTextField().setText(system.getName());
getSiteTextField().setText(site.getCountyName());
getSiteTextField().setText(mCurrentSite.getCountyName());
getNameTextField().setText("Control");
if(!siteFrequencies.isEmpty())
@@ -456,7 +456,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
channel.setName("LCN " + siteFrequency.getLogicalChannelNumber());
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency(getFrequency(siteFrequency));
channel.setSourceConfiguration(sourceConfigTuner);
@@ -478,20 +479,19 @@ public class SiteEditor extends GridPane
private void createControlChannel()
{
SiteFrequency control = null;
List<SiteFrequency> controls = new ArrayList<>();
for(SiteFrequency siteFrequency: getSiteFrequencyTableView().getItems())
{
if(isControl(siteFrequency) && siteFrequency.getFrequency() > 0.01)
{
control = siteFrequency;
continue;
controls.add(siteFrequency);
}
}
if(control == null)
if(controls.isEmpty())
{
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control Channel", ButtonType.OK);
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control Channel(s)", ButtonType.OK);
alert.setTitle("Create Channel Configuration");
alert.setHeaderText("Can't Create Channel Configuration");
alert.initOwner((getCreateChannelConfigurationButton()).getScene().getWindow());
@@ -511,11 +511,26 @@ public class SiteEditor extends GridPane
decoderType = DecoderType.P25_PHASE1;
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(), mCurrentSystemInformation));
if(controls.size() == 1)
{
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency((long)(controls.get(0).getFrequency() * 1E6));
channel.setSourceConfiguration(sourceConfigTuner);
}
else
{
SourceConfigTunerMultipleFrequency sourceConfig = new SourceConfigTunerMultipleFrequency();
for(SiteFrequency control: controls)
{
sourceConfig.addFrequency((long)(control.getFrequency() * 1E6));
}
channel.setSourceConfiguration(sourceConfig);
}
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency((long)(control.getFrequency() * 1E6));
channel.setSourceConfiguration(sourceConfigTuner);
mPlaylistManager.getChannelModel().addChannel(channel);
if(getGoToChannelEditorCheckBox().isSelected())
@@ -547,7 +562,7 @@ public class SiteEditor extends GridPane
if(siteFrequencies.isEmpty())
{
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control or Alternate Channels", ButtonType.OK);
Alert alert = new Alert(Alert.AlertType.ERROR, "Site Has No Control or Alternate Channel(s)", ButtonType.OK);
alert.setTitle("Create Channel Configuration");
alert.setHeaderText("Can't Create Channel Configuration");
alert.initOwner((getCreateChannelConfigurationButton()).getScene().getWindow());
@@ -569,7 +584,8 @@ public class SiteEditor extends GridPane
decoderType = DecoderType.P25_PHASE1;
}
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
if(siteFrequencies.size() == 1)
{
@@ -605,7 +621,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
channel.setName("LCN " + siteFrequency.getLogicalChannelNumber());
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency(getFrequency(siteFrequency));
channel.setSourceConfiguration(sourceConfigTuner);
@@ -659,7 +676,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
if(siteFrequencies.size() == 1)
{
@@ -695,7 +713,8 @@ public class SiteEditor extends GridPane
Channel channel = getChannelTemplate();
channel.setName("LCN " + siteFrequency.getLogicalChannelNumber());
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite, mCurrentSystemInformation));
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
mCurrentSystemInformation));
SourceConfigTuner sourceConfigTuner = new SourceConfigTuner();
sourceConfigTuner.setFrequency(getFrequency(siteFrequency));
@@ -946,7 +965,6 @@ public class SiteEditor extends GridPane
{
mSiteFrequencyTableView = new TableView<>();
mSiteFrequencyTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
mSiteFrequencyTableView.setMaxHeight(Double.MAX_VALUE);
mSiteFrequencyTableView.setPlaceholder(new Label("Please select a site to view frequencies"));
mTypeColumn = new TableColumn("Type");

View File

@@ -54,6 +54,7 @@ import jiconfont.javafx.IconNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -281,13 +282,14 @@ public class SystemEditor extends VBox
List<Site> sites = mRadioReference.getService().getSites(system.getSystemId());
//The service api doesn't provide the county name, so we run a separate query to update each value
List<EnrichedSite> enrichedSites = new ArrayList<>();
for(Site site: sites)
{
CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
site.setCountyName(countyInfo.getName());
enrichedSites.add(new EnrichedSite(site, countyInfo));
}
Platform.runLater(() -> getSystemSiteSelectionEditor().setSystem(system, sites, mRadioReferenceDecoder,
Platform.runLater(() -> getSystemSiteSelectionEditor().setSystem(system, enrichedSites, mRadioReferenceDecoder,
systemInformation));
//Query and load the talkgroup view second

View File

@@ -25,7 +25,6 @@ package io.github.dsheirer.gui.playlist.radioreference;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.rrapi.type.Flavor;
import io.github.dsheirer.rrapi.type.Site;
import io.github.dsheirer.rrapi.type.System;
import io.github.dsheirer.rrapi.type.SystemInformation;
import io.github.dsheirer.rrapi.type.Type;
@@ -60,7 +59,7 @@ public class SystemSiteSelectionEditor extends GridPane
private Label mProtocolLabel;
private Label mFlavorLabel;
private Label mVoiceLabel;
private TableView<Site> mSiteTableView;
private TableView<EnrichedSite> mSiteTableView;
private SiteEditor mSiteEditor;
private RadioReferenceDecoder mRadioReferenceDecoder;
private System mCurrentSystem;
@@ -113,7 +112,8 @@ public class SystemSiteSelectionEditor extends GridPane
* Updates this view with the specified system
* @param system to view
*/
public void setSystem(System system, List<Site> sites, RadioReferenceDecoder decoder, SystemInformation systemInformation)
public void setSystem(System system, List<EnrichedSite> sites, RadioReferenceDecoder decoder,
SystemInformation systemInformation)
{
mCurrentSystem = system;
mCurrentSystemInformation = systemInformation;
@@ -205,13 +205,12 @@ public class SystemSiteSelectionEditor extends GridPane
return mPlaceholderLabel;
}
private TableView<Site> getSiteTableView()
private TableView<EnrichedSite> getSiteTableView()
{
if(mSiteTableView == null)
{
mSiteTableView = new TableView<>();
mSiteTableView.setPlaceholder(getPlaceholderLabel());
mSiteTableView.setPrefHeight(125);
TableColumn numberColumn = new TableColumn();
numberColumn.setText("Site");
@@ -236,8 +235,11 @@ public class SystemSiteSelectionEditor extends GridPane
mSiteTableView.getColumns().addAll(numberColumn, rfssColumn, countyColumn, descriptionColumn);
mSiteTableView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
mSiteTableView.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, selected) -> getSiteEditor()
.setSite(selected, mCurrentSystem, mCurrentSystemInformation, mRadioReferenceDecoder));
.addListener((observable, oldValue, selected) ->
{
getSiteEditor().setSite(selected, mCurrentSystem, mCurrentSystemInformation,
mRadioReferenceDecoder);
});
}
return mSiteTableView;

View File

@@ -21,6 +21,7 @@ package io.github.dsheirer.gui.preference;
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
import io.github.dsheirer.gui.preference.duplicate.DuplicateCallPreferenceEditor;
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
@@ -37,6 +38,8 @@ public class PreferenceEditorFactory
{
switch(preferenceEditorType)
{
case AUDIO_DUPLICATE_CALL_DETECTION:
return new DuplicateCallPreferenceEditor(userPreferences);
case AUDIO_OUTPUT:
return new PlaybackPreferenceEditor(userPreferences);
case AUDIO_RECORD:

View File

@@ -29,6 +29,7 @@ public enum PreferenceEditorType
JMBE_LIBRARY("JMBE Audio Library"),
AUDIO_RECORD("Record"),
AUDIO_OUTPUT("Output/Tones"),
AUDIO_DUPLICATE_CALL_DETECTION("Duplicate Calls"),
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
TALKGROUP_FORMAT("Talkgroup & Radio ID"),

View File

@@ -167,6 +167,7 @@ public class UserPreferencesEditor extends BorderPane
TreeItem<String> treeRoot = new TreeItem<>("Root node");
TreeItem<String> audioItem = new TreeItem<>("Audio");
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_DUPLICATE_CALL_DETECTION));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_OUTPUT));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
treeRoot.getChildren().add(audioItem);

View File

@@ -22,50 +22,99 @@ package io.github.dsheirer.gui.preference.decoder;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.jmbe.JmbeCreator;
import io.github.dsheirer.jmbe.JmbeEditorRequest;
import io.github.dsheirer.jmbe.github.GitHub;
import io.github.dsheirer.jmbe.github.Release;
import io.github.dsheirer.jmbe.github.Version;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
import io.github.dsheirer.util.ThreadPool;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
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.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Preference settings for decoders
*/
public class JmbeLibraryPreferenceEditor extends HBox
public class JmbeLibraryPreferenceEditor extends VBox
{
private final static Logger mLog = LoggerFactory.getLogger(JmbeLibraryPreferenceEditor.class);
private static final String PATH_NOT_SET = "(not set)";
private JmbeLibraryPreference mJmbeLibraryPreference;
private static final String CREATE_LIBRARY = "Create Library";
private static final String CHECK_FOR_UPDATE = "Check For Library Update";
private static final String LIBRARY_NOT_SETUP = "None. Click Select or Create Library button to setup.";
private UserPreferences mUserPreferences;
private GridPane mEditorPane;
private Label mJmbeLibraryLabel;
private Button mPathToJmbeLibraryButton;
private Button mPathToJmbeLibraryResetButton;
private Label mJmbeVersionLabel;
private Label mPathToJmbeLibraryLabel;
private Button mSelectButton;
private Button mResetButton;
private Button mCreateButton;
private HBox mButtonsBox;
private CheckBox mAlertUserWhenMissingCheckBox;
public JmbeLibraryPreferenceEditor(UserPreferences userPreferences)
{
mJmbeLibraryPreference = userPreferences.getJmbeLibraryPreference();
mUserPreferences = userPreferences;
//Register to receive directory preference update notifications so we can update the path labels
MyEventBus.getEventBus().register(this);
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
getChildren().add(getEditorPane());
setPadding(new Insets(10,10,10,10));
setSpacing(10);
getChildren().addAll(getEditorPane(), getButtonsBox(), getAlertUserWhenMissingCheckBox());
}
private CheckBox getAlertUserWhenMissingCheckBox()
{
if(mAlertUserWhenMissingCheckBox == null)
{
mAlertUserWhenMissingCheckBox = new CheckBox("Alert when decoder requires missing JMBE library");
mAlertUserWhenMissingCheckBox.setSelected(mUserPreferences.getJmbeLibraryPreference()
.getAlertIfMissingLibraryRequired());
mAlertUserWhenMissingCheckBox.setOnAction(event -> {
boolean alert = mAlertUserWhenMissingCheckBox.isSelected();
mUserPreferences.getJmbeLibraryPreference().setAlertIfMissingLibraryRequired(alert);
});
}
return mAlertUserWhenMissingCheckBox;
}
private HBox getButtonsBox()
{
if(mButtonsBox == null)
{
mButtonsBox = new HBox();
mButtonsBox.setSpacing(10);
mButtonsBox.getChildren().addAll(getCreateButton(), getSelectButton(), getResetButton());
}
return mButtonsBox;
}
private GridPane getEditorPane()
@@ -73,85 +122,213 @@ public class JmbeLibraryPreferenceEditor extends HBox
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
mEditorPane.setVgap(10);
mEditorPane.setHgap(10);
int row = 0;
GridPane.setMargin(getJmbeLibraryLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getJmbeLibraryLabel(), 0, row);
mEditorPane.add(getJmbeLibraryLabel(), 0, row, 2, 1);
Label versionLabel = new Label("Current Version:");
GridPane.setHalignment(versionLabel, HPos.RIGHT);
mEditorPane.add(versionLabel, 0, ++row);
mEditorPane.add(getJmbeVersionLabel(), 1, row);
Label fileLabel = new Label("File:");
GridPane.setHalignment(fileLabel, HPos.RIGHT);
mEditorPane.add(fileLabel, 0, ++row);
GridPane.setMargin(getPathToJmbeLibraryLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getPathToJmbeLibraryLabel(), 1, row);
GridPane.setMargin(getPathToJmbeLibraryButton(), new Insets(2, 10, 2, 0));
mEditorPane.add(getPathToJmbeLibraryButton(), 2, row);
GridPane.setMargin(getPathToJmbeLibraryResetButton(), new Insets(2, 0, 2, 0));
mEditorPane.add(getPathToJmbeLibraryResetButton(), 3, row++);
}
return mEditorPane;
}
private Button getCreateButton()
{
if(mCreateButton == null)
{
mCreateButton = new Button();
mCreateButton.setText(mUserPreferences.getJmbeLibraryPreference()
.getPathJmbeLibrary() != null ? CHECK_FOR_UPDATE : CREATE_LIBRARY);
mCreateButton.setOnAction(event -> checkForUpdatedLibrary());
}
return mCreateButton;
}
/**
* Checks for availability of an updated JMBE library
*/
private void checkForUpdatedLibrary()
{
ThreadPool.SCHEDULED.execute(() -> {
try
{
Version current = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
final Release release = GitHub.getLatestRelease(JmbeCreator.GITHUB_JMBE_RELEASES_URL);
mLog.info("Checking for JMBE Library Updates ...");
mLog.info("Current: " + (current != null ? current.toString() : "empty"));
final boolean canUpdate = (release != null) && ((current == null) ||
(release.getVersion().compareTo(current) > 0));
if(release != null)
{
mLog.info("Available: " + release.getVersion().toString());
}
if(canUpdate)
{
mLog.info("JMBE Library update is available");
}
else
{
mLog.info("No JMBE library update is available at this time");
}
Platform.runLater(new Runnable()
{
@Override
public void run()
{
try
{
if(canUpdate)
{
String content = "JMBE library version " + release.getVersion().toString() +
" is available. Would you like to download the latest source code and " +
"create an updated JMBE library?";
Alert alert = new Alert(Alert.AlertType.INFORMATION, content, ButtonType.YES, ButtonType.NO);
alert.setResizable(true);
//Workaroud for dialog sizing issue on Windows
alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
alert.setTitle("JMBE Library Update Check");
alert.setHeaderText("Update is available");
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait().ifPresent(buttonType -> {
if(buttonType == ButtonType.YES)
{
mLog.info("Posting JMBE editor request to event bus");
MyEventBus.getEventBus().post(new JmbeEditorRequest(release));
}
});
}
else
{
Alert alert = new Alert(Alert.AlertType.INFORMATION,
"No JMBE library update is available.", ButtonType.OK);
alert.setTitle("JMBE Library Update Check");
if(release != null)
{
alert.setHeaderText("No update available");
}
else
{
alert.setHeaderText("Unable to determine latest release version. Check network connection");
}
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait();
}
}
catch(Throwable t)
{
mLog.error("Error during JavaFX portion of create library");
}
}
});
}
catch(Throwable t)
{
mLog.error("Error during create library", t);
}
});
}
private Label getJmbeLibraryLabel()
{
if(mJmbeLibraryLabel == null)
{
mJmbeLibraryLabel = new Label("JMBE Audio Library:");
mJmbeLibraryLabel = new Label("JMBE Audio Library");
}
return mJmbeLibraryLabel;
}
private Button getPathToJmbeLibraryButton()
private Label getJmbeVersionLabel()
{
if(mPathToJmbeLibraryButton == null)
if(mJmbeVersionLabel == null)
{
mPathToJmbeLibraryButton = new Button("Change...");
mPathToJmbeLibraryButton.setOnAction(new EventHandler<ActionEvent>()
mJmbeVersionLabel = new Label();
Version version = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
if(version != null)
{
mJmbeVersionLabel.setText(version.toString());
}
else
{
mJmbeVersionLabel.setText(LIBRARY_NOT_SETUP);
}
}
return mJmbeVersionLabel;
}
private Button getSelectButton()
{
if(mSelectButton == null)
{
mSelectButton = new Button("Select...");
mSelectButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Select JMBE Audio Library Location");
Stage stage = (Stage)getPathToJmbeLibraryButton().getScene().getWindow();
Path path = mUserPreferences.getDirectoryPreference().getDefaultJmbeDirectory();
if(Files.exists(path))
{
fileChooser.setInitialDirectory(path.toFile());
}
Stage stage = (Stage)getSelectButton().getScene().getWindow();
File selected = fileChooser.showOpenDialog(stage);
if(selected != null)
{
mJmbeLibraryPreference.setPathJmbeLibrary(selected.toPath());
mUserPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(selected.toPath());
}
}
});
}
return mPathToJmbeLibraryButton;
return mSelectButton;
}
private Button getPathToJmbeLibraryResetButton()
private Button getResetButton()
{
if(mPathToJmbeLibraryResetButton == null)
if(mResetButton == null)
{
mPathToJmbeLibraryResetButton = new Button("Reset");
mPathToJmbeLibraryResetButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
mJmbeLibraryPreference.resetPathJmbeLibrary();
}
});
mResetButton = new Button("Reset");
mResetButton.setOnAction(event -> mUserPreferences.getJmbeLibraryPreference().resetPathJmbeLibrary());
}
return mPathToJmbeLibraryResetButton;
return mResetButton;
}
private Label getPathToJmbeLibraryLabel()
{
if(mPathToJmbeLibraryLabel == null)
{
Path path = mJmbeLibraryPreference.getPathJmbeLibrary();
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
mPathToJmbeLibraryLabel = new Label(path != null ? path.toString() : PATH_NOT_SET);
}
@@ -163,8 +340,14 @@ public class JmbeLibraryPreferenceEditor extends HBox
{
if(preferenceType != null && preferenceType == PreferenceType.JMBE_LIBRARY)
{
Path path = mJmbeLibraryPreference.getPathJmbeLibrary();
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
getPathToJmbeLibraryLabel().setText(path != null ? path.toString() : PATH_NOT_SET);
Version version = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
getJmbeVersionLabel().setText(version != null ? version.toString() : LIBRARY_NOT_SETUP);
getCreateButton().setText(mUserPreferences.getJmbeLibraryPreference()
.getPathJmbeLibrary() != null ? CHECK_FOR_UPDATE : CREATE_LIBRARY);
getAlertUserWhenMissingCheckBox().setSelected(mUserPreferences.getJmbeLibraryPreference()
.getAlertIfMissingLibraryRequired());
}
}
}

View File

@@ -68,6 +68,11 @@ public class DirectoryPreferenceEditor extends HBox
private Button mResetEventLogsButton;
private Label mEventLogsPathLabel;
private Label mJmbeLabel;
private Button mChangeJmbeButton;
private Button mResetJmbeButton;
private Label mJmbePathLabel;
private Label mPlaylistLabel;
private Button mChangePlaylistButton;
private Button mResetPlaylistButton;
@@ -157,6 +162,19 @@ public class DirectoryPreferenceEditor extends HBox
mEditorPane.add(getResetEventLogsButton(), 3, row++);
GridPane.setMargin(getJmbeLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getJmbeLabel(), 0, row);
GridPane.setMargin(getJmbePathLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getJmbePathLabel(), 1, row);
GridPane.setMargin(getChangeJmbeButton(), new Insets(2, 10, 2, 0));
mEditorPane.add(getChangeJmbeButton(), 2, row);
GridPane.setMargin(getResetJmbeButton(), new Insets(2, 0, 2, 0));
mEditorPane.add(getResetJmbeButton(), 3, row++);
GridPane.setMargin(getPlaylistLabel(), new Insets(0, 10, 0, 0));
mEditorPane.add(getPlaylistLabel(), 0, row);
@@ -407,6 +425,71 @@ public class DirectoryPreferenceEditor extends HBox
return mEventLogsPathLabel;
}
private Label getJmbeLabel()
{
if(mJmbeLabel == null)
{
mJmbeLabel = new Label("JMBE Libraries");
}
return mJmbeLabel;
}
private Button getChangeJmbeButton()
{
if(mChangeJmbeButton == null)
{
mChangeJmbeButton = new Button("Change...");
mChangeJmbeButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("Select JMBE Folder");
directoryChooser.setInitialDirectory(mDirectoryPreference.getDirectoryJmbe().toFile());
Stage stage = (Stage)getChangeJmbeButton().getScene().getWindow();
File selected = directoryChooser.showDialog(stage);
if(selected != null)
{
mDirectoryPreference.setDirectoryJmbe(selected.toPath());
}
}
});
}
return mChangeJmbeButton;
}
private Button getResetJmbeButton()
{
if(mResetJmbeButton == null)
{
mResetJmbeButton = new Button("Reset");
mResetJmbeButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
mDirectoryPreference.resetDirectoryJmbe();
}
});
}
return mResetJmbeButton;
}
private Label getJmbePathLabel()
{
if(mJmbePathLabel == null)
{
mJmbePathLabel = new Label(mDirectoryPreference.getDirectoryJmbe().toString());
}
return mJmbePathLabel;
}
private Label getPlaylistLabel()
{
if(mPlaylistLabel == null)

View File

@@ -0,0 +1,215 @@
/*
* *****************************************************************************
* 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.preference.duplicate;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Preference settings for duplicate call audio handling
*/
public class DuplicateCallPreferenceEditor extends HBox
{
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallPreferenceEditor.class);
private DuplicateCallDetectionPreference mPreference;
private GridPane mEditorPane;
private ToggleSwitch mDetectDuplicateTalkgroups;
private ToggleSwitch mDetectDuplicateRadios;
private ToggleSwitch mSuppressDuplicateListening;
private ToggleSwitch mSuppressDuplicateRecording;
private ToggleSwitch mSuppressDuplicateStreaming;
/**
* Constructs an instance
*/
public DuplicateCallPreferenceEditor(UserPreferences userPreferences)
{
mPreference = userPreferences.getDuplicateCallDetectionPreference();
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
getChildren().add(getEditorPane());
}
private GridPane getEditorPane()
{
if(mEditorPane == null)
{
int row = 0;
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
mEditorPane.setHgap(10);
mEditorPane.setVgap(10);
Label detectionLabel = new Label("Duplicate Call Detection. Detect duplicate" +
" calls across channels that share a common System name in each channel configuration.");
detectionLabel.setWrapText(true);
GridPane.setConstraints(detectionLabel, 0, row, 2, 1);
mEditorPane.getChildren().add(detectionLabel);
GridPane.setConstraints(getDetectDuplicateTalkgroups(), 0, ++row);
mEditorPane.getChildren().add(getDetectDuplicateTalkgroups());
Label talkgroupLabel = new Label("Talkgroup. Detect duplicate calls by matching talkgroup or " +
"patchgroup values");
talkgroupLabel.setWrapText(true);
GridPane.setConstraints(talkgroupLabel, 1, row);
mEditorPane.getChildren().add(talkgroupLabel);
GridPane.setConstraints(getDetectDuplicateRadios(), 0, ++row);
mEditorPane.getChildren().add(getDetectDuplicateRadios());
Label radioLabel = new Label("Radio ID. Detect duplicate calls by matching radio identifiers.");
radioLabel.setWrapText(true);
GridPane.setConstraints(radioLabel, 1, row);
mEditorPane.getChildren().add(radioLabel);
Label warningLabel = new Label("Note: be careful when enabling duplicate call detection by Radio ID " +
"because this can produce unintended side-effects. For example, if you have two talkgroups with " +
"talkgroup 1 set to record and talkgroup 2 set to stream and dispatch radio ID 1234 makes a " +
"simultaneous call to both talkgroup 1 and talkgroup 2, there is no way to control which call audio " +
"(talkgroup 1 or 2) gets flagged as the duplicate call and therefore either the audio for talkgroup 1 " +
"doesn't record, or the audio for talkgroup 2 doesn't stream.");
warningLabel.setWrapText(true);
GridPane.setConstraints(warningLabel, 0, ++row, 2, 1);
mEditorPane.getChildren().add(warningLabel);
Separator separator = new Separator();
GridPane.setHgrow(separator, Priority.ALWAYS);
GridPane.setConstraints(separator, 0, ++row, 2, 1);
mEditorPane.getChildren().add(separator);
Label suppressionLabel = new Label("Duplicate Call Suppression. When duplicate call audio is detected, " +
"suppress the audio during:");
suppressionLabel.setWrapText(true);
GridPane.setConstraints(suppressionLabel, 0, ++row, 2, 1);
mEditorPane.getChildren().add(suppressionLabel);
GridPane.setConstraints(getSuppressDuplicateListening(), 0, ++row);
mEditorPane.getChildren().add(getSuppressDuplicateListening());
Label listeningLabel = new Label("Listening");
listeningLabel.setWrapText(true);
GridPane.setConstraints(listeningLabel, 1, row);
mEditorPane.getChildren().add(listeningLabel);
GridPane.setConstraints(getSuppressDuplicateRecording(), 0, ++row);
mEditorPane.getChildren().add(getSuppressDuplicateRecording());
Label recordingLabel = new Label("Recording");
recordingLabel.setWrapText(true);
GridPane.setConstraints(recordingLabel, 1, row);
mEditorPane.getChildren().add(recordingLabel);
GridPane.setConstraints(getSuppressDuplicateStreaming(), 0, ++row);
mEditorPane.getChildren().add(getSuppressDuplicateStreaming());
Label streamingLabel = new Label("Streaming");
streamingLabel.setWrapText(true);
GridPane.setConstraints(streamingLabel, 1, row);
mEditorPane.getChildren().add(streamingLabel);
}
return mEditorPane;
}
private ToggleSwitch getDetectDuplicateTalkgroups()
{
if(mDetectDuplicateTalkgroups == null)
{
mDetectDuplicateTalkgroups = new ToggleSwitch();
mDetectDuplicateTalkgroups.setSelected(mPreference.isDuplicateCallDetectionByTalkgroupEnabled());
mDetectDuplicateTalkgroups.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateCallDetectionByTalkgroupEnabled(newValue));
}
return mDetectDuplicateTalkgroups;
}
private ToggleSwitch getDetectDuplicateRadios()
{
if(mDetectDuplicateRadios == null)
{
mDetectDuplicateRadios = new ToggleSwitch();
mDetectDuplicateRadios.setSelected(mPreference.isDuplicateCallDetectionByRadioEnabled());
mDetectDuplicateRadios.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateCallDetectionByRadioEnabled(newValue));
}
return mDetectDuplicateRadios;
}
private ToggleSwitch getSuppressDuplicateListening()
{
if(mSuppressDuplicateListening == null)
{
mSuppressDuplicateListening = new ToggleSwitch();
mSuppressDuplicateListening.disableProperty()
.bind(Bindings.and(getDetectDuplicateTalkgroups().selectedProperty().not(),
getDetectDuplicateRadios().selectedProperty().not()));
mSuppressDuplicateListening.setSelected(mPreference.isDuplicatePlaybackSuppressionEnabled());
mSuppressDuplicateListening.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicatePlaybackSuppressionEnabled(newValue));
}
return mSuppressDuplicateListening;
}
private ToggleSwitch getSuppressDuplicateRecording()
{
if(mSuppressDuplicateRecording == null)
{
mSuppressDuplicateRecording = new ToggleSwitch();
mSuppressDuplicateRecording.disableProperty()
.bind(Bindings.and(getDetectDuplicateTalkgroups().selectedProperty().not(),
getDetectDuplicateRadios().selectedProperty().not()));
mSuppressDuplicateRecording.setSelected(mPreference.isDuplicateRecordingSuppressionEnabled());
mSuppressDuplicateRecording.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateRecordingSuppressionEnabled(newValue));
}
return mSuppressDuplicateRecording;
}
private ToggleSwitch getSuppressDuplicateStreaming()
{
if(mSuppressDuplicateStreaming == null)
{
mSuppressDuplicateStreaming = new ToggleSwitch();
mSuppressDuplicateStreaming.disableProperty()
.bind(Bindings.and(getDetectDuplicateTalkgroups().selectedProperty().not(),
getDetectDuplicateRadios().selectedProperty().not()));
mSuppressDuplicateStreaming.setSelected(mPreference.isDuplicateStreamingSuppressionEnabled());
mSuppressDuplicateStreaming.selectedProperty()
.addListener((observable, oldValue, newValue) -> mPreference.setDuplicateStreamingSuppressionEnabled(newValue));
}
return mSuppressDuplicateStreaming;
}
}

View File

@@ -0,0 +1,323 @@
/*
* *****************************************************************************
* 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.jmbe;
import io.github.dsheirer.jmbe.github.Asset;
import io.github.dsheirer.jmbe.github.GitHub;
import io.github.dsheirer.jmbe.github.Release;
import io.github.dsheirer.util.FileUtil;
import io.github.dsheirer.util.OSType;
import io.github.dsheirer.util.ThreadPool;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.apache.commons.io.FileUtils;
import org.rauschig.jarchivelib.ArchiveFormat;
import org.rauschig.jarchivelib.Archiver;
import org.rauschig.jarchivelib.ArchiverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Creates the JMBE library by downloading the JMBE Creator CLI application from GitHub and executing it.
*/
public class JmbeCreator
{
private final static Logger mLog = LoggerFactory.getLogger(JmbeCreator.class);
public static final String GITHUB_JMBE_RELEASES_URL = "https://api.github.com/repos/dsheirer/jmbe/releases";
public static final String CREATOR_SCRIPT_LINUX = "creator";
public static final String CREATOR_SCRIPT_WINDOWS = "creator.bat";
private StringProperty mConsoleOutput = new SimpleStringProperty();
private BooleanProperty mCompleteProperty = new SimpleBooleanProperty();
private boolean mHasErrors = false;
private Release mRelease;
private Path mLibraryPath;
private StringBuilder mConsoleStringBuilder = new StringBuilder();
/**
* Creates an instance.
* @param release for the version to create
* @param library path where the library should be created
*/
public JmbeCreator(Release release, Path library)
{
mRelease = release;
mLibraryPath = library;
}
/**
* Path to where the library should be created
*/
public Path getLibraryPath()
{
return mLibraryPath;
}
/**
* Property that indicates if the creation process has finished.
*/
public BooleanProperty completeProperty()
{
return mCompleteProperty;
}
/**
* Console output from the build process.
*/
public StringProperty consoleOutputProperty()
{
return mConsoleOutput;
}
/**
* Prints (adds) the message text to the console property
* @param message to add
*/
private void printToConsole(String message)
{
mConsoleStringBuilder.append(message).append("\n");
final String console = mConsoleStringBuilder.toString();
Platform.runLater(() -> consoleOutputProperty().setValue(console));
}
/**
* Indicates if the build process completed successfully (false) or there were errors (true).
*/
public boolean hasErrors()
{
return mHasErrors;
}
/**
* Executes the build process that downloads the JMBE creator utility, unzips it, and commands the utility
* to create the library at the specified library path.
*/
public void execute()
{
final Asset asset = getJMBECreatorAsset(mRelease);
if(asset != null)
{
ThreadPool.SCHEDULED.execute(() -> {
Path tempDirectory = null;
try
{
tempDirectory = Files.createTempDirectory("sdrtrunk-jmbe-creator");
printToConsole("Created: Temp Directory [" + tempDirectory.toString() + "]");
printToConsole("Downloading: JMBE Creator [" + asset.toString() + "]");
printToConsole("Please wait ...");
Path creator = GitHub.downloadArtifact(asset.getDownloadUrl(), tempDirectory);
if(creator != null)
{
printToConsole("Downloaded: JMBE Creator [" + creator.toString() + "]");
Path unzipped = creator.getParent();
Archiver archiver = ArchiverFactory.createArchiver(ArchiveFormat.ZIP);
archiver.extract(creator.toFile(), unzipped.toFile());
printToConsole("Unzipped: [" + unzipped.toString() + "]");
Path script = null;
OSType osType = OSType.getCurrentOSType();
if(osType.isLinux())
{
script = FileUtil.findFile(unzipped, CREATOR_SCRIPT_LINUX);
}
else if(osType.isWindows())
{
script = FileUtil.findFile(unzipped, CREATOR_SCRIPT_WINDOWS);
}
if(script != null)
{
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command(script.toString(), mLibraryPath.toString());
try
{
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while((line = reader.readLine()) != null)
{
printToConsole(line);
}
int exitCode = process.waitFor();
if(exitCode == 0)
{
printToConsole("Library Created Successfully!");
Platform.runLater(() -> completeProperty().set(true));
}
else
{
terminateWithErrors("Failed: Exit Code [" + exitCode + "]");
mLog.error("Script failed with exit code: " + exitCode);
}
}
catch(InterruptedException ie)
{
terminateWithErrors("Failed: Script Process was interrupted");
mLog.error("Interrupted", ie);
}
}
else
{
terminateWithErrors("Failed: Unable to find JMBE creator launch script for this OS");
mLog.error("Script was null. Unable to find JMBE creator launch script");
}
}
}
catch(Throwable t)
{
terminateWithErrors("Failed: Unknown Error - " + t.getLocalizedMessage());
mLog.error("Failed to create the JMBE library", t);
}
finally
{
if(tempDirectory != null)
{
try
{
FileUtils.deleteDirectory(tempDirectory.toFile());
printToConsole("Deleted: Temporary Directory [" + tempDirectory.toString() + "]");
}
catch(IOException ioe)
{
printToConsole("Failed: Deleting Temporary Directory [" + tempDirectory.toString() + "]");
mLog.error("Error deleting temporary directory [" + tempDirectory.toString() + "]");
}
}
}
});
}
else
{
terminateWithErrors("Failed: Unable to identify correct JMBE creator utility from GitHub " +
"repository for this computer's operating system and architecture");
mLog.error("Unable to create JMBE library. Can't find JMBE Creator for this OS and architecture");
}
}
/**
* Terminates the execution and updates flags to indicate error state
* @param message to display as console output
*/
private void terminateWithErrors(String message)
{
printToConsole(message);
printToConsole("Please follow the instructions for manually creating and installing the JMBE library");
Platform.runLater(() -> {
mHasErrors = true;
mCompleteProperty.set(true);
});
}
/**
* Attempts to find the correct JMBE creator for this operating system and architecture for the specified release
* @param release to find
* @return JMBE creator asset or null.
*/
public static Asset getJMBECreatorAsset(Release release)
{
OSType osType = OSType.getCurrentOSType();
if(release != null)
{
for(Asset asset: release.getAssets())
{
if(isCorrectAsset(asset, osType))
{
return asset;
}
}
}
return null;
}
/**
* Indicates if the asset is correct for the host operating system and architecture
* @param asset to check
* @param osType for the current host (OS & architecture)
* @return true if the asset is correct for this host.
*/
public static boolean isCorrectAsset(Asset asset, OSType osType)
{
if(isJMBECreator(asset))
{
String name = asset.getName();
switch(osType)
{
case LINUX_32:
return name.contains("linux") && name.contains("_32");
case LINUX_64:
return name.contains("linux") && name.contains("_64");
case OSX_64:
return name.contains("osx") && name.contains("_64");
case WINDOWS_32:
return name.contains("windows") && name.contains("_32");
case WINDOWS_64:
return name.contains("windows") && name.contains("_64");
case UNKNOWN:
case OSX_32:
default:
return false;
}
}
return false;
}
/**
* Indicates if the GitHub asset has a non-null asset name and is a JMBE Creator asset
*/
private static boolean isJMBECreator(Asset asset)
{
return asset.getName() != null && asset.getName().startsWith("jmbe-creator");
}
public static void main(String[] args)
{
Release release = GitHub.getLatestRelease(GITHUB_JMBE_RELEASES_URL);
Path library = Paths.get("/home/denny/temp/jmbe.jar");
JmbeCreator jmbeCreator = new JmbeCreator(release, library);
jmbeCreator.execute();
while(true);
}
}

View File

@@ -0,0 +1,327 @@
/*
* *****************************************************************************
* 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.jmbe;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.jmbe.github.Release;
import io.github.dsheirer.jmbe.github.Version;
import io.github.dsheirer.preference.UserPreferences;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Path;
/**
* Editor pane for JMBE Library creator
*/
public class JmbeEditor extends VBox
{
private final static Logger mLog = LoggerFactory.getLogger(JmbeEditor.class);
private UserPreferences mUserPreferences;
private Release mCurrentRelease;
private Label mCurrentVersionLabel;
private Label mUpdatedVersionLabel;
private Label mLibraryPathLabel;
private Button mChangeDirectoryButton;
private GridPane mLabelGridPane;
private Path mLibraryDirectoryPath;
private Button mCreateButton;
private TextArea mConsoleTextArea;
/**
* Constructs an instance
* @param userPreferences
*/
public JmbeEditor(UserPreferences userPreferences)
{
mUserPreferences = userPreferences;
setPadding(new Insets(10,10,10,10));
setSpacing(10);
Separator separator = new Separator();
separator.setMaxWidth(Double.MAX_VALUE);
VBox.setVgrow(getConsoleTextArea(), Priority.ALWAYS);
getChildren().addAll(getLabelGridPane(), separator, getCreateButton(), getConsoleTextArea());
}
/**
* Processes a request to configure the editor to build the release version specified in the request.
* @param request to build a specific release version
*/
public void process(JmbeEditorRequest request)
{
if(request != null)
{
setCurrentRelease(request.getCurrentRelease());
}
}
private TextArea getConsoleTextArea()
{
if(mConsoleTextArea == null)
{
mConsoleTextArea = new TextArea();
mConsoleTextArea.setMaxHeight(Double.MAX_VALUE);
mConsoleTextArea.setMaxWidth(Double.MAX_VALUE);
}
return mConsoleTextArea;
}
private Button getCreateButton()
{
if(mCreateButton == null)
{
mCreateButton = new Button("Create Library");
mCreateButton.setOnAction(event -> {
String noticeText = "The JMBE library is available as source code for educational purposes " +
"only. It is a written description of how certain voice encoding/decoding algorithms " +
"could be implemented. Executable objects compiled or derived from this package may " +
"be covered by one or more patents. Users are strongly advised to check for any " +
"patent restrictions or licensing requirements before compiling or using this " +
"source code.\n\nClicking the YES button indicates that you are requesting to download " +
"the JMBE source code and compile the JMBE library.";
Alert alert = new Alert(Alert.AlertType.WARNING, noticeText, ButtonType.YES, ButtonType.NO);
alert.setResizable(true);
//Workaroud for dialog sizing issue on Windows
alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
alert.setTitle("Download and Compile JMBE Library?");
alert.setHeaderText("Do you want to continue?");
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait().ifPresent(buttonType -> {
if(buttonType == ButtonType.YES)
{
Path library = getLibraryDirectoryPath().resolve(getJarName(mCurrentRelease.getVersion()));
getCreateButton().setDisable(true);
createJmbeLibrary(library);
}
});
});
}
return mCreateButton;
}
/**
* Downloads the JMBE creator and creates the library using the specified path argument.
* @param library path
*/
private void createJmbeLibrary(Path library)
{
JmbeCreator jmbeCreator = new JmbeCreator(mCurrentRelease, library);
getConsoleTextArea().textProperty().bind(jmbeCreator.consoleOutputProperty());
jmbeCreator.completeProperty().addListener((observable, oldValue, newValue) -> {
getCreateButton().setDisable(false);
boolean failed = jmbeCreator.hasErrors();
String content = null;
if(failed)
{
content = "JMBE library creation attempt failed. Please update the library manually by " +
"downloading the JMBE creator application.";
}
else
{
content = "JMBE library successfully created/updated";
mUserPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(jmbeCreator.getLibraryPath());
getCurrentVersionLabel().setText(mCurrentRelease.getVersion().toString());
}
Alert alert = new Alert(Alert.AlertType.INFORMATION, content, ButtonType.OK);
alert.setTitle("JMBE Library Creator");
alert.setHeaderText((failed ? "Attempt failed." : "Success!") + " Please click OK to close this window");
alert.initOwner(getCreateButton().getScene().getWindow());
alert.showAndWait().ifPresent(buttonType -> {
jmbeCreator.completeProperty().unbind();
MyEventBus.getEventBus().post(new JmbeEditorRequest(true));
});
});
jmbeCreator.execute();
}
/**
* Creates a jar name for the specified release version
* @param version of the GitHub JMBE release
* @return JMBE library jar name
*/
public static String getJarName(Version version)
{
String name = version.toString();
name = name.replace("v", "");
return "jmbe-" + name + ".jar";
}
private GridPane getLabelGridPane()
{
if(mLabelGridPane == null)
{
mLabelGridPane = new GridPane();
mLabelGridPane.setHgap(10);
mLabelGridPane.setVgap(10);
int row = 0;
Label topLabel = new Label("Create or Update JMBE Library");
mLabelGridPane.add(topLabel, 0, row, 2, 1);
Label currentLabel = new Label("Current Version:");
GridPane.setHalignment(currentLabel, HPos.RIGHT);
mLabelGridPane.add(currentLabel, 0, ++row);
mLabelGridPane.add(getCurrentVersionLabel(), 1, row);
Label updateLabel = new Label("Updating To Version:");
GridPane.setHalignment(updateLabel, HPos.RIGHT);
mLabelGridPane.add(updateLabel, 0, ++row);
mLabelGridPane.add(getUpdatedVersionLabel(), 1, row);
Label pathLabel = new Label("Store Library At:");
GridPane.setHalignment(pathLabel, HPos.RIGHT);
mLabelGridPane.add(pathLabel, 0, ++row);
mLabelGridPane.add(getLibraryPathLabel(), 1, row);
mLabelGridPane.add(getChangeDirectoryButton(), 1, ++row);
}
return mLabelGridPane;
}
private Path getLibraryDirectoryPath()
{
if(mLibraryDirectoryPath == null)
{
mLibraryDirectoryPath = mUserPreferences.getDirectoryPreference().getDefaultJmbeDirectory();
}
return mLibraryDirectoryPath;
}
private Button getChangeDirectoryButton()
{
if(mChangeDirectoryButton == null)
{
mChangeDirectoryButton = new Button("Change Directory ...");
mChangeDirectoryButton.setOnAction(event -> {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setInitialDirectory(getLibraryDirectoryPath().toFile());
chooser.setTitle("JMBE Library Directory");
File newDirectory = chooser.showDialog(getChangeDirectoryButton().getScene().getWindow());
setLibraryDirectoryPath(newDirectory.toPath());
});
}
return mChangeDirectoryButton;
}
/**
* Sets the library path and updates the display label
*/
private void setLibraryDirectoryPath(Path path)
{
if(path != null)
{
mLibraryDirectoryPath = path;
getLibraryPathLabel().setText(mLibraryDirectoryPath.toString());
}
}
private Label getLibraryPathLabel()
{
if(mLibraryPathLabel == null)
{
mLibraryPathLabel = new Label();
if(getLibraryDirectoryPath() != null)
{
mLibraryPathLabel.setText(getLibraryDirectoryPath().toString());
}
}
return mLibraryPathLabel;
}
private Label getCurrentVersionLabel()
{
if(mCurrentVersionLabel == null)
{
mCurrentVersionLabel = new Label("(empty)");
Version currentVersion = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
if(currentVersion != null)
{
mCurrentVersionLabel.setText(currentVersion.toString());
}
}
return mCurrentVersionLabel;
}
private Label getUpdatedVersionLabel()
{
if(mUpdatedVersionLabel == null)
{
mUpdatedVersionLabel = new Label("(empty)");
}
return mUpdatedVersionLabel;
}
/**
* Current release version available from GitHub.
* @return current release or null if the release hasn't been set.
*/
private Release getCurrentRelease()
{
return mCurrentRelease;
}
/**
* Sets the current release version available from GitHub.
* @param release version
*/
public void setCurrentRelease(Release release)
{
mCurrentRelease = release;
if(release != null)
{
Version updateVersion = release.getVersion();
if(updateVersion != null)
{
getUpdatedVersionLabel().setText(updateVersion.toString());
}
}
}
}

View File

@@ -0,0 +1,65 @@
/*
* *****************************************************************************
* 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.jmbe;
import io.github.dsheirer.jmbe.github.Release;
/**
* Request to create a JMBE library from the specified GitHub release version
*/
public class JmbeEditorRequest
{
private Release mCurrentRelease;
private boolean mCloseEditor = false;
/**
* Constructs an instance to request an edit of the specified release
* @param release available on GitHub
*/
public JmbeEditorRequest(Release release)
{
mCurrentRelease = release;
}
/**
* Constructs a request to close and dispose of the editor
* @param close
*/
public JmbeEditorRequest(boolean close)
{
mCloseEditor = close;
}
/**
* Indicates if this is a close (and dispose) editor request
*/
public boolean isCloseEditorRequest()
{
return mCloseEditor;
}
/**
* Release version requested
*/
public Release getCurrentRelease()
{
return mCurrentRelease;
}
}

View File

@@ -0,0 +1,75 @@
/*
* *****************************************************************************
* 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.jmbe.github;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
/**
* GitHub Asset
*/
public class Asset
{
private JsonObject mJsonObject;
public Asset(JsonObject jsonObject)
{
mJsonObject = jsonObject;
}
public String getName()
{
return getTag("name");
}
/**
* URL to download this asset
*/
public String getDownloadUrl()
{
return getTag("browser_download_url");
}
@Override
public String toString()
{
return getName();
}
/**
* Access the string contents of a json tag
* @param tagName to access
* @return string value or null
*/
private String getTag(String tagName)
{
if(mJsonObject != null)
{
JsonElement element = mJsonObject.get(tagName);
if(!element.isJsonNull())
{
return element.toString().replace("\"", "");
}
}
return null;
}
}

View File

@@ -0,0 +1,175 @@
/*
* ******************************************************************************
* Copyright (C) 2015-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.jmbe.github;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
/**
* Utilities for accessing GitHub repositories and releases
*/
public class GitHub
{
private final static Logger mLog = LoggerFactory.getLogger(GitHub.class);
private GitHub()
{
//There's nothing to construct in this class
}
/**
* Download an artifact to the specified directory
* @param url of the artifact to download
* @param directory to store the downloaded file
* @return path to the downloaded artifact or null
*/
public static Path downloadArtifact(String url, Path directory)
{
HttpClient client = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).build();
try
{
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFileDownload(directory,
StandardOpenOption.CREATE, StandardOpenOption.WRITE));
if(response.statusCode() != 200)
{
mLog.error("HTTP Download Status Code: " + response.statusCode());
}
return response.body();
}
catch(Exception e)
{
mLog.error("Error downloading source code from GitHub", e);
}
return null;
}
/**
* Obtain the latest release object from the repository.
* @param repositoryURL for the GitHub api
* @return the latest release or null
*/
public static Release getLatestRelease(String repositoryURL)
{
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(repositoryURL)).build();
try
{
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() == 200)
{
return parseResponse(response.body());
}
else
{
mLog.error("Error while fetching latest releases - HTTP:" + response.statusCode());
}
}
catch(IOException | InterruptedException e)
{
mLog.error("Error while detecting the current release version of JMBE library", e);
}
return null;
}
/**
* Parses a json string containing an array or GitHub release objects and returns the latest release.
* @param json string returned from URL call
* @return latest release version
*/
private static Release parseResponse(String json)
{
Release release = null;
if(json != null)
{
JsonElement element = JsonParser.parseString(json);
if(element.isJsonArray())
{
JsonArray array = element.getAsJsonArray();
for(JsonElement child: array)
{
if(child.isJsonObject())
{
JsonObject releaseObject = child.getAsJsonObject();
Version version = getVersion(releaseObject);
if(version != null)
{
if(release == null || release.getVersion().compareTo(version) < 0)
{
release = new Release(version, releaseObject);
}
}
}
}
}
}
return release;
}
/**
* Extracts a version object from the tag_name.
* @param jsonObject containing a tag_name formatted as: vAAAAA.BBBBB.CCCCx where:
* A = major, 0-99999
* B = minor, 0-99999
* C = release, 0-99999
* x = (optional)patch, a-z
*
* @return version instance or null
*/
private static Version getVersion(JsonObject jsonObject)
{
JsonElement tagName = jsonObject.get("tag_name");
if(tagName != null)
{
return Version.fromString(tagName.toString());
}
return null;
}
}

View File

@@ -0,0 +1,159 @@
/*
* *****************************************************************************
* 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.jmbe.github;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* GitHub release
*/
public class Release
{
private final static Logger mLog = LoggerFactory.getLogger(Release.class);
private Version mVersion;
private JsonObject mJsonObject;
/**
* Constructs an instance
* @param version of the release
* @param jsonObject that represents the release
*/
public Release(Version version, JsonObject jsonObject)
{
mVersion = version;
mJsonObject = jsonObject;
}
/**
* Version stamp for the release
*/
public Version getVersion()
{
return mVersion;
}
/**
* JsonObject for the release
*/
public JsonObject getJsonObject()
{
return mJsonObject;
}
/**
* Name of the release
*/
public String getName()
{
return getTag("name");
}
/**
* Assets URL
*/
public String getAssetsUrl()
{
return getTag("assets_url");
}
public List<Asset> getAssets()
{
List<Asset> assets = new ArrayList<>();
JsonArray array = mJsonObject.getAsJsonArray("assets");
if(!array.isJsonNull())
{
for(JsonElement element: array)
{
assets.add(new Asset(element.getAsJsonObject()));
}
}
return assets;
}
/**
* HTML URL
*/
public String getHtmlUrl()
{
return getTag("html_url");
}
/**
* Zip File Download URL
*/
public String getDownloadUrl()
{
return getTag("zipball_url");
}
/**
* Access the string contents of a json tag
* @param tagName to access
* @return string value or null
*/
private String getTag(String tagName)
{
if(mJsonObject != null)
{
JsonElement element = mJsonObject.get(tagName);
if(!element.isJsonNull())
{
return element.toString().replace("\"", "");
}
}
return null;
}
/**
* Indicates if the release has a download URL
*/
public boolean hasDownloadUrl()
{
return getDownloadUrl() != null;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("\nName:").append(getName());
sb.append("\nVersion: ").append(getVersion().toString());
sb.append("\nHTML URL:").append(getHtmlUrl());
sb.append("\nDownload URL:").append(getDownloadUrl());
for(Asset asset: getAssets())
{
sb.append("\nAsset: " + asset.toString());
}
return sb.toString();
}
}

View File

@@ -0,0 +1,226 @@
/*
* *****************************************************************************
* 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.jmbe.github;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Version representation and parsing class
*/
public class Version implements Comparable<Version>
{
public final static Pattern VERSION_PATTERN = Pattern.compile("v?(\\d{1,5}).(\\d{1,5}).(\\d{1,5})(\\w*)");
private Integer mMajor;
private Integer mMinor;
private Integer mRelease;
private Character mPatch;
/**
* Constructs an instance
* @param major version
* @param minor version
* @param release version
* @param patch version (optional)
*/
public Version(int major, int minor, int release, Character patch)
{
mMajor = major;
mMinor = minor;
mRelease = release;
mPatch = patch;
}
/**
* Parses the argument into a version instance.
* @param version string (e.g. v1.0.6a)
* @return
*/
public static Version fromString(String version)
{
if(version != null)
{
Matcher m = VERSION_PATTERN.matcher(version.replace("\"", ""));
if(m.matches())
{
int major = 0;
int minor = 0;
int release = 0;
Character patch = null;
try
{
major = Integer.parseInt(m.group(1));
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
try
{
minor = Integer.parseInt(m.group(2));
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
try
{
release = Integer.parseInt(m.group(3));
}
catch(Exception e)
{
//Do nothing, we couldn't parse the value
}
String rawPatch = m.group(4);
if(rawPatch != null && rawPatch.length() >= 1)
{
patch = rawPatch.charAt(0);
}
return new Version(major, minor, release, patch);
}
}
return null;
}
public Integer getMajor()
{
return mMajor;
}
public boolean hasMajor()
{
return mMajor != null;
}
public Integer getMinor()
{
return mMinor;
}
public boolean hasMinor()
{
return mMinor != null;
}
public Integer getRelease()
{
return mRelease;
}
public boolean hasRelease()
{
return mRelease != null;
}
public char getPatch()
{
return mPatch;
}
public boolean hasPatch()
{
return mPatch != null;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(hasMajor() ? getMajor() : "x");
sb.append(".").append(hasMinor() ? getMinor() : "x");
sb.append(".").append(hasRelease() ? getRelease() : "x");
if(hasPatch())
{
sb.append(mPatch);
}
return sb.toString();
}
@Override
public int compareTo(Version other)
{
if(hasMajor() && other.hasMajor())
{
if(getMajor() != other.getMajor())
{
return Integer.compare(getMajor(), other.getMajor());
}
else
{
if(hasMinor() && other.hasMinor())
{
if(getMinor() != other.getMinor())
{
return Integer.compare(getMinor(), other.getMinor());
}
else
{
if(hasRelease() && other.hasRelease())
{
if(getRelease() != other.getRelease())
{
return Integer.compare(getRelease(), other.getRelease());
}
else
{
if(hasPatch() && other.hasPatch())
{
if(getPatch() != other.getPatch())
{
return Character.compare(getPatch(), other.getPatch());
}
else
{
return 0;
}
}
else
{
return hasPatch() ? 1 : -1;
}
}
}
else
{
return hasRelease() ? -1 : 1;
}
}
}
else
{
return hasMinor() ? -1 : 1;
}
}
}
else
{
return hasMajor() ? -1 : 1;
}
}
}

View File

@@ -34,7 +34,12 @@ import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
/**
* Logback and SLF4j logging implementation.
@@ -126,6 +131,17 @@ public class ApplicationLog
((ch.qos.logback.classic.Logger)logger).addAppender(mRollingFileAppender);
// StatusPrinter.print(loggerContext);
Attributes atts = findManifestAttributes();
if (atts != null) {
mLog.info("SDRTrunk Version : " + atts.getValue("Implementation-Version"));
mLog.info("Gradle Version : " + atts.getValue("Created-By"));
mLog.info("Build Timestamp : " + atts.getValue("Build-Timestamp"));
mLog.info("Build-JDK : " + atts.getValue("Build-JDK"));
mLog.info("Build OS : " + atts.getValue("Build-OS"));
}
else{
mLog.info("Failed to find build information.");
}
mLog.info("");
mLog.info("*******************************************************************");
@@ -163,4 +179,25 @@ public class ApplicationLog
mRollingFileAppender = null;
}
}
private Attributes findManifestAttributes() {
try {
Enumeration<URL> resources = getClass().getClassLoader().getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
try {
Manifest manifest = new Manifest(resources.nextElement().openStream());
Attributes atts = manifest.getMainAttributes();
Boolean hasTitle = atts.containsValue("sdrtrunk project");
if (hasTitle) {
return atts;
}
} catch (IOException E) {
return null;
}
}
} catch (Exception ex) {
return null;
}
return null;
}
}

View File

@@ -226,24 +226,30 @@ public class DecoderFactory
ChannelMap channelMap = channelMapModel.getChannelMap(mptConfig.getChannelMapName());
Sync sync = mptConfig.getSync();
modules.add(new MPT1327Decoder(sync));
modules.add(new AudioModule(aliasList));
final int callTimeoutMilliseconds = mptConfig.getCallTimeoutSeconds() * 1000;
// Set max segment audio sample length slightly above call timeout to
// not create a new segment if the processing chain finishes a bit after
// actual call timeout.
int maxAudioSegmentLengthMillis = (callTimeoutMilliseconds + 5000);
modules.add(new AudioModule(aliasList, maxAudioSegmentLengthMillis));
SourceType sourceType = channel.getSourceConfiguration().getSourceType();
if(sourceType == SourceType.TUNER || sourceType == SourceType.TUNER_MULTIPLE_FREQUENCIES)
{
modules.add(new FMDemodulatorModule(FM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
}
long timeout = mptConfig.getCallTimeout() * 1000;
if(channelType == ChannelType.STANDARD)
{
MPT1327TrafficChannelManager trafficChannelManager = new MPT1327TrafficChannelManager(channel, channelMap);
modules.add(trafficChannelManager);
modules.add(new MPT1327DecoderState(trafficChannelManager, channelType, timeout));
modules.add(new MPT1327DecoderState(trafficChannelManager, channelType, callTimeoutMilliseconds));
}
else
{
modules.add(new MPT1327DecoderState(channelType, timeout));
modules.add(new MPT1327DecoderState(channelType, callTimeoutMilliseconds));
}
//Add a channel rotation monitor when we have multiple control channel frequencies specified
@@ -479,7 +485,7 @@ public class DecoderFactory
case MPT1327:
DecodeConfigMPT1327 originalMPT = (DecodeConfigMPT1327)config;
DecodeConfigMPT1327 copyMPT = new DecodeConfigMPT1327();
copyMPT.setCallTimeout(originalMPT.getCallTimeout());
copyMPT.setCallTimeoutSeconds(originalMPT.getCallTimeoutSeconds());
copyMPT.setChannelMapName(originalMPT.getChannelMapName());
copyMPT.setSync(originalMPT.getSync());
copyMPT.setTrafficChannelPoolSize(originalMPT.getTrafficChannelPoolSize());

View File

@@ -55,7 +55,7 @@ public abstract class DecodeConfiguration extends Configuration
{
public static final int DEFAULT_CALL_TIMEOUT_DELAY_SECONDS = 45;
public static final int CALL_TIMEOUT_MINIMUM = 1;
public static final int CALL_TIMEOUT_MAXIMUM = 300; //5 minutes
public static final int CALL_TIMEOUT_MAXIMUM = 180;
public static final int TRAFFIC_CHANNEL_LIMIT_DEFAULT = 3;
public static final int TRAFFIC_CHANNEL_LIMIT_MINIMUM = 0;

View File

@@ -0,0 +1,34 @@
/*
*
* * ******************************************************************************
* * 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.module.decode.config;
/**
* Trait of decoder configurations that support providing information about call timeout.
*/
public interface WithCallTimeout
{
/**
* @return call timeout in seconds
*/
int getCallTimeoutSeconds();
}

View File

@@ -20,6 +20,9 @@
package io.github.dsheirer.module.decode.ip;
import io.github.dsheirer.identifier.Identifier;
import java.util.List;
/**
* Data packet interface
*/
@@ -28,16 +31,21 @@ public interface IPacket
/**
* Header for the data packet.
*/
public IHeader getHeader();
IHeader getHeader();
/**
* Optional payload for the packet
* @return null if there is no payload
*/
public IPacket getPayload();
IPacket getPayload();
/**
* Indicates if this packet contains an optional payload
*/
public boolean hasPayload();
boolean hasPayload();
/**
* List of identifiers for this packet and any child payload packets.
*/
List<Identifier> getIdentifiers();
}

View File

@@ -22,6 +22,8 @@ package io.github.dsheirer.module.decode.ip;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.module.decode.ip.ars.ARSPacket;
import io.github.dsheirer.module.decode.ip.cellocator.MCGPHeader;
import io.github.dsheirer.module.decode.ip.cellocator.MCGPMessageFactory;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
import io.github.dsheirer.module.decode.ip.udp.UDPPacket;
@@ -82,15 +84,23 @@ public class PacketMessageFactory
/**
* Creates a UDP/IP packet payload parser
*
* @param sourcePort for the packet (to identify the protocol/format)
* @param destinationPort for the packet (to identify the protocol/format)
* @param binaryMessage containing the IP/UDP payload
* @param offset in the message to the start of the payload
* @return constructed packet messge parser
*/
public static IPacket createUDPPayload(int destinationPort, BinaryMessage binaryMessage, int offset)
public static IPacket createUDPPayload(int sourcePort, int destinationPort, BinaryMessage binaryMessage, int offset)
{
switch(destinationPort)
{
case 231:
//Cellocator
if(MCGPHeader.isCellocatorMessage(binaryMessage, offset))
{
return MCGPMessageFactory.create(binaryMessage, offset);
}
break;
case 4001: //Location Service
break;
case 4004: //XCMP Service
@@ -103,6 +113,23 @@ public class PacketMessageFactory
break;
}
switch(sourcePort)
{
case 231:
//Cellocator
if(MCGPHeader.isCellocatorMessage(binaryMessage, offset))
{
return MCGPMessageFactory.create(binaryMessage, offset);
}
break;
}
//This is normally source or destination port 231, but can be on other ports as well.
if(MCGPHeader.isCellocatorMessage(binaryMessage, offset))
{
return MCGPMessageFactory.create(binaryMessage, offset);
}
return new UnknownPacket(binaryMessage, offset);
}
}

View File

@@ -21,7 +21,10 @@
package io.github.dsheirer.module.decode.ip;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.sndcp.SNDCPPacketHeader;
import java.util.Collections;
import java.util.List;
public class UnknownCompressedHeaderPacket implements IPacket
{
@@ -87,4 +90,10 @@ public class UnknownCompressedHeaderPacket implements IPacket
{
return false;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -21,6 +21,9 @@
package io.github.dsheirer.module.decode.ip;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import java.util.Collections;
import java.util.List;
public class UnknownPacket implements IPacket
{
@@ -78,4 +81,10 @@ public class UnknownPacket implements IPacket
{
return false;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -21,8 +21,11 @@
package io.github.dsheirer.module.decode.ip.ars;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.ip.IPacket;
import io.github.dsheirer.module.decode.ip.Packet;
import java.util.Collections;
import java.util.List;
public class ARSPacket extends Packet
{
@@ -64,4 +67,10 @@ public class ARSPacket extends Packet
//There are no payloads for ARS packets ... the header parses all of the information.
return null;
}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,141 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
/**
* Controller (Inbound) Acknowledge Packet
*/
public class AcknowledgeMessage extends MCGPPacket
{
//This field is byte reversed (ie big endian)
private static final int[] TARGET_UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMAND_NUMERATOR = new int[]{32, 33, 34, 35, 36, 37, 38, 39};
//This field is (possibly) byte reversed (ie big endian)
private static final int[] AUTHENTICATION_CODE = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 56, 57, 58, 59, 60, 61,
62, 63, 48, 49, 50, 51, 52, 53, 54, 55, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] ACTION_CODE = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
//This field is byte reversed (ie big endian)
private static final int[] MAIN_ACKNOWLEDGE_NUMBER = new int[]{88, 89, 90, 91, 92, 93, 94, 95, 80, 81, 82, 83, 84,
85, 86, 87};
//This field is byte reversed (ie big endian)
private static final int[] SECONDARY_ACKNOWLEDGE_NUMBER = new int[]{104, 105, 106, 107, 108, 109, 110, 111, 96, 97,
98, 99, 100, 101, 102, 103};
private static final int[] RESERVED = new int[]{112, 113, 114, 115, 116, 117, 118, 119};
private static final int[] YEAR = new int[]{121, 122, 123, 124, 125, 126, 127};
private static final int[] MONTH = new int[]{133, 134, 135, 120};
private static final int[] DAY = new int[]{128, 129, 130, 131, 132};
private static final int[] RESERVED_2 = new int[]{152, 153, 154, 155, 156, 157, 158}; //Always 0x40
private static final int[] HOUR = new int[]{139, 140, 141, 142, 143};
private static final int[] MINUTE = new int[]{149, 150, 151, 136, 137, 138};
private static final int[] SECOND = new int[]{159, 144, 145, 146, 147, 148};
private CellocatorRadioIdentifier mTargetRadioId;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public AcknowledgeMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
public CellocatorRadioIdentifier getRadioId()
{
if(mTargetRadioId == null)
{
mTargetRadioId = CellocatorRadioIdentifier.createTo(getMessage().getInt(TARGET_UNIT_ID, getOffset()));
}
return mTargetRadioId;
}
public MCGPMessageType getAcknowledgedCommand()
{
int command = getMessage().getInt(COMMAND_NUMERATOR, getOffset());
return MCGPMessageType.getOutboundMessageType(command);
}
public String getAuthenticationCode()
{
return Integer.toHexString(getMessage().getInt(AUTHENTICATION_CODE, getOffset())).toUpperCase();
}
/**
* Always set to zero for an acknowledge message
*/
public int getActionCode()
{
return getMessage().getInt(ACTION_CODE, getOffset());
}
public int getMainAcknowledgeNumber()
{
return getMessage().getInt(MAIN_ACKNOWLEDGE_NUMBER, getOffset());
}
/**
* Currently not used and set to zero.
*/
public int getSecondaryAcknowledgeNumber()
{
return getMessage().getInt(SECONDARY_ACKNOWLEDGE_NUMBER, getOffset());
}
public long getTimestamp()
{
Calendar calendar = new GregorianCalendar();
calendar.clear();
int year = 2000 + getMessage().getInt(YEAR, getOffset());
int month = getMessage().getInt(MONTH, getOffset()) - 1; //Calendar month is zero based
int day = getMessage().getInt(DAY, getOffset());
int hour = getMessage().getInt(HOUR, getOffset());
int minute = getMessage().getInt(MINUTE, getOffset());
int second = getMessage().getInt(SECOND, getOffset());
calendar.set(year, month, day, hour, minute, second);
return calendar.getTimeInMillis();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR RADIO:");
sb.append(getRadioId());
sb.append(" ACKNOWLEDGE:").append(getAcknowledgedCommand());
sb.append(" MESSAGE NUMBER:").append(getMainAcknowledgeNumber());
sb.append(" TIME:").append(new Date(getTimestamp()));
sb.append(" AUTH:").append(getAuthenticationCode());
return sb.toString();
}
}

View File

@@ -0,0 +1,68 @@
/*
*
* * ******************************************************************************
* * 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.module.decode.ip.cellocator;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.protocol.Protocol;
/**
* Cellocator Radio Identifier
*/
public class CellocatorRadioIdentifier extends RadioIdentifier
{
public CellocatorRadioIdentifier(Integer value, Role role)
{
super(value, role);
}
@Override
public Protocol getProtocol()
{
return Protocol.CELLOCATOR;
}
/**
* Creates a TO radio identifier
*/
public static CellocatorRadioIdentifier createTo(int radioId)
{
return new CellocatorRadioIdentifier(radioId, Role.TO);
}
/**
* Creates a FROM radio identifier
*/
public static CellocatorRadioIdentifier createFrom(int radioId)
{
return new CellocatorRadioIdentifier(radioId, Role.FROM);
}
/**
* Creates an ANY radio identifier
*/
public static CellocatorRadioIdentifier createAny(int radioId)
{
return new CellocatorRadioIdentifier(radioId, Role.ANY);
}
}

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.module.decode.ip.cellocator;
import java.util.Map;
import java.util.TreeMap;
/**
* Cellocator - Commands Enumeration
*/
public enum Command
{
IMMEDIATE_STATUS_REQUEST(0x0, "IMMEDIATE STATUS AND LOCATION"),
UNIT_STATUS_CHANGE(0x2, "CHANGE UNIT STATUS"),
OUTPUT_STATE_CHANGE(0x3, "CHANGE OUTPUT STATE"),
DISABLE_ACTIVE_TRANSMISSIONS(0x4, "DISABLE ACTIVE TRANSMISSIONS"),
TRACKING_CONTROL(0x5, "TRACKING CONTROL"),
ALARM_CADENCE_CONTROL(0x6, "ALARM CADENCE CONTROL"),
COMMENCE_GRADUAL_ENGINE_STOP(0x7, "COMMENCE GRADUAL ENGINE STOP"),
INITIATE_CSD_SESSION(0xC, "INITIATE CSD SESSION"),
ERASE_TRACKING_LOG(0xD, "ERASE TRACKING LOG"),
RESET_GPS_RECEIVER(0xE, "RESET GPS RECEIVER"),
SEQUENCE_DETECTION_LEARN(0xF, "SEQUENCE DETECTION LEARN"),
FORCE_GPS_ENERGIZING(0x10, "FORCE GPS ACTIVATION"),
CONNECT_TO_SERVER(0x12, "CONNECT TO SERVER"),
MANUFACTURER_RESERVED(0x13, "MANUFACTURER RESERVED"),
CALIBRATE_FREQUENCY_COUNTERS(0x14, "CALIBRATE FREQUENCY COUNTERS"),
CONTROL_COM_PORT_TRANSPARENT_MODE(0x15, "CONTROL COM PORT TRANSPARENT MODE"),
QUERY_CONNECTED_TRAILER_ID(0x16, "QUERY CONNECTED TRAILER ID"),
CAMERA_SUPPORT(0x18, "CAMERA SUPPORT"),
NANO_WAKEUP_AND_STATUS_REQUEST(0x19, "NANO WAKEUP AND STATUS REQUEST"),
ENABLE_MODEM_FOTA_SESSION(0x1A, "ENABLE MODEM FOTA SESSION"),
PHSN_CONTROL(0x1B, "PHSN CONTROL"),
CALL_CONTROL(0x1C, "CALL SHARING 2 CONTROL"),
CELLO_TRACK_T(0x1D, "CELLO TRACK T"),
CALIBRATE_CAN_GPS_SPEED(0x1F, "CALIBRATE CAN GPS SPEED"),
UNKNOWN(-1, "UNKNOWN COMMAND");
private int mValue;
private String mLabel;
/**
* Constructs an instance
* @param value of the command
* @param label for pretty printing
*/
Command(int value, String label)
{
mValue = value;
mLabel = label;
}
/**
* Value of the command
*/
public int getValue()
{
return mValue;
}
private static Map<Integer,Command> LOOKUP_MAP = new TreeMap<>();
static
{
for(Command command: Command.values())
{
LOOKUP_MAP.put(command.getValue(), command);
}
}
/**
* Lookup a command from its value
* @param value of command to lookup
* @return command or UNKNOWN
*/
public static Command fromValue(int value)
{
if(LOOKUP_MAP.containsKey(value))
{
return LOOKUP_MAP.get(value);
}
return UNKNOWN;
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -0,0 +1,111 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
/**
* Communication Control Field parser
*/
public class CommunicationControl
{
public enum Initiative {ACTIVE, RESPONSE};
public enum SpeedType {MOMENTARY, MAX};
private static final int MASK_CAN_ORIGINATED_ODOMETER = 0x8000;
private static final int MASK_CAN_ORIGINATED_SPEED = 0x4000;
private static final int MASK_MULTI_PURPOSE_FIELD_CONTENT = 0x3000;
private static final int MASK_CR200_CR300_MESSAGE_SOURCE = 0x800;
private static final int MASK_GARMIN_CONNECTED = 0x400;
private static final int MASK_GARMIN_ENABLED = 0x200;
private static final int MASK_CR200_CR300_MESSAGE_INITIATIVE = 0x100;
private static final int MASK_CR200_CR300_GSM_HIBERNATION_INDICATOR = 0x80;
private static final int MASK_MOMENTARY_OR_MAX_SPEED_VALUE = 0x40;
private static final int MASK_BUSINESS_OR_PRIVATE_MODE = 0x20;
private static final int MASK_FIRMWARE_SUB_VERSION = 0x1F;
private int mValue;
/**
* Constructs an instance
*
* @param value of the 16-bit field
*/
public CommunicationControl(int value)
{
mValue = value;
}
/**
* Indicates if the odometer value originated from the vehicle CAN bus
*/
public boolean isCANOriginatedOdometer()
{
return isSet(MASK_CAN_ORIGINATED_ODOMETER);
}
/**
* Indicates if the speed value originated from the vehicle CAN bus
*/
public boolean isCANOriginatedSpeed()
{
return isSet(MASK_CAN_ORIGINATED_SPEED);
}
/**
* Indicates if the Garmin GPS is connected to the unit
*/
public boolean isGarminConnected()
{
return isSet(MASK_GARMIN_CONNECTED);
}
/**
* Indicates if the Garmin GPS is enabled
*/
public boolean isGarminEnabled()
{
return isSet(MASK_GARMIN_ENABLED);
}
/**
* Indicates if the message was initiated by the unit or if the message is a response to a request.
*/
public Initiative getMessageSource()
{
return isSet(MASK_CR200_CR300_MESSAGE_INITIATIVE) ? Initiative.RESPONSE : Initiative.ACTIVE;
}
/**
* Indicates the context of the speed value contained in the message, momentary or max recorded from last event.
*/
public SpeedType getSpeedType()
{
return isSet(MASK_MOMENTARY_OR_MAX_SPEED_VALUE) ? SpeedType.MAX : SpeedType.MOMENTARY;
}
/**
* Indicates if the masked value is set for this field
* @param mask to check
* @return true if all set bits of the mask value are also set in the field bits
*/
private boolean isSet(int mask)
{
return (mValue & mask) == mask;
}
}

View File

@@ -0,0 +1,135 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
/**
* Unit (Outbound) data being forwarded over the wireless interface to a central application
*/
public class ForwardedDataMessage extends MCGPPacket
{
//This field is byte reversed (ie big endian)
private static final int[] TARGET_UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMAND_NUMERATOR = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] FORWARDED_MESSAGE_NUMBER = new int[]{88, 89, 90, 91, 92, 93, 94, 95};
private static final int[] FRAGMENT_CONTROL = new int[]{96, 97, 98, 99, 100, 101, 102, 103};
private static final int[] PAYLOAD_LENGTH = new int[]{112, 113, 114, 115, 116, 117, 118, 119, 104, 105, 106, 107,
108, 109, 110, 111};
private static final int PAYLOAD_START = 120;
private CellocatorRadioIdentifier mTargetRadioId;
private FragmentControl mFragmentControl;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public ForwardedDataMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
/**
* Target radio ID to receive the forwarded data
*/
public CellocatorRadioIdentifier getRadioId()
{
if(mTargetRadioId == null)
{
mTargetRadioId = CellocatorRadioIdentifier.createTo(getMessage().getInt(TARGET_UNIT_ID, getOffset()));
}
return mTargetRadioId;
}
/**
* Message fragment control structure
*/
public FragmentControl getFragmentControl()
{
if(mFragmentControl == null)
{
mFragmentControl = new FragmentControl(getMessage().getInt(FRAGMENT_CONTROL, getOffset()));
}
return mFragmentControl;
}
/**
* This message number
*/
public int getMessageNumerator()
{
return getMessage().getInt(COMMAND_NUMERATOR, getOffset());
}
/**
* Message number for the forwarded message
*/
public int getForwardedMessageNumerator()
{
return getMessage().getInt(FORWARDED_MESSAGE_NUMBER, getOffset());
}
/**
* Length of the payload in bytes
*/
private int getPayloadLength()
{
return getMessage().getInt(PAYLOAD_LENGTH, getOffset());
}
/**
* Hexadecimal representation of the payload data.
*/
public String getPayloadData()
{
int payloadEnd = PAYLOAD_START + (getPayloadLength() * 8);
return getMessage().get(PAYLOAD_START, payloadEnd).toHexString();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR TO RADIO:");
sb.append(getRadioId());
sb.append(" MESSAGE NUMBER:").append(getMessageNumerator());
if(getHeader().getMessageType() == MCGPMessageType.OUTBOUND_FORWARDED_REALTIME_DATA)
{
sb.append(" FORWARDED SERIAL PORT DATA");
}
else
{
sb.append(" FORWARDED LOGGED DATA");
}
sb.append(" MESSAGE NUMBER:").append(getForwardedMessageNumerator());
sb.append(" ").append(getFragmentControl());
return sb.toString();
}
}

View File

@@ -0,0 +1,70 @@
package io.github.dsheirer.module.decode.ip.cellocator;
/**
* Provides fragment information for messages that are too long and have to be fragmented.
*/
public class FragmentControl
{
public enum Origination{UNIT,APPLICATION};
private static final int BIT_MASK_ORIGINATION = 0x80;
private static final int BIT_MASK_LAST_FRAGMENT = 0x40;
private static final int BIT_MASK_FRAGMENT_INDEX = 0x3F;
private int mValue;
/**
* Constructs an instance
* @param value of the fragment control field byte
*/
public FragmentControl(int value)
{
mValue = value;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(getOrigination().name());
sb.append(" ORIGINATED FRAGMENT #").append(getFragmentIndex());
if(isLastFragment())
{
sb.append(" FINAL");
}
return sb.toString();
}
/**
* Indicates the origination of the message fragment
* @return origination, unit or application
*/
public Origination getOrigination()
{
return isSet(BIT_MASK_ORIGINATION) ? Origination.UNIT : Origination.APPLICATION;
}
/**
* Indicates if this is the last fragment
*/
public boolean isLastFragment()
{
return isSet(BIT_MASK_LAST_FRAGMENT);
}
/**
* Fragment index for this fragment, 0-31
*/
public int getFragmentIndex()
{
return (mValue & BIT_MASK_FRAGMENT_INDEX);
}
/**
* Indicates if the bits that are set in the bit mask are also set in the value field
*/
private boolean isSet(int bitMask)
{
return (mValue & bitMask) == bitMask;
}
}

View File

@@ -0,0 +1,93 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
/**
* Controller (Inbound) Generic Command
*/
public class GenericCommandMessage extends MCGPPacket
{
//This field is byte reversed (ie big endian)
private static final int[] TARGET_UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMAND_NUMERATOR = new int[]{32, 33, 34, 35, 36, 37, 38, 39};
//This field is (possibly) byte reversed (ie big endian)
private static final int[] AUTHENTICATION_CODE = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 56, 57, 58, 59, 60, 61,
62, 63, 48, 49, 50, 51, 52, 53, 54, 55, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] COMMAND_CODE = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private CellocatorRadioIdentifier mTargetRadioId;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public GenericCommandMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
public CellocatorRadioIdentifier getRadioId()
{
if(mTargetRadioId == null)
{
mTargetRadioId = CellocatorRadioIdentifier.createTo(getMessage().getInt(TARGET_UNIT_ID, getOffset()));
}
return mTargetRadioId;
}
public int getCommandNumerator()
{
return getMessage().getInt(COMMAND_NUMERATOR, getOffset());
}
public String getAuthenticationCode()
{
return Integer.toHexString(getMessage().getInt(AUTHENTICATION_CODE, getOffset())).toUpperCase();
}
public int getCommandCode()
{
return getMessage().getInt(COMMAND_CODE, getOffset());
}
public Command getCommand()
{
return Command.fromValue(getCommandCode());
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR RADIO:");
sb.append(getRadioId());
sb.append(" REQUEST:").append(getCommand());
sb.append(" SEQUENCE:").append(getCommandNumerator());
sb.append(" AUTH:").append(getAuthenticationCode());
return sb.toString();
}
}

View File

@@ -0,0 +1,237 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import org.apache.commons.math3.util.FastMath;
/**
* Unit (Outbound) Status and Location
*/
public class LocationStatusMessage extends MCGPPacket
{
private static final int[] SOURCE_UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMUNICATION_CONTROL = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
45, 46, 47};
private static final int[] MESSAGE_NUMERATOR = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] HARDWARE_VERSION = new int[]{56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] SOFTWARE_VERSION = new int[]{64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] PROTOCOL_VERSION = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private static final int[] UNIT_STATUS = new int[]{80, 81, 82, 83};
private static final int[] CURRENT_GSM_OPERATOR = new int[]{84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 152,
153, 154, 155, 156, 157, 158, 159};
private static final int[] TRANSMISSION_REASON_SPECIFIC_DATA = new int[]{96, 97, 98, 99, 100, 101, 102, 103};
private static final int[] TRANSMISSION_REASON = new int[]{104, 105, 106, 107, 108, 109, 110, 111};
private static final int[] UNIT_MODE_OF_OPERATION = new int[]{112, 113, 114, 115, 116, 117, 118, 119}; //Byte 20
private static final int[] UNIT_IO_STATUS = new int[]{120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131,
132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151};
private static final int[] ANALOG_INPUT_1 = new int[]{160, 161, 162, 163, 164, 165, 166, 167};
private static final int[] ANALOG_INPUT_2 = new int[]{168, 169, 170, 171, 172, 173, 174, 175};
private static final int[] ANALOG_INPUT_3 = new int[]{176, 177, 178, 179, 180, 181, 182, 183};
private static final int[] ANALOG_INPUT_4 = new int[]{184, 185, 186, 187, 188, 189, 190, 191};
private static final int[] MILEAGE_COUNTER = new int[]{192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203,
204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215};
private static final int[] MULTI_PURPOSE_FIELD = new int[]{216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226,
227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248,
249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263};
private static final int[] LAST_GPS_FIX = new int[]{264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276,
277, 278, 279};
private static final int[] LOCATION_STATUS = new int[]{280, 281, 282, 283, 284, 285, 286, 287};
private static final int[] GPS_MODE_1 = new int[]{288, 289, 290, 291, 292, 293, 294, 295};
private static final int[] GPS_MODE_2 = new int[]{296, 297, 298, 299, 300, 301, 302, 303};
private static final int[] GPS_SATELLITES_USED = new int[]{304, 305, 306, 307, 308, 309, 310, 311};
//Original signed 32-bit big endian field rearranged in little endian byte format
private static final int[] LONGITUDE = new int[]{336, 337, 338, 339, 340, 341, 342, 343, 328, 329, 330, 331, 332,
333, 334, 335, 320, 321, 322, 323, 324, 325, 326, 327, 312, 313, 314, 315, 316, 317, 318, 319};
//Original signed 32-bit big endian field rearranged in little endian byte format
private static final int[] LATITUDE = new int[]{368, 369, 370, 371, 372, 373, 374, 375, 360, 361, 362, 363, 364,
365, 366, 367, 352, 353, 354, 355, 356, 357, 358, 359, 344, 345, 346, 347, 348, 349, 350, 351};
//Original signed 32-bit big endian field rearranged in little endian byte format
private static final int[] ALTITUDE = new int[]{400, 401, 402, 403, 404, 405, 406, 407, 392, 393, 394, 395, 396,
397, 398, 399, 384, 385, 386, 387, 388, 389, 390, 391, 376, 377, 378, 379, 380, 381, 382, 383};
//Original signed 32-bit big endian field rearranged in little endian byte format
private static final int[] GROUND_SPEED = new int[]{432, 433, 434, 435, 436, 437, 438, 439, 424, 425, 426, 427, 428,
429, 430, 431, 416, 417, 418, 419, 420, 421, 422, 423, 408, 409, 410, 411, 412, 413, 414, 415};
//Original signed 32-bit big endian field rearranged in little endian byte format
private static final int[] HEADING_TRUE = new int[]{448, 449, 450, 451, 452, 453, 454, 455, 440, 441, 442, 443,
444, 445, 446, 447};
private static final int[] UTC_TIME_SECOND = new int[]{456, 457, 458, 459, 460, 461, 462, 463};
private static final int[] UTC_TIME_MINUTE = new int[]{464, 465, 466, 467, 468, 469, 470, 471};
private static final int[] UTC_TIME_HOUR = new int[]{472, 473, 474, 475, 476, 477, 478, 479};
private static final int[] UTC_TIME_DAY = new int[]{480, 481, 482, 483, 484, 485, 486, 487};
private static final int[] UTC_TIME_MONTH = new int[]{488, 489, 490, 491, 492, 493, 494, 495};
private static final int[] UTC_TIME_YEAR = new int[]{496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507,
508, 509, 510, 511};
private CellocatorRadioIdentifier mSourceRadioId;
private CommunicationControl mCommunicationControl;
private Long mUTCTimestamp;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public LocationStatusMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
/**
* Source radio or unit ID
*/
public CellocatorRadioIdentifier getRadioId()
{
if(mSourceRadioId == null)
{
mSourceRadioId = CellocatorRadioIdentifier.createFrom(getMessage().getInt(SOURCE_UNIT_ID, getOffset()));
}
return mSourceRadioId;
}
/**
* Communication control bitmap field parser.
*/
public CommunicationControl getCommunicationControl()
{
if(mCommunicationControl == null)
{
mCommunicationControl = new CommunicationControl(getMessage().getInt(COMMUNICATION_CONTROL, getOffset()));
}
return mCommunicationControl;
}
/**
* Message number. This is a one-up sequence maintained by the source unit that is reset to zero after
* each power cycle and used to track the message through receipt.
*/
public int getMessageNumerator()
{
return getMessage().getInt(MESSAGE_NUMERATOR, getOffset());
}
/**
* Number of satellites used by the GPS for calculating the position/fix.
*/
public int getSatelliteCount()
{
return getMessage().getInt(GPS_SATELLITES_USED, getOffset());
}
/**
* Latitude in degrees decimal format for WGS-84 datum
*/
public double getLatitude()
{
int value = getMessage().getInt(LATITUDE, getOffset());
double radians = (double)value / 1E8d;
return FastMath.toDegrees(radians);
}
/**
* Longitude in degrees decimal format for WGS-84 datum
*/
public double getLongitude()
{
int value = getMessage().getInt(LONGITUDE, getOffset());
double radians = (double)value / 1E8d;
return FastMath.toDegrees(radians);
}
/**
* Altitude in meters
*/
public double getAltitude()
{
int centimeters = getMessage().getInt(ALTITUDE, getOffset());
return (double)centimeters / 1E2d;
}
/**
* Speed over ground in kph
*/
public double getSpeed()
{
int centimetersPerSecond = getMessage().getInt(GROUND_SPEED, getOffset());
double kilometersPerSecond = (double)centimetersPerSecond / 1E5d;
return kilometersPerSecond * 3600;
}
/**
* Heading relative to true North, 0-360 degrees.
*/
public double getHeading()
{
int headingThousandthsRadian = getMessage().getInt(HEADING_TRUE, getOffset());
double radians = (double)headingThousandthsRadian / 1E3d;
return FastMath.toDegrees(radians);
}
/**
* UTC timestamp for the location and status
* @return timestamp in milliseconds since epoch
*/
public long getUTCTimestamp()
{
if(mUTCTimestamp == null)
{
int year = getMessage().getInt(UTC_TIME_YEAR, getOffset());
int month = getMessage().getInt(UTC_TIME_MONTH, getOffset());
int day = getMessage().getInt(UTC_TIME_DAY, getOffset());
int hour = getMessage().getInt(UTC_TIME_HOUR, getOffset());
int minute = getMessage().getInt(UTC_TIME_MINUTE, getOffset());
int second = getMessage().getInt(UTC_TIME_SECOND, getOffset());
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
calendar.clear();
calendar.set(year, month, day, hour, minute, second);
mUTCTimestamp = calendar.getTimeInMillis();
}
return mUTCTimestamp;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR RADIO:").append(getRadioId());
sb.append(" GPS LOCATION: ").append(getLatitude()).append(" ").append(getLongitude());
sb.append(" HEADING:").append(getHeading());
sb.append(" SPEED:").append(getSpeed()).append("kph");
sb.append(" AT:").append(new Date(getUTCTimestamp()));
sb.append(" MESSAGE #").append(getMessageNumerator());
return sb.toString();
}
}

View File

@@ -0,0 +1,127 @@
/*
* ******************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.module.decode.ip.Header;
/**
* Cellocator MCGP Header
*/
public class MCGPHeader extends Header
{
private static final int[] M_CHARACTER = {0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] C_CHARACTER = {8, 9, 10, 11, 12, 13, 14, 15};
private static final int[] G_CHARACTER = {16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] P_CHARACTER = {24, 25, 26, 27, 28, 29, 30, 31};
private static final int M_VALUE = 0x4D;
private static final int C_VALUE = 0x43;
private static final int G_VALUE = 0x47;
private static final int P_VALUE = 0x50;
private static final int[] MESSAGE_TYPE = {32, 33, 34, 35, 36, 37, 38, 39};
public static final int HEADER_LENGTH = 40;
private MCGPMessageType mMessageType;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param message containing the header
* @param offset to the header within the message
*/
public MCGPHeader(BinaryMessage message, int offset)
{
super(message, offset);
}
/**
* Performs simple check of the first 32 bits of the message looking for the
* signature 'MCGP' header for cellocator messages.
* @param message to inspect
* @param offset to the cellocator payload
* @return true if the payload starts with MCGP
*/
public static boolean isCellocatorMessage(BinaryMessage message, int offset)
{
return message.getInt(M_CHARACTER, offset) == M_VALUE &&
message.getInt(C_CHARACTER, offset) == C_VALUE &&
message.getInt(G_CHARACTER, offset) == G_VALUE &&
message.getInt(P_CHARACTER, offset) == P_VALUE;
}
@Override
public int getLength()
{
return HEADER_LENGTH;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR ").append(getMessageType());
if(getMessageType() == MCGPMessageType.UNKNOWN)
{
sb.append(" PACKET:").append(getMessage().toHexString());
}
return sb.toString();
}
/**
* Indicates the type of message
*/
public MCGPMessageType getMessageType()
{
if(mMessageType == null)
{
mMessageType = getMessageType(getMessage(), getOffset());
}
return mMessageType;
}
/**
* Sets/Overrides the message type
*/
void setMessageType(MCGPMessageType type)
{
mMessageType = type;
}
/**
* Indicates the type of MCGP message contained in the message starting at the offset index.
*/
public static MCGPMessageType getMessageType(BinaryMessage message, int offset)
{
int payloadLengthBytes = (message.size() - offset) / 8;
return MCGPMessageType.fromValue(message.getInt(MESSAGE_TYPE, offset), payloadLengthBytes);
}
/**
* Numeric value for the message type for this header
*/
public int getMessageTypeValue()
{
return getMessage().getInt(MESSAGE_TYPE, getOffset());
}
}

View File

@@ -0,0 +1,77 @@
/*
* ******************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
/**
* Creates an MCGP packet and header for parsing the contents of the packet.
*/
public class MCGPMessageFactory
{
/**
* Creates an MCGP packet message from the binary message at the specified binary message offset.
* @param message containing transmitted bits
* @param offset into the message for the start of the MCGP payload
* @return MCGP packet message parser
*/
public static MCGPPacket create(BinaryMessage message, int offset)
{
MCGPHeader header = new MCGPHeader(message, offset);
int payloadOffset = offset + MCGPHeader.HEADER_LENGTH;
switch(header.getMessageType())
{
case INBOUND_GENERIC_COMMAND:
return new GenericCommandMessage(header, message, payloadOffset);
case INBOUND_GENERAL_ACKNOWLEDGE:
return new AcknowledgeMessage(header, message, payloadOffset);
case INBOUND_PROGRAMMING_COMMAND:
return new ProgrammingCommandMessage(header, message, payloadOffset);
case OUTBOUND_LOCATION_STATUS:
return new LocationStatusMessage(header, message, payloadOffset);
case OUTBOUND_PROGRAMMING_STATUS:
return new ProgrammingStatusMessage(header, message, payloadOffset);
case OUTBOUND_FORWARDED_REALTIME_DATA:
case OUTBOUND_FORWARDED_LOGGED_DATA_FRAGMENT:
return new ForwardedDataMessage(header, message, payloadOffset);
case INBOUND_MODULAR_REQUEST:
case OUTBOUND_MODULAR_RESPONSE:
//Both messages are variable length and use message type 9 so we have to inspect the total message
//length field for the response message which should be 0 for the request variant and non-zero for
//the response variant.
if(ModularResponseMessage.getTotalMessageLength(message, payloadOffset) > 0)
{
header.setMessageType(MCGPMessageType.OUTBOUND_MODULAR_RESPONSE);
return new ModularResponseMessage(header, message, payloadOffset);
}
else
{
return new ModularRequestMessage(header, message, payloadOffset);
}
case UNKNOWN:
default:
return new UnknownMCGPMessage(header, message, offset);
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import java.util.EnumSet;
/**
* Cellocator MCGP Message Types enumeration with indicated message lengths.
*
* Note: message length of -1 indicates a variable length message.
*/
public enum MCGPMessageType
{
INBOUND_GENERIC_COMMAND(0, 25 * 8, "COMMAND"),
INBOUND_PROGRAMMING_COMMAND(1, 34 * 8, "PROGRAMMING COMMAND"),
INBOUND_GENERAL_ACKNOWLEDGE(4, 28 * 8, "ACKNOWLEDGE"),
INBOUND_FORWARD_DATA_COMMAND(5, -1, "FORWARD DATA COMMAND"),
INBOUND_MODULAR_REQUEST(9, -1, "MODULAR MESSAGE REQUEST"),
OUTBOUND_LOCATION_STATUS(0, 70 * 8, "LOCATION AND STATUS"),
OUTBOUND_PROGRAMMING_STATUS(3, 31 * 8, "CONFIGURATION"),
OUTBOUND_FORWARDED_LOGGED_DATA_FRAGMENT(7, -1, "FORWARDED LOGGED DATA"),
OUTBOUND_FORWARDED_REALTIME_DATA(8, -1, "FORWARDED SERIAL_PORT DATA"),
OUTBOUND_MODULAR_RESPONSE(9, -1, "MODULAR MESSAGE RESPONSE"),
OUTBOUND_FIRMWARE_UPDATE(10,-1, "FIRMWARE UPDATE"),
UNKNOWN(-1, -1, "UNKNOWN");
private int mValue;
private int mLength;
private String mLabel;
MCGPMessageType(int value, int length, String label)
{
mValue = value;
mLength = length;
mLabel = label;
}
public static EnumSet<MCGPMessageType> INBOUND = EnumSet.range(INBOUND_GENERIC_COMMAND, INBOUND_MODULAR_REQUEST);
public static EnumSet<MCGPMessageType> OUTBOUND = EnumSet.range(OUTBOUND_LOCATION_STATUS, OUTBOUND_FIRMWARE_UPDATE);
public int getValue()
{
return mValue;
}
public int getLength()
{
return mLength;
}
/**
* Utility method to lookup an entry from a value.
*/
public static MCGPMessageType fromValue(int value, int messageByteLength)
{
switch(value)
{
case 0:
if(messageByteLength == 25)
{
return INBOUND_GENERIC_COMMAND;
}
else if(messageByteLength == 70)
{
return OUTBOUND_LOCATION_STATUS;
}
break;
case 1:
return INBOUND_PROGRAMMING_COMMAND;
case 3:
return OUTBOUND_PROGRAMMING_STATUS;
case 4:
return INBOUND_GENERAL_ACKNOWLEDGE;
case 5:
return INBOUND_FORWARD_DATA_COMMAND;
case 7:
return OUTBOUND_FORWARDED_LOGGED_DATA_FRAGMENT;
case 8:
return OUTBOUND_FORWARDED_REALTIME_DATA;
case 9:
//Defer to the message factory to detect inbound vs outbound message type
return INBOUND_MODULAR_REQUEST;
case 10:
return OUTBOUND_FIRMWARE_UPDATE;
}
return UNKNOWN;
}
public static MCGPMessageType getOutboundMessageType(int value)
{
for(MCGPMessageType type: OUTBOUND)
{
if(type.getValue() == value)
{
return type;
}
}
return UNKNOWN;
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -0,0 +1,79 @@
/*
* ******************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.ip.IPacket;
import io.github.dsheirer.module.decode.ip.Packet;
import java.util.Collections;
import java.util.List;
/**
* Cellocator MCGP Packet
*/
public abstract class MCGPPacket extends Packet
{
private MCGPHeader mHeader;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public MCGPPacket(MCGPHeader header, BinaryMessage message, int offset)
{
super(message, offset);
mHeader = header;
}
@Override
public MCGPHeader getHeader()
{
return mHeader;
}
/**
* Unit ID (to/from) for this packet
*/
protected abstract CellocatorRadioIdentifier getRadioId();
@Override
public IPacket getPayload()
{
return null;
}
@Override
public List<Identifier> getIdentifiers()
{
CellocatorRadioIdentifier radioIdentifier = getRadioId();
if(radioIdentifier != null)
{
return Collections.singletonList(radioIdentifier);
}
return Collections.emptyList();
}
}

View File

@@ -0,0 +1,44 @@
package io.github.dsheirer.module.decode.ip.cellocator;
/**
* Modular message data types
*/
public enum ModularDataType
{
FIRMWARE_PLATFORM_MANIFEST(1),
CAN_DATA(2),
CAN_TRIGGER_DATA(3),
TIME_AND_LOCATION_DATA(4),
ACCELEROMETER_DATA(5),
PSP_ALARM_SYSTEM_DATA(6),
USAGE_COUNTER_DATA(7),
COMMAND_AUTHENTICATION_TABLE_DATA(8),
GSM_NEIGHBOR_LIST(9),
MAINTENANCE_SERVER_PLATFORM_MANIFEST(10),
UNKNOWN(0);
private int mValue;
ModularDataType(int value)
{
mValue = value;
}
public int getValue()
{
return mValue;
}
public static ModularDataType fromValue(int value)
{
for(ModularDataType type: ModularDataType.values())
{
if(type.getValue() == value)
{
return type;
}
}
return UNKNOWN;
}
}

View File

@@ -0,0 +1,110 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
/**
* Inbound (from Controller) Modular Request Message
*/
public class ModularRequestMessage extends MCGPPacket
{
//This field is byte reversed (ie big endian)
private static final int[] TARGET_UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMAND_NUMERATOR = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] PACKET_CONTROL = new int[]{88, 89, 90, 91, 92, 93, 94, 95};
private static final int[] TOTAL_MESSAGE_LENGTH = new int[]{96, 97, 98, 99, 100, 101, 102, 103};
private static final int[] SUB_DATA_1_MESSAGE_TYPE = new int[]{104, 105, 106, 107, 108, 109, 110, 111};
private CellocatorRadioIdentifier mTargetRadioId;
private PacketControl mPacketControl;
/**
* Constructs a parser for the message.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public ModularRequestMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
/**
* Static access to message total length (bytes) field.
* @param message containing modular request message
* @param offset to start of modular request message
* @return value from the total message length field
*/
public static int getTotalMessageLength(BinaryMessage message, int offset)
{
return message.getInt(TOTAL_MESSAGE_LENGTH, offset);
}
/**
* Radio/Unit identifier, either source or target as specified in the PacketControl field.
*/
public CellocatorRadioIdentifier getRadioId()
{
if(mTargetRadioId == null)
{
int radioId = getMessage().getInt(TARGET_UNIT_ID, getOffset());
mTargetRadioId = CellocatorRadioIdentifier.createTo(radioId);
}
return mTargetRadioId;
}
public PacketControl getPacketControl()
{
if(mPacketControl == null)
{
mPacketControl = new PacketControl(getMessage().getInt(PACKET_CONTROL, getOffset()));
}
return mPacketControl;
}
public int getCommandNumerator()
{
return getMessage().getInt(COMMAND_NUMERATOR, getOffset());
}
/**
* Modular data type for the first sub-chunk
*/
public ModularDataType getModularDataType()
{
return ModularDataType.fromValue(getMessage().getInt(SUB_DATA_1_MESSAGE_TYPE, getOffset()));
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR RADIO:");
sb.append(getRadioId());
sb.append(" MODULAR REQUEST TYPE:").append(getModularDataType());
sb.append(" MESSAGE NUMBER:").append(getCommandNumerator());
return sb.toString();
}
}

View File

@@ -0,0 +1,139 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
/**
* Modular Message
*/
public class ModularResponseMessage extends MCGPPacket
{
//This field is byte reversed (ie big endian)
private static final int[] UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMUNICATION_CONTROL = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
46, 47};
private static final int[] MESSAGE_NUMERATOR = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] PACKET_CONTROL = new int[]{56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] TOTAL_MESSAGE_LENGTH = new int[]{64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] SUB_DATA_1_MESSAGE_TYPE = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private CellocatorRadioIdentifier mRadioId;
private CommunicationControl mCommunicationControl;
private PacketControl mPacketControl;
/**
* Constructs a parser for the message.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public ModularResponseMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
/**
* Static access to message total length (bytes) field.
* @param message containing modular response message
* @param offset to start of modular response message
* @return value from the total message length field
*/
public static int getTotalMessageLength(BinaryMessage message, int offset)
{
return message.getInt(TOTAL_MESSAGE_LENGTH, offset);
}
/**
* Radio/Unit identifier, either source or target as specified in the PacketControl field.
*/
public CellocatorRadioIdentifier getRadioId()
{
if(mRadioId == null)
{
int radioId = getMessage().getInt(UNIT_ID, getOffset());
if(getPacketControl().getDirection() == PacketControl.Direction.FROM_UNIT)
{
mRadioId = CellocatorRadioIdentifier.createFrom(radioId);
}
else
{
mRadioId = CellocatorRadioIdentifier.createTo(radioId);
}
}
return mRadioId;
}
public PacketControl getPacketControl()
{
if(mPacketControl == null)
{
mPacketControl = new PacketControl(getMessage().getInt(PACKET_CONTROL, getOffset()));
}
return mPacketControl;
}
public CommunicationControl getCommunicationControl()
{
if(mCommunicationControl == null)
{
mCommunicationControl = new CommunicationControl(getMessage().getInt(COMMUNICATION_CONTROL, getOffset()));
}
return mCommunicationControl;
}
public int getMessageNumerator()
{
return getMessage().getInt(MESSAGE_NUMERATOR, getOffset());
}
/**
* Modular data type for the first sub-chunk
*/
public ModularDataType getModularDataType()
{
return ModularDataType.fromValue(getMessage().getInt(SUB_DATA_1_MESSAGE_TYPE, getOffset()));
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR RADIO:");
if(getPacketControl().getDirection() == PacketControl.Direction.FROM_UNIT)
{
sb.append(" FROM:");
}
else
{
sb.append(" TO:");
}
sb.append(getRadioId());
sb.append(" MODULAR TYPE:").append(getModularDataType());
sb.append(" MESSAGE NUMBER:").append(getMessageNumerator());
return sb.toString();
}
}

View File

@@ -0,0 +1,60 @@
package io.github.dsheirer.module.decode.ip.cellocator;
/**
* Provides fragment information for messages that are too long and have to be fragmented.
*/
public class PacketControl
{
public enum Direction {TO_UNIT,FROM_UNIT};
private static final int BIT_MASK_DIRECTION = 0x80;
private static final int BIT_MASK_OUT_OF_SPACE = 0x40;
private int mValue;
/**
* Constructs an instance
* @param value of the fragment control field byte
*/
public PacketControl(int value)
{
mValue = value;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(" MODULAR PACKET ").append(getDirection());
if(isOutOfSpace())
{
sb.append(" (MESSAGE EXCEEDED SPACE)");
}
return sb.toString();
}
/**
* Indicates the origination of the message fragment
* @return origination, unit or application
*/
public Direction getDirection()
{
return isSet(BIT_MASK_DIRECTION) ? Direction.TO_UNIT : Direction.FROM_UNIT;
}
/**
* Indicates if the message contents exceeded the available space in this message
*/
public boolean isOutOfSpace()
{
return isSet(BIT_MASK_OUT_OF_SPACE);
}
/**
* Indicates if the bits that are set in the bit mask are also set in the value field
*/
private boolean isSet(int bitMask)
{
return (mValue & bitMask) == bitMask;
}
}

View File

@@ -0,0 +1,104 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
import org.apache.commons.lang3.StringUtils;
/**
* Controller (Inbound) Programming Command
*/
public class ProgrammingCommandMessage extends MCGPPacket
{
//This field is byte reversed (ie big endian)
private static final int[] TARGET_UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMAND_NUMERATOR = new int[]{32, 33, 34, 35, 36, 37, 38, 39};
//This field is (possibly) byte reversed (ie big endian)
private static final int[] AUTHENTICATION_CODE = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 56, 57, 58, 59, 60, 61,
62, 63, 48, 49, 50, 51, 52, 53, 54, 55, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] BLOCK_CODE = new int[]{72, 73, 74, 75, 76, 77, 78, 79};
private static final int[] BITMASK = new int[]{80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95};
private static final int DATA_BLOCK_START = 96;
private static final int DATA_BLOCK_END = 224;
private CellocatorRadioIdentifier mTargetRadioId;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public ProgrammingCommandMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
public CellocatorRadioIdentifier getRadioId()
{
if(mTargetRadioId == null)
{
mTargetRadioId = CellocatorRadioIdentifier.createTo(getMessage().getInt(TARGET_UNIT_ID, getOffset()));
}
return mTargetRadioId;
}
public int getCommandNumerator()
{
return getMessage().getInt(COMMAND_NUMERATOR, getOffset());
}
public String getAuthenticationCode()
{
return Integer.toHexString(getMessage().getInt(AUTHENTICATION_CODE, getOffset())).toUpperCase();
}
public int getBlockCode()
{
return getMessage().getInt(BLOCK_CODE, getOffset());
}
public String getBitMask()
{
return StringUtils.leftPad(Integer.toHexString(getMessage().getInt(BITMASK, getOffset())).toUpperCase(), 4, "0");
}
public String getDataBlock()
{
return getMessage().getSubMessage(DATA_BLOCK_START + getOffset(), DATA_BLOCK_END + getOffset()).toHexString();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR RADIO:");
sb.append(getRadioId());
sb.append(" PROGRAM SEQUENCE NUMBER:").append(getCommandNumerator());
sb.append(" AUTH:").append(getAuthenticationCode());
sb.append(" BLOCK:").append(getBlockCode());
sb.append(" BITMASK:").append(getBitMask());
sb.append(" DATA:").append(getDataBlock());
return sb.toString();
}
}

View File

@@ -0,0 +1,101 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
/**
* Unit (Outbound) Programming Status/Response
*/
public class ProgrammingStatusMessage extends MCGPPacket
{
//This field is byte reversed (ie big endian)
private static final int[] SOURCE_UNIT_ID = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 16, 17, 18, 19, 20, 21, 22,
23, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] COMMUNICATION_CONTROL = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45,
46, 47};
private static final int[] MESSAGE_NUMERATOR = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] SPARE = new int[]{56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] BLOCK_CODE = new int[]{64, 65, 66, 67, 68, 69, 70, 71};
private static final int DATA_BLOCK_START = 72;
private static final int DATA_BLOCK_END = DATA_BLOCK_START + 128;
private CellocatorRadioIdentifier mSourceRadioId;
private CommunicationControl mCommunicationControl;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public ProgrammingStatusMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
public CellocatorRadioIdentifier getRadioId()
{
if(mSourceRadioId == null)
{
mSourceRadioId = CellocatorRadioIdentifier.createTo(getMessage().getInt(SOURCE_UNIT_ID, getOffset()));
}
return mSourceRadioId;
}
public CommunicationControl getCommunicationControl()
{
if(mCommunicationControl == null)
{
mCommunicationControl = new CommunicationControl(getMessage().getInt(COMMUNICATION_CONTROL, getOffset()));
}
return mCommunicationControl;
}
public int getMessageNumerator()
{
return getMessage().getInt(MESSAGE_NUMERATOR, getOffset());
}
public int getBlockCode()
{
return getMessage().getInt(BLOCK_CODE, getOffset());
}
public String getDataBlock()
{
return getMessage().getSubMessage(DATA_BLOCK_START + getOffset(), DATA_BLOCK_END + getOffset()).toHexString();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR RADIO:");
sb.append(getRadioId());
sb.append(" PROGRAM MESSAGE NUMBER:").append(getMessageNumerator());
sb.append(" BLOCK:").append(getBlockCode());
sb.append(" DATA:").append(getDataBlock());
return sb.toString();
}
}

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* 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.module.decode.ip.cellocator;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.module.decode.ip.IPacket;
import io.github.dsheirer.module.decode.ip.UnknownPacket;
/**
* MCGP Packet - Unknown Message Type
*/
public class UnknownMCGPMessage extends MCGPPacket
{
private IPacket mPayload;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header for this message
* @param message containing the packet
* @param offset to the packet within the message
*/
public UnknownMCGPMessage(MCGPHeader header, BinaryMessage message, int offset)
{
super(header, message, offset);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CELLOCATOR UNKNOWN MESSAGE TYPE [");
sb.append(getHeader().getMessageTypeValue()).append("] ").append(getPayload());
return sb.toString();
}
@Override
public IPacket getPayload()
{
if(mPayload == null)
{
mPayload = new UnknownPacket(getMessage(), getOffset());
}
return mPayload;
}
@Override
protected CellocatorRadioIdentifier getRadioId()
{
return null;
}
}

View File

@@ -21,10 +21,13 @@
package io.github.dsheirer.module.decode.ip.ipv4;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.ip.IPacket;
import io.github.dsheirer.module.decode.ip.Packet;
import io.github.dsheirer.module.decode.ip.PacketMessageFactory;
import io.github.dsheirer.module.decode.ip.UnknownPacket;
import java.util.ArrayList;
import java.util.List;
/**
* IPV4 packet
@@ -33,6 +36,7 @@ public class IPV4Packet extends Packet
{
private IPV4Header mHeader;
private IPacket mPayload;
private List<Identifier> mIdentifiers;
/**
* Constructs an IPV4 packet parser
@@ -109,4 +113,23 @@ public class IPV4Packet extends Packet
return mPayload;
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
if(hasPayload())
{
mIdentifiers.addAll(getPayload().getIdentifiers());
}
mIdentifiers.add(getHeader().getToAddress());
mIdentifiers.add(getHeader().getFromAddress());
}
return mIdentifiers;
}
}

View File

@@ -21,11 +21,15 @@
package io.github.dsheirer.module.decode.ip.udp;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.ip.IPacket;
import io.github.dsheirer.module.decode.ip.Packet;
import io.github.dsheirer.module.decode.ip.PacketMessageFactory;
import io.github.dsheirer.module.decode.ip.UnknownPacket;
import java.util.Collections;
import java.util.List;
public class UDPPacket extends Packet
{
private UDPHeader mHeader;
@@ -73,16 +77,31 @@ public class UDPPacket extends Packet
@Override
public IPacket getPayload()
{
if(mPayload == null && getHeader().isValid())
if(mPayload == null)
{
mPayload = PacketMessageFactory.createUDPPayload(getHeader().getDestinationPort(), getMessage(), getOffset() + getHeader().getLength());
}
else
{
//Set the offset to the message offset plus 64 bits for UDP header
mPayload = new UnknownPacket(getMessage(), getOffset() + 64);
if(getHeader().isValid())
{
mPayload = PacketMessageFactory.createUDPPayload(getHeader().getSourcePort(),
getHeader().getDestinationPort(), getMessage(), getOffset() + getHeader().getLength());
}
else
{
//Set the offset to the message offset plus 64 bits for UDP header
mPayload = new UnknownPacket(getMessage(), getOffset() + 64);
}
}
return mPayload;
}
@Override
public List<Identifier> getIdentifiers()
{
if(hasPayload())
{
return getPayload().getIdentifiers();
}
return Collections.emptyList();
}
}

View File

@@ -25,14 +25,15 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.config.WithCallTimeout;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
public class DecodeConfigMPT1327 extends DecodeConfiguration
public class DecodeConfigMPT1327 extends DecodeConfiguration implements WithCallTimeout
{
private String mChannelMapName;
private Sync mSync = Sync.NORMAL;
private int mCallTimeout = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
private int mCallTimeoutSeconds = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
public DecodeConfigMPT1327()
@@ -68,25 +69,25 @@ public class DecodeConfigMPT1327 extends DecodeConfiguration
}
@JacksonXmlProperty(isAttribute = true, localName = "call_timeout")
public int getCallTimeout()
public int getCallTimeoutSeconds()
{
return mCallTimeout;
return mCallTimeoutSeconds;
}
/**
* Sets the call timeout value in seconds ( 10 - 600 );
* Sets the call timeout value in seconds ( 1 - 180 );
*
* @param timeout
*/
public void setCallTimeout(int timeout)
public void setCallTimeoutSeconds(int timeout)
{
if(CALL_TIMEOUT_MINIMUM <= timeout && timeout <= CALL_TIMEOUT_MAXIMUM)
{
mCallTimeout = timeout;
mCallTimeoutSeconds = timeout;
}
else
{
mCallTimeout = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
mCallTimeoutSeconds = DEFAULT_CALL_TIMEOUT_DELAY_SECONDS;
}
}

View File

@@ -42,7 +42,6 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
@@ -59,26 +58,30 @@ public class MPT1327DecoderState extends DecoderState
private ChannelType mChannelType;
private long mFrequency = 0;
private long mCallTimeout;
private long mCallTimeoutMilliseconds;
private MPT1327TrafficChannelManager mMPT1327TrafficChannelManager;
/**
* Constructs an MPT-1327 decoder state with an optional traffic channel manager for allocating traffic channels
* when go to channel (GTC) messages are detected.
* when go to channel (GTC) messages are detected. This constructor assumes that channel type is STANDARD.
*
* @param trafficChannelManager for managing MPT1327 Traffic Channels
* @param callTimeoutMilliseconds for when traffic channels should automatically be stopped.
*/
public MPT1327DecoderState(MPT1327TrafficChannelManager trafficChannelManager, ChannelType channelType, long callTimeout)
public MPT1327DecoderState(MPT1327TrafficChannelManager trafficChannelManager, ChannelType channelType,
long callTimeoutMilliseconds)
{
mMPT1327TrafficChannelManager = trafficChannelManager;
mChannelType = channelType;
mCallTimeout = callTimeout;
mCallTimeoutMilliseconds = callTimeoutMilliseconds;
}
/**
* Constructs an MPT1327 decoder state that does not allocate traffic channels
*/
public MPT1327DecoderState(ChannelType channelType, long callTimeout)
public MPT1327DecoderState(ChannelType channelType, long callTimeoutMilliseconds)
{
this(null, channelType, callTimeout);
this(null, channelType, callTimeoutMilliseconds);
}
@Override
@@ -250,7 +253,7 @@ public class MPT1327DecoderState extends DecoderState
{
//When we receive a MAINT message and we're configured as a standard channel, apply the call
// timeout specified by the user. Otherwise we'll be using the shorter default call timeout
broadcast(new ChangeChannelTimeoutEvent(this, mChannelType, mCallTimeout));
broadcast(new ChangeChannelTimeoutEvent(this, mChannelType, mCallTimeoutMilliseconds));
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
ic.remove(IdentifierClass.USER);

View File

@@ -325,23 +325,14 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent
switch(channelEvent.getEvent())
{
case NOTIFICATION_PROCESSING_STOP:
MPT1327Channel toRemove = null;
for(Map.Entry<MPT1327Channel,Channel> entry : mAllocatedTrafficChannelMap.entrySet())
{
if(entry.getValue() == channel)
{
toRemove = entry.getKey();
continue;
}
}
final var toRemove = mptChannelForChannel(channel);
if(toRemove != null)
{
mAllocatedTrafficChannelMap.remove(toRemove);
mAvailableTrafficChannelQueue.add(channel);
MPT1327ChannelGrantEvent event = mChannelGrantEventMap.get(toRemove);
final var event = mChannelGrantEventMap.remove(toRemove);
if(event != null)
{
@@ -351,23 +342,14 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent
}
break;
case NOTIFICATION_PROCESSING_START_REJECTED:
MPT1327Channel rejected = null;
for(Map.Entry<MPT1327Channel,Channel> entry : mAllocatedTrafficChannelMap.entrySet())
{
if(entry.getValue() == channel)
{
rejected = entry.getKey();
continue;
}
}
final var rejected = mptChannelForChannel(channel);
if(rejected != null)
{
mAllocatedTrafficChannelMap.remove(rejected);
mAvailableTrafficChannelQueue.add(channel);
MPT1327ChannelGrantEvent event = mChannelGrantEventMap.get(rejected);
final var event = mChannelGrantEventMap.remove(rejected);
if(event != null)
{
@@ -386,5 +368,17 @@ public class MPT1327TrafficChannelManager extends Module implements IDecodeEvent
}
}
}
private MPT1327Channel mptChannelForChannel(final Channel channel)
{
for (final var entry : mAllocatedTrafficChannelMap.entrySet())
{
if (entry.getValue() == channel)
{
return entry.getKey();
}
}
return null;
}
}
}

View File

@@ -67,7 +67,6 @@ import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;
/**
* Monitors channel grant and channel grant update messages to allocate traffic channels to capture
@@ -98,10 +97,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
private Queue<Channel> mAvailablePhase2TrafficChannelQueue = new ConcurrentLinkedQueue<>();
private List<Channel> mManagedPhase2TrafficChannels;
private Map<APCO25Channel,Channel> mAllocatedTrafficChannelMap = new ConcurrentHashMap<>();
private Map<APCO25Channel,P25ChannelGrantEvent> mPhase1ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<APCO25Channel,P25ChannelGrantEvent> mPhase2TS0ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<APCO25Channel,P25ChannelGrantEvent> mPhase2TS1ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<Long,Channel> mAllocatedTrafficChannelMap = new ConcurrentHashMap<>();
private Map<Long,P25ChannelGrantEvent> mTS0ChannelGrantEventMap = new ConcurrentHashMap<>();
private Map<Long,P25ChannelGrantEvent> mTS1ChannelGrantEventMap = new ConcurrentHashMap<>();
private Listener<ChannelEvent> mChannelEventListener;
private Listener<IDecodeEvent> mDecodeEventListener;
@@ -114,10 +112,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
private boolean mIgnoreDataCalls;
/**
* Monitors call events and allocates traffic decoder channels in response
* to traffic channel allocation call events. Manages a pool of reusable
* traffic channel allocations.
*
* Constructs an instance.
* @param parentChannel that owns this traffic channel manager
*/
public P25TrafficChannelManager(Channel parentChannel)
@@ -273,8 +268,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
private void processPhase1ChannelGrant(APCO25Channel apco25Channel, ServiceOptions serviceOptions,
IdentifierCollection identifierCollection, Opcode opcode, long timestamp)
{
long frequency = apco25Channel.getDownlinkFrequency();
P25ChannelGrantEvent event = mPhase1ChannelGrantEventMap.get(apco25Channel);
P25ChannelGrantEvent event = mTS0ChannelGrantEventMap.get(frequency);
if(event != null && isSameCall(identifierCollection, event.getIdentifierCollection()))
{
@@ -294,7 +290,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, continuationGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, continuationGrantEvent);
broadcast(continuationGrantEvent);
}
}
@@ -305,7 +301,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
//Even though we have an event, the initial channel grant may have been rejected. Check to see if there
//is a traffic channel allocated. If not, allocate one and update the event description.
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel) && !(mIgnoreDataCalls && opcode.isDataChannelGrant()))
if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !(mIgnoreDataCalls && opcode.isDataChannelGrant()))
{
Channel trafficChannel = mAvailablePhase1TrafficChannelQueue.poll();
@@ -316,9 +312,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
event.setChannelDescriptor(apco25Channel);
broadcast(event);
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection));
}
}
@@ -335,8 +331,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
broadcast(channelGrantEvent);
return;
}
@@ -348,11 +343,10 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
//Allocate a traffic channel for the downlink frequency if one isn't already allocated
//NOTE: we could also allocate a traffic channel for the uplink frequency here, in the future
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel))
if(!mAllocatedTrafficChannelMap.containsKey(frequency))
{
Channel trafficChannel = mAvailablePhase1TrafficChannelQueue.poll();
@@ -364,9 +358,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
}
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection));
}
@@ -392,16 +386,17 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
}
int timeslot = apco25Channel.getTimeslot();
long frequency = apco25Channel.getDownlinkFrequency();
P25ChannelGrantEvent event = null;
if(timeslot == 0)
{
event = mPhase2TS0ChannelGrantEventMap.get(apco25Channel);
event = mTS0ChannelGrantEventMap.get(frequency);
}
else if(timeslot == 1)
{
event = mPhase2TS1ChannelGrantEventMap.get(apco25Channel);
event = mTS1ChannelGrantEventMap.get(frequency);
}
else
{
@@ -431,11 +426,11 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
if(timeslot == 0)
{
mPhase2TS0ChannelGrantEventMap.put(apco25Channel, continuationGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, continuationGrantEvent);
}
else
{
mPhase2TS1ChannelGrantEventMap.put(apco25Channel, continuationGrantEvent);
mTS1ChannelGrantEventMap.put(frequency, continuationGrantEvent);
}
broadcast(continuationGrantEvent);
@@ -448,8 +443,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
//Even though we have an event, the initial channel grant may have been rejected. Check to see if there
//is a traffic channel allocated. If not, allocate one and update the event description.
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel) &&
!(mIgnoreDataCalls && opcode.isDataChannelGrant()))
if(!mAllocatedTrafficChannelMap.containsKey(frequency) && !(mIgnoreDataCalls && opcode.isDataChannelGrant()))
{
Channel trafficChannel = mAvailablePhase2TrafficChannelQueue.poll();
@@ -460,9 +454,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
event.setChannelDescriptor(apco25Channel);
broadcast(event);
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
//If we have valid scramble/randomizer parameters, set them in the decode config
if(mPhase2ScrambleParameters != null)
@@ -487,7 +481,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.identifiers(identifierCollection)
.build();
mPhase1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
broadcast(channelGrantEvent);
return;
@@ -502,16 +496,15 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
if(timeslot == 0)
{
mPhase2TS0ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS0ChannelGrantEventMap.put(frequency, channelGrantEvent);
}
else
{
mPhase2TS1ChannelGrantEventMap.put(apco25Channel, channelGrantEvent);
mTS1ChannelGrantEventMap.put(frequency, channelGrantEvent);
}
//Allocate a traffic channel for the downlink frequency if one isn't already allocated
//NOTE: we could also allocate a traffic channel for the uplink frequency here, in the future
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel))
if(!mAllocatedTrafficChannelMap.containsKey(apco25Channel.getDownlinkFrequency()))
{
Channel trafficChannel = mAvailablePhase2TrafficChannelQueue.poll();
@@ -523,9 +516,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
}
SourceConfigTuner sourceConfig = new SourceConfigTuner();
sourceConfig.setFrequency(apco25Channel.getDownlinkFrequency());
sourceConfig.setFrequency(frequency);
trafficChannel.setSourceConfiguration(sourceConfig);
mAllocatedTrafficChannelMap.put(apco25Channel, trafficChannel);
mAllocatedTrafficChannelMap.put(frequency, trafficChannel);
broadcast(new ChannelGrantEvent(trafficChannel, Event.REQUEST_ENABLE, apco25Channel, identifierCollection));
}
@@ -660,6 +653,11 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
{
broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE));
}
for(Channel trafficChannel : mAvailablePhase2TrafficChannelQueue)
{
broadcast(new ChannelEvent(trafficChannel, Event.REQUEST_DISABLE));
}
}
/**
@@ -701,6 +699,9 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
mLog.info("Stopping traffic channel: " + channel);
broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE));
}
mTS0ChannelGrantEventMap.clear();
mTS1ChannelGrantEventMap.clear();
}
/**
@@ -715,21 +716,16 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
{
if(mMessageListener == null)
{
mMessageListener = new Listener<IMessage>()
{
@Override
public void receive(IMessage message)
mMessageListener = message -> {
if(mPhase2ScrambleParameters == null && message.isValid())
{
if(mPhase2ScrambleParameters == null && message.isValid())
if(message instanceof NetworkStatusBroadcast)
{
if(message instanceof NetworkStatusBroadcast)
{
mPhase2ScrambleParameters = ((NetworkStatusBroadcast)message).getScrambleParameters();
}
else if(message instanceof AMBTCNetworkStatusBroadcast)
{
mPhase2ScrambleParameters = ((AMBTCNetworkStatusBroadcast)message).getScrambleParameters();
}
mPhase2ScrambleParameters = ((NetworkStatusBroadcast)message).getScrambleParameters();
}
else if(message instanceof AMBTCNetworkStatusBroadcast)
{
mPhase2ScrambleParameters = ((AMBTCNetworkStatusBroadcast)message).getScrambleParameters();
}
}
};
@@ -770,24 +766,22 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
public class TrafficChannelTeardownMonitor implements Listener<ChannelEvent>
{
/**
* Removes any call events that are associated with the specified channel.
* @param channel that was removed
* Removes an allocated traffic channel and adds it to the available channel queue
* @param channel to reset
* @param frequency of the channel
* @param isPhase1 true or false if it is a phase 2 channel
*/
private void cleanupCallEvents(Channel channel)
private void resetTrafficChannel(Channel channel, long frequency, boolean isPhase1)
{
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
mAllocatedTrafficChannelMap.remove(frequency);
if(channel.getDecodeConfiguration().getDecoderType() == DecoderType.P25_PHASE1)
{
mPhase1ChannelGrantEventMap.entrySet().removeIf(entry -> entry.getKey().getDownlinkFrequency() == frequency);
}
else
{
mPhase2TS0ChannelGrantEventMap.entrySet().removeIf(entry -> entry.getKey().getDownlinkFrequency() == frequency);
mPhase2TS1ChannelGrantEventMap.entrySet().removeIf(entry -> entry.getKey().getDownlinkFrequency() == frequency);
}
if(isPhase1)
{
mAvailablePhase1TrafficChannelQueue.add(channel);
}
else
{
mAvailablePhase2TrafficChannelQueue.add(channel);
}
}
@@ -810,48 +804,43 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
.filter(entry -> entry.getValue() == channel)
.map(Map.Entry::getKey)
.findFirst()
.ifPresent(toRemove -> {
mAllocatedTrafficChannelMap.remove(toRemove);
if (isPhase1) {
mAvailablePhase1TrafficChannelQueue.add(channel);
} else {
mAvailablePhase2TrafficChannelQueue.add(channel);
}
.ifPresent(frequencyToRemove -> {
resetTrafficChannel(channel, frequencyToRemove, isPhase1);
mTS0ChannelGrantEventMap.remove(frequencyToRemove);
mTS1ChannelGrantEventMap.remove(frequencyToRemove);
});
cleanupCallEvents(channel);
break;
case NOTIFICATION_PROCESSING_START_REJECTED:
mAllocatedTrafficChannelMap.entrySet().stream()
.filter(entry -> entry.getValue() == channel)
.map(Map.Entry::getKey)
.findFirst()
.ifPresent(rejected -> {
mAllocatedTrafficChannelMap.remove(rejected);
.ifPresent(rejectedFrequency -> {
resetTrafficChannel(channel, rejectedFrequency, isPhase1);
if (isPhase1) {
mAvailablePhase1TrafficChannelQueue.add(channel);
} else {
mAvailablePhase2TrafficChannelQueue.add(channel);
P25ChannelGrantEvent event = mTS0ChannelGrantEventMap.remove(rejectedFrequency);
if(event == null)
{
event = mTS1ChannelGrantEventMap.remove(rejectedFrequency);
}
P25ChannelGrantEvent event = mPhase1ChannelGrantEventMap.get(rejected);
if (event != null) {
if (event != null)
{
event.setEventDescription(event.getEventDescription() + " - Rejected");
if (channelEvent.getDescription() != null) {
if (channelEvent.getDescription() != null)
{
event.setDetails(channelEvent.getDescription() + " - " + event.getDetails());
} else {
}
else
{
event.setDetails(CHANNEL_START_REJECTED + " - " + event.getDetails());
}
broadcast(event);
}
});
cleanupCallEvents(channel);
break;
}
}

View File

@@ -32,7 +32,6 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration
{
private P25P1Decoder.Modulation mModulation = P25P1Decoder.Modulation.C4FM;
private int mCallTimeout = 1;
private int mTrafficChannelPoolSize = TRAFFIC_CHANNEL_LIMIT_DEFAULT;
private boolean mIgnoreDataCalls = false;
@@ -68,28 +67,6 @@ public class DecodeConfigP25Phase1 extends DecodeConfiguration
mIgnoreDataCalls = ignore;
}
/**
* Note: this field is now deprecated.
*
* @return
*/
@JsonIgnore
@Deprecated
public int getCallTimeout()
{
return mCallTimeout;
}
/**
* Sets the call timeout value in seconds ( 10 - 600 );
*
* @param timeout
*/
@Deprecated
public void setCallTimeout(int timeout)
{
}
@JacksonXmlProperty(isAttribute = true, localName = "traffic_channel_pool_size")
public int getTrafficChannelPoolSize()

View File

@@ -43,6 +43,7 @@ import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.ip.IPacket;
import io.github.dsheirer.module.decode.ip.ars.ARSPacket;
import io.github.dsheirer.module.decode.ip.cellocator.MCGPPacket;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
import io.github.dsheirer.module.decode.ip.udp.UDPPacket;
import io.github.dsheirer.module.decode.p25.P25DecodeEvent;
@@ -935,6 +936,10 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
ARSPacket arsPacket = (ARSPacket)udpPayload;
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier: packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent packetEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
@@ -945,9 +950,32 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
broadcast(packetEvent);
}
else if(udpPayload instanceof MCGPPacket)
{
MCGPPacket mcgpPacket = (MCGPPacket)udpPayload;
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier: packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent cellocatorEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
.eventDescription("Cellocator")
.identifiers(ic)
.details(mcgpPacket.toString() + " " + ipv4.toString())
.build();
broadcast(cellocatorEvent);
}
else
{
MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
for(Identifier identifier: packet.getIdentifiers())
{
ic.update(identifier);
}
DecodeEvent packetEvent = P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())

View File

@@ -25,7 +25,6 @@ import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.alias.id.record.Record;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.bits.BitSetFullException;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
@@ -34,7 +33,6 @@ import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageProvider;
import io.github.dsheirer.message.Message;
import io.github.dsheirer.message.MessageProviderModule;
import io.github.dsheirer.message.StuffBitsMessage;
@@ -42,23 +40,17 @@ import io.github.dsheirer.message.SyncLossMessage;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P2AudioModule;
import io.github.dsheirer.module.decode.p25.phase1.message.P25Message;
import io.github.dsheirer.module.decode.p25.phase1.message.P25MessageFactory;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUMessageFactory;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.PDUSequence;
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.packet.PacketMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessageFactory;
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
import io.github.dsheirer.module.decode.p25.phase2.P25P2DecoderState;
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageFramer;
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageProcessor;
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.record.AudioRecordingManager;
import io.github.dsheirer.record.binary.BinaryReader;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableByteBuffer;
import org.slf4j.Logger;
@@ -201,6 +193,17 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
{
mBinaryMessage.add(dibit.getBit1());
mBinaryMessage.add(dibit.getBit2());
if(mBinaryMessage.isFull())
{
//TDU's have a trailing status symbol that has to be removed -- set flag to true to suppress it.
if(mDataUnitID.hasTrailingStatusDibit())
{
mTrailingDibitsToSuppress = 1;
}
dispatchMessage();
}
}
catch(BitSetFullException bsfe)
{
@@ -209,17 +212,6 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
//Reset so that we can start over again
reset(0);
}
if(mBinaryMessage.isFull())
{
//TDU's have a trailing status symbol that has to be removed -- set flag to true to suppress it.
if(mDataUnitID.hasTrailingStatusDibit())
{
mTrailingDibitsToSuppress = 1;
}
dispatchMessage();
}
}
else
{
@@ -458,7 +450,7 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
public static void main(String[] args)
{
Path directory = Paths.get("/home/denny/temp/Harris P25-2 Logs/bits");
Path directory = Paths.get("/home/denny/Documents/TMR/APCO25/GPS/cnyicc_cellocator");
// Path directory = Paths.get("/media/denny/500G1EXT4/PBITRecordings");
UserPreferences userPreferences = new UserPreferences();
@@ -479,11 +471,13 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
MessageProviderModule messageProviderModule = new MessageProviderModule();
processingChain.addModule(messageProviderModule);
mLog.info("Processing Directory: " + directory.toString());
try(OutputStream logOutput = Files.newOutputStream(directory.resolve("log.txt")))
{
try
{
DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*PHASE1_Hills*TRAFFIC.bits");
DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*.bits");
stream.forEach(new Consumer<Path>()
{
@@ -520,7 +514,11 @@ public class P25P1MessageFramer implements Listener<Dibit>, IP25P1DataUnitDetect
{
mLog.error("Error", ioe);
}
mLog.debug(message.toString());
if(message instanceof PacketMessage)
{
mLog.debug(message.toString());
}
}
}
});

View File

@@ -264,11 +264,9 @@ public class PacketMessage extends P25Message
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getPDUSequence().getHeader().getLLID());
if(getPacket() instanceof IPV4Packet)
if(getPacket() != null)
{
IPV4Packet ipv4 = (IPV4Packet)getPacket();
mIdentifiers.add(ipv4.getHeader().getFromAddress());
mIdentifiers.add(ipv4.getHeader().getToAddress());
mIdentifiers.addAll(getPacket().getIdentifiers());
}
}

View File

@@ -32,6 +32,7 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.log.EventLogManager;
@@ -113,10 +114,7 @@ public class PlaylistManager implements Listener<ChannelEvent>
mAliasModel.aliasList().addListener((ListChangeListener<Alias>)c -> schedulePlaylistSave());
mChannelMapModel.addListener(t -> {
//Save the playlist for all channel map events
schedulePlaylistSave();
});
mChannelMapModel.getChannelMaps().addListener((ListChangeListener<ChannelMap>)c -> schedulePlaylistSave());
mBroadcastModel.addListener(broadcastEvent -> {
switch(broadcastEvent.getEvent())

View File

@@ -26,6 +26,7 @@ public enum PreferenceType
{
DECODE_EVENT,
DIRECTORY,
DUPLICATE_CALL_DETECTION,
JMBE_LIBRARY,
MULTI_FREQUENCY,
PLAYLIST,

View File

@@ -22,6 +22,7 @@ package io.github.dsheirer.preference;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
import io.github.dsheirer.preference.directory.DirectoryPreference;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import io.github.dsheirer.preference.event.DecodeEventPreference;
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
import io.github.dsheirer.preference.javafx.JavaFxPreferences;
@@ -52,16 +53,18 @@ import io.github.dsheirer.sample.Listener;
*/
public class UserPreferences implements Listener<PreferenceType>
{
private JmbeLibraryPreference mJmbeLibraryPreference;
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
private DecodeEventPreference mDecodeEventPreference;
private DirectoryPreference mDirectoryPreference;
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
private JmbeLibraryPreference mJmbeLibraryPreference;
private PlaybackPreference mPlaybackPreference;
private PlaylistPreference mPlaylistPreference;
private RadioReferencePreference mRadioReferencePreference;
private RecordPreference mRecordPreference;
private TalkgroupFormatPreference mTalkgroupFormatPreference;
private TunerPreference mTunerPreference;
private SwingPreference mSwingPreference = new SwingPreference();
private JavaFxPreferences mJavaFxPreferences = new JavaFxPreferences();
@@ -170,15 +173,24 @@ public class UserPreferences implements Listener<PreferenceType>
return mSwingPreference;
}
/**
* Duplicate call detection preferences
*/
public DuplicateCallDetectionPreference getDuplicateCallDetectionPreference()
{
return mDuplicateCallDetectionPreference;
}
/**
* Loads the managed preferences
*/
private void loadPreferenceTypes()
{
mDecodeEventPreference = new DecodeEventPreference(this::receive);
mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive);
mDirectoryPreference = new DirectoryPreference(this::receive);
mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive);
mDecodeEventPreference = new DecodeEventPreference(this::receive);
mDirectoryPreference = new DirectoryPreference(this::receive);
mDuplicateCallDetectionPreference = new DuplicateCallDetectionPreference(this::receive);
mJmbeLibraryPreference = new JmbeLibraryPreference(this::receive);
mPlaybackPreference = new PlaybackPreference(this::receive);
mPlaylistPreference = new PlaylistPreference(this::receive, mDirectoryPreference);
mRadioReferencePreference = new RadioReferencePreference(this::receive);

View File

@@ -22,6 +22,7 @@
package io.github.dsheirer.preference.decoder;
import io.github.dsheirer.jmbe.github.Version;
import io.github.dsheirer.preference.Preference;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
@@ -32,6 +33,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Decoder preferences
@@ -42,7 +45,10 @@ public class JmbeLibraryPreference extends Preference
private Preferences mPreferences = Preferences.userNodeForPackage(JmbeLibraryPreference.class);
private static final String PREFERENCE_KEY_PATH_JMBE_LIBRARY = "path.jmbe.library.1.0.0";
private static final String PREFERENCE_KEY_PATH_ALERT_LIBRARY_REQUIRED = "alert.jmbe.required";
private final Pattern VERSION_PATTERN = Pattern.compile(".*jmbe-(\\d{1,5}.\\d{1,5}.\\d{1,5}\\w*)\\.jar");
private Path mPathJmbeLibrary;
private Boolean mAlertIfMissingLibraryRequired;
/**
* Constructs this preference with an update listener
@@ -60,6 +66,23 @@ public class JmbeLibraryPreference extends Preference
return PreferenceType.JMBE_LIBRARY;
}
public Version getCurrentVersion()
{
Path path = getPathJmbeLibrary();
if(path != null)
{
Matcher m = VERSION_PATTERN.matcher(path.toString());
if(m.matches())
{
return Version.fromString(m.group(1));
}
}
return null;
}
/*
* Path to the JMBE audio codec library
*/
@@ -73,6 +96,14 @@ public class JmbeLibraryPreference extends Preference
return mPathJmbeLibrary;
}
/**
* Indicates if a path to a JMBE library is setup (ie non-null).
*/
public boolean hasJmbeLibraryPath()
{
return getPathJmbeLibrary() != null;
}
/**
* Sets the path to the JMBE library
*/
@@ -93,6 +124,30 @@ public class JmbeLibraryPreference extends Preference
notifyPreferenceUpdated();
}
/**
* Indicates if the user should be alerted when the JMBE library is not setup, but required for decoding.
*/
public boolean getAlertIfMissingLibraryRequired()
{
if(mAlertIfMissingLibraryRequired == null)
{
mAlertIfMissingLibraryRequired = mPreferences.getBoolean(PREFERENCE_KEY_PATH_ALERT_LIBRARY_REQUIRED, true);
}
return mAlertIfMissingLibraryRequired;
}
/**
* Sets the value for alerting when the library is required for decoding but the library is not currently setup.
* @param alert true if the user should be alerted when the library is not setup, but required for decoding.
*/
public void setAlertIfMissingLibraryRequired(boolean alert)
{
mAlertIfMissingLibraryRequired = alert;
mPreferences.putBoolean(PREFERENCE_KEY_PATH_ALERT_LIBRARY_REQUIRED, mAlertIfMissingLibraryRequired);
notifyPreferenceUpdated();
}
/**
* Returns the path stored in preferences and referenced by the key argument if it exists, otherwise returns the
* default path.

View File

@@ -42,6 +42,7 @@ public class DirectoryPreference extends Preference
private static final String DIRECTORY_APPLICATION_ROOT = "SDRTrunk";
private static final String DIRECTORY_APPLICATION_LOG = "logs";
private static final String DIRECTORY_EVENT_LOG = "event_logs";
private static final String DIRECTORY_JMBE = "jmbe";
private static final String DIRECTORY_PLAYLIST = "playlist";
private static final String DIRECTORY_RECORDING = "recordings";
private static final String DIRECTORY_SCREEN_CAPTURE = "screen_captures";
@@ -50,6 +51,7 @@ public class DirectoryPreference extends Preference
private static final String PREFERENCE_KEY_DIRECTORY_APPLICATION_ROOT = "directory.application.root";
private static final String PREFERENCE_KEY_DIRECTORY_APPLICATION_LOGS = "directory.application.logs";
private static final String PREFERENCE_KEY_DIRECTORY_EVENT_LOGS = "directory.event.logs";
private static final String PREFERENCE_KEY_DIRECTORY_JMBE = "directory.jmbe";
private static final String PREFERENCE_KEY_DIRECTORY_PLAYLIST = "directory.playlist";
private static final String PREFERENCE_KEY_DIRECTORY_RECORDING = "directory.recording";
private static final String PREFERENCE_KEY_DIRECTORY_SCREEN_CAPTURE = "directory.screen.capture";
@@ -58,6 +60,7 @@ public class DirectoryPreference extends Preference
private Path mDirectoryApplicationRoot;
private Path mDirectoryApplicationLogs;
private Path mDirectoryEventLogs;
private Path mDirectoryJmbe;
private Path mDirectoryPlaylist;
private Path mDirectoryRecording;
private Path mDirectoryScreenCapture;
@@ -115,6 +118,7 @@ public class DirectoryPreference extends Preference
{
mDirectoryApplicationLogs = null;
mDirectoryEventLogs = null;
mDirectoryJmbe = null;
mDirectoryPlaylist = null;
mDirectoryRecording = null;
mDirectoryScreenCapture = null;
@@ -204,6 +208,40 @@ public class DirectoryPreference extends Preference
notifyPreferenceUpdated();
}
/**
* Path to the folder for storing the JMBE audio library
*/
public Path getDirectoryJmbe()
{
if(mDirectoryJmbe == null)
{
mDirectoryJmbe = getPath(PREFERENCE_KEY_DIRECTORY_JMBE, getDefaultJmbeDirectory());
createDirectory(mDirectoryJmbe);
}
return mDirectoryJmbe;
}
/**
* Sets the path to the JMBE folder
*/
public void setDirectoryJmbe(Path path)
{
mDirectoryJmbe = path;
mPreferences.put(PREFERENCE_KEY_DIRECTORY_JMBE, path.toString());
notifyPreferenceUpdated();
}
/**
* Removes a stored playlist directory preference so that the default path can be used again
*/
public void resetDirectoryJmbe()
{
mPreferences.remove(PREFERENCE_KEY_DIRECTORY_JMBE);
mDirectoryJmbe = null;
notifyPreferenceUpdated();
}
/**
* Path to the folder for storing playlist
*/
@@ -364,6 +402,14 @@ public class DirectoryPreference extends Preference
return getDirectoryApplicationRoot().resolve(DIRECTORY_EVENT_LOG);
}
/**
* Default JMBE directory
*/
public Path getDefaultJmbeDirectory()
{
return getDirectoryApplicationRoot().resolve(DIRECTORY_JMBE);
}
/**
* Default playlist directory
*/

View File

@@ -0,0 +1,187 @@
/*
* *****************************************************************************
* 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.preference.duplicate;
import io.github.dsheirer.preference.Preference;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.prefs.Preferences;
/**
* User preferences for duplicate call detection
*/
public class DuplicateCallDetectionPreference extends Preference
{
private static final String PREFERENCE_KEY_DETECT_DUPLICATE_TALKGROUP = "duplicate.call.detect.talkgroup";
private static final String PREFERENCE_KEY_DETECT_DUPLICATE_RADIO = "duplicate.call.detect.radio";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_PLAYBACK = "suppress.duplicate.audio.playback";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_RECORDING = "suppress.duplicate.audio.recording";
private static final String PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING = "suppress.duplicate.audio.streaming";
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetectionPreference.class);
private Preferences mPreferences = Preferences.userNodeForPackage(DuplicateCallDetectionPreference.class);
private Boolean mDuplicateCallDetectionByTalkgroupEnabled;
private Boolean mDuplicateCallDetectionByRadioEnabled;
private Boolean mDuplicatePlaybackSuppressionEnabled;
private Boolean mDuplicateRecordingSuppressionEnabled;
private Boolean mDuplicateStreamingSuppressionEnabled;
/**
* Constructs an instance
* @param updateListener to receive notifications that a preference has been updated
*/
public DuplicateCallDetectionPreference(Listener<PreferenceType> updateListener)
{
super(updateListener);
}
@Override
public PreferenceType getPreferenceType()
{
return PreferenceType.DUPLICATE_CALL_DETECTION;
}
/**
* Indicates if duplicate call detection is enabled for either talkgroups or radio identifiers.
*/
public boolean isDuplicateCallDetectionEnabled()
{
return isDuplicateCallDetectionByTalkgroupEnabled() || isDuplicateCallDetectionByRadioEnabled();
}
/**
* Preference for detecting duplicate calls by talkgroup or patchgroup
*/
public boolean isDuplicateCallDetectionByTalkgroupEnabled()
{
if(mDuplicateCallDetectionByTalkgroupEnabled == null)
{
mDuplicateCallDetectionByTalkgroupEnabled =
mPreferences.getBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_TALKGROUP, true);
}
return mDuplicateCallDetectionByTalkgroupEnabled;
}
/**
* Sets the preference for detecting duplicate calls by talkgroup
*/
public void setDuplicateCallDetectionByTalkgroupEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_TALKGROUP, enabled);
mDuplicateCallDetectionByTalkgroupEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for detecting duplicate calls by radio identifier
*/
public boolean isDuplicateCallDetectionByRadioEnabled()
{
if(mDuplicateCallDetectionByRadioEnabled == null)
{
mDuplicateCallDetectionByRadioEnabled = mPreferences.getBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_RADIO, false);
}
return mDuplicateCallDetectionByRadioEnabled;
}
/**
* Sets the preference for detecting duplicate calls by radio identifier
*/
public void setDuplicateCallDetectionByRadioEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_DETECT_DUPLICATE_RADIO, enabled);
mDuplicateCallDetectionByRadioEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for suppressig playback of duplicate calls.
*/
public boolean isDuplicatePlaybackSuppressionEnabled()
{
if(mDuplicatePlaybackSuppressionEnabled == null)
{
mDuplicatePlaybackSuppressionEnabled = mPreferences.getBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_PLAYBACK, true);
}
return mDuplicatePlaybackSuppressionEnabled;
}
/**
* Sets the preference for suppressing playback of duplicate calls.
*/
public void setDuplicatePlaybackSuppressionEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_PLAYBACK, enabled);
mDuplicatePlaybackSuppressionEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for suppressing recording of duplicate calls.
*/
public boolean isDuplicateRecordingSuppressionEnabled()
{
if(mDuplicateRecordingSuppressionEnabled == null)
{
mDuplicateRecordingSuppressionEnabled = mPreferences.getBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_RECORDING, true);
}
return mDuplicateRecordingSuppressionEnabled;
}
/**
* Sets the preference for suppressing recording of duplicate calls.
*/
public void setDuplicateRecordingSuppressionEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_RECORDING, enabled);
mDuplicateRecordingSuppressionEnabled = enabled;
notifyPreferenceUpdated();
}
/**
* Preference for suppressing streaming of duplicate calls
*/
public boolean isDuplicateStreamingSuppressionEnabled()
{
if(mDuplicateStreamingSuppressionEnabled == null)
{
mDuplicateStreamingSuppressionEnabled = mPreferences.getBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING, true);
}
return mDuplicateStreamingSuppressionEnabled;
}
/**
* Sets the preference for suppressing streaming of duplicate calls
*/
public void setDuplicateStreamingSuppressionEnabled(boolean enabled)
{
mPreferences.putBoolean(PREFERENCE_KEY_SUPPRESS_DUPLICATE_STREAMING, enabled);
mDuplicateStreamingSuppressionEnabled = enabled;
notifyPreferenceUpdated();;
}
}

View File

@@ -30,6 +30,7 @@ public enum Protocol
{
APCO25("APCO-25", "APCO25PHASE1", 9600),
APCO25_PHASE2("APCO-25 P2", "APCO25PHASE2", 12000),
CELLOCATOR("CELLOCATOR", "CELLOCATOR", 0),
FLEETSYNC("Fleetsync", "FLEETSYNC", 1200),
IPV4("IPV4", "IPV4", 0),
LOJACK("LoJack", "LOJACK", 1200),

View File

@@ -139,18 +139,25 @@ public class AudioRecordingManager implements Listener<AudioSegment>
while(audioSegment != null)
{
Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat);
try
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateRecordingSuppressionEnabled())
{
AudioSegmentRecorder.record(audioSegment, path, recordFormat);
audioSegment.decrementConsumerCount();
}
catch(IOException ioe)
else
{
mLog.error("Error recording audio segment to [" + path.toString() + "]");
}
Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat);
audioSegment.decrementConsumerCount();
try
{
AudioSegmentRecorder.record(audioSegment, path, recordFormat);
}
catch(IOException ioe)
{
mLog.error("Error recording audio segment to [" + path.toString() + "]");
}
audioSegment.decrementConsumerCount();
}
//Grab the next one to record
audioSegment = mCompletedAudioSegmentQueue.poll();

View File

@@ -20,7 +20,6 @@ import io.github.dsheirer.sample.adapter.RealChannelShortAdapter;
import io.github.dsheirer.sample.adapter.RealShortAdapter;
import io.github.dsheirer.source.config.SourceConfigMixer;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.MixerTunerDataLine;
import io.github.dsheirer.source.tuner.MixerTunerType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,7 +34,6 @@ import javax.sound.sampled.Port;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
@@ -46,7 +44,10 @@ public class MixerManager
{
private final static Logger mLog = LoggerFactory.getLogger(MixerManager.class);
public MixerManager() {}
public MixerManager()
{
}
public static RealMixerSource getSource(SourceConfiguration config)
{
@@ -193,33 +194,69 @@ public class MixerManager
return outputMixers;
}
public static Collection<MixerTunerDataLine> getMixerTunerDataLines()
/**
* Obtains a target data line for the specified mixer tuner type
* @param mixerTunerType to find
* @return target data line or null.
*/
public static TargetDataLine getTunerTargetDataLine(MixerTunerType mixerTunerType)
{
List<MixerTunerDataLine> tuners = new ArrayList<>();
for(Mixer.Info mixerInfo : AudioSystem.getMixerInfo())
{
//Sort between the mixers and the tuner mixers, and load each
MixerTunerType mixerTunerType = MixerTunerType.getMixerTunerType(mixerInfo);
MixerTunerType type = MixerTunerType.getMixerTunerType(mixerInfo);
if(mixerTunerType != MixerTunerType.UNKNOWN)
if(type != null && type == mixerTunerType)
{
TargetDataLine tdl = getTargetDataLine(mixerInfo, mixerTunerType.getAudioFormat());
Mixer mixer = AudioSystem.getMixer(mixerInfo);
if(tdl != null)
if(mixer != null)
{
switch(mixerTunerType)
for(Line.Info info : mixer.getTargetLineInfo())
{
case FUNCUBE_DONGLE_PRO:
case FUNCUBE_DONGLE_PRO_PLUS:
tuners.add(new MixerTunerDataLine(tdl, mixerTunerType));
break;
if(info instanceof DataLine.Info)
{
try
{
Line line = mixer.getLine(info);
if(line instanceof TargetDataLine)
{
return (TargetDataLine)line;
}
}
catch(LineUnavailableException lue)
{
mLog.error("Line Unavailable. Unable to get TargetDataLine for Mixer Tuner: " + mixerTunerType);
}
}
}
}
}
}
return tuners;
return null;
}
/**
* Finds the target data line supported format that most closely matches the specified audio format without
* matching sample rates.
* @param info for a data line
* @param audioFormat to find a match
* @return matching audio format or null.
*/
public static AudioFormat getMatchingFormat(DataLine.Info info, AudioFormat audioFormat)
{
for(AudioFormat audioFormatToTest: info.getFormats())
{
if(audioFormat.getSampleSizeInBits() == audioFormatToTest.getSampleSizeInBits() &&
audioFormat.getChannels() == audioFormatToTest.getChannels() &&
!(audioFormat.isBigEndian() ^ audioFormatToTest.isBigEndian()))
{
return audioFormatToTest;
}
}
return null;
}
private static TargetDataLine getTargetDataLine(Mixer.Info mixerInfo, AudioFormat format)
@@ -232,6 +269,20 @@ public class MixerManager
{
try
{
for(Line line: mixer.getTargetLines())
{
mLog.debug("Line: " + line.getLineInfo().toString());
}
for(Line line: mixer.getSourceLines())
{
mLog.debug("Line: " + line.getLineInfo().toString());
}
Mixer.Info info = mixer.getMixerInfo();
mLog.debug(info.toString());
DataLine.Info datalineInfo = new DataLine.Info(TargetDataLine.class, format);
retVal = (TargetDataLine)mixer.getLine(datalineInfo);
@@ -318,6 +369,11 @@ public class MixerManager
.append("\n");
Mixer mixer = AudioSystem.getMixer(mixerInfo);
Line.Info lineInfo1 = mixer.getLineInfo();
String a = lineInfo1.toString();
Line.Info[] infos = mixer.getTargetLineInfo();
Line.Info[] sourceLines = mixer.getSourceLineInfo();
@@ -337,6 +393,41 @@ public class MixerManager
.append("\n class:").append(lineInfo.getClass())
.append("\n lineclass:").append(lineInfo.getLineClass())
.append("\n");
if(lineInfo instanceof DataLine.Info)
{
DataLine.Info dli = (DataLine.Info)lineInfo;
for(AudioFormat format: dli.getFormats())
{
sb.append(" FORMAT:").append(format.toString()).append("\n");
if(mixerInfo.getName().startsWith("V"))
{
sb.append("Iterating formats for " + mixerInfo.getName() + " " + mixerInfo.getDescription()).append("\n");
try
{
Line line = mixer.getLine(lineInfo);
sb.append(line.getLineInfo().toString()).append("\n");
if(line instanceof TargetDataLine)
{
TargetDataLine tdl = (TargetDataLine)line;
tdl.open();
tdl.start();
byte[] bytes = new byte[1024];
int read = tdl.read(bytes, 0, bytes.length);
sb.append("READ:" + read).append("\n");
tdl.close();
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
}
}
Line.Info portInfo = new Line.Info(Port.class);
@@ -389,9 +480,35 @@ public class MixerManager
public static void main(String[] args)
{
for(MixerChannelConfiguration config : getOutputMixers())
MixerTunerType mixerTunerType = MixerTunerType.FUNCUBE_DONGLE_PRO_PLUS;
TargetDataLine targetDataLine = getTunerTargetDataLine(mixerTunerType);
if(targetDataLine != null)
{
mLog.debug(config.toString());
try
{
targetDataLine.open(mixerTunerType.getAudioFormat());
mLog.info(targetDataLine.getFormat().toString());
targetDataLine.start();
byte[] bytes = new byte[1024];
int read = targetDataLine.read(bytes, 0, bytes.length);
while(read > 0)
{
mLog.debug("Read:" + read);
read = 0;
// read = targetDataLine.read(bytes, 0, bytes.length);
}
targetDataLine.close();
mLog.debug("Done!");
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
}

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