Compare commits

...

14 Commits

Author SHA1 Message Date
Denny Sheirer
2e37f1e687 Updates gradle wrapper to 6.3. Updates embeded java to JDK 14 for building platform releases. (#729)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-04 09:22:13 -04:00
Denny Sheirer
5e8f2d0523 Updates build to 0.4.0 final and updates CHANGELOG revision history. (#727)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-04 08:19:29 -04:00
crsawyer
9fb6c33319 Fix HackRF-JawBreaker and HackRF sample conversion (#725) 2020-04-04 08:02:17 -04:00
Denny Sheirer
fdecac4230 Resovles #721 Suppress RID values of 0. (#723)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-01 05:05:28 -04:00
Denny Sheirer
85c27a6cca Resolves #719 P25 Phase 2 display channel number (#722)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-01 03:28:26 -04:00
Denny Sheirer
57f56d31a2 #713 Update Version 0.4.0 Beta 8 (#714)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 12:06:23 -04:00
Denny Sheirer
df25675d3f Resolves #707 separate DTMF tones of the same value get merged together (#712)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 12:03:47 -04:00
Denny Sheirer
277a84702a Resolves #580 channel map selection and editing (#711)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 11:52:39 -04:00
Denny Sheirer
c651296378 Resolves #709 MPT1327 no audio on traffic channels. (#710)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 11:33:15 -04:00
Denny Sheirer
866d25e86a #705 Updates release version 0.4.0 beta 7 (#706)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 06:17:06 -04:00
Denny Sheirer
5e62f1b889 Resolves #677 Streaming Audio Metadata UNKNOWN/UNKNOWN (#704)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 05:58:21 -04:00
Denny Sheirer
e9d524ad14 Resolves #699 Channel Multi-Frequency rotation delay user preference does not persist. (#703)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 05:53:58 -04:00
Denny Sheirer
158c8b4b8a #701 P25 Phase 2 Tone Metadata and Channel State (#702)
* Resolves #701 Updates P25P2 Tone metadata handling and P25P2 channel state management.
2020-03-28 05:51:11 -04:00
Denny Sheirer
41df207bb9 Resolves #694. Address null pointer when no system audio devices are available. (#695)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-23 04:45:06 -04:00
43 changed files with 794 additions and 718 deletions

2
.idea/misc.xml generated
View File

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

View File

@@ -1,3 +1,18 @@
0.4.0 20200404
- Release includes embedded Java 14 & JavaFX 13 runtime
- Adds Polyphase Channelizer with 2x channel oversampling and redesigned source management
- Adds P25 Phase 2 decoder
- Adds P25 radio identifier aliasing
- JMBE library 1.0.4 with support for AMBE tone metadata
- New JavaFX User Preferences dialog
- HackRF tuner enhanced (sample rates and saturation issues)
- Adds support for Airspy mini tuner
- Redesigned audio sub-system that uses AudioSegments
- USB tuner bus saturation management/monitoring
- Bitstream (.bits) recording
- MP3 Audio Recording
- Numerous bug fixes
0.3.4 20180418
- New P25 C4FM decoder that uses Differential QPSK decoder instead of FM demodulation with 4FSK.
- Adds channel auto-start and ordering capability with startup dialog allowing user to invoke or bypass channel

View File

@@ -33,7 +33,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.4.0-beta.6'
version = '0.4.0'
sourceCompatibility = '13'
sourceSets {
@@ -114,7 +114,7 @@ jar {
attributes (
'Implementation-Title' : 'sdrtrunk project',
'Implementation-Version' : archiveVersion,
'Class-Path' : 'jmbe-1.0.1.jar',
'Class-Path' : 'jmbe-1.0.4.jar',
'Created-By' : "Gradle ${gradle.gradleVersion}",
'Build-Timestamp' : new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
@@ -129,11 +129,11 @@ jar {
* tasks to produce platform-specific builds. If none of these paths exists, then an image will be created for the
* host OS using the installed JDK.
*/
def jdk_linux_x86_64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-13.0.1'
def jdk_linux_x86_32 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-13.0.1'
def jdk_osx_x86_64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-13.0.1.jdk'
def jdk_windows_x86_64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-13.0.1'
def jdk_windows_x86_32 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-13.0.1'
def jdk_linux_x86_64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-14'
def jdk_linux_x86_32 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-14'
def jdk_osx_x86_64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-14.jdk'
def jdk_windows_x86_64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-14'
def jdk_windows_x86_32 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-14'
def file_jdk_linux_x86_64 = new File(jdk_linux_x86_64)
def file_jdk_linux_x86_32 = new File(jdk_linux_x86_32)

Binary file not shown.

View File

@@ -1,27 +1,5 @@
#
# /*
# * ******************************************************************************
# * Copyright (C) 2014-2019 Dennis Sheirer
# *
# * This program is free software: you can redistribute it and/or modify
# * it under the terms of the GNU General Public License as published by
# * the Free Software Foundation, either version 3 of the License, or
# * (at your option) any later version.
# *
# * This program is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# * GNU General Public License for more details.
# *
# * You should have received a copy of the GNU General Public License
# * along with this program. If not, see <http://www.gnu.org/licenses/>
# * *****************************************************************************
# */
#
#
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

51
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
@@ -138,19 +154,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

18
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

View File

@@ -78,8 +78,8 @@ public class Radio extends AliasID
return false;
}
TalkgroupFormat talkgroupFormat = TalkgroupFormat.get(mProtocol);
return talkgroupFormat.getMinimumValidValue() <= mValue && mValue <= talkgroupFormat.getMaximumValidValue();
RadioFormat radioFormat = RadioFormat.get(mProtocol);
return radioFormat.getMinimumValidValue() <= mValue && mValue <= radioFormat.getMaximumValidValue();
}
public String toString()

View File

@@ -39,7 +39,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
private static final int MAX_SEGMENT_AUDIO_SAMPLE_LENGTH = 8000 * 60 * 1; //8 kHz - 1 minute
private Listener<AudioSegment> mAudioSegmentListener;
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
private AliasList mAliasList;
private AudioSegment mAudioSegment;

View File

@@ -37,6 +37,7 @@ import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -408,11 +409,28 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
{
//Only process add updates that match this timeslot
if(identifierUpdateNotification.getTimeslot() == getTimeslot() &&
(identifierUpdateNotification.isAdd() || identifierUpdateNotification.isSilentAdd()))
if(identifierUpdateNotification.getTimeslot() == getTimeslot())
{
addIdentifier(identifierUpdateNotification.getIdentifier());
if(identifierUpdateNotification.isAdd() || identifierUpdateNotification.isSilentAdd())
{
addIdentifier(identifierUpdateNotification.getIdentifier());
}
mIdentifierUpdateNotificationBroadcaster.broadcast(identifierUpdateNotification);
}
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Audio Segment\n Timeslot:").append(getTimeslot()).append("\n");
sb.append(" Start Time:").append(new Date(getStartTimestamp()));
sb.append(" Linked:").append(isLinked()).append("\n");
sb.append(" Recordable:").append(recordAudioProperty().get()).append("\n");
sb.append("Do Not Monitor:").append(isDoNotMonitor()).append("\n");
sb.append(" Stream To:").append(getBroadcastChannels()).append("\n");
sb.append(" Identifiers:\n").append(getIdentifierCollection()).append("\n");
return sb.toString();
}
}

View File

@@ -20,6 +20,7 @@
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.AudioSegmentRecorder;
import io.github.dsheirer.record.RecordFormat;
@@ -145,8 +146,11 @@ public class AudioStreamingManager implements Listener<AudioSegment>
try
{
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3);
IdentifierCollection identifierCollectionCopy =
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(),
audioSegment.getIdentifierCollection(), audioSegment.getStartTimestamp(), length);
identifierCollectionCopy, audioSegment.getStartTimestamp(), length);
mAudioRecordingListener.receive(audioRecording);
}
catch(IOException ioe)

View File

@@ -81,15 +81,23 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
MyEventBus.getEventBus().register(this);
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
try
if(configuration != null)
{
setMixerChannelConfiguration(configuration);
try
{
setMixerChannelConfiguration(configuration);
}
catch(AudioException ae)
{
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
}
}
catch(AudioException ae)
else
{
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
mLog.warn("No audio output devices available");
}
}

View File

@@ -105,7 +105,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
//Set the proxy as a listener so that echo'd updates are broadcast externally
mutableIdentifierCollection.setIdentifierUpdateListener(mIdentifierUpdateNotificationProxy);
StateMachine stateMachine = new StateMachine(timeslot);
StateMachine stateMachine = new StateMachine(timeslot, State.MULTI_CHANNEL_ACTIVE_STATES);
mStateMachineMap.put(timeslot, stateMachine);
stateMachine.addListener(this);
@@ -133,7 +133,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
public void stateChanged(State state, int timeslot)
{
//Broadcast current channel state so that channel rotation monitor can track
if(state.isActiveState())
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state))
{
broadcast(DecoderStateEvent.activeState(timeslot));
}
@@ -148,9 +148,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
switch(state)
{
case IDLE:
broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE));
break;
case RESET:
reset(timeslot);
mStateMachineMap.get(timeslot).setState(State.IDLE);
@@ -173,19 +170,49 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
*/
private void checkTeardown()
{
boolean teardown = true;
boolean teardown = false;
boolean active = false;
for(StateMachine stateMachine: mStateMachineMap.values())
{
if(stateMachine.getState().isActiveState())
State state = stateMachine.getState();
//If we have an active state in either timeslot, don't teardown. IDLE is a special state that is active
//but doesn't prevent a teardown when the other timeslot is in TEARDOWN
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state) && state != State.IDLE)
{
teardown = false;
active = true;
}
else if(state == State.TEARDOWN)
{
teardown = true;
}
}
if(teardown)
if(teardown && !active)
{
broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
if(mChannel.isTrafficChannel())
{
broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
}
else
{
for(StateMachine stateMachine: mStateMachineMap.values())
{
stateMachine.setState(State.RESET);
}
}
}
//If one timeslot is teardown but the other is still active, reset the teardown timeslot to IDLE
else if(teardown && active)
{
for(StateMachine stateMachine: mStateMachineMap.values())
{
if(stateMachine.getState() == State.TEARDOWN)
{
stateMachine.setState(State.RESET);
}
}
}
}
@@ -340,11 +367,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
{
mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers();
if(mChannel.getChannelType() == ChannelType.TRAFFIC)
{
mStateMachineMap.get(timeslot).setState(State.ACTIVE);
}
mStateMachineMap.get(timeslot).setState(State.RESET);
}
}
@@ -476,7 +499,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
//in an active (ie sync locked) state.
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
{
if(mStateMachineMap.get(timeslot).getState().isActiveState())
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState()))
{
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
mChannel.getChannelType().name()));
@@ -511,7 +534,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
case CONTINUATION:
case DECODE:
case START:
if(State.CALL_STATES.contains(event.getState()))
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(event.getState()))
{
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
}
@@ -519,11 +542,11 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
case END:
if(mChannel.isTrafficChannel())
{
mStateMachineMap.get(event.getTimeslot()).setState(State.TEARDOWN);
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
}
else
{
mStateMachineMap.get(event.getTimeslot()).setState(State.FADE);
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
}
break;
case RESET:

View File

@@ -94,7 +94,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
private SourceEventListener mInternalSourceEventListener;
private ChannelMetadata mChannelMetadata;
private StateMachine mStateMachine = new StateMachine(0);
private StateMachine mStateMachine = new StateMachine(0, State.SINGLE_CHANNEL_ACTIVE_STATES);
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
public SingleChannelState(Channel channel, AliasModel aliasModel)
@@ -123,7 +123,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
public void stateChanged(State state, int timeslot)
{
//Broadcast current channel state so that channel rotation monitor can track
if(state.isActiveState())
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(state))
{
broadcast(DecoderStateEvent.activeState(timeslot));
}
@@ -377,7 +377,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//Rebroadcast frequency error measurements to external listener if we're currently
//in an active (ie sync locked) state.
if(mStateMachine.getState().isActiveState())
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState()))
{
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
mChannel.getChannelType().name()));
@@ -411,7 +411,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
case CONTINUATION:
case DECODE:
case START:
if(State.CALL_STATES.contains(event.getState()))
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(event.getState()))
{
mStateMachine.setState(event.getState());
}

View File

@@ -43,6 +43,7 @@ public enum State
state == DATA ||
state == ENCRYPTED ||
state == FADE ||
state == IDLE ||
state == TEARDOWN ||
state == RESET;
}
@@ -60,6 +61,7 @@ public enum State
state == DATA ||
state == ENCRYPTED ||
state == FADE ||
state == IDLE ||
state == TEARDOWN ||
state == RESET;
}
@@ -121,7 +123,8 @@ public enum State
}
},
/**
* Indicates the channel is idle and not actively decoding any messages
* Indicates the channel is idle and not actively decoding any messages or in a multiple timeslot decoder this
* state reflects active decoding that indicates the timeslot is idle.
*/
IDLE("IDLE")
{
@@ -159,8 +162,8 @@ public enum State
private String mDisplayValue;
public static final EnumSet<State> CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> IDLE_STATES = EnumSet.of(IDLE, FADE);
public static final EnumSet<State> SINGLE_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> MULTI_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED, IDLE);
private State(String displayValue)
{
@@ -169,17 +172,6 @@ public enum State
public abstract boolean canChangeTo(State state);
/**
* Indicates that this state is an active (ie decoding) state and is one of the states
* enumerated in the CALL_STATES enumeration set.
*
* @return true if this state is an active state
*/
public boolean isActiveState()
{
return CALL_STATES.contains(this);
}
public String getDisplayValue()
{
return mDisplayValue;

View File

@@ -30,8 +30,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
/**
* State machine for tracking a channel state.
*/
public class StateMachine
{
private final static Logger mLog = LoggerFactory.getLogger(StateMachine.class);
@@ -42,13 +46,21 @@ public class StateMachine
protected long mEndTimeout;
protected long mEndTimeoutBuffer = 0;
private int mTimeslot;
private EnumSet<State> mActiveStates;
private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD;
private List<IStateMachineListener> mStateMachineListeners = new ArrayList<>();
private Listener<IdentifierUpdateNotification> mIdentifierUpdateListener;
public StateMachine(int timeslot)
/**
* Constructs an instance
*
* @param timeslot for this state machine
* @param activeStates set of states considered active for updating the fade timeout
*/
public StateMachine(int timeslot, EnumSet<State> activeStates)
{
mTimeslot = timeslot;
mActiveStates = activeStates;
}
public void addListener(IStateMachineListener listener)
@@ -73,7 +85,7 @@ public class StateMachine
public void checkState()
{
if(mState.isActiveState() && mFadeTimeout <= System.currentTimeMillis())
if(mActiveStates.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
{
setState(State.FADE);
}
@@ -92,18 +104,22 @@ public class StateMachine
{
if(state == mState)
{
if(State.CALL_STATES.contains(state))
if(mActiveStates.contains(state))
{
updateFadeTimeout();
}
}
else if(mState.canChangeTo(state))
{
if(mActiveStates.contains(state))
{
updateFadeTimeout();
}
switch(state)
{
case ACTIVE:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.ACTIVE);
break;
case CONTROL:
@@ -111,23 +127,19 @@ public class StateMachine
if(mChannelType == Channel.ChannelType.STANDARD)
{
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.CONTROL);
}
break;
case DATA:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.DATA);
break;
case ENCRYPTED:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.ENCRYPTED);
break;
case CALL:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.CALL);
break;
case FADE:

View File

@@ -28,6 +28,7 @@ import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -62,7 +63,7 @@ import java.util.List;
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0");
private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 25000;
//Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers
@@ -154,7 +155,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
channels--;
}
mLog.debug("Sample Rate [" + sampleRate + "] providing [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] Hz each");
mLog.info("Sample Rate [" + DECIMAL_FORMAT.format(sampleRate) + "] providing [" + channels +
"] channels at [" + DECIMAL_FORMAT.format(sampleRate / (double)channels) + "] Hz each");
return channels;
}

View File

@@ -1,48 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.identifier.tone;
import com.google.common.base.Joiner;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.StringListIdentifier;
import java.util.List;
/**
* Call Progress (ie dial tone, busy tone) tones identifier
*/
public abstract class CallProgressIdentifier extends StringListIdentifier
{
public CallProgressIdentifier(List<String> values)
{
super(values, IdentifierClass.USER, Form.CALL_PROGRESS_TONE, Role.TO);
}
@Override
public String toString()
{
return "CALL:" + Joiner.on("").join(getValue());
}
}

View File

@@ -1,48 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.identifier.tone;
import com.google.common.base.Joiner;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.StringListIdentifier;
import java.util.List;
/**
* Dual-Tone Multi-Frequency (DTMF) tones identifier
*/
public abstract class DtmfIdentifier extends StringListIdentifier
{
public DtmfIdentifier(List<String> values)
{
super(values, IdentifierClass.USER, Form.DTMF, Role.TO);
}
@Override
public String toString()
{
return "DTMF:" + Joiner.on("").join(getValue());
}
}

View File

@@ -1,48 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.identifier.tone;
import com.google.common.base.Joiner;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.StringListIdentifier;
import java.util.List;
/**
* Knox tones identifier
*/
public abstract class KnoxIdentifier extends StringListIdentifier
{
public KnoxIdentifier(List<String> values)
{
super(values, IdentifierClass.USER, Form.KNOX_TONE, Role.TO);
}
@Override
public String toString()
{
return "KNOX:" + Joiner.on("").join(getValue());
}
}

View File

@@ -1,46 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.identifier.tone;
import io.github.dsheirer.protocol.Protocol;
import java.util.List;
public class P25CallProgressIdentifier extends CallProgressIdentifier
{
public P25CallProgressIdentifier(List<String> values)
{
super(values);
}
@Override
public Protocol getProtocol()
{
return Protocol.APCO25;
}
public static P25CallProgressIdentifier create(List<String> values)
{
return new P25CallProgressIdentifier(values);
}
}

View File

@@ -1,46 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.identifier.tone;
import io.github.dsheirer.protocol.Protocol;
import java.util.List;
public class P25DtmfIdentifier extends DtmfIdentifier
{
public P25DtmfIdentifier(List<String> values)
{
super(values);
}
@Override
public Protocol getProtocol()
{
return Protocol.APCO25;
}
public static P25DtmfIdentifier create(List<String> values)
{
return new P25DtmfIdentifier(values);
}
}

View File

@@ -1,46 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.identifier.tone;
import io.github.dsheirer.protocol.Protocol;
import java.util.List;
public class P25KnoxIdentifier extends KnoxIdentifier
{
public P25KnoxIdentifier(List<String> values)
{
super(values);
}
@Override
public Protocol getProtocol()
{
return Protocol.APCO25;
}
public static P25KnoxIdentifier create(List<String> values)
{
return new P25KnoxIdentifier(values);
}
}

View File

@@ -26,11 +26,14 @@ import io.github.dsheirer.protocol.Protocol;
import java.util.List;
/**
* P25 Phase II AMBE audio codec tone metadata
*/
public class P25ToneIdentifier extends ToneIdentifier
{
public P25ToneIdentifier(List<String> values)
public P25ToneIdentifier(String value)
{
super(values);
super(value);
}
@Override
@@ -39,8 +42,8 @@ public class P25ToneIdentifier extends ToneIdentifier
return Protocol.APCO25;
}
public static P25ToneIdentifier create(List<String> values)
public static P25ToneIdentifier create(String value)
{
return new P25ToneIdentifier(values);
return new P25ToneIdentifier(value);
}
}

View File

@@ -22,27 +22,18 @@
package io.github.dsheirer.identifier.tone;
import com.google.common.base.Joiner;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.StringListIdentifier;
import java.util.List;
import io.github.dsheirer.identifier.string.StringIdentifier;
/**
* Pass-band audio tones identifier
*/
public abstract class ToneIdentifier extends StringListIdentifier
public abstract class ToneIdentifier extends StringIdentifier
{
public ToneIdentifier(List<String> values)
public ToneIdentifier(String value)
{
super(values, IdentifierClass.USER, Form.TONE, Role.TO);
}
@Override
public String toString()
{
return "TONES:" + Joiner.on("").join(getValue());
super(value, IdentifierClass.USER, Form.TONE, Role.FROM);
}
}

View File

@@ -30,6 +30,8 @@ import io.github.dsheirer.gui.editor.EditorValidationException;
import io.github.dsheirer.gui.editor.ValidatingEditor;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
@@ -41,10 +43,13 @@ import javax.swing.event.ChangeListener;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.List;
public class MPT1327DecoderEditor extends ValidatingEditor<Channel>
{
private final static Logger mLog = LoggerFactory.getLogger(MPT1327DecoderEditor.class);
private static final long serialVersionUID = 1L;
private JComboBox<ChannelMap> mComboChannelMaps;
@@ -65,12 +70,17 @@ public class MPT1327DecoderEditor extends ValidatingEditor<Channel>
{
setLayout(new MigLayout("insets 0 0 0 0,wrap 4", "[right][grow,fill][right][grow,fill]", ""));
List<ChannelMap> maps = mChannelMapModel.getChannelMaps();
ChannelMap[] mapArray = maps.toArray(new ChannelMap[maps.size()]);
mComboChannelMaps = new JComboBox<ChannelMap>();
mComboChannelMaps.setModel(new DefaultComboBoxModel<ChannelMap>(mapArray));
refreshChannelMaps();
mComboChannelMaps.setEnabled(false);
mComboChannelMaps.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
add(new JLabel("Channel Map:"));
add(mComboChannelMaps);
@@ -80,8 +90,17 @@ public class MPT1327DecoderEditor extends ValidatingEditor<Channel>
@Override
public void actionPerformed(ActionEvent e)
{
final ChannelMapManagerFrame manager =
new ChannelMapManagerFrame(mChannelMapModel);
final ChannelMapManagerFrame manager = new ChannelMapManagerFrame(mChannelMapModel);
manager.addWindowListener(new WindowAdapter()
{
@Override
public void windowClosed(WindowEvent e)
{
super.windowClosed(e);
mLog.debug("Refreshing channel maps combo box");
refreshChannelMaps();
}
});
EventQueue.invokeLater(new Runnable()
{
@@ -139,6 +158,22 @@ public class MPT1327DecoderEditor extends ValidatingEditor<Channel>
add(mCallTimeout);
}
private void refreshChannelMaps()
{
List<ChannelMap> maps = mChannelMapModel.getChannelMaps();
ChannelMap[] mapArray = new ChannelMap[maps.size() + 1];
int index = 1;
for(ChannelMap channelMap: maps)
{
mapArray[index++] = channelMap;
}
mComboChannelMaps.setModel(new DefaultComboBoxModel<ChannelMap>(mapArray));
}
@Override
public void save()
{

View File

@@ -280,7 +280,13 @@ public class MPT1327DecoderState extends DecoderState
@Override
public void start()
{
//Send call start event for traffic channels to unsquelch the audio. Decoded return to channel message
//or fade timeout expire will end the call event.
if(mChannelType == ChannelType.TRAFFIC)
{
//Broadcast a start call event so the squelch kicks on and the audio path opens
broadcast(new DecoderStateEvent(this, Event.START, State.CALL));
}
}
@Override

View File

@@ -572,7 +572,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
if(type == null)
{
mLog.debug("Unrecognized opcode for determining decode event type: " + opcode.name());
mLog.error("Unrecognized opcode for determining decode event type: " + opcode.name());
type = DecodeEventType.CALL;
}
@@ -697,7 +697,7 @@ public class P25TrafficChannelManager extends Module implements IDecodeEventProv
//Issue a disable request for each traffic channel
for(Channel channel: channels)
{
mLog.debug("Stopping traffic channel: " + channel);
mLog.info("Stopping traffic channel: " + channel);
broadcast(new ChannelEvent(channel, Event.REQUEST_DISABLE));
}
}

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.module.decode.p25.audio;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
import io.github.dsheirer.audio.squelch.SquelchState;
@@ -27,9 +28,7 @@ import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.IdentifierUpdateProvider;
import io.github.dsheirer.identifier.tone.P25CallProgressIdentifier;
import io.github.dsheirer.identifier.tone.P25DtmfIdentifier;
import io.github.dsheirer.identifier.tone.P25KnoxIdentifier;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.tone.P25ToneIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence;
@@ -51,17 +50,9 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
{
private final static Logger mLog = LoggerFactory.getLogger(P25P2AudioModule.class);
private static final String METADATA_TYPE_DTMF = "DTMF";
private static final String METADATA_TYPE_KNOX = "KNOX";
private static final String METADATA_TYPE_TONE = "TONE";
private static final String METADATA_TYPE_CALL_PROGRESS = "CALL PROGRESS";
private Listener<IdentifierUpdateNotification> mIdentifierUpdateNotificationListener;
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private MetadataProcessor mDtmfMetadataProcessor;
private MetadataProcessor mKnoxMetadataProcessor;
private MetadataProcessor mToneMetadataProcessor;
private MetadataProcessor mCallProcessMetadataProcessor;
private ToneMetadataProcessor mToneMetadataProcessor = new ToneMetadataProcessor();
private int mTimeslot;
private Queue<AbstractVoiceTimeslot> mQueuedAudioTimeslots = new ArrayDeque<>();
private boolean mEncryptedCallStateEstablished = false;
@@ -92,10 +83,10 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
@Override
public void reset()
{
mCallProcessMetadataProcessor = null;
mDtmfMetadataProcessor = null;
mKnoxMetadataProcessor = null;
mToneMetadataProcessor = null;
//Explicitly clear FROM identifiers to ensure previous call TONE identifiers are cleared.
mIdentifierCollection.remove(Role.FROM);
mToneMetadataProcessor.reset();
mQueuedAudioTimeslots.clear();
//Reset encrypted call handling flags
@@ -179,107 +170,49 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
{
byte[] voiceFrameBytes = voiceFrame.getBytes();
IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes);
processMetadata(audioWithMetadata);
addAudio(audioWithMetadata.getAudio());
try
{
IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes);
addAudio(audioWithMetadata.getAudio());
processMetadata(audioWithMetadata);
}
catch(Exception e)
{
mLog.error("Error synthesizing AMBE audio - continuing [" + e.getLocalizedMessage() + "]");
}
}
}
}
/**
* Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.)
* Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the
* tone metadata can be converted into a FROM identifier and included with any call segment.
*/
private void processMetadata(IAudioWithMetadata audioWithMetadata)
{
if(mIdentifierUpdateNotificationListener != null)
if(audioWithMetadata.hasMetadata())
{
if(audioWithMetadata.hasMetadata())
//JMBE only places 1 entry in the map, but for consistency we'll process the map entry set
for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet())
{
Map<String,String> metadata = audioWithMetadata.getMetadata();
//Each metadata map entry contains a tone-type (key) and tone (value)
Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue());
if(metadata.containsKey(METADATA_TYPE_DTMF))
if(metadataIdentifier != null)
{
String dtmf = metadata.get(METADATA_TYPE_DTMF);
if(dtmf != null)
{
if(mDtmfMetadataProcessor == null)
{
mDtmfMetadataProcessor = new MetadataProcessor();
}
List<String> dtmfTones = mDtmfMetadataProcessor.process(dtmf);
broadcast(P25DtmfIdentifier.create(dtmfTones));
}
}
else if(metadata.containsKey(METADATA_TYPE_KNOX))
{
String knox = metadata.get(METADATA_TYPE_KNOX);
if(knox != null)
{
if(mKnoxMetadataProcessor == null)
{
mKnoxMetadataProcessor = new MetadataProcessor();
}
List<String> knoxTones = mKnoxMetadataProcessor.process(knox);
broadcast(P25KnoxIdentifier.create(knoxTones));
}
}
else if(metadata.containsKey(METADATA_TYPE_TONE))
{
String tone = metadata.get(METADATA_TYPE_TONE);
if(tone != null)
{
if(mToneMetadataProcessor == null)
{
mToneMetadataProcessor = new MetadataProcessor();
}
List<String> tones = mToneMetadataProcessor.process(tone);
broadcast(P25ToneIdentifier.create(tones));
}
}
else if(metadata.containsKey(METADATA_TYPE_CALL_PROGRESS))
{
String callProgressTone = metadata.get(METADATA_TYPE_CALL_PROGRESS);
if(callProgressTone != null)
{
if(mCallProcessMetadataProcessor == null)
{
mCallProcessMetadataProcessor = new MetadataProcessor();
}
List<String> tones = mCallProcessMetadataProcessor.process(callProgressTone);
broadcast(P25CallProgressIdentifier.create(tones));
}
}
}
else
{
if(mDtmfMetadataProcessor != null)
{
mDtmfMetadataProcessor.noMetadata();
}
if(mKnoxMetadataProcessor != null)
{
mKnoxMetadataProcessor.noMetadata();
}
if(mCallProcessMetadataProcessor != null)
{
mCallProcessMetadataProcessor.noMetadata();
}
if(mToneMetadataProcessor != null)
{
mToneMetadataProcessor.noMetadata();
broadcast(metadataIdentifier);
}
}
}
else
{
mToneMetadataProcessor.closeMetadata();
}
}
/**
* Broadcasts the identifier to a registered listener
*/
private void broadcast(Identifier identifier)
{
if(mIdentifierUpdateNotificationListener != null)
@@ -289,12 +222,18 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
}
}
/**
* Registers the listener to receive identifier updates
*/
@Override
public void setIdentifierUpdateListener(Listener<IdentifierUpdateNotification> listener)
{
mIdentifierUpdateNotificationListener = listener;
}
/**
* Unregisters a listener from receiving identifier updates
*/
@Override
public void removeIdentifierUpdateListener()
{
@@ -302,40 +241,134 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
}
/**
* Processes metadata string values to provide a de-duplicated list of metadata values.
*
* This is necessary for metadata such as DTMF where a tone can span multiple 20ms voice frames but is intended
* to be represented as a single tone. An empty (no-metadata) frame causes the repeat checker to stop and allow
* the next metadata value to be added to the list.
* Process AMBE audio frame tone metadata. Tracks the count of sequential frames containing tone metadata to
* provide a list of each unique tone and a time duration (milliseconds) for the tone. Tones are concatenated into
* a comma separated list and included as call segment metadata.
*/
public class MetadataProcessor
public class ToneMetadataProcessor
{
private List<String> mValues = new ArrayList<>();
private String mLastInput;
private List<ToneMetadata> mToneMetadata = new ArrayList<>();
private ToneMetadata mCurrentToneMetadata;
public List<String> process(String value)
/**
* Resets or clears any accumulated call tone metadata to prepare for the next call.
*/
public void reset()
{
if(value != null)
{
if(mLastInput != null && mLastInput.equalsIgnoreCase(value))
{
//Suppress the repeat and return the current value
return mValues;
}
else
{
mLastInput = value;
mValues.add(value);
return mValues;
}
}
return mValues;
mToneMetadata.clear();
}
public void noMetadata()
/**
* Process the tone metadata
* @param type of tone
* @param value of tone
* @return an identifier with the accumulated tone metadata set
*/
public Identifier process(String type, String value)
{
mLastInput = null;
if(type == null || value == null)
{
return null;
}
if(mCurrentToneMetadata != null && mCurrentToneMetadata.matches(type, value))
{
mCurrentToneMetadata.incrementCount();
}
else
{
mCurrentToneMetadata = new ToneMetadata(type, value);
mToneMetadata.add(mCurrentToneMetadata);
mCurrentToneMetadata.incrementCount();
}
return P25ToneIdentifier.create(Joiner.on(",").join(mToneMetadata));
}
/**
* Closes current tone metadata when there is no metadata for the current audio frame.
*/
public void closeMetadata()
{
mCurrentToneMetadata = null;
}
}
/**
* Metadata about the tone type being transmitted and the duration.
*
* Note: each AMBE frame is 20 milliseconds in duration, so total duration is the count of 20 ms tone frames
* of the same metadata type.
*/
public class ToneMetadata
{
private String mType;
private String mValue;
private int mCount;
/**
* Constructs an instance
* @param type of tone
* @param value of the tone
*/
public ToneMetadata(String type, String value)
{
mType = type;
mValue = value;
mCount = 0;
}
/**
* Indicates if this tone metadata matches the tone metadata represented by the arguments
* @param type of tone
* @param value of tone
* @return true if they match
*/
public boolean matches(String type, String value)
{
return type != null && value != null && type.matches(mType) && value.matches(mValue);
}
/**
* Type of tone
*/
public String getType()
{
return mType;
}
/**
* Value of tone
*/
public String getValue()
{
return mValue;
}
/**
* Number of times this tone has occurred sequentially
*/
public int getCount()
{
return mCount;
}
/**
* Increments the count of the number of sequential frames that contained the tone
*/
public void incrementCount()
{
mCount++;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append(mType);
sb.append(" ").append(mValue);
sb.append(" (").append(mCount * 20).append("ms)");
return sb.toString();
}
}
@@ -359,5 +392,4 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
}
}
}
}

View File

@@ -189,25 +189,40 @@ public class P25Channel implements IChannelDescriptor
{
StringBuilder sb = new StringBuilder();
int downlinkChannelNumber = getDownlinkChannelNumber();
if(isTDMAChannel())
{
downlinkChannelNumber -= downlinkChannelNumber % getTimeslotCount();
}
if(getDownlinkBandIdentifier() == getUplinkBandIdentifier() && getDownlinkChannelNumber() == getUplinkChannelNumber())
{
sb.append(getDownlinkBandIdentifier()).append("-").append(getDownlinkLogicalChannelNumber());
sb.append(getDownlinkBandIdentifier()).append("-").append(downlinkChannelNumber);
}
else if(hasUplinkChannel())
{
int uplinkChannelNumber = getUplinkChannelNumber();
if(isTDMAChannel())
{
uplinkChannelNumber -= uplinkChannelNumber % getTimeslotCount();
}
sb.append(getDownlinkBandIdentifier())
.append("-")
.append(getDownlinkLogicalChannelNumber())
.append(downlinkChannelNumber)
.append("/")
.append(getUplinkBandIdentifier())
.append("-")
.append(getUplinkLogicalChannelNumber());
.append(uplinkChannelNumber);
}
else
{
sb.append(getDownlinkBandIdentifier())
.append("-")
.append(getDownlinkLogicalChannelNumber())
.append(downlinkChannelNumber)
.append("/-----");
}

View File

@@ -24,7 +24,7 @@ package io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
import java.util.ArrayList;
@@ -94,7 +94,7 @@ public class LCMotorolaTalkComplete extends LinkControlWord
{
if(mAddress == null)
{
mAddress = APCO25Talkgroup.create(getMessage().getInt(ADDRESS));
mAddress = APCO25RadioIdentifier.createFrom(getMessage().getInt(ADDRESS));
}
return mAddress;

View File

@@ -43,6 +43,7 @@ import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.p25.P25DecodeEvent;
import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage;
import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacPduType;
@@ -102,6 +103,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
private PatchGroupManager mPatchGroupManager = new PatchGroupManager();
private P25P2NetworkConfigurationMonitor mNetworkConfigurationMonitor = new P25P2NetworkConfigurationMonitor();
private DecodeEvent mCurrentCallEvent;
private int mEndPttOnFacchCounter = 0;
/**
* Constructs an APCO-25 decoder state for a traffic channel.
@@ -138,7 +140,8 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
protected void resetState()
{
super.resetState();
closeCurrentCallEvent(System.currentTimeMillis(), true);
closeCurrentCallEvent(System.currentTimeMillis(), true, MacPduType.MAC_3_IDLE);
mEndPttOnFacchCounter = 0;
}
/**
@@ -151,28 +154,68 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
{
if(message instanceof MacMessage)
{
processMacMessage((MacMessage)message);
MacMessage macMessage = (MacMessage)message;
processMacMessage(macMessage);
MacPduType macPduType = macMessage.getMacPduType();
//Ignore End PTT - this is handled in the processMacMessage() method
if(macPduType != MacPduType.MAC_2_END_PTT)
{
mEndPttOnFacchCounter = 0;
continueState(getStateFromPduType(macPduType));
}
if(macPduType == MacPduType.MAC_3_IDLE)
{
closeCurrentCallEvent(message.getTimestamp(), true, macPduType);
}
}
else if(message instanceof AbstractVoiceTimeslot)
{
if(isEncrypted())
if(mCurrentCallEvent != null)
{
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ENCRYPTED, getTimeslot()));
}
else
{
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
if(isEncrypted())
{
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ENCRYPTED, getTimeslot()));
}
else
{
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
}
}
updateCurrentCall(null, null, message.getTimestamp());
}
else if(message instanceof EncryptionSynchronizationSequence)
{
//We don't send any state events for this message since it can only occur in conjunction with
//an audio frame that already sends the call state event
getIdentifierCollection().update(message.getIdentifiers());
}
}
}
/**
* Updates the channel state according to the PDU type
*/
private static State getStateFromPduType(MacPduType macPduType)
{
switch(macPduType)
{
case MAC_1_PTT:
return State.CALL;
case MAC_2_END_PTT:
return State.TEARDOWN;
case MAC_4_ACTIVE:
case MAC_6_HANGTIME:
return State.ACTIVE;
case MAC_3_IDLE:
default:
return State.IDLE;
}
}
/**
* Adds the current channel to the local identifier collection which will cause it to be broadcast to all of the
* other listeners and will allow both timeslots on this channel to receive it and update accordingly.
@@ -215,30 +258,67 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
{
//Add the set of identifiers before we close out the call event to ensure they're captured in
//the closing event.
for(Identifier identifier : mac.getIdentifiers())
if(mCurrentCallEvent != null)
{
if(identifier.getRole() == Role.FROM && identifier instanceof APCO25RadioIdentifier)
for(Identifier identifier : mac.getIdentifiers())
{
int value = ((APCO25RadioIdentifier)identifier).getValue();
if(value == SYSTEM_CONTROLLER)
if(identifier.getRole() == Role.FROM)
{
//ignore so we don't overwrite the real from radio identifier
if(identifier instanceof APCO25RadioIdentifier)
{
int value = ((APCO25RadioIdentifier)identifier).getValue();
//Group call End PTT uses a FROM value of 0xFFFFFF - don't overwrite the correct id
if(value != SYSTEM_CONTROLLER)
{
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
}
else if(identifier.getRole() == Role.TO)
{
if(identifier instanceof APCO25Talkgroup)
{
int value = ((APCO25Talkgroup)identifier).getValue();
//Individual call End PTT uses a TO value of 0 - don't overwrite the correct id
if(value != 0)
{
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
}
else
{
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
else
{
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
closeCurrentCallEvent(message.getTimestamp(), true);
if(message.getDataUnitID().isFACCH())
{
mEndPttOnFacchCounter++;
//FNE sending 2 or more End PTT in FACCH timeslots indicates a channel teardown event.
if(mEndPttOnFacchCounter > 1)
{
if(mChannelType == ChannelType.TRAFFIC)
{
closeCurrentCallEvent(message.getTimestamp(), true, MacPduType.MAC_2_END_PTT);
//Don't send a continue state here ... the close() method will send the TEARDOWN state
}
//Otherwise the channel state is set to ACTIVE in anticipation of further call activity
else
{
closeCurrentCallEvent(message.getTimestamp(), true, MacPduType.MAC_4_ACTIVE);
continueState(State.RESET);
}
}
}
else
{
closeCurrentCallEvent(message.getTimestamp(), true, MacPduType.MAC_4_ACTIVE);
}
}
break;
case TDMA_0_NULL_INFORMATION_MESSAGE:
@@ -246,23 +326,18 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
if(type == MacPduType.MAC_3_IDLE || type == MacPduType.MAC_6_HANGTIME)
{
closeCurrentCallEvent(message.getTimestamp(), true);
closeCurrentCallEvent(message.getTimestamp(), true, type);
}
continueState(State.ACTIVE);
break;
case TDMA_1_GROUP_VOICE_CHANNEL_USER_ABBREVIATED:
if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME)
{
closeCurrentCallEvent(message.getTimestamp(), false);
getIdentifierCollection().remove(Role.FROM);
closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME);
for(Identifier identifier : mac.getIdentifiers())
{
if(!(identifier.getForm() == Form.TALKGROUP && identifier.getRole() == Role.FROM))
{
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
else
@@ -291,16 +366,12 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case TDMA_33_GROUP_VOICE_CHANNEL_USER_EXTENDED:
if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME)
{
closeCurrentCallEvent(message.getTimestamp(), false);
getIdentifierCollection().remove(Role.FROM);
closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME);
for(Identifier identifier : mac.getIdentifiers())
{
if(!(identifier.getForm() == Form.TALKGROUP && identifier.getRole() == Role.FROM))
{
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
else
@@ -329,16 +400,12 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case TDMA_2_UNIT_TO_UNIT_VOICE_CHANNEL_USER:
if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME)
{
closeCurrentCallEvent(message.getTimestamp(), false);
getIdentifierCollection().remove(Role.FROM);
closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME);
for(Identifier identifier : mac.getIdentifiers())
{
if(!(identifier.getForm() == Form.TALKGROUP && identifier.getRole() == Role.FROM))
{
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
else
@@ -367,16 +434,12 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case TDMA_34_UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED:
if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME)
{
closeCurrentCallEvent(message.getTimestamp(), false);
getIdentifierCollection().remove(Role.FROM);
closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME);
for(Identifier identifier : mac.getIdentifiers())
{
if(!(identifier.getForm() == Form.TALKGROUP && identifier.getRole() == Role.FROM))
{
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
else
@@ -405,16 +468,12 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case TDMA_3_TELEPHONE_INTERCONNECT_VOICE_CHANNEL_USER:
if(message.getMacPduType() == MacPduType.MAC_6_HANGTIME)
{
closeCurrentCallEvent(message.getTimestamp(), false);
getIdentifierCollection().remove(Role.FROM);
closeCurrentCallEvent(message.getTimestamp(), false, MacPduType.MAC_6_HANGTIME);
for(Identifier identifier : mac.getIdentifiers())
{
if(!(identifier.getForm() == Form.TALKGROUP && identifier.getRole() == Role.FROM))
{
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
//Add to the identifier collection after filtering through the patch group manager
getIdentifierCollection().update(mPatchGroupManager.update(identifier));
}
}
else
@@ -439,7 +498,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
}
}
}
continueState(State.ACTIVE);
break;
case TDMA_5_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE:
if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdateMultiple)
@@ -461,7 +519,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcastCurrentChannel(gvcgum.getChannelC());
}
}
continueState(State.ACTIVE);
break;
case TDMA_37_GROUP_VOICE_CHANNEL_GRANT_UPDATE_MULTIPLE_EXPLICIT:
if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdateMultipleExplicit)
@@ -478,7 +535,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcastCurrentChannel(gvcgume.getChannelB());
}
}
continueState(State.ACTIVE);
break;
case PHASE1_64_GROUP_VOICE_CHANNEL_GRANT_ABBREVIATED:
if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantAbbreviated)
@@ -490,7 +546,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcastCurrentChannel(gvcga.getChannel());
}
}
continueState(State.ACTIVE);
break;
case PHASE1_66_GROUP_VOICE_CHANNEL_GRANT_UPDATE:
if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdate)
@@ -507,7 +562,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcastCurrentChannel(gvcgu.getChannelB());
}
}
continueState(State.ACTIVE);
break;
case PHASE1_70_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_ABBREVIATED:
if(getCurrentChannel() == null && mac instanceof UnitToUnitVoiceChannelGrantAbbreviated)
@@ -519,7 +573,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcastCurrentChannel(uuvcga.getChannel());
}
}
continueState(State.ACTIVE);
break;
case PHASE1_195_GROUP_VOICE_CHANNEL_GRANT_UPDATE_EXPLICIT:
if(getCurrentChannel() == null && mac instanceof GroupVoiceChannelGrantUpdateExplicit)
@@ -531,7 +584,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcastCurrentChannel(gvcgue.getChannel());
}
}
continueState(State.ACTIVE);
break;
case PHASE1_198_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_UPDATE_EXTENDED:
if(getCurrentChannel() == null && mac instanceof UnitToUnitVoiceChannelGrantUpdateExtended)
@@ -543,7 +595,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcastCurrentChannel(uuvcgue.getChannel());
}
}
continueState(State.ACTIVE);
break;
case TDMA_17_INDIRECT_GROUP_PAGING:
MutableIdentifierCollection icGroupPaging = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
@@ -556,7 +607,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.details("GROUP PAGE")
.identifiers(icGroupPaging)
.build());
continueState(State.ACTIVE);
break;
case TDMA_18_INDIVIDUAL_PAGING_MESSAGE_WITH_PRIORITY:
if(mac instanceof IndividualPagingMessage)
@@ -575,7 +625,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icIndividualPaging)
.build());
}
continueState(State.ACTIVE);
break;
case TDMA_48_POWER_CONTROL_SIGNAL_QUALITY:
if(mac instanceof PowerControlSignalQuality)
@@ -593,7 +642,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icPowerControl)
.build());
}
continueState(State.ACTIVE);
break;
case TDMA_49_MAC_RELEASE:
if(mac instanceof MacRelease)
@@ -604,7 +652,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
icMacRelease.remove(IdentifierClass.USER);
icMacRelease.update(mac.getIdentifiers());
closeCurrentCallEvent(message.getTimestamp(), true);
closeCurrentCallEvent(message.getTimestamp(), true, message.getMacPduType());
broadcast(P25DecodeEvent.builder(message.getTimestamp())
.channel(getCurrentChannel())
@@ -630,7 +678,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icMacRelease)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_68_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_ABBREVIATED:
if(mac instanceof UnitToUnitVoiceChannelGrantAbbreviated)
@@ -647,7 +694,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icGrant)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_69_UNIT_TO_UNIT_ANSWER_REQUEST_ABBREVIATED:
if(mac instanceof UnitToUnitAnswerRequestAbbreviated)
@@ -664,7 +710,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icRequest)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_74_TELEPHONE_INTERCONNECT_ANSWER_REQUEST:
if(mac instanceof TelephoneInterconnectAnswerRequest)
@@ -681,7 +726,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icRequest)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_76_RADIO_UNIT_MONITOR_COMMAND_ABBREVIATED:
if(mac instanceof RadioUnitMonitorCommand)
@@ -699,7 +743,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icRequest)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_84_SNDCP_DATA_CHANNEL_GRANT:
if(mac instanceof SNDCPDataChannelGrant)
@@ -716,7 +759,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icGrant)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_85_SNDCP_DATA_PAGE_REQUEST:
if(mac instanceof SNDCPDataPageRequest)
@@ -733,7 +775,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icPage)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_88_STATUS_UPDATE_ABBREVIATED:
if(mac instanceof StatusUpdateAbbreviated)
@@ -750,7 +791,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icStatusUpdate)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_90_STATUS_QUERY_ABBREVIATED:
if(mac instanceof StatusQueryAbbreviated)
@@ -766,7 +806,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icStatusUpdate)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_92_MESSAGE_UPDATE_ABBREVIATED:
if(mac instanceof MessageUpdateAbbreviated)
@@ -783,7 +822,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icStatusUpdate)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_94_RADIO_UNIT_MONITOR_COMMAND_ENHANCED:
if(mac instanceof RadioUnitMonitorCommandEnhanced)
@@ -802,7 +840,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icRequest)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_95_CALL_ALERT_ABBREVIATED:
MutableIdentifierCollection icCallAlert = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
@@ -814,7 +851,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.eventDescription(DecodeEventType.CALL_ALERT.toString())
.identifiers(icCallAlert)
.build());
continueState(State.ACTIVE);
break;
case PHASE1_96_ACK_RESPONSE:
if(mac instanceof AcknowledgeResponse)
@@ -831,7 +867,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icAckResponse)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_97_QUEUED_RESPONSE:
if(mac instanceof QueuedResponse)
@@ -850,7 +885,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icQueuedResponse)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_100_EXTENDED_FUNCTION_COMMAND_ABBREVIATED:
if(mac instanceof ExtendedFunctionCommand)
@@ -868,7 +902,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icExtendedFunction)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_103_DENY_RESPONSE:
if(mac instanceof DenyResponse)
@@ -885,7 +918,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icDenyResponse)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_106_GROUP_AFFILIATION_QUERY_ABBREVIATED:
MutableIdentifierCollection icGroupAffiliationQuery = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
@@ -898,7 +930,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.details("GROUP AFFILIATION")
.identifiers(icGroupAffiliationQuery)
.build());
continueState(State.ACTIVE);
break;
case PHASE1_109_UNIT_REGISTRATION_COMMAND_ABBREVIATED:
MutableIdentifierCollection icUnitRegisterCommand = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
@@ -911,7 +942,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.details("UNIT REGISTRATION")
.identifiers(icUnitRegisterCommand)
.build());
continueState(State.ACTIVE);
break;
case PHASE1_197_UNIT_TO_UNIT_ANSWER_REQUEST_EXTENDED:
if(mac instanceof UnitToUnitAnswerRequestExtended)
@@ -930,7 +960,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icUnitAnswerRequestExtended)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_204_RADIO_UNIT_MONITOR_COMMAND_EXTENDED:
if(mac instanceof RadioUnitMonitorCommandExtended)
@@ -948,7 +977,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icRequest)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_216_STATUS_UPDATE_EXTENDED:
if(mac instanceof StatusUpdateExtended)
@@ -965,7 +993,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icStatusUpdate)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_218_STATUS_QUERY_EXTENDED:
MutableIdentifierCollection icStatusQueryExtended = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers());
@@ -978,7 +1005,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.details("STATUS QUERY")
.identifiers(icStatusQueryExtended)
.build());
continueState(State.ACTIVE);
break;
case PHASE1_220_MESSAGE_UPDATE_EXTENDED:
if(mac instanceof MessageUpdateExtended)
@@ -995,7 +1021,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icStatusUpdate)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_223_CALL_ALERT_EXTENDED:
if(mac instanceof CallAlertExtended)
@@ -1010,7 +1035,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icCallAlertExtended)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_228_EXTENDED_FUNCTION_COMMAND_EXTENDED:
if(mac instanceof ExtendedFunctionCommandExtended)
@@ -1028,7 +1052,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icExtendedFunction)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_234_GROUP_AFFILIATION_QUERY_EXTENDED:
if(mac instanceof GroupAffiliationQueryExtended)
@@ -1045,7 +1068,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.identifiers(icGroupAffiliationQueryExtended)
.build());
}
continueState(State.ACTIVE);
break;
case PHASE1_115_IDENTIFIER_UPDATE_TDMA:
@@ -1071,8 +1093,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
case PHASE1_196_UNIT_TO_UNIT_VOICE_CHANNEL_GRANT_EXTENDED:
case UNKNOWN:
default:
continueState(State.ACTIVE);
//ignore
break;
}
}
@@ -1135,7 +1155,15 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
.build();
broadcast(mCurrentCallEvent);
broadcast(new DecoderStateEvent(this, Event.START, State.CALL, getTimeslot()));
if(isEncrypted())
{
broadcast(new DecoderStateEvent(this, Event.START, State.ENCRYPTED, getTimeslot()));
}
else
{
broadcast(new DecoderStateEvent(this, Event.START, State.CALL, getTimeslot()));
}
}
else
{
@@ -1152,7 +1180,6 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
mCurrentCallEvent.setIdentifierCollection(getIdentifierCollection().copyOf());
mCurrentCallEvent.end(timestamp);
broadcast(mCurrentCallEvent);
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
}
}
@@ -1160,8 +1187,10 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
* Ends/closes the current call event.
*
* @param timestamp of the message that indicates the event has ended.
* @param resetIdentifiers to reset the FROM/TO identifiers
* @param pduType of the message that caused the close call event - to determine channel state after call
*/
private void closeCurrentCallEvent(long timestamp, boolean resetIdentifiers)
private void closeCurrentCallEvent(long timestamp, boolean resetIdentifiers, MacPduType pduType)
{
if(mCurrentCallEvent != null)
{
@@ -1169,21 +1198,21 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcast(mCurrentCallEvent);
mCurrentCallEvent = null;
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.ACTIVE, getTimeslot()));
}
else
{
continueState(State.ACTIVE);
}
broadcast(new DecoderStateEvent(this, Event.END, getStateFromPduType(pduType), getTimeslot()));
if(resetIdentifiers)
{
getIdentifierCollection().remove(IdentifierClass.USER);
if(resetIdentifiers)
{
getIdentifierCollection().remove(IdentifierClass.USER);
}
else
{
//Only clear the from identifier at this point ... the channel may still be allocated to the TO talkgroup
getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM);
}
}
else
else if(pduType == MacPduType.MAC_2_END_PTT)
{
//Only clear the from identifier at this point ... the channel may still be allocated to the TO talkgroup
getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM);
broadcast(new DecoderStateEvent(this, Event.END, getStateFromPduType(pduType), getTimeslot()));
}
}
@@ -1215,14 +1244,17 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
@Override
public void receiveDecoderStateEvent(DecoderStateEvent event)
{
switch(event.getEvent())
if(event.getTimeslot() == getTimeslot())
{
case RESET:
resetState();
mNetworkConfigurationMonitor.reset();
break;
default:
break;
switch(event.getEvent())
{
case RESET:
resetState();
mNetworkConfigurationMonitor.reset();
break;
default:
break;
}
}
}

View File

@@ -224,25 +224,30 @@ public class P25P2MessageFramer implements Listener<Dibit>
public static void main(String[] args)
{
UserPreferences userPreferences = new UserPreferences();
userPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(Path.of("/home/denny/JMBE/jmbe-1.0.2.jar"));
userPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(Path.of("/home/denny/JMBE/jmbe-1.0.3.jar"));
ApplicationLog applicationLog = new ApplicationLog(userPreferences);
applicationLog.start();
// Path directory = Paths.get("/home/denny/SDRTrunk/recordings");
Path directory = Paths.get("/home/denny/temp/Harris P25-2 Logs/bits");
// Path directory = Paths.get("/media/denny/500G1EXT4/PBITRecordings");
// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 1186, 1189); //Nugent
ScrambleParameters scrambleParameters = new ScrambleParameters(123654, 813, 10); //Hills
// ScrambleParameters scrambleParameters = new ScrambleParameters(1, 972, 972));
// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 686, 677)); //CNYICC - Rome
// ScrambleParameters scrambleParameters = new ScrambleParameters(781824, 686, 677); //CNYICC - Rome
// Channel channel = new Channel("Phase 2 Test", Channel.ChannelType.STANDARD);
Channel channel = new Channel("Phase 2 Test", Channel.ChannelType.TRAFFIC);
Channel channel = new Channel("Phase 2 Test");
channel.setDecodeConfiguration(new DecodeConfigP25Phase2());
AliasList aliasList = new AliasList("Test Alias List");
Alias alias = new Alias("TG Range 1-65535");
alias.addAliasID(new Record());
// Alias alias = new Alias("TG Range 1-65535");
// alias.addAliasID(new Record());
// alias.addAliasID(new TalkgroupRange(Protocol.APCO25, 1, 65535));
alias.addAliasID(new Talkgroup(Protocol.APCO25, 11185));
aliasList.addAlias(alias);
// alias.addAliasID(new Talkgroup(Protocol.APCO25, 11857));
// alias.addAliasID(new Talkgroup(Protocol.APCO25, 12601));
// aliasList.addAlias(alias);
AudioRecordingManager recordingManager = new AudioRecordingManager(userPreferences);
recordingManager.start();
ProcessingChain processingChain = new ProcessingChain(channel, new AliasModel());
@@ -254,20 +259,21 @@ public class P25P2MessageFramer implements Listener<Dibit>
processingChain.addModule(new P25P2AudioModule(userPreferences, 1, aliasList));
MessageProviderModule messageProviderModule = new MessageProviderModule();
processingChain.addModule(messageProviderModule);
processingChain.start();
try(OutputStream logOutput = Files.newOutputStream(directory.resolve("log.txt")))
{
try
{
DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*PHASE2_Hills*TRAFFIC.bits");
// DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*042102*TRAFFIC.bits");
// DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*042102*.bits");
DirectoryStream<Path> stream = Files.newDirectoryStream(directory, "*214601*Hills*TRAFFIC.bits");
stream.forEach(new Consumer<Path>()
{
@Override
public void accept(Path path)
{
mLog.debug("Processing:" + path.toString());
mLog.debug("\n\nProcessing:" + path.toString() + "\n\n");
try
{
logOutput.write(("Processing:" + path.toString() + "\n").getBytes());
@@ -277,6 +283,8 @@ public class P25P2MessageFramer implements Listener<Dibit>
mLog.error("Error", ioe);
}
processingChain.start();
P25P2MessageFramer messageFramer = new P25P2MessageFramer(null, DecoderType.P25_PHASE2.getProtocol().getBitRate());
messageFramer.setScrambleParameters(scrambleParameters);
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
@@ -313,6 +321,12 @@ public class P25P2MessageFramer implements Listener<Dibit>
{
ioe.printStackTrace();
}
mLog.debug("**STOPPING PROCESSING CHAIN**");
processingChain.stop();
mLog.debug("\n============================================================================================================================\n");
}
}
);
@@ -326,5 +340,7 @@ public class P25P2MessageFramer implements Listener<Dibit>
{
mLog.error("Error", ioe);
}
recordingManager.stop();
}
}

View File

@@ -65,6 +65,22 @@ public enum DataUnitID
return mValueWithParity;
}
/**
* Indicates if this DUID is a SACCH, either scrambled or unscrambled
*/
public boolean isSACCH()
{
return this == SCRAMBLED_SACCH || this == UNSCRAMBLED_SACCH;
}
/**
* Indicates if this DUID is a FACCH, either scrambled or unscrambled
*/
public boolean isFACCH()
{
return this == SCRAMBLED_FACCH || this == UNSCRAMBLED_FACCH;
}
@Override
public String toString()
{

View File

@@ -350,19 +350,26 @@ public class PlaybackPreference extends Preference
{
MixerChannelConfiguration defaultConfig = MixerManager.getDefaultOutputMixer();
String configName = mPreferences.get(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, defaultConfig.toString());
for(MixerChannelConfiguration config: MixerManager.getOutputMixers())
if(defaultConfig != null)
{
if(config.toString().contentEquals(configName))
String configName = mPreferences.get(PREFERENCE_KEY_MIXER_CHANNEL_CONFIG, defaultConfig.toString());
for(MixerChannelConfiguration config: MixerManager.getOutputMixers())
{
mMixerChannelConfiguration = config;
if(config.toString().contentEquals(configName))
{
mMixerChannelConfiguration = config;
}
}
if(mMixerChannelConfiguration == null)
{
mMixerChannelConfiguration = defaultConfig;
}
}
if(mMixerChannelConfiguration == null)
else
{
mMixerChannelConfiguration = defaultConfig;
mLog.error("Error - no system audio devices available");
}
}

View File

@@ -37,7 +37,7 @@ public class ChannelMultiFrequencyPreference extends Preference
private Preferences mPreferences = Preferences.userNodeForPackage(ChannelMultiFrequencyPreference.class);
private static final String PREFERENCE_KEY_ROTATION_DELAY = "rotation.delay";
private static final long DEFAULT_ROTATION_DELAY = 5000; //5 seconds
private Long mRotationDelay = DEFAULT_ROTATION_DELAY;
private Long mRotationDelay;
/**
* Constructs a tuner preference with the update listener

View File

@@ -39,7 +39,6 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
@@ -55,6 +54,8 @@ public class AudioRecordingManager implements Listener<AudioSegment>
private ScheduledFuture<?> mQueueProcessorHandle;
private UserPreferences mUserPreferences;
private int mUnknownAudioRecordingIndex = 1;
private int mDuplicateAudioRecordingSuffix = 1;
private String mPreviousRecordingPath = null;
/**
* Constructs an instance
@@ -105,6 +106,19 @@ public class AudioRecordingManager implements Listener<AudioSegment>
*/
public void processCompletedAudioSegment(AudioSegment audioSegment)
{
//Debug
if(audioSegment.getAudioBufferCount() == 0)
{
mLog.debug("Audio Segment detected with 0 audio buffers");
}
List<Identifier> toIdentifiers = audioSegment.getIdentifierCollection().getIdentifiers(Role.TO);
if(toIdentifiers.isEmpty())
{
mLog.debug("Audio Segment detected with NO TO identifiers");
}
if(audioSegment.recordAudioProperty().get())
{
mCompletedAudioSegmentQueue.add(audioSegment);
@@ -120,28 +134,26 @@ public class AudioRecordingManager implements Listener<AudioSegment>
*/
private void processAudioSegments()
{
List<AudioSegment> audioSegments = new ArrayList<>();
mCompletedAudioSegmentQueue.drainTo(audioSegments);
RecordFormat recordFormat = mUserPreferences.getRecordPreference().getAudioRecordFormat();
AudioSegment audioSegment = mCompletedAudioSegmentQueue.poll();
if(!audioSegments.isEmpty())
while(audioSegment != null)
{
RecordFormat recordFormat = mUserPreferences.getRecordPreference().getAudioRecordFormat();
Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat);
for(AudioSegment audioSegment: audioSegments)
try
{
Path path = getAudioRecordingPath(audioSegment.getIdentifierCollection(), recordFormat);
try
{
AudioSegmentRecorder.record(audioSegment, path, recordFormat);
}
catch(IOException ioe)
{
mLog.error("Error recording audio segment to [" + path.toString() + "]");
}
audioSegment.decrementConsumerCount();
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();
}
}
@@ -197,7 +209,7 @@ public class AudioRecordingManager implements Listener<AudioSegment>
if(!toIdentifiers.isEmpty())
{
sb.append("_TO_").append(toIdentifiers.get(0)).toString();
sb.append("_TO_").append(toIdentifiers.get(0));
}
}
@@ -214,7 +226,14 @@ public class AudioRecordingManager implements Listener<AudioSegment>
if(!fromIdentifiers.isEmpty())
{
sb.append("_FROM_").append(fromIdentifiers.get(0)).toString();
for(Identifier identifier: fromIdentifiers)
{
if(identifier.getForm() != Form.TONE)
{
sb.append("_FROM_").append(identifier);
break;
}
}
}
}
}
@@ -233,6 +252,26 @@ public class AudioRecordingManager implements Listener<AudioSegment>
//Remove any illegal filename characters
sbFinal.append(StringUtils.replaceIllegalCharacters(sb.toString()));
if(mPreviousRecordingPath != null && mPreviousRecordingPath.contentEquals(sbFinal.toString()))
{
sbFinal.append("_V").append(mDuplicateAudioRecordingSuffix++);
}
else
{
mDuplicateAudioRecordingSuffix = 2;
mPreviousRecordingPath = sbFinal.toString();
}
for(Identifier identifier: identifierCollection.getIdentifiers(Role.FROM))
{
if(identifier.getForm() == Form.TONE)
{
sbFinal.append("_WITH_TONES");
break;
}
}
sbFinal.append(recordFormat.getExtension());
return getRecordingBasePath().resolve(sbFinal.toString());

View File

@@ -40,6 +40,7 @@ public enum TunerClass
GIGABYTE_GTU7300( TunerType.FITIPOWER_FC0012, "1B80", "D393", "Gigabyte", "GT-U7300" ),
GTEK_T803( TunerType.FITIPOWER_FC0012, "1F4D", "B803", "GTek", "T803" ),
HACKRF_ONE( TunerType.HACKRF, "1D50", "6089", "Great Scott Gadgets", "HackRF One" ),
HACKRF_JAWBREAKER( TunerType.HACKRF, "1D50", "604B", "Great Scott Gadgets", "HackRF Jawbreaker" ),
RAD1O( TunerType.HACKRF, "1D50", "CC15", "Munich hackerspace", "Rad1o" ),
LIFEVIEW_LV5T_DELUXE( TunerType.FITIPOWER_FC0012, "1F4D", "C803", "Liveview", "LV5T Deluxe" ),
MYGICA_TD312( TunerType.FITIPOWER_FC0012, "1F4D", "D286", "MyGica", "TD312" ),
@@ -196,6 +197,10 @@ public enum TunerClass
{
retVal = HACKRF_ONE;
}
else if( productID == 24651 ) //604B
{
retVal = HACKRF_JAWBREAKER;
}
break;
case 8013: //1F4D
if( productID == 47107 ) //B803

View File

@@ -304,6 +304,7 @@ public class TunerManager
case FUNCUBE_DONGLE_PRO_PLUS:
return initFuncubeProPlusTuner(device, descriptor);
case HACKRF_ONE:
case HACKRF_JAWBREAKER:
case RAD1O:
return initHackRFTuner(device, descriptor);
case COMPRO_VIDEOMATE_U620F:

View File

@@ -25,7 +25,7 @@ import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.usb.USBTransferProcessor;
import io.github.dsheirer.source.tuner.usb.USBTunerController;
import io.github.dsheirer.source.tuner.usb.converter.ByteSampleConverter;
import io.github.dsheirer.source.tuner.usb.converter.SignedByteSampleConverter;
import io.github.dsheirer.source.tuner.usb.converter.NativeBufferConverter;
import org.apache.commons.io.EndianUtils;
import org.slf4j.Logger;
@@ -60,7 +60,7 @@ public class HackRFTunerController extends USBTunerController
public static final double USABLE_BANDWIDTH_PERCENT = 0.75;
public static final int DC_SPIKE_AVOID_BUFFER = 5000;
private NativeBufferConverter mNativeBufferConverter = new ByteSampleConverter();
private NativeBufferConverter mNativeBufferConverter = new SignedByteSampleConverter();
private USBTransferProcessor mUSBTransferProcessor;
private HackRFSampleRate mSampleRate = HackRFSampleRate.RATE2_016MHZ;

View File

@@ -81,7 +81,7 @@ public class USBMasterProcessor
{
if(mRunning.compareAndSet(false, true))
{
mLog.warn("Starting USB master processor thread");
mLog.info("Starting USB master processor thread");
if(mProcessorThread == null)
{
mProcessor.reset();
@@ -110,7 +110,7 @@ public class USBMasterProcessor
{
if(mRunning.compareAndSet(true, false))
{
mLog.warn("Stopping USB master processor thread");
mLog.info("Stopping USB master processor thread");
if(mProcessorThread != null)
{
mProcessor.stop();

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.source.tuner.usb.converter;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
public class SignedByteSampleConverter extends NativeBufferConverter
{
private final static float[] LOOKUP_VALUES;
//Creates a static lookup table that converts the signed 8-bit valued range from -128 to 127 into scaled float values
//of -1.0 to 0 to 1.0
static
{
LOOKUP_VALUES = new float[256];
for(int x = 0; x < 256; x++)
{
LOOKUP_VALUES[x] = (float)((byte)x) / 128.0f;
}
}
private FloatBuffer mFloatBuffer;
/**
* Converts native byte buffers containing signed 8-bit complex samples into complex float samples loaded into a tracked,
* reusable complex sample buffer. Internally tracks the reusable buffer until all downstream consumers have finished
* processing the buffer contents and then reuses the buffer (and memory) for subsequent samples.
*/
public SignedByteSampleConverter()
{
}
/**
* Converts the signed 8-bit complex samples contained in the native buffer into floats that are loaded into a float
* buffer and subsequently transferred to a reusable complex buffer by the parent class.
*
* @param nativeBuffer containing signed 8-bit complex samples
* @param length of bytes to read from the native buffer
* @return float buffer loaded with converted samples
*/
@Override
protected FloatBuffer convertSamples(ByteBuffer nativeBuffer, int length)
{
nativeBuffer.rewind();
if(mFloatBuffer == null || mFloatBuffer.capacity() != nativeBuffer.capacity())
{
mFloatBuffer = FloatBuffer.allocate(nativeBuffer.capacity());
}
mFloatBuffer.rewind();
int count = 0;
while(nativeBuffer.hasRemaining() && count < length)
{
byte sample = nativeBuffer.get();
count++;
mFloatBuffer.put(LOOKUP_VALUES[(sample & 0xFF)]);
}
return mFloatBuffer;
}
}

View File

@@ -21,7 +21,7 @@ package io.github.dsheirer.util;
public class StringUtils
{
private static final String[] ILLEGAL_FILENAME_CHARACTERS = {"#", "%", "&", "{", "}", "\\", "<", ">",
"*", "?", "/", " ", "$", "!", "'", "\"", ":", "@", "+", "`", "|", "="};
"*", "?", "/", " ", "$", "!", "'", "\"", ":", "@", "+", "`", "|", "=", ","};
/**
* Compares string a to b and returns true if a is non-null and non-empty, b is non-null and non-empty, and