Compare commits

...

40 Commits

Author SHA1 Message Date
denny
d8a20b5adb Updating build to beta 5 2016-02-13 09:10:06 -05:00
denny
c670f1d855 Resolves issue with audio playback state becoming disconnected with
AudioManager connection class monitoring of the playback state.  Adds a
line listener to monitor when the line is started/stopped and responds
accordingly.

Removes squelch control from mono and stereo audio playback components
which was a holdover -- squelch state is now managed within the 
audio component for each processing chain.
2016-02-13 09:07:37 -05:00
denny
1dc0125ef4 Resolves issue with LTR standard calls - ending state was not monitoring
the correct channel number on call ends.
2016-02-12 20:54:14 -05:00
denny
225e747d6e Updated README 2016-02-07 10:04:23 -05:00
denny
36f9953156 Updated README 2016-02-07 09:40:25 -05:00
denny
47c452e7e6 README update 2016-02-07 09:28:09 -05:00
denny
06c8bba97e Updated README with version 0.2.0 wiki updates. 2016-02-07 09:26:25 -05:00
denny
a25bb83dea Talkgroup ID Editor - updates help text in editor window 2016-02-06 09:34:13 -05:00
denny
8e11cb4554 Removes audio inversion from talkgroup id and editor. The audio
processing architecture will be updated in the next version and will
handle identifying and processing audio inversion on a per-alias basis.
2016-02-06 09:30:52 -05:00
denny
2892e4609e Removes the 'No Tuner' color change sub-menu from the decoding channels
list.  This is no longer a valid channel state since any decoding
channels that fail to get a tuner source will not be displayed.
2016-02-06 08:24:46 -05:00
denny
d6e161f3e9 Updating build to 0.2.0.beta4 2016-02-05 09:11:37 -05:00
denny
ba952af721 Added support for parsing additional Packet Data Unit unrecognized types
and removed logging of unrecognized PDUC messages.
2016-02-05 09:10:23 -05:00
denny
d9be346d7f Added new message for Packet Data Unit Confirmed messages with unknown
PDUType.
2016-02-04 21:14:16 -05:00
denny
133adec27a Adds support for PacketData messages that were being flagged as Packet
Data Unit - Confirmed unknown message types.
2016-02-04 20:51:13 -05:00
denny
981a8fec55 Removes logger from TrafficChannelManager that was logging when a
traffic channel is allocated but fails to get a source and is
subsequently torn down.  Logging was ambiguous and unneeded.
2016-02-04 20:50:17 -05:00
denny
9c94266adb Resolves issue with Audio Channel display showing a call in progress
with TO/FROM identifiers displayed, but no audio playing.  This occurred
when the decoder sent a few audio buffers which caused the display to
update from the metadata, however the decoder stopped sending buffers
and the audio start threshold is never met.  Clear display is invoked
upon audio stop, so the panel was never started or stopped or cleared
until the next call came in.

Changed to store the incoming metadata and only display it once the
audio playback starts and reset the display once the audio stops.
2016-02-04 20:49:11 -05:00
denny
4460f1ee30 Suppresses logging of attempts to change to invalid channel state. The
state enumeration is designed to block these attempts which can occur
when a non-standard call sequence of events occurs, normally due to
intermittent decoding.
2016-02-02 20:14:33 -05:00
denny
ddfefe4b85 Resolves issue with ChannelState FADE monitor - changes from
starting/stopping the monitor for each call to simply leaving the
monitor task running continuously.  Was causing issue with scheduled
thread executor not shutting down the task in a timely manner -- new
approach should avoid start/stop related issues altogether.

Updates P25DecoderState with reordering of timing of when we send
updates for the TO/FROM identifiers so that the audio manager receives
up-to-date metadata and hopefully resolves issue with 'Do Not Monitor'
talkgroups being monitored by the audio manager.

Updates the AudioManager with a try/catch block around the processing
task so that we can catch any audio processing exceptions that occur
during the timer task and might get suppressed by the scheduled thread
executor.
2016-02-02 19:44:18 -05:00
Denny Sheirer
debdc39a5f Resolves threading issue in ChannelState.
Resolves issue in tuner controller where controller was not releasing
sourced tuner channels, preventing re-tuning to other frequencies.
Resolves issue with channel timeout on MPT1327 monitored traffic
channels
2016-02-01 09:17:56 +00:00
Denny Sheirer
6f76f6fca4 Resolves issue with Tuner retaining released TunerChannels, preventing
the tuner from sourcing additional out-of-band channels.
2016-01-30 16:58:52 +00:00
denny
1290b8c4d3 Adds VSELP message and unknown Data Unit ID handling to P25 decoder. 2016-01-28 19:18:05 -05:00
denny
b95b3ff415 Updated readme 2016-01-23 21:19:54 -05:00
denny
3e8db5b216 Updates version 0.2.0 build to beta.3 2016-01-23 14:41:01 -05:00
denny
f0c11f2590 Cleanup. 2016-01-23 14:39:52 -05:00
denny
99a5fc9bcf Closes issue #46. Removes the recordings tree node from the
configuration controller until such time that it can be fully
implemented.  Existing skeleton framework was a placeholder that did not
materialize.
2016-01-23 14:38:02 -05:00
denny
f5fadc5004 Stores selected audio output mixer name and channels (mono/stereo) to
system properties so settings are loaded on startup.
2016-01-23 14:28:21 -05:00
denny
c922029233 Adds capability to persist spectral display fft size and frame rate
between sessions.
2016-01-23 14:07:01 -05:00
denny
94d85f636d Resolves Audio issues (underwater/overlapping audio buffers).
Redesigned audio manager handling of audio outputs to ensure that only
one channel's audio can be routed to an audio output at a time.

Changed audio outputs to use the shared thread pool manager for
scheduled tasks, instead of creating a thread pool for each audio
output.
2016-01-23 10:19:47 -05:00
denny
1db6779dec Audio outputs - added exception handler to catch any errors that occur
while the buffer processor thread is running.

Added check for 0 space available in data line buffers to avoid trying
to write a zero-length audio chunk to the data line.

Added thread sleep for 100 milliseconds when we detect that the data
line buffer is full, to allow time for the data line to drain before we
check it again.  Hopefully this should minimize the chattering during
playback when the code was repeatedly hitting the available() method to
check for space availability.
2016-01-22 16:35:18 -05:00
denny
676d26f4d8 Updated default color for data channel state 2016-01-22 16:31:59 -05:00
denny
abca4ebbd7 Updated to use concurrent containers to avoid concurrent modification
exceptions.
2016-01-22 16:30:58 -05:00
denny
cffc6b6eed Updates TDMA synchronization broadcast message to ICD 2016-01-22 10:39:09 -05:00
Denny Sheirer
39ebec78a8 Updates channel type description for TDMA channels. 2016-01-19 04:06:20 +00:00
Denny Sheirer
4066a4b3c3 Adds TDMA channel IdentifierUpdate message parsing. 2016-01-19 04:00:18 +00:00
denny
c12f535f3c Issue #47 - work in progress 2016-01-18 04:53:49 -05:00
denny
810bb7384e Closes #42 - ltr standard call state was not accurately tracking call
state. 

Note: a related issue described in ticket #42 involves issues with the
audio output module.  A new ticket will be opened with that issue.
2016-01-17 19:42:13 -05:00
denny
0c5692cb1e Closes issue #44. Creates /sdrtrunk folder within the .zip and .tar.gz
output products so that upon extracting the contents they are contained
within the /sdrtrunk folder.
2016-01-15 10:37:45 -05:00
denny
663378ad20 Logging cleanup 2016-01-15 09:50:10 -05:00
denny
5807bb191e Resolved issue with loading 'loading.png' image file for the map 2016-01-15 09:49:43 -05:00
denny
ab9ef2d8cc Updated build script internal variable naming. 2016-01-01 10:20:10 -05:00
72 changed files with 2897 additions and 2313 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/*.class
/classes*
/product*
/product*
/bin/

View File

@@ -1,19 +1,22 @@
# sdrtrunk
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
# sdrtrunk #
A cross-platform java application for decoding, monitoring and recording trunked
mobile and related radio protocols using Software Defined Radios (SDR).
# Documentation #
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted)
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/UserManual)
* [Download](https://drive.google.com/folderview?id=0B7BHsssXUq8eOHBTNndCczZTd0k&usp=sharing)
* [User's Manual (Version 0.2.0)](https://github.com/DSheirer/sdrtrunk/wiki/UserManual)
* [Wiki](https://github.com/DSheirer/sdrtrunk/wiki)
# Support Group and Error Reporting #
You can get help with setting up the sdrtrunk application or report any errors
that you encounter while running the application in the sdrtrunk Google Group at:
* [Support](https://groups.google.com/forum/#!forum/sdrtrunk)
# Updates #
Subscribe to this group to receive updates about new releases and the ongoing
application development effort.
* [Subscribe for Updates](https://groups.google.com/forum/#!forum/sdrtrunk)
![](https://github.com/DSheirer/sdrtrunk/wiki/images/SDRTrunk_application.png)
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-67489234-1', 'auto');
ga('send', 'pageview');
</script>

View File

@@ -6,114 +6,116 @@
<!-- version properties -->
<property name="major" value="0"/>
<property name="minor" value="2"/>
<property name="build" value="0"/>
<property name="release" value="beta.2"/>
<property name="patch" value="0"/>
<property name="release" value="beta.5"/>
<property name="filename" value="sdrtrunk_${major}.${minor}.${patch}-${release}"/>
<!-- Build properties -->
<property name="classes" location="../classes"/>
<property name="configs" location="../config"/>
<property name="images" location="../images"/>
<property name="libs" location="../imports"/>
<property name="license" location="../license"/>
<property name="product" location="../product"/>
<property name="scripts" location="../scripts"/>
<property name="src" location="../src"/>
<property name="repo" location=".."/>
<property name="source.code" location="${repo}/src"/>
<property name="source.configs" location="${repo}/config"/>
<property name="source.images" location="${repo}/images"/>
<property name="source.libs" location="${repo}/imports"/>
<property name="source.license" location="${repo}/license"/>
<property name="source.scripts" location="${repo}/scripts"/>
<property name="output" location="${repo}/product"/>
<property name="output.classes" location="${repo}/classes"/>
<property name="output.folder" location="${output}/sdrtrunk"/>
<property name="output.config" location="${output.folder}/config"/>
<property name="output.images" location="${output.folder}/images"/>
<property name="output.libs" location="${output.folder}/libs"/>
<path id="classpath">
<fileset dir="${libs}" includes="*.jar" />
<fileset dir="${source.libs}" includes="*.jar" />
</path>
<target name="init">
<!-- Create the classes folder to contain the compiled java classes -->
<mkdir dir="${classes}"/>
<mkdir dir="${output.classes}"/>
<!-- Create version file -->
<echo message="${major}.${minor}.${build}-${release}" file="${classes}/sdrtrunk-version"/>
<echo message="${major}.${minor}.${patch}-${release}" file="${output.classes}/sdrtrunk-version"/>
</target>
<target name="compile" depends="clean,init"
description="Compile java classes" >
<!-- Compile the java code from ${src} into ${classes} -->
<javac srcdir="${src}"
destdir="${classes}"
classpathref="classpath"
includeantruntime="false" />
<target name="compile" depends="clean,init" description="Compile java classes" >
<javac srcdir="${source.code}" destdir="${output.classes}" classpathref="classpath" includeantruntime="false" />
</target>
<target name="SDRTrunk" depends="compile" description="Create SDRTrunk application" >
<!-- Create the distribution directory -->
<mkdir dir="${product}"/>
<mkdir dir="${output.folder}"/>
<!-- Create jar file -->
<jar jarfile="${product}/SDRTrunk.jar" basedir="${classes}">
<jar jarfile="${output.folder}/sdrtrunk.jar" basedir="${output.classes}">
<manifest>
<attribute name="Title" value="sdrtrunk"/>
<attribute name="Main-Class" value="gui.SDRTrunk"/>
<attribute name="Version" value="${major}.${minor}.${patch}-${release}"/>
</manifest>
<fileset dir="${configs}">
<fileset dir="${source.configs}">
<exclude name="*.rules"/>
</fileset>
<fileset dir="${license}">
<fileset dir="${source.license}">
<include name="license.txt"/>
</fileset>
</jar>
<!-- Copy third-party libraries -->
<mkdir dir="${product}/libs"/>
<copy todir="${product}/libs">
<fileset dir="${libs}" includes="*.jar" />
<mkdir dir="${output.libs}"/>
<copy todir="${output.libs}">
<fileset dir="${source.libs}" includes="*.jar" />
</copy>
<!-- Copy config files -->
<mkdir dir="${product}/config"/>
<mkdir dir="${output.config}"/>
<copy todir="${product}/config">
<fileset dir="${configs}">
<copy todir="${output.config}">
<fileset dir="${source.configs}">
<include name="*.rules"/>
</fileset>
</copy>
<!-- Copy image files -->
<mkdir dir="${product}/images"/>
<copy todir="${product}/images">
<fileset dir="${images}" />
<mkdir dir="${output.images}"/>
<copy todir="${output.images}">
<fileset dir="${source.images}" />
</copy>
<!-- Copy license file -->
<copy todir="${product}">
<fileset dir="${license}" />
<copy todir="${output.folder}">
<fileset dir="${source.license}">
<include name="license.txt"/>
</fileset>
</copy>
<!-- Copy program scripts -->
<copy todir="${product}">
<fileset dir="${scripts}" />
<copy todir="${output.folder}">
<fileset dir="${source.scripts}" />
</copy>
<!-- Set linux permissions on start script -->
<chmod file="${product}/run_sdrtrunk_linux.sh" perm="777" />
<tstamp prefix="build" />
<property name="filename" value="sdrtrunk_${build.DSTAMP}_${build.TSTAMP}_${major}.${minor}.${build}-${release}"/>
<!-- Bundle the files into .zip -->
<zip destfile="${product}/${filename}.zip" basedir="${product}" />
<zip destfile="${output}/${filename}.zip" basedir="${output}" />
<!-- Remove linux start script before tar task -->
<delete file="${output.folder}/run_sdrtrunk_linux.sh"/>
<!-- Bundle files into .tar.gz and copy/set permissions on linux start script -->
<tar destfile="${output}/${filename}.tar.gz" compression="gzip">
<tarfileset dir="${output.folder}" prefix="sdrtrunk" />
<tarfileset dir="${source.scripts}" prefix="sdrtrunk" filemode="777">
<include name="run_sdrtrunk_linux.sh"/>
</tarfileset>
</tar>
<!-- Bundle the files into .tar.gz to preserve linux script permissions -->
<tar destfile="${product}/${filename}.tar.gz" compression="gzip">
<tarfileset dir="${product}" filemode="777">
<include name="*.sh"/>
</tarfileset>
<tarfileset dir="${product}">
<exclude name="*.sh"/>
<exclude name="${filename}.zip"/>
</tarfileset>
</tar>
<delete dir="${output.folder}"/>
</target>
<target name="clean" description="Clean previous build artifacts" >
<delete dir="${classes}"/>
<delete dir="${product}"/>
<delete dir="${output.classes}"/>
<delete dir="${output}"/>
</target>
</project>

View File

@@ -1 +1 @@
java -XX:+UseG1GC -cp "SDRTrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk
java -XX:+UseG1GC -cp "sdrtrunk.jar;*;libs/*;config/*;images/*" gui.SDRTrunk

View File

@@ -27,7 +27,6 @@ import audio.inverted.AudioType;
public class TalkgroupID extends AliasID
{
private String mTalkgroup;
private AudioType mAudioType;
public TalkgroupID()
{
@@ -44,20 +43,9 @@ public class TalkgroupID extends AliasID
mTalkgroup = talkgroup;
}
@XmlAttribute
public AudioType getAudioType()
{
return mAudioType;
}
public void setAudioType( AudioType type )
{
mAudioType = type;
}
public boolean hasAudioType()
{
return mAudioType != null;
return false;
}
public String toString()
@@ -66,13 +54,6 @@ public class TalkgroupID extends AliasID
sb.append( "Talkgroup: " + mTalkgroup );
if( mAudioType != null )
{
sb.append( " [Audio " );
sb.append( mAudioType.getShortDisplayString() );
sb.append( "]" );
}
return sb.toString();
}

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2014-2016 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
@@ -20,9 +20,7 @@ package alias.id.talkgroup;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
@@ -30,7 +28,6 @@ import javax.swing.JTextArea;
import javax.swing.JTextField;
import net.miginfocom.swing.MigLayout;
import audio.inverted.AudioType;
import controller.ConfigurableNode;
public class TalkgroupIDEditor extends JPanel implements ActionListener
@@ -38,19 +35,15 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
private static final long serialVersionUID = 1L;
private TalkgroupIDNode mTalkgroupIDNode;
private JLabel mLabelName;
private JTextField mTextTalkgroupID;
private JLabel mLabelAudio;
private JComboBox<AudioType> mComboAudioTypes;
private String mHelpText = "Enter a formatted talkgroup identifier.\n\n"
+ "P25 Talkgroup Format: hexadecimal value, 4 characters for "
+ "talkgroup or 6 characters for a mobile radio\n\n"
+ "LTR Talkgroup Format: A-HH-TTT (A=Area, H=Home Repeater, "
+ "T=Talkgroup)\n\n"
+ "Passport Talkgroup Format: xxxxx (up to 5-digit number)\n\n"
+ "Wildcard: use one or more asterisks (*) for any talkgroup digits.\n\n"
+ "Audio: if the talkgroup uses simple frequency inversion, specify"
+ " the inversion frequency from the dropdown list to automatically"
+ " un-invert audio for this specific talkgroup";
+ "Wildcard: use one or more asterisks (*) for any talkgroup digits.";
public TalkgroupIDEditor( TalkgroupIDNode talkgroupIDNode )
{
@@ -72,22 +65,6 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
add( mTextTalkgroupID, "growx,push" );
add( new JLabel( "Audio:" ) );
mComboAudioTypes = new JComboBox<AudioType>();
mComboAudioTypes.setModel(
new DefaultComboBoxModel<AudioType>( AudioType.values() ) );
AudioType audioType = mTalkgroupIDNode.getTalkgroupID().getAudioType();
if( audioType != null )
{
mComboAudioTypes.setSelectedItem( audioType );
}
add( mComboAudioTypes, "growx,push" );
JButton btnSave = new JButton( "Save" );
btnSave.addActionListener( TalkgroupIDEditor.this );
add( btnSave, "growx,push" );
@@ -117,14 +94,6 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
((ConfigurableNode)mTalkgroupIDNode.getParent()).sort();
AudioType selected = mComboAudioTypes
.getItemAt( mComboAudioTypes.getSelectedIndex() );
if( selected != null )
{
mTalkgroupIDNode.getTalkgroupID().setAudioType( selected );
}
mTalkgroupIDNode.save();
mTalkgroupIDNode.show();
@@ -139,9 +108,6 @@ public class TalkgroupIDEditor extends JPanel implements ActionListener
{
mTextTalkgroupID.setText(
mTalkgroupIDNode.getTalkgroupID().getTalkgroup() );
mComboAudioTypes.setSelectedItem(
mTalkgroupIDNode.getTalkgroupID().getAudioType() );
}
mTalkgroupIDNode.refresh();

View File

@@ -48,8 +48,11 @@ public class AudioChannelPanel extends JPanel
private JLabel mFromLabel = new JLabel( "FROM:" );
private JLabel mFrom = new JLabel( "" );
private JLabel mFromAlias = new JLabel( "" );
private enum AudioStatus { STARTED,STOPPED };
private AudioStatus mAudioStatus = AudioStatus.STOPPED;
private boolean mConfigured = false;
private AudioMetadata mAudioMetadata;
public AudioChannelPanel( SettingsManager settingsManager, AudioOutput audioOutput )
{
@@ -134,15 +137,19 @@ public class AudioChannelPanel extends JPanel
{
switch( audioEvent.getType() )
{
case AUDIO_STOPPED:
EventQueue.invokeLater( new Runnable()
case AUDIO_STARTED:
synchronized( mAudioStatus )
{
@Override
public void run()
{
resetLabels();
}
} );
mAudioStatus = AudioStatus.STARTED;
updateLabels();
}
break;
case AUDIO_STOPPED:
synchronized( mAudioStatus )
{
mAudioStatus = AudioStatus.STOPPED;
resetLabels();
}
break;
case AUDIO_MUTED:
case AUDIO_UNMUTED:
@@ -166,13 +173,55 @@ public class AudioChannelPanel extends JPanel
*/
private void resetLabels()
{
mFrom.setText( "" );
updateAlias( mFromAlias, null );
mTo.setText( "" );
updateAlias( mToAlias, null );
mConfigured = false;
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
mFrom.setText( "" );
updateAlias( mFromAlias, null );
mTo.setText( "" );
updateAlias( mToAlias, null );
}
} );
}
private void updateLabels()
{
if( mAudioMetadata != null )
{
final Metadata from = mAudioMetadata.getMetadata( MetadataType.FROM );
final Metadata to = mAudioMetadata.getMetadata( MetadataType.TO );
EventQueue.invokeLater( new Runnable()
{
@Override
public void run()
{
if( from != null )
{
mFrom.setText( from.getValue() );
updateAlias( mFromAlias, from.getAlias() );
}
else
{
mFrom.setText( "-----" );
}
if( to != null )
{
mTo.setText( to.getValue() );
updateAlias( mToAlias, to.getAlias() );
}
else
{
mTo.setText( "-----" );
}
}
} );
}
}
/**
@@ -214,32 +263,17 @@ public class AudioChannelPanel extends JPanel
@Override
public void receive( AudioMetadata audioMetadata )
{
if( !mConfigured || audioMetadata.isUpdated() )
mAudioMetadata = audioMetadata;
if( audioMetadata.isUpdated() )
{
final Metadata from = audioMetadata.getMetadata( MetadataType.FROM );
final Metadata to = audioMetadata.getMetadata( MetadataType.TO );
EventQueue.invokeLater( new Runnable()
synchronized( mAudioStatus )
{
@Override
public void run()
if( mAudioStatus == AudioStatus.STARTED )
{
if( from != null )
{
mFrom.setText( from.getValue() );
updateAlias( mFromAlias, from.getAlias() );
}
if( to != null )
{
mTo.setText( to.getValue() );
updateAlias( mToAlias, to.getAlias() );
}
updateLabels();
}
} );
mConfigured = true;
}
}
}
}

View File

@@ -2,7 +2,7 @@ package audio;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
* Copyright (C) 2014-2016 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
@@ -17,16 +17,12 @@ package audio;
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -37,10 +33,12 @@ import javax.sound.sampled.Mixer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Broadcaster;
import sample.Listener;
import source.mixer.MixerChannel;
import source.mixer.MixerChannelConfiguration;
import source.mixer.MixerManager;
import audio.AudioEvent.Type;
import audio.output.AudioOutput;
import audio.output.MonoAudioOutput;
@@ -53,7 +51,9 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
private static final Logger mLog = LoggerFactory.getLogger( AudioManager.class );
public static final int AUDIO_TIMEOUT = 2000; //2 seconds
public static final int NO_SOURCE = 0;
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null );
@@ -62,19 +62,19 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
new AudioEvent( Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null );
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioChannelAssignment> mChannelAssignments = new HashMap<>();
private int mLowestPrioritySource;
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
private AudioOutputConnection mLowestPriorityConnection;
private int mAvailableConnectionCount;
private List<AudioOutput> mAvailableAudioOutputs = new ArrayList<>();
private Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
private List<String> mChannelNames = new ArrayList<>();
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mProcessingTask;
private MixerChannelConfiguration mMixerChannel;
private MixerChannelConfiguration mMixerChannelConfiguration;
/**
* Processes all audio produced by the decoding channels and routes audio
@@ -85,26 +85,66 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
mThreadPoolManager = manager;
//TODO: save audio system preferences to the settings manager
loadSettings();
}
private void loadSettings()
{
MixerChannelConfiguration configuration = null;
/* Use the system default mixer and mono channel as default startup */
Mixer mixer = AudioSystem.getMixer( null );
SystemProperties properties = SystemProperties.getInstance();
if( mixer != null )
Mixer defaultMixer = AudioSystem.getMixer( null );
String mixer = properties.get( AUDIO_MIXER_PROPERTY, defaultMixer.getMixerInfo().getName() );
String channels = properties.get( AUDIO_CHANNELS_PROPERTY, MixerChannel.MONO.name() );
MixerChannelConfiguration[] mixerConfigurations =
MixerManager.getInstance().getOutputMixers();
for( MixerChannelConfiguration mixerConfig: mixerConfigurations )
{
mMixerChannel = new MixerChannelConfiguration( mixer, MixerChannel.MONO );
if( mixerConfig.matches( mixer, channels ) )
{
configuration = mixerConfig;
}
}
if( configuration == null )
{
configuration = getDefaultConfiguration();
}
try
{
setMixerChannelConfiguration( configuration );
}
catch( Exception e )
{
mLog.error( "Couldn't set stored audio mixer/channel configuration"
+ " - using default", e );
try
{
setMixerChannelConfiguration( mMixerChannel );
}
catch ( AudioException e )
setMixerChannelConfiguration( getDefaultConfiguration() );
}
catch( Exception e2 )
{
mLog.error( "Couldn't create an audio output device", e );
mLog.error( "Couldn't set default audio mixer/channel "
+ "configuration - no audio will be available", e2 );
}
}
}
private MixerChannelConfiguration getDefaultConfiguration()
{
/* Use the system default mixer and mono channel as default startup */
Mixer defaultMixer = AudioSystem.getMixer( null );
return new MixerChannelConfiguration( defaultMixer, MixerChannel.MONO );
}
public void dispose()
{
if( mThreadPoolManager != null && mProcessingTask != null )
@@ -116,6 +156,15 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mThreadPoolManager = null;
mProcessingTask = null;
mChannelConnectionMap.clear();
for( AudioOutputConnection connection: mAudioOutputConnections )
{
connection.dispose();
}
mAudioOutputConnections.clear();
}
@Override
@@ -124,207 +173,44 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mAudioPacketQueue.add( packet );
}
public class AudioPacketProcessor implements Runnable
{
@Override
public void run()
{
removeCompletedChannelAssignments();
if( mAudioPacketQueue != null )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mAudioPacketQueue.drainTo( packets );
for( AudioPacket packet: packets )
{
/* Don't process any packet's marked as do not monitor */
if( !packet.getAudioMetadata().isDoNotMonitor() )
{
int source = packet.getAudioMetadata().getSource();
if( packet.getType() != AudioPacket.Type.END )
{
/* Existing channel assignment */
if( mChannelAssignments.containsKey( source ) )
{
mChannelAssignments.get( source ).getAudioOutput()
.receive( packet );
}
/* Start a new channel assignment */
else if( !mAvailableAudioOutputs.isEmpty() )
{
AudioOutput channel = mAvailableAudioOutputs.remove( 0 );
mChannelAssignments.put( packet.getAudioMetadata().getSource(),
new AudioChannelAssignment( channel, packet.getAudioMetadata().getPriority() ) );
updateLowestPriorityAssignment();
channel.receive( packet );
}
/* Override the lowest priority assignment */
else
{
if( mLowestPrioritySource > -1 )
{
int priority = packet.getAudioMetadata().getPriority();
if( packet.getAudioMetadata().isSelected() ||
priority < mChannelAssignments
.get( mLowestPrioritySource ).getPriority() )
{
AudioChannelAssignment assignment =
mChannelAssignments.remove( mLowestPrioritySource );
if( assignment != null )
{
assignment.setPriority( priority );
int overrideSource = packet.getAudioMetadata().getSource();
mChannelAssignments.put( overrideSource, assignment );
mLowestPrioritySource = overrideSource;
assignment.getAudioOutput().receive( packet );
}
else
{
mLowestPrioritySource = -1;
}
}
}
}
}
}
}
}
}
}
/**
* Checks each audio channel assignment and removes any completed channels.
* Checks each audio channel assignment and disconnects any inactive connections
*/
private void removeCompletedChannelAssignments()
private void disconnectInactiveChannelAssignments()
{
if( !mChannelAssignments.isEmpty() )
boolean changed = false;
for( AudioOutputConnection connection: mAudioOutputConnections )
{
Iterator<Entry<Integer,AudioChannelAssignment>> it =
mChannelAssignments.entrySet().iterator();
Entry<Integer,AudioChannelAssignment> entry;
boolean removed = false;
while( it.hasNext() )
if( connection.isInactive() &&
mChannelConnectionMap.containsKey( connection.getSource() ) )
{
entry = it.next();
if( entry.getValue().isComplete() )
{
mAvailableAudioOutputs.add( entry.getValue().getAudioOutput() );
entry.getValue().dispose();
it.remove();
removed = true;
}
}
if( removed )
{
updateLowestPriorityAssignment();
mChannelConnectionMap.remove( connection.getSource() );
connection.disconnect();
changed = true;
}
}
if( changed )
{
updateLowestPriorityAssignment();
}
}
/**
* Identifies the lowest priority channel assignment ... the one that can
* be overridden with a higher priority call
* Identifies the lowest priority channel connection
*/
private void updateLowestPriorityAssignment()
{
int lowestPriority = -1;
mLowestPriorityConnection = null;
mLowestPrioritySource = -1;
for( int source: mChannelAssignments.keySet() )
for( AudioOutputConnection connection: mAudioOutputConnections )
{
AudioChannelAssignment assignment = mChannelAssignments.get( source );
if( assignment.getPriority() > lowestPriority )
if( connection.isConnected() &&
( mLowestPriorityConnection == null ||
mLowestPriorityConnection.getPriority() > connection.getPriority() ) )
{
lowestPriority = assignment.getPriority();
mLowestPrioritySource = source;
}
}
}
public void endAllAudioAssignments()
{
for( AudioOutput output: mAudioOutputMap.values() )
{
output.receive( new AudioPacket( AudioPacket.Type.END, null ) );
}
}
public class AudioChannelAssignment implements Listener<AudioEvent>
{
private static final long CALL_END_DELAY = 2000; //2 seconds
private AudioOutput mAudioOutput;
private int mPriority;
private long mAudioStoppedTime = System.currentTimeMillis();
public AudioChannelAssignment( AudioOutput audioOutput, int priority )
{
mAudioOutput = audioOutput;
mPriority = priority;
mAudioOutput.addAudioEventListener( this );
}
public boolean isComplete()
{
return mAudioStoppedTime != Long.MAX_VALUE &&
mAudioStoppedTime + CALL_END_DELAY < System.currentTimeMillis();
}
public void dispose()
{
mAudioOutput.removeAudioEventListener( this );
mAudioOutput.reset();
mAudioOutput = null;
}
public AudioOutput getAudioOutput()
{
return mAudioOutput;
}
public int getPriority()
{
return mPriority;
}
public void setPriority( int priority )
{
mPriority = priority;
}
@Override
public void receive( AudioEvent event )
{
switch( event.getType() )
{
case AUDIO_STARTED:
case AUDIO_STOPPED:
case AUDIO_CONTINUATION:
mAudioStoppedTime = System.currentTimeMillis();
break;
default:
break;
mLowestPriorityConnection = connection;
}
}
}
@@ -342,41 +228,29 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
mThreadPoolManager.cancel( mProcessingTask );
}
endAllAudioAssignments();
for( Entry<Integer,AudioChannelAssignment> e: mChannelAssignments.entrySet() )
{
e.getValue().getAudioOutput().dispose();
}
mChannelAssignments.clear();
disposeCurrentConfiguration();
for( AudioOutput output: mAvailableAudioOutputs )
{
output.dispose();
}
mAvailableAudioOutputs.clear();
mChannelNames.clear();
mAudioOutputMap.clear();
switch( entry.getMixerChannel() )
{
case MONO:
AudioOutput mono = new MonoAudioOutput( entry.getMixer() );
mAvailableAudioOutputs.add( mono );
mChannelNames.add( mono.getChannelName() );
AudioOutput mono = new MonoAudioOutput( mThreadPoolManager,
entry.getMixer() );
mAudioOutputConnections.add( new AudioOutputConnection( mono ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( mono.getChannelName(), mono );
break;
case STEREO:
AudioOutput left = new StereoAudioOutput( entry.getMixer(), MixerChannel.LEFT );
mAvailableAudioOutputs.add( left );
mChannelNames.add( left.getChannelName() );
AudioOutput left = new StereoAudioOutput( mThreadPoolManager,
entry.getMixer(), MixerChannel.LEFT );
mAudioOutputConnections.add( new AudioOutputConnection( left ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( left.getChannelName(), left );
AudioOutput right = new StereoAudioOutput( entry.getMixer(), MixerChannel.RIGHT );
mAvailableAudioOutputs.add( right );
mChannelNames.add( right.getChannelName() );
AudioOutput right = new StereoAudioOutput( mThreadPoolManager,
entry.getMixer(), MixerChannel.RIGHT );
mAudioOutputConnections.add( new AudioOutputConnection( right ) );
mAvailableConnectionCount++;
mAudioOutputMap.put( right.getChannelName(), right );
break;
default:
@@ -389,13 +263,42 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
TimeUnit.MILLISECONDS );
mControllerBroadcaster.broadcast( CONFIGURATION_CHANGE_COMPLETE );
SystemProperties properties = SystemProperties.getInstance();
properties.set( AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName() );
properties.set( AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name() );
properties.save();
}
}
/**
* Clears all channel assignments and terminates all audio outputs in
* preparation for complete shutdown or change to another mixer/channel
* configuration
*/
private void disposeCurrentConfiguration()
{
mChannelConnectionMap.clear();
for( AudioOutputConnection connection : mAudioOutputConnections )
{
connection.dispose();
}
mAvailableConnectionCount = 0;
mAudioOutputConnections.clear();
mAudioOutputMap.clear();
mLowestPriorityConnection = null;
}
@Override
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
{
return mMixerChannel;
return mMixerChannelConfiguration;
}
@Override
@@ -426,4 +329,244 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
mControllerBroadcaster.removeListener( listener );
}
/**
* Returns an audio output connection for the packet if one is
* available, or overrides an existing lower priority connection.
* Returns null if no connection is available for the audio packet.
*
* @param packet from a source
* @return an audio output connection or null
*/
private AudioOutputConnection getConnection( AudioPacket packet )
{
int source = packet.getAudioMetadata().getSource();
//Use an existing connection
if( mChannelConnectionMap.containsKey( source ) )
{
return mChannelConnectionMap.get( source );
}
//Connect to and use an available connection
if( mAvailableConnectionCount > 0 )
{
for( AudioOutputConnection connection: mAudioOutputConnections )
{
if( connection.isDisconnected() )
{
connection.connect( source );
mChannelConnectionMap.put( source, connection );
return connection;
}
}
}
//Preempt an existing connection and connect it to this higher priority source
else //Check for a channel priority override
{
int priority = packet.getAudioMetadata().getPriority();
if( mLowestPriorityConnection != null &&
priority < mLowestPriorityConnection.getPriority() )
{
AudioOutputConnection connection = mLowestPriorityConnection;
mChannelConnectionMap.remove( connection.getSource() );
connection.connect( source );
mChannelConnectionMap.put( source, connection );
updateLowestPriorityAssignment();
return connection;
}
}
return null;
}
public class AudioPacketProcessor implements Runnable
{
@Override
public void run()
{
try
{
disconnectInactiveChannelAssignments();
if( mAudioPacketQueue != null )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mAudioPacketQueue.drainTo( packets );
for( AudioPacket packet: packets )
{
/* Don't process any packet's marked as do not monitor */
if( !packet.getAudioMetadata().isDoNotMonitor() &&
packet.getType() == AudioPacket.Type.AUDIO )
{
AudioOutputConnection connection = getConnection( packet );
if( connection != null )
{
connection.receive( packet );
}
}
}
}
}
catch( Exception e )
{
mLog.error( "Encountered error while processing audio packets", e );
}
}
}
/**
* Audio output connection manages a connection between a source and an audio
* output and maintains current state information about the audio activity
* received form the source.
*/
public class AudioOutputConnection implements Listener<AudioEvent>
{
private static final long CALL_END_DELAY = 2000; //2 seconds
private static final int DISCONNECTED = -1;
private AudioOutput mAudioOutput;
private int mPriority = 0;
private long mLastActivity = System.currentTimeMillis();
private int mSource = DISCONNECTED;
public AudioOutputConnection( AudioOutput audioOutput )
{
mAudioOutput = audioOutput;
mAudioOutput.addAudioEventListener( this );
}
public void receive( AudioPacket packet )
{
if( packet.hasAudioMetadata() &&
packet.getAudioMetadata().getSource() == mSource )
{
int priority = packet.getAudioMetadata().getPriority();
if( mPriority != priority )
{
mPriority = priority;
updateLowestPriorityAssignment();
}
updateTimestamp();
if( mAudioOutput != null )
{
mAudioOutput.receive( packet );
}
}
else
{
if( packet.hasAudioMetadata() )
{
mLog.error( "Received audio packet from channel source [" +
packet.getAudioMetadata().getSource() +
"] however this assignment is currently connected "
+ "to channel source [" + mSource + "]" );
}
else
{
mLog.error( "Received audio packet with no metadata - "
+ "cannot route audio packet" );
}
}
}
private void updateTimestamp()
{
mLastActivity = System.currentTimeMillis();
}
/**
* Terminates the audio output and prepares this connection for disposal
*/
public void dispose()
{
mAudioOutput.dispose();
mAudioOutput = null;
}
/**
* Indicates if this assignment is currently disconnected from a channel source
*/
public boolean isDisconnected()
{
return mSource == DISCONNECTED;
}
/**
* Indicates if this assignment is currently connected to a channel source
*/
public boolean isConnected()
{
return !isDisconnected();
}
/**
* Connects this assignment to the indicated source so that audio
* packets from this source can be sent to the audio output
*/
public void connect( int source )
{
mSource = source;
updateTimestamp();
}
/**
* Currently connected source or -1 if disconnected
*/
public int getSource()
{
return mSource;
}
/**
* Disconnects this assignment from the source and prevents any audio
* from being routed to the audio output until another source is assigned
*/
public void disconnect()
{
mSource = DISCONNECTED;
}
/**
* Indicates if audio output is current inactive, meaning that the
* audio output hasn't recently processed any audio packets.
*/
public boolean isInactive()
{
return mLastActivity != Long.MAX_VALUE &&
mLastActivity + CALL_END_DELAY < System.currentTimeMillis();
}
public int getPriority()
{
return mPriority;
}
@Override
public void receive( AudioEvent event )
{
switch( event.getType() )
{
case AUDIO_STARTED:
case AUDIO_STOPPED:
case AUDIO_CONTINUATION:
updateTimestamp();
break;
default:
break;
}
}
}
}

View File

@@ -1,10 +1,9 @@
package audio.output;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,23 +11,26 @@ import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener;
import audio.AudioEvent;
import audio.AudioEvent.Type;
import audio.AudioPacket;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
public abstract class AudioOutput implements Listener<AudioPacket>
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
{
private final static Logger mLog = LoggerFactory.getLogger( AudioOutput.class );
private AtomicBoolean mSquelched = new AtomicBoolean( false );
private Listener<AudioMetadata> mAudioMetadataListener;
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
protected ScheduledExecutorService mExecutorService;
protected ThreadPoolManager mThreadPoolManager;
protected boolean mCanProcessAudio;
protected long mLastActivity;
public AudioOutput( ThreadPoolManager threadPoolManager )
{
mThreadPoolManager = threadPoolManager;
}
public void reset()
{
broadcast( new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
@@ -41,14 +43,11 @@ public abstract class AudioOutput implements Listener<AudioPacket>
mBuffer.clear();
if( mExecutorService != null )
{
mExecutorService.shutdown();
}
mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null;
mAudioMetadataListener = null;
mThreadPoolManager = null;
}
public abstract String getChannelName();
@@ -72,19 +71,6 @@ public abstract class AudioOutput implements Listener<AudioPacket>
mAudioEventBroadcaster.broadcast( audioEvent );
}
public void setSquelched( boolean squelched )
{
mSquelched.set( squelched );
broadcast( new AudioEvent( squelched ? Type.AUDIO_SQUELCHED :
Type.AUDIO_UNSQUELCHED, getChannelName() ) );
}
public boolean isSquelched()
{
return mSquelched.get();
}
/**
* Registers a single listener to receive audio start and audio stop events
*/

View File

@@ -5,13 +5,14 @@ import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
@@ -24,8 +25,8 @@ import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import audio.output.StereoAudioOutput.QueueProcessor;
import controller.NamingThreadFactory;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/**
* Mono Audio output with automatic flow control based on the availability of
@@ -50,14 +51,14 @@ public class MonoAudioOutput extends AudioOutput
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent;
private ScheduledFuture<?> mProcessorTask;
public MonoAudioOutput( Mixer mixer )
public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer )
{
super();
super( threadPoolManager );
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
@@ -74,15 +75,8 @@ public class MonoAudioOutput extends AudioOutput
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mOutput.addLineListener( this );
mCanProcessAudio = true;
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (mono) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
}
}
catch ( LineUnavailableException e )
@@ -115,12 +109,10 @@ public class MonoAudioOutput extends AudioOutput
mixer.getMixerInfo().getName() + "]" );
}
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (mono) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
}
}
@@ -132,102 +124,108 @@ public class MonoAudioOutput extends AudioOutput
public void dispose()
{
if( mProcessorTask != null && mThreadPoolManager != null )
{
mThreadPoolManager.cancel( mProcessorTask );
}
mProcessorTask = null;
super.dispose();
if( mOutput != null )
{
mOutput.close();
}
mOutput = null;
mGainControl = null;
mMuteControl = null;
}
public class QueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
public QueueProcessor()
{
}
@Override
public void run()
{
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
try
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
broadcast( mAudioContinuationEvent );
List<AudioPacket> packets = new ArrayList<AudioPacket>();
for( AudioPacket packet: packets )
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
if( mCanProcessAudio )
for( AudioPacket packet: packets )
{
if( isSquelched() )
broadcast( mAudioContinuationEvent );
if( mCanProcessAudio )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
float[] samples = packet.getAudioBuffer().getSamples();
/* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 2 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
for( float sample: samples )
if( packet.getType() == Type.AUDIO )
{
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
float[] samples = packet.getAudioBuffer().getSamples();
/* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 2 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
mLastActivity = System.currentTimeMillis();
for( float sample: samples )
{
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
//This is a blocking write
mOutput.write( buffer.array(), 0, buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
mLastActivity = System.currentTimeMillis();
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
else
{
checkStop();
}
mProcessing.set( false );
}
catch( Exception e )
{
mLog.error( "Error while processing audio buffers", e );
}
}
private void checkStart()
{
if( mCanProcessAudio && !isSquelched() &&
if( mCanProcessAudio &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
mOutput.available() >= STOP_THRESHOLD )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@@ -266,4 +264,19 @@ public class MonoAudioOutput extends AudioOutput
{
return mGainControl != null;
}
@Override
public void update( LineEvent event )
{
LineEvent.Type type = event.getType();
if( type == LineEvent.Type.START )
{
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
else if( type == LineEvent.Type.STOP )
{
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}

View File

@@ -5,13 +5,14 @@ import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
@@ -24,7 +25,8 @@ import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import controller.NamingThreadFactory;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
/**
* Mono Audio output in stereo format with automatic flow control based on the
@@ -39,9 +41,9 @@ public class StereoAudioOutput extends AudioOutput
private final static int BUFFER_SIZE = 96000;
/* The queue processor will run every 20 milliseconds checking for inbound
* audio and automatically starting or stopping audio playback. We set the
* audio and automatically start or stop audio playback. We set the
* data line available space threshold to start when we're 20 ms away from
* being full, and the stop threshold when 20 ms away from being empty */
* being full and the stop threshold when 20 ms away from being empty */
private final static int SAMPLE_SIZE_40_MS = BUFFER_SIZE / 25;
private final static int START_THRESHOLD = SAMPLE_SIZE_40_MS;
private final static int STOP_THRESHOLD = BUFFER_SIZE - SAMPLE_SIZE_40_MS;
@@ -49,16 +51,16 @@ public class StereoAudioOutput extends AudioOutput
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent;
private MixerChannel mMixerChannel;
private ScheduledFuture<?> mProcessorTask;
public StereoAudioOutput( Mixer mixer, MixerChannel channel )
public StereoAudioOutput( ThreadPoolManager threadPoolManager,
Mixer mixer, MixerChannel channel )
{
super();
super( threadPoolManager );
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT );
@@ -79,6 +81,7 @@ public class StereoAudioOutput extends AudioOutput
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE );
mOutput.addLineListener( this );
mCanProcessAudio = true;
}
}
@@ -113,12 +116,10 @@ public class StereoAudioOutput extends AudioOutput
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
}
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (stereo) output" ) );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new QueueProcessor(),
40, TimeUnit.MILLISECONDS );
}
}
@@ -130,20 +131,30 @@ public class StereoAudioOutput extends AudioOutput
public void dispose()
{
if( mProcessorTask != null && mThreadPoolManager != null )
{
mThreadPoolManager.cancel( mProcessorTask );
}
mProcessorTask = null;
super.dispose();
if( mOutput != null )
{
mOutput.close();
}
mOutput = null;
mGainControl = null;
mMuteControl = null;
}
private ByteBuffer convert( float[] samples )
{
/* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 4 )
.order( ByteOrder.LITTLE_ENDIAN );
ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
@@ -178,72 +189,72 @@ public class StereoAudioOutput extends AudioOutput
@Override
public void run()
{
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
try
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
/* The mProcessing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
broadcast( mAudioContinuationEvent );
List<AudioPacket> packets = new ArrayList<AudioPacket>();
for( AudioPacket packet: packets )
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
if( mCanProcessAudio )
for( AudioPacket packet: packets )
{
if( isSquelched() )
broadcast( mAudioContinuationEvent );
if( mCanProcessAudio )
{
checkStop();
}
else if( packet.getType() == Type.AUDIO )
{
ByteBuffer buffer = convert(
packet.getAudioBuffer().getSamples() );
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
mLastActivity = System.currentTimeMillis();
if( packet.getType() == Type.AUDIO )
{
ByteBuffer buffer = convert(
packet.getAudioBuffer().getSamples() );
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
mLastActivity = System.currentTimeMillis();
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
else
{
checkStop();
}
mProcessing.set( false );
}
catch( Exception e )
{
mLog.error( "Error while processing audio buffers", e );
}
}
private void checkStart()
{
if( mCanProcessAudio && !isSquelched() &&
if( mCanProcessAudio &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
mOutput.available() >= STOP_THRESHOLD )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@@ -282,4 +293,20 @@ public class StereoAudioOutput extends AudioOutput
{
return mGainControl != null;
}
@Override
public void update( LineEvent event )
{
LineEvent.Type type = event.getType();
if( type == LineEvent.Type.START )
{
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
else if( type == LineEvent.Type.STOP )
{
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}

View File

@@ -58,6 +58,16 @@ public class MessageFramer implements Listener<Boolean>,
mMessageLength = messageLength;
}
public void reset()
{
for( MessageAssembler assembler: mMessageAssemblers )
{
assembler.dispose();
}
mMessageAssemblers.clear();
}
public void dispose()
{
mBroadcaster.dispose();

View File

@@ -33,7 +33,6 @@ import module.decode.DecoderType;
import module.decode.config.DecodeConfigFactory;
import playlist.PlaylistNode;
import source.config.SourceConfigTuner;
import source.recording.RecordingGroupNode;
import source.tuner.Tuner;
import source.tuner.TunerGroupNode;
import source.tuner.TunerNode;
@@ -60,7 +59,7 @@ public class ConfigurationControllerModel extends DefaultTreeModel
private PlaylistNode mPlaylistNode;
private TunerGroupNode mTunerGroupNode;
private RecordingGroupNode mRecordingGroupNode;
// private RecordingGroupNode mRecordingGroupNode;
private IconManager mIconManagerFrame;
@@ -121,15 +120,15 @@ public class ConfigurationControllerModel extends DefaultTreeModel
/**
* Add the recording group node
*/
mRecordingGroupNode = new RecordingGroupNode();
insertNodeInto( mRecordingGroupNode, (MutableTreeNode)root, 1 );
mRecordingGroupNode.loadRecordings();
// mRecordingGroupNode = new RecordingGroupNode();
// insertNodeInto( mRecordingGroupNode, (MutableTreeNode)root, 1 );
// mRecordingGroupNode.loadRecordings();
/**
* Add the playlist node
*/
mPlaylistNode = new PlaylistNode();
insertNodeInto( mPlaylistNode, (MutableTreeNode)root, 2 );
insertNodeInto( mPlaylistNode, (MutableTreeNode)root, 1 );
mPlaylistNode.loadPlaylist();
}

View File

@@ -36,24 +36,20 @@ public class ThreadPoolManager
private HashMap<ScheduledFuture<?>,ThreadType> mTasks =
new HashMap<ScheduledFuture<?>,ThreadType>();
private ScheduledExecutorService mExecutor;
private ScheduledExecutorService mExecutor =
Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
public ThreadPoolManager()
{
}
public ScheduledFuture<?> scheduleFixedRate( ThreadType type,
Runnable command,
long period,
TimeUnit unit )
throws RejectedExecutionException
{
if( mExecutor == null )
{
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
}
ScheduledFuture<?> task =
mExecutor.scheduleAtFixedRate( command, 0, period, unit );
@@ -65,20 +61,16 @@ public class ThreadPoolManager
public void scheduleOnce( Runnable command, long delay, TimeUnit unit )
throws RejectedExecutionException
{
if( mExecutor == null )
{
mExecutor = Executors.newScheduledThreadPool( THREAD_POOL_SIZE,
new NamingThreadFactory( "sdrtrunk" ) );
}
mExecutor.schedule( command, delay, unit );
}
public void cancel( ScheduledFuture<?> task )
public boolean cancel( ScheduledFuture<?> task )
{
task.cancel( true );
boolean success = task.cancel( true );
mTasks.remove( task );
return success;
}
public int getTaskCount( ThreadType type )

View File

@@ -809,8 +809,7 @@ public class Channel extends Configuration
{
if( mProcessingChain == null )
{
mProcessingChain = new ProcessingChain(
mResourceManager.getThreadPoolManager() );
mProcessingChain = new ProcessingChain();
/* Get the optional alias list for the decode modules to use */
AliasList aliasList = mResourceManager.getPlaylistManager()
@@ -901,6 +900,8 @@ public class Channel extends Configuration
{
if( !mEnabled && mProcessingChain != null )
{
fireChannelEvent( Event.CHANNEL_PROCESSING_STOPPED );
mProcessingChain.stop();
if( mChannelType == ChannelType.STANDARD )
@@ -910,8 +911,6 @@ public class Channel extends Configuration
}
mSelected = false;
fireChannelEvent( Event.CHANNEL_PROCESSING_STOPPED );
}
}

View File

@@ -421,7 +421,7 @@ public class SDRTrunk
if( version != null )
{
sb.append( " V" );
sb.append( " v" );
sb.append( version );
}
}

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
* Copyright (C) 2014-2016 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
@@ -17,52 +17,94 @@
******************************************************************************/
package message;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import map.Plottable;
import module.decode.event.MessageDetailsProvider;
import alias.Alias;
public abstract class Message implements MessageDetailsProvider
public abstract class Message
{
protected long mTimeReceived;
protected MessageType mType;
public Message()
{
this( MessageType.UN_KNWN );
}
public Message( MessageType type )
{
mTimeReceived = System.currentTimeMillis();
mType = type;
}
public abstract Plottable getPlottable();
public abstract String toString();
public abstract boolean isValid();
public long getTimeReceived()
{
return mTimeReceived;
}
public Date getDateReceived()
/**
* Map plottable
*/
public Plottable getPlottable()
{
return new Date( mTimeReceived );
}
public MessageType getType()
{
return mType;
//TODO: move this to an IPlottable interface that only gets implemented as needed
return null;
}
/**
* Decoded textual representation of the message
*/
public abstract String toString();
/**
* Indicates if the message is valid and has passed crc/integrity checks
*/
public abstract boolean isValid();
/**
* Status of the CRC check of the message
*/
public abstract String getErrorStatus();
/**
* Parsed Message
* @return
*/
public abstract String getMessage();
/**
* Raw ( 0 & 1 ) message bits
*/
public abstract String getBinaryMessage();
/**
* Decoded protocol
*/
public abstract String getProtocol();
/**
* Event - call, data, idle, etc.
*/
public abstract String getEventType();
/**
* Formatted from identifier
*/
public abstract String getFromID();
/**
* From identifier alias (from AliasManager)
*/
public abstract Alias getFromIDAlias();
/**
* Formatted to identifier
*/
public abstract String getToID();
/**
* To identifier alias (from AliasManager)
* @return
*/
public abstract Alias getToIDAlias();
/**
* Provides a listing of aliases contained in the message.
*/

View File

@@ -99,8 +99,7 @@ public class ProcessingChain implements IChannelEventListener
private Broadcaster<RealBuffer> mRealBufferBroadcaster = new Broadcaster<>();
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mBufferProcessingTask;
// private ThreadPoolManager mThreadPoolManager;
private AtomicBoolean mRunning = new AtomicBoolean();
protected Source mSource;
@@ -109,14 +108,8 @@ public class ProcessingChain implements IChannelEventListener
public ProcessingChain()
{
this( new ThreadPoolManager() );
}
public ProcessingChain( ThreadPoolManager threadManager )
{
mThreadPoolManager = threadManager;
}
public void dispose()
{
stop();
@@ -128,14 +121,6 @@ public class ProcessingChain implements IChannelEventListener
mModules.clear();
// mComplexReceiver.dispose();
// mComplexReceiver = null;
//
// mRealReceiver.dispose();
// mRealReceiver = null;
mThreadPoolManager = null;
mBufferProcessingTask = null;
mFrequencyCorrectionController = null;
mAudioPacketBroadcaster.dispose();
@@ -496,12 +481,6 @@ public class ProcessingChain implements IChannelEventListener
mSource = null;
}
if( mBufferProcessingTask != null )
{
mThreadPoolManager.cancel( mBufferProcessingTask );
mBufferProcessingTask = null;
}
/* Stop each of the modules */
for( Module module: mModules )
{

View File

@@ -228,7 +228,8 @@ public class DecoderFactory
Sync sync = mptConfig.getSync();
modules.add( new MPT1327Decoder( aliasList, sync ) );
modules.add( new MPT1327DecoderState( aliasList, channelMap, channelType ) );
modules.add( new MPT1327DecoderState( aliasList, channelMap,
channelType, mptConfig.getCallTimeout() * 1000 ) );
if( channelType == ChannelType.STANDARD )
{

View File

@@ -310,6 +310,11 @@ public abstract class CallEvent implements Comparable<CallEvent>
@Override
public int compareTo( CallEvent other )
{
if( other != null && other == this )
{
return 0;
}
return Long.compare( mEventStart, other.getEventStartTime() );
}
}

View File

@@ -19,6 +19,7 @@ package module.decode.event;
import java.awt.EventQueue;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
import javax.swing.table.AbstractTableModel;
@@ -174,7 +175,7 @@ public class MessageActivityModel extends AbstractTableModel
switch( columnIndex )
{
case sTIME:
return mSDFTime.format( message.getDateReceived() );
return mSDFTime.format( new Date( message.getTimeReceived() ) );
case sPROTOCOL:
return message.getProtocol();
case sERROR_STATUS:

View File

@@ -1,80 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package module.decode.event;
import alias.Alias;
/**
* Getter methods to support the Message Activity Table/Model
*/
public interface MessageDetailsProvider
{
/**
* Indicates if the message is valid and has passed crc/integrity checks
*/
public abstract boolean isValid();
/**
* Status of the CRC check of the message
*/
public abstract String getErrorStatus();
/**
* Parsed Message
* @return
*/
public abstract String getMessage();
/**
* Raw ( 0 & 1 ) message bits
*/
public abstract String getBinaryMessage();
/**
* Decoded protocol
*/
public abstract String getProtocol();
/**
* Event - call, data, idle, etc.
*/
public abstract String getEventType();
/**
* Formatted from identifier
*/
public abstract String getFromID();
/**
* From identifier alias (from AliasManager)
*/
public abstract Alias getFromIDAlias();
/**
* Formatted to identifier
*/
public abstract String getToID();
/**
* To identifier alias (from AliasManager)
* @return
*/
public abstract Alias getToIDAlias();
}

View File

@@ -51,17 +51,16 @@ public class LTRCallEvent extends CallEvent
*/
public boolean isMatchingTalkgroup( LTRNetMessage message )
{
String talkgroup = message.getTalkgroupID();
if( talkgroup != null && getToID() != null &&
talkgroup.contentEquals( getToID() ) )
{
return true;
}
return false;
return isMatchingTalkgroup( message.getTalkgroupID() );
}
public boolean isMatchingTalkgroup( String talkgroup )
{
return talkgroup != null &&
getToID() != null &&
talkgroup.contentEquals( getToID() );
}
private Alias getAlias( String talkgroup )
{
if( hasAliasList() )

View File

@@ -112,6 +112,7 @@ public class LTRNetDecoder extends Decoder implements IRealBufferListener, Instr
@Override
public void reset()
{
mLTRMessageFramer.reset();
}
@Override

View File

@@ -76,6 +76,7 @@ public class LTRStandardDecoder extends Decoder implements IRealBufferListener
@Override
public void reset()
{
mLTRMessageFramer.reset();
}
@Override

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
* Copyright (C) 2014-2016 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
@@ -17,21 +17,31 @@
******************************************************************************/
package module.decode.ltrstandard;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import message.Message;
import module.decode.DecoderType;
import module.decode.event.CallEvent.CallEventType;
import module.decode.ltrnet.LTRCallEvent;
import module.decode.ltrstandard.message.CallEndMessage;
import module.decode.ltrstandard.message.CallMessage;
import module.decode.ltrstandard.message.IdleMessage;
import module.decode.ltrstandard.message.LTRStandardMessage;
import module.decode.state.ChangedAttribute;
import module.decode.state.DecoderState;
import module.decode.state.DecoderStateEvent;
import module.decode.state.DecoderStateEvent.Event;
import module.decode.state.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import alias.Alias;
import alias.AliasList;
import audio.metadata.Metadata;
@@ -39,15 +49,17 @@ import audio.metadata.MetadataType;
public class LTRStandardDecoderState extends DecoderState
{
private HashMap<Integer,String> mActiveCalls = new HashMap<Integer,String>();
private HashSet<String> mTalkgroupsFirstHeard = new HashSet<String>();
private TreeSet<String> mTalkgroups = new TreeSet<String>();
private TreeSet<Integer> mActiveLCNs = new TreeSet<Integer>();
private final static Logger mLog =
LoggerFactory.getLogger( LTRStandardDecoderState.class );
private Map<Integer,LTRCallEvent> mActiveCalls = new HashMap<>();
private Set<String> mTalkgroupsFirstHeard = new HashSet<>();
private Set<String> mTalkgroups = new TreeSet<>();
private String mTalkgroup;
private Alias mTalkgroupAlias;
private int mChannelNumber;
private long mFrequency;
private LCNTracker mLCNTracker = new LCNTracker();
public LTRStandardDecoderState( AliasList aliasList )
{
@@ -70,171 +82,128 @@ public class LTRStandardDecoderState extends DecoderState
{
}
private LTRCallEvent getCurrentCallEvent()
{
return (LTRCallEvent)mCurrentCallEvent;
}
@Override
public void receive( Message message )
{
if( message.isValid() )
if( message.isValid() && message instanceof LTRStandardMessage )
{
if( message instanceof LTRStandardOSWMessage )
switch( ((LTRStandardMessage)message).getMessageType() )
{
LTRStandardOSWMessage ltr = ((LTRStandardOSWMessage)message);
switch( ltr.getMessageType() )
{
case CA_STRT:
if( 0 < ltr.getChannel() && ltr.getChannel() <=20 )
{
/* If we haven't captured the LCN yet ... */
if( mChannelNumber == 0 )
{
mChannelNumber = ltr.getChannel();
broadcast( ChangedAttribute.CHANNEL_NUMBER );
}
case CA_STRT:
CallMessage start = (CallMessage)message;
int channel = start.getChannel();
mLCNTracker.processCallChannel( channel );
mLCNTracker.processFreeChannel( start.getFree() );
/* If this call event is for this channel, then update
* talkgroup and alias */
if( ltr.getChannel() == mChannelNumber )
{
mTalkgroup = ltr.getTalkgroupID();
broadcast( ChangedAttribute.TO_TALKGROUP );
mTalkgroupAlias = ltr.getTalkgroupIDAlias();
broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS );
LTRCallEvent current = getCurrentCallEvent();
//Only process calls on this LCN, or call detects for
//talkgroups that are homed on this LCN
if( mLCNTracker.isValidChannel( channel ) &&
( mLCNTracker.isCurrentChannel( channel ) ||
mLCNTracker.isCurrentChannel( start.getHomeRepeater() ) ) )
{
LTRCallEvent event = mActiveCalls.get( channel );
if( event == null ||
!event.isMatchingTalkgroup( start.getToID() ) )
{
//Check for different talkgroup
if( event != null )
{
event.end();
mActiveCalls.remove( channel );
}
/* Detect when we haven't reset from the previous
* call and now have a new call coming in with
* a different talkgroup */
if( current != null && isDifferentTalkgroup( mTalkgroup ) )
{
mCurrentCallEvent.end();
broadcast( mCurrentCallEvent );
mCurrentCallEvent = null;
}
if( current == null )
{
mCurrentCallEvent =
new LTRCallEvent.Builder(
DecoderType.LTR_STANDARD, CallEventType.CALL )
.to( ltr.getTalkgroupID() )
.aliasList( getAliasList() )
.channel( String.valueOf( ltr.getChannel() ) )
.frequency( mFrequency )
.build();
boolean current = mLCNTracker.isCurrentChannel( channel );
event = new LTRCallEvent.Builder(
DecoderType.LTR_STANDARD,
current ? CallEventType.CALL : CallEventType.CALL_DETECT )
.to( start.getToID() )
.aliasList( getAliasList() )
.channel( start.getChannelFormatted() )
.frequency( current ? mFrequency : 0 )
.build();
mActiveCalls.put( channel, event );
broadcast( event );
if( mLCNTracker.isCurrentChannel( channel ) )
{
mTalkgroup = start.getToID();
broadcast( ChangedAttribute.TO_TALKGROUP );
processTalkgroup( mTalkgroup );
mTalkgroupAlias = start.getToIDAlias();
broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS );
broadcast( mCurrentCallEvent );
broadcast( new Metadata( MetadataType.TO,
mTalkgroup, mTalkgroupAlias, true ) );
broadcast( new Metadata( MetadataType.TO,
mTalkgroup, mTalkgroupAlias, true ) );
}
}
broadcast( new DecoderStateEvent( this,
Event.CONTINUATION, State.CALL ) );
}
break;
case CA_ENDD:
CallEndMessage end = (CallEndMessage)message;
broadcast( new DecoderStateEvent( this,
Event.START, State.CALL ) );
}
else
{
broadcast( new DecoderStateEvent( this,
Event.CONTINUATION, State.CALL ) );
}
}
}
break;
case CA_ENDD:
mTalkgroup = ltr.getTalkgroupID();
broadcast( ChangedAttribute.TO_TALKGROUP );
//Home channel is 31 for call end -- use the free channel
//as the call end channel
int repeater = end.getFree();
mLCNTracker.processCallChannel( repeater );
if( mLCNTracker.isCurrentChannel( repeater ) )
{
mTalkgroup = end.getToID();
broadcast( ChangedAttribute.TO_TALKGROUP );
processTalkgroup( mTalkgroup );
mTalkgroupAlias = end.getToIDAlias();
broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS );
mTalkgroupAlias = ltr.getTalkgroupIDAlias();
broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS );
LTRCallEvent event = mActiveCalls.remove( repeater );
if( event != null )
{
event.end();
broadcast( event );
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
}
}
break;
case SY_IDLE:
IdleMessage idle = (IdleMessage)message;
if( mCurrentCallEvent != null )
{
mCurrentCallEvent.end();
broadcast( mCurrentCallEvent );
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
mCurrentCallEvent = null;
}
int home = ltr.getHomeRepeater();
if( 0 < home && home <= 20 )
{
String talkgroup = ltr.getTalkgroupID( false );
if( mTalkgroupsFirstHeard.contains( talkgroup ) )
{
mTalkgroups.add( talkgroup );
}
else
{
mTalkgroupsFirstHeard.add( talkgroup );
}
}
int lcn = idle.getChannel();
int channel = ltr.getChannel();
if( 0 < channel && channel <= 20 )
{
if( channel == home )
{
if( mChannelNumber != channel )
{
mChannelNumber = channel;
broadcast( ChangedAttribute.CHANNEL_NUMBER );
}
mActiveLCNs.add( channel );
}
int free = ltr.getFree();
if( 0 < free && free <= 20 )
{
mActiveLCNs.add( free );
}
}
break;
case SY_IDLE:
int idleChannel = ltr.getChannel();
if( idleChannel == ltr.getHomeRepeater() &&
idleChannel == ltr.getFree() )
{
if( mChannelNumber != idleChannel )
{
mChannelNumber = idleChannel;
broadcast( ChangedAttribute.CHANNEL_NUMBER );
}
mActiveLCNs.add( idleChannel );
}
broadcast( new DecoderStateEvent( this,
Event.CONTINUATION, State.IDLE ) );
break;
case UN_KNWN:
default:
break;
}
mLCNTracker.processCallChannel( lcn );
break;
case UN_KNWN:
default:
break;
}
}
}
/**
* Indicates if the talkgroup is different than the talkgroup specified in
* the current call event
*/
private boolean isDifferentTalkgroup( String talkgroup )
private void processTalkgroup( String talkgroup )
{
return talkgroup != null &&
mCurrentCallEvent != null &&
mCurrentCallEvent.getToID() != null &&
!mCurrentCallEvent.getToID().contentEquals( talkgroup );
if( mTalkgroupsFirstHeard.contains( talkgroup ) )
{
mTalkgroups.add( talkgroup );
}
else
{
mTalkgroupsFirstHeard.add( talkgroup );
}
}
/**
@@ -245,8 +214,7 @@ public class LTRStandardDecoderState extends DecoderState
mActiveCalls.clear();
mTalkgroupsFirstHeard.clear();
mTalkgroups.clear();
mActiveLCNs.clear();
mChannelNumber = 0;
mLCNTracker.reset();
resetState();
}
@@ -256,13 +224,20 @@ public class LTRStandardDecoderState extends DecoderState
*/
private void resetState()
{
if( mCurrentCallEvent != null )
for( Integer key: mActiveCalls.keySet() )
{
mCurrentCallEvent.setEnd( System.currentTimeMillis() );
broadcast( mCurrentCallEvent );
mCurrentCallEvent = null;
LTRCallEvent event = mActiveCalls.get( key );
if( event != null )
{
event.end();
broadcast( event );
}
}
mActiveCalls.clear();
mTalkgroup = null;
broadcast( ChangedAttribute.TO_TALKGROUP );
@@ -282,12 +257,12 @@ public class LTRStandardDecoderState extends DecoderState
public boolean hasChannelNumber()
{
return mChannelNumber != 0;
return mLCNTracker.getCurrentChannel() != 0;
}
public int getChannelNumber()
{
return mChannelNumber;
return mLCNTracker.getCurrentChannel();
}
@Override
@@ -322,28 +297,28 @@ public class LTRStandardDecoderState extends DecoderState
sb.append( "Monitored LCN: " );
if( mChannelNumber > 0 )
if( hasChannelNumber() )
{
sb.append( mChannelNumber );
sb.append( getChannelNumber() );
}
else
{
sb.append( "unknown" );
sb.append( "*Insufficient Data*" );
}
sb.append( "\n" );
Integer[] lcns = mActiveLCNs.toArray( new Integer[0] );
sb.append( "Active LCNs:\t" );
List<Integer> lcns = mLCNTracker.getActiveLCNs();
if( lcns.length > 0 )
if( lcns.size() > 0 )
{
sb.append( Arrays.toString( lcns ) );
sb.append( mLCNTracker.getActiveLCNs() );
}
else
{
sb.append( "none" );
sb.append( "*Insufficient Data*" );
}
sb.append( "\n\n" );
@@ -355,19 +330,15 @@ public class LTRStandardDecoderState extends DecoderState
}
else
{
Iterator<String> it = mTalkgroups.iterator();
while( it.hasNext() )
for( String talkgroup: mTalkgroups )
{
String tgid = it.next();
sb.append( " " );
sb.append( formatTalkgroup( tgid ) );
sb.append( talkgroup );
sb.append( " " );
if( hasAliasList() )
{
Alias alias = getAliasList().getTalkgroupAlias( tgid );
Alias alias = getAliasList().getTalkgroupAlias( talkgroup );
if( alias != null )
{
@@ -376,30 +347,147 @@ public class LTRStandardDecoderState extends DecoderState
}
sb.append( "\n" );
}
}
return sb.toString();
}
public static String formatTalkgroup( String talkgroup )
/**
* Tracks the set of call and free channels for a system in order to
* dynamically determine the current LCN and minimize false triggers from
* bogus decoded LTR messages. Tracks the number of occurances of each
* LCN and uses a dynamic threshold to determine LCN validity.
*/
public class LCNTracker
{
StringBuilder sb = new StringBuilder();
private static final int DEFAULT_COUNT = 10;
private int[] mCallLCNCounts;
private int[] mFreeLCNCounts;
private int mCallHighestCount;
private int mFreeHighestCount;
private int mCurrentLCN;
if( talkgroup.length() == 6 )
public LCNTracker()
{
sb.append( talkgroup.substring( 0, 1 ) );
sb.append( "-" );
sb.append( talkgroup.substring( 1, 3 ) );
sb.append( "-" );
sb.append( talkgroup.substring( 3, 6 ) );
return sb.toString();
reset();
}
else
public void logStatistics()
{
return talkgroup;
for( int x = 1; x <= 20; x++ )
{
mLog.debug( "Call " + x + ": " + mCallLCNCounts[ x ] );
}
mLog.debug( "Call Highest Count: " + mCallHighestCount );
for( int x = 1; x <= 20; x++ )
{
mLog.debug( "Free " + x + ": " + mFreeLCNCounts[ x ] );
}
mLog.debug( "Free Highest Count: " + mFreeHighestCount );
mLog.debug( "Current LCN: " + mCurrentLCN );
}
public void reset()
{
//Dim channel arrays to 21 -- ignore the 0 index
mCallLCNCounts = new int[ 21 ];
mFreeLCNCounts = new int[ 21 ];
mCallHighestCount = 3;
mFreeHighestCount = DEFAULT_COUNT;
mCurrentLCN = 0;
}
public boolean isCurrentChannel( int channel )
{
if( mCurrentLCN == 0 )
{
return true;
}
else
{
return channel == mCurrentLCN;
}
}
public int getCurrentChannel()
{
return mCurrentLCN;
}
/**
* Indicates if the channel is valid based on the observed current and
* free channels reported by the system. Tracks the count of each
* reported free channel the highest free channel count is used as a
* threshold where a channel is valid when its count exceeds 20% of the
* highest free channel count. A channel is also valid when it is
* the currently monitored channel.
*/
public boolean isValidChannel( int channel )
{
if( isCurrentChannel( channel ) )
{
return true;
}
int count = mFreeLCNCounts[ channel ];
int threshold = (int)( (double)mFreeHighestCount * 0.2 );
return count >= threshold;
}
public void processCallChannel( int channel )
{
if( 1 <= channel && channel <= 20 )
{
mCallLCNCounts[ channel ]++;
if( mCallLCNCounts[ channel ] > mCallHighestCount )
{
mCallHighestCount = mCallLCNCounts[ channel ];
mCurrentLCN = channel;
broadcast( ChangedAttribute.CHANNEL_NUMBER );
}
}
}
public void processFreeChannel( int channel )
{
if( 1 <= channel && channel <= 20 )
{
mFreeLCNCounts[ channel ]++;
if( mFreeLCNCounts[ channel ] > mFreeHighestCount )
{
mFreeHighestCount = mFreeLCNCounts[ channel ];
}
}
}
public List<Integer> getActiveLCNs()
{
List<Integer> active = new ArrayList<>();
if( mFreeHighestCount > DEFAULT_COUNT )
{
for( int x = 1; x <= 20; x++ )
{
if( mCurrentLCN != 0 && isCurrentChannel( x ) )
{
active.add( x );
}
else if( isValidChannel( x ) )
{
active.add( x );
}
}
}
return active;
}
}
}

View File

@@ -1,238 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package module.decode.ltrstandard;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import map.Plottable;
import message.MessageDirection;
import message.MessageType;
import alias.Alias;
import alias.AliasList;
import bits.BinaryMessage;
public class LTRStandardISWMessage extends LTRStandardMessage
{
private static final int sCALL_END = 31;
private static final int sIDLE = 255;
public LTRStandardISWMessage( BinaryMessage message, AliasList list )
{
super( message, MessageDirection.OSW, list );
mMessageType = getType();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( mDatestampFormatter.format(
new Date( System.currentTimeMillis() ) ) );
sb.append( " LTR OSW [" );
sb.append( mCRC.getAbbreviation() );
sb.append( "] " );
sb.append( getMessage() );
pad( sb, 100 );
sb.append( mMessage.toString() );
return sb.toString();
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
switch( mMessageType )
{
case SY_IDLE:
sb.append( "IDLE AREA:" );
sb.append( getArea() );
sb.append( " LCN:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " HOME:" );
sb.append( format( getHomeRepeater(), 2 ) );
sb.append( " GRP:" );
sb.append( format( getGroup(), 3 ) );
sb.append( " FREE:" );
sb.append( format( getFree(), 2 ) );
break;
case CA_STRT:
sb.append( "CALL LCN:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " TG [ " );
sb.append( getTalkgroupID() );
sb.append( "/" );
sb.append( getTalkgroupIDAlias() );
sb.append( " ] FREE:" );
sb.append( format( getFree(), 2 ) );
break;
case CA_ENDD:
sb.append( "END* AREA:" );
sb.append( getArea() );
sb.append( " LCN:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " TG [" );
sb.append( getTalkgroupID() );
sb.append( "/" );
sb.append( getTalkgroupIDAlias() );
sb.append( "] FREE:" );
sb.append( format( getFree(), 2 ) );
break;
default:
sb.append( "UNKNOWN A:" );
sb.append( getArea() );
sb.append( " C:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " H:" );
sb.append( format( getHomeRepeater(), 2 ) );
sb.append( " G:" );
sb.append( format( getGroup(), 3 ) );
sb.append( " F:" );
sb.append( format( getFree(), 2 ) );
}
return sb.toString();
}
public String getTalkgroupID()
{
return getTalkgroupID( true );
}
public String getTalkgroupID( boolean format )
{
StringBuilder sb = new StringBuilder();
sb.append( getArea() );
if( format )
{
sb.append( "-" );
}
sb.append( format( getHomeRepeater(), 2 ) );
if( format )
{
sb.append( "-" );
}
sb.append( format( getGroup(), 3 ) );
return sb.toString();
}
public Alias getTalkgroupIDAlias()
{
StringBuilder sb = new StringBuilder();
sb.append( getArea() );
sb.append( format( getHomeRepeater(), 2 ) );
sb.append( format( getGroup(), 3 ) );
return mAliasList.getTalkgroupAlias( sb.toString() );
}
public MessageType getType()
{
MessageType retVal = MessageType.UN_KNWN;
int group = getGroup();
if( group == sIDLE )
{
retVal = MessageType.SY_IDLE;
}
else
{
int channel = getChannel();
if( channel == sCALL_END )
{
retVal = MessageType.CA_ENDD;
}
else
{
retVal = MessageType.CA_STRT;
}
}
return retVal;
}
@Override
public String getFromID()
{
if( mMessageType == MessageType.CA_STRT ||
mMessageType == MessageType.CA_ENDD )
{
return getTalkgroupID();
}
else
{
return null;
}
}
@Override
public Alias getFromIDAlias()
{
if( mMessageType == MessageType.CA_STRT ||
mMessageType == MessageType.CA_ENDD )
{
return getTalkgroupIDAlias();
}
else
{
return null;
}
}
@Override
public Plottable getPlottable()
{
// TODO Auto-generated method stub
return null;
}
/**
* Provides a listing of aliases contained in the message.
*/
public List<Alias> getAliases()
{
List<Alias> aliases = new ArrayList<Alias>();
Alias from = getFromIDAlias();
if( from != null )
{
aliases.add( from );
}
Alias to = getToIDAlias();
if( to != null )
{
aliases.add( to );
}
return aliases;
}
}

View File

@@ -1,254 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package module.decode.ltrstandard;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import message.Message;
import message.MessageDirection;
import message.MessageType;
import module.decode.DecoderType;
import alias.Alias;
import alias.AliasList;
import bits.BinaryMessage;
import edac.CRC;
import edac.CRCLTR;
public abstract class LTRStandardMessage extends Message
{
protected static final String sUNKNOWN = "**UNKNOWN**";
protected static final int sINT_NULL_VALUE = -1;
protected static final double sDOUBLE_NULL_VALUE = -1.0D;
protected SimpleDateFormat mDatestampFormatter =
new SimpleDateFormat( "yyyyMMdd HHmmss" );
protected DecimalFormat mDecimalFormatter = new DecimalFormat("0.00000");
protected static final int[] sSYNC = { 8,7,6,5,4,3,2,1,0 };
protected static final int[] sAREA = { 9 };
protected static final int[] sCHANNEL = { 14,13,12,11,10 };
protected static final int[] sHOME_REPEATER = { 19,18,17,16,15 };
protected static final int[] sGROUP = { 27,26,25,24,23,22,21,20 };
protected static final int[] sFREE = { 32,31,30,29,28 };
protected static final int[] sCRC = { 39,38,37,36,35,34,33 };
protected BinaryMessage mMessage;
protected MessageType mMessageType;
protected CRC mCRC;
protected AliasList mAliasList;
public LTRStandardMessage( BinaryMessage message,
MessageDirection direction,
AliasList list )
{
mMessage = message;
mCRC = CRCLTR.check( message, direction );
mAliasList = list;
}
public MessageType getMessageType()
{
return mMessageType;
}
public boolean isValid()
{
return mCRC != CRC.FAILED_CRC && mCRC != CRC.FAILED_PARITY;
}
/**
* Appends spaces to the end of the stringbuilder to make it length long
*/
protected void pad( StringBuilder sb, int length )
{
while( sb.length() < length )
{
sb.append( " " );
}
}
@Override
public String getBinaryMessage()
{
return mMessage.toString();
}
/**
* Returns a value formatted in hex notation.
*
* @param value - integer value
* @return - hex digits returned
*/
protected static String getHex( int value, int digits )
{
return String.format( "%0" + digits + "X", value );
}
protected int getInt( int[] bits )
{
int retVal = 0;
for( int x = 0; x < bits.length; x++ )
{
if( mMessage.get( bits[ x ] ) )
{
retVal += 1<<x;
}
}
return retVal;
}
/**
* Pads an integer value with additional zeroes to make it decimalPlaces long
*/
public String format( int number, int decimalPlaces )
{
StringBuilder sb = new StringBuilder();
int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() );
for( int x = 0; x < paddingRequired; x++)
{
sb.append( "0" );
}
sb.append( number );
return sb.toString();
}
public String format( String val, int places )
{
StringBuilder sb = new StringBuilder();
sb.append( val );
while( sb.length() < places )
{
sb.append( " " );
}
return sb.toString();
}
public CRC getCRC()
{
return mCRC;
}
public int getArea()
{
return getInt( sAREA );
}
public int getChannel()
{
return getInt( sCHANNEL );
}
public int getHomeRepeater()
{
return getInt( sHOME_REPEATER );
}
public int getGroup()
{
return getInt( sGROUP );
}
public String getTalkgroupID()
{
return getTalkgroupID( true );
}
public String getTalkgroupID( boolean format )
{
StringBuilder sb = new StringBuilder();
sb.append( getArea() );
if( format )
{
sb.append( "-" );
}
sb.append( format( getHomeRepeater(), 2 ) );
if( format )
{
sb.append( "-" );
}
sb.append( format( getGroup(), 3 ) );
return sb.toString();
}
public Alias getTalkgroupIDAlias()
{
return mAliasList.getTalkgroupAlias( getTalkgroupID() );
}
public int getFree()
{
return getInt( sFREE );
}
public int getCRCChecksum()
{
return getInt( sCRC );
}
@Override
public String getProtocol()
{
return DecoderType.LTR_STANDARD.getDisplayString();
}
@Override
public String getEventType()
{
if( mMessageType != null )
{
return mMessageType.getDisplayText();
}
else
{
return MessageType.UN_KNWN.getDisplayText();
}
}
@Override
public String getToID()
{
return null;
}
@Override
public Alias getToIDAlias()
{
return null;
}
@Override
public String getErrorStatus()
{
return mCRC.getDisplayText();
}
}

View File

@@ -6,6 +6,7 @@ import java.util.List;
import message.Message;
import message.MessageType;
import module.decode.ltrstandard.message.LTRStandardMessage;
import filter.Filter;
import filter.FilterElement;

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
* Copyright (C) 2014-2016 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
@@ -19,9 +19,16 @@ package module.decode.ltrstandard;
import message.Message;
import message.MessageDirection;
import module.decode.ltrstandard.message.CallEndMessage;
import module.decode.ltrstandard.message.CallMessage;
import module.decode.ltrstandard.message.IdleMessage;
import module.decode.ltrstandard.message.LTRStandardMessage;
import module.decode.ltrstandard.message.UnknownMessage;
import sample.Listener;
import alias.AliasList;
import bits.BinaryMessage;
import edac.CRC;
import edac.CRCLTR;
public class LTRStandardMessageProcessor implements Listener<BinaryMessage>
{
@@ -29,6 +36,12 @@ public class LTRStandardMessageProcessor implements Listener<BinaryMessage>
private AliasList mAliasList;
private Listener<Message> mMessageListener;
/**
* Processes raw binary messages and converts them to the correct message class
*
* @param direction - inbound (ISW) or outbound (OSW)
* @param list - alias list
*/
public LTRStandardMessageProcessor( MessageDirection direction, AliasList list )
{
mDirection = direction;
@@ -36,27 +49,66 @@ public class LTRStandardMessageProcessor implements Listener<BinaryMessage>
}
@Override
public void receive( BinaryMessage buffer )
public void receive( BinaryMessage binaryMessage )
{
if( mMessageListener != null )
{
LTRStandardMessage message;
if( mDirection == MessageDirection.OSW )
//Inbound Status Word (ISW) is a bit-flipped version of the Outbound
//(OSW), so flip the bits and process it as an OSW
if( mDirection == MessageDirection.ISW )
{
message = new LTRStandardOSWMessage( buffer, mAliasList );
binaryMessage.flip( 0, 40 );
}
else
CRC crc = CRCLTR.check( binaryMessage, mDirection );
if( crc != CRC.FAILED_CRC &&
crc != CRC.FAILED_PARITY )
{
buffer.flip( 0, 40 );
LTRStandardMessage message;
message = new LTRStandardISWMessage( buffer, mAliasList );
int channel = binaryMessage.getInt( LTRStandardMessage.CHANNEL );
int home = binaryMessage.getInt( LTRStandardMessage.HOME_REPEATER );
int free = binaryMessage.getInt( LTRStandardMessage.FREE );
int group = binaryMessage.getInt( LTRStandardMessage.GROUP );
if( isValidChannel( channel ) &&
isValidChannel( home ) &&
isValidChannel( free ) )
{
if( channel == free && group == 255 )
{
message = new IdleMessage( binaryMessage, mDirection, mAliasList, crc );
}
else
{
message = new CallMessage( binaryMessage, mDirection, mAliasList, crc );
}
}
else if( channel == 31 &&
isValidChannel( home ) &&
isValidChannel( free ) )
{
message = new CallEndMessage( binaryMessage, mDirection, mAliasList, crc );
}
else
{
message = new UnknownMessage( binaryMessage, mDirection, mAliasList, crc );
}
mMessageListener.receive( message );
}
mMessageListener.receive( message );
}
}
/**
* Checks the channel (LCN) number to ensure it is in the range 1 -20
*/
private boolean isValidChannel( int channel )
{
return ( 1 <= channel && channel <= 20 );
}
public void setMessageListener( Listener<Message> listener )
{
mMessageListener = listener;

View File

@@ -1,248 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package module.decode.ltrstandard;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import map.Plottable;
import message.MessageDirection;
import message.MessageType;
import alias.Alias;
import alias.AliasList;
import bits.BinaryMessage;
public class LTRStandardOSWMessage extends LTRStandardMessage
{
private static final int sCALL_END = 31;
private static final int sIDLE = 255;
public LTRStandardOSWMessage( BinaryMessage message, AliasList list )
{
super( message, MessageDirection.OSW, list );
mMessageType = getType();
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( mDatestampFormatter.format(
new Date( System.currentTimeMillis() ) ) );
sb.append( " LTR OSW [" );
sb.append( mCRC.getAbbreviation() );
sb.append( "] " );
sb.append( getMessage() );
pad( sb, 100 );
sb.append( mMessage.toString() );
return sb.toString();
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
switch( mMessageType )
{
case SY_IDLE:
sb.append( "IDLE AREA:" );
sb.append( getArea() );
sb.append( " LCN:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " HOME:" );
sb.append( format( getHomeRepeater(), 2 ) );
sb.append( " GRP:" );
sb.append( format( getGroup(), 3 ) );
sb.append( " FREE:" );
sb.append( format( getFree(), 2 ) );
break;
case CA_STRT:
sb.append( "CALL LCN:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " TG [ " );
sb.append( getTalkgroupID() );
sb.append( "/" );
Alias alias = getTalkgroupIDAlias();
if( alias != null )
{
sb.append( getTalkgroupIDAlias().getName() );
}
else
{
sb.append( "UNKNOWN" );
}
sb.append( " ] FREE:" );
sb.append( format( getFree(), 2 ) );
break;
case CA_ENDD:
sb.append( "END* AREA:" );
sb.append( getArea() );
sb.append( " LCN:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " TG [" );
sb.append( getTalkgroupID() );
sb.append( "/" );
Alias endAlias = getTalkgroupIDAlias();
if( endAlias != null )
{
sb.append( getTalkgroupIDAlias().getName() );
}
else
{
sb.append( "UNKNOWN" );
}
sb.append( "] FREE:" );
sb.append( format( getFree(), 2 ) );
break;
default:
sb.append( "UNKNOWN A:" );
sb.append( getArea() );
sb.append( " C:" );
sb.append( format( getChannel(), 2 ) );
sb.append( " H:" );
sb.append( format( getHomeRepeater(), 2 ) );
sb.append( " G:" );
sb.append( format( getGroup(), 3 ) );
sb.append( " F:" );
sb.append( format( getFree(), 2 ) );
}
return sb.toString();
}
public String getTalkgroupID()
{
return getTalkgroupID( true );
}
public String getTalkgroupID( boolean format )
{
StringBuilder sb = new StringBuilder();
sb.append( getArea() );
if( format )
{
sb.append( "-" );
}
sb.append( format( getHomeRepeater(), 2 ) );
if( format )
{
sb.append( "-" );
}
sb.append( format( getGroup(), 3 ) );
return sb.toString();
}
public Alias getTalkgroupIDAlias()
{
return mAliasList.getTalkgroupAlias( getTalkgroupID() );
}
public MessageType getType()
{
MessageType retVal = MessageType.UN_KNWN;
/* Group 255 when the channel equals the free is the IDLE message */
if( getGroup() == sIDLE && getChannel() == getFree() )
{
retVal = MessageType.SY_IDLE;
}
else
{
int channel = getChannel();
if( channel == sCALL_END )
{
retVal = MessageType.CA_ENDD;
}
else
{
retVal = MessageType.CA_STRT;
}
}
return retVal;
}
@Override
public String getFromID()
{
if( mMessageType == MessageType.CA_STRT ||
mMessageType == MessageType.CA_ENDD )
{
return getTalkgroupID();
}
else
{
return null;
}
}
@Override
public Alias getFromIDAlias()
{
if( mMessageType == MessageType.CA_STRT ||
mMessageType == MessageType.CA_ENDD )
{
return getTalkgroupIDAlias();
}
else
{
return null;
}
}
@Override
public Plottable getPlottable()
{
// TODO Auto-generated method stub
return null;
}
/**
* Provides a listing of aliases contained in the message.
*/
public List<Alias> getAliases()
{
List<Alias> aliases = new ArrayList<Alias>();
Alias from = getFromIDAlias();
if( from != null )
{
aliases.add( from );
}
Alias to = getToIDAlias();
if( to != null )
{
aliases.add( to );
}
return aliases;
}
}

View File

@@ -0,0 +1,67 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 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 module.decode.ltrstandard.message;
import message.MessageDirection;
import message.MessageType;
import alias.Alias;
import alias.AliasList;
import bits.BinaryMessage;
import edac.CRC;
public class CallEndMessage extends LTRStandardMessage
{
public CallEndMessage( BinaryMessage message, MessageDirection direction,
AliasList list, CRC crc )
{
super( message, direction, list, crc );
}
@Override
public MessageType getMessageType()
{
return MessageType.CA_ENDD;
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "END* AREA:" );
sb.append( getArea() );
sb.append( " LCN:" );
sb.append( getChannelFormatted() );
sb.append( " TG [" );
sb.append( getToID() );
sb.append( "/" );
Alias endAlias = getToIDAlias();
if( endAlias != null )
{
sb.append( getToIDAlias().getName() );
}
else
{
sb.append( "UNKNOWN" );
}
sb.append( "] FREE:" );
sb.append( getFreeFormatted() );
return sb.toString();
}
}

View File

@@ -0,0 +1,68 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 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 module.decode.ltrstandard.message;
import message.MessageDirection;
import message.MessageType;
import alias.Alias;
import alias.AliasList;
import bits.BinaryMessage;
import edac.CRC;
public class CallMessage extends LTRStandardMessage
{
public CallMessage( BinaryMessage message, MessageDirection direction,
AliasList list, CRC crc )
{
super( message, direction, list, crc );
}
@Override
public MessageType getMessageType()
{
return MessageType.CA_STRT;
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "CALL LCN:" );
sb.append( getChannelFormatted() );
sb.append( " TG [ " );
sb.append( getToID() );
sb.append( "/" );
Alias alias = getToIDAlias();
if( alias != null )
{
sb.append( getToIDAlias().getName() );
}
else
{
sb.append( "UNKNOWN" );
}
sb.append( " ] FREE:" );
sb.append( getFreeFormatted() );
return sb.toString();
}
}

View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 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 module.decode.ltrstandard.message;
import message.MessageDirection;
import message.MessageType;
import alias.AliasList;
import bits.BinaryMessage;
import edac.CRC;
public class IdleMessage extends LTRStandardMessage
{
public IdleMessage( BinaryMessage message, MessageDirection direction,
AliasList list, CRC crc )
{
super( message, direction, list, crc );
}
@Override
public MessageType getMessageType()
{
return MessageType.SY_IDLE;
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "IDLE AREA:" );
sb.append( getArea() );
sb.append( " LCN:" );
sb.append( getChannelFormatted() );
sb.append( " HOME:" );
sb.append( getHomeRepeaterFormatted() );
sb.append( " GRP:" );
sb.append( getGroupFormatted() );
sb.append( " FREE:" );
sb.append( getFreeFormatted() );
return sb.toString();
}
}

View File

@@ -0,0 +1,213 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 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 module.decode.ltrstandard.message;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import message.Message;
import message.MessageDirection;
import message.MessageType;
import module.decode.DecoderType;
import org.apache.commons.lang3.StringUtils;
import alias.Alias;
import alias.AliasList;
import bits.BinaryMessage;
import edac.CRC;
public abstract class LTRStandardMessage extends Message
{
protected SimpleDateFormat mDatestampFormatter =
new SimpleDateFormat( "yyyyMMdd HHmmss" );
public static final int[] SYNC = { 0,1,2,3,4,5,6,7,8 };
public static final int[] AREA = { 9 };
public static final int[] CHANNEL = { 10,11,12,13,14 };
public static final int[] HOME_REPEATER = { 15,16,17,18,19 };
public static final int[] GROUP = { 20,21,22,23,24,25,26,27 };
public static final int[] FREE = { 28,29,30,31,32 };
public static final int[] CHECKSUM = { 33,34,35,36,37,38,39 };
protected BinaryMessage mMessage;
protected MessageDirection mMessageDirection;
protected AliasList mAliasList;
protected CRC mCRC;
public LTRStandardMessage( BinaryMessage message,
MessageDirection direction,
AliasList list,
CRC crc )
{
mMessage = message;
mMessageDirection = direction;
mAliasList = list;
mCRC = crc;
}
public abstract MessageType getMessageType();
public boolean isValid()
{
return mCRC != CRC.FAILED_CRC && mCRC != CRC.FAILED_PARITY;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append( mDatestampFormatter.format(
new Date( System.currentTimeMillis() ) ) );
sb.append( " LTR " );
sb.append( mMessageDirection.name() );
sb.append( " [" );
sb.append( mCRC.getAbbreviation() );
sb.append( "] " );
sb.append( getMessage() );
return sb.toString();
}
@Override
public String getBinaryMessage()
{
return mMessage.toString();
}
public CRC getCRC()
{
return mCRC;
}
public int getArea()
{
return mMessage.getInt( AREA );
}
public int getChannel()
{
return mMessage.getInt( CHANNEL );
}
public String getChannelFormatted()
{
return StringUtils.leftPad( String.valueOf( getChannel() ), 2, "0" );
}
public int getHomeRepeater()
{
return mMessage.getInt( HOME_REPEATER );
}
public String getHomeRepeaterFormatted()
{
return StringUtils.leftPad( String.valueOf( getHomeRepeater() ), 2, "0" );
}
public int getGroup()
{
return mMessage.getInt( GROUP );
}
public String getGroupFormatted()
{
return StringUtils.leftPad( String.valueOf( getGroup() ), 3, "0" );
}
public int getFree()
{
return mMessage.getInt( FREE );
}
public String getFreeFormatted()
{
return StringUtils.leftPad( String.valueOf( getFree() ), 2, "0" );
}
@Override
public String getFromID()
{
return null;
}
@Override
public Alias getFromIDAlias()
{
return null;
}
@Override
public String getToID()
{
StringBuilder sb = new StringBuilder();
sb.append( getArea() );
sb.append( "-" );
sb.append( getHomeRepeaterFormatted() );
sb.append( "-" );
sb.append( getGroupFormatted() );
return sb.toString();
}
@Override
public Alias getToIDAlias()
{
return mAliasList.getTalkgroupAlias( getToID() );
}
public List<Alias> getAliases()
{
List<Alias> aliases = new ArrayList<>();
Alias talkgroupAlias = getToIDAlias();
if( talkgroupAlias != null )
{
aliases.add( talkgroupAlias );
}
return aliases;
}
public int getCRCChecksum()
{
return mMessage.getInt( CHECKSUM );
}
@Override
public String getProtocol()
{
return DecoderType.LTR_STANDARD.getDisplayString();
}
@Override
public String getEventType()
{
return getMessageType().getDisplayText();
}
@Override
public String getErrorStatus()
{
return mCRC.getDisplayText();
}
}

View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 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 module.decode.ltrstandard.message;
import message.MessageDirection;
import message.MessageType;
import alias.AliasList;
import bits.BinaryMessage;
import edac.CRC;
public class UnknownMessage extends LTRStandardMessage
{
public UnknownMessage( BinaryMessage message, MessageDirection direction,
AliasList list, CRC crc )
{
super( message, direction, list, crc );
}
@Override
public MessageType getMessageType()
{
return MessageType.UN_KNWN;
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "UNKNOWN AREA:" );
sb.append( getArea() );
sb.append( " LCN:" );
sb.append( getChannelFormatted() );
sb.append( " HOME:" );
sb.append( getHomeRepeaterFormatted() );
sb.append( " GRP:" );
sb.append( getGroupFormatted() );
sb.append( " FREE:" );
sb.append( getFreeFormatted() );
return sb.toString();
}
}

View File

@@ -301,8 +301,8 @@ public class MPT1327Decoder extends Decoder
@Override
public void reset()
{
// TODO Auto-generated method stub
mControlMessageFramer.reset();
mTrafficMessageFramer.reset();
}
@Override

View File

@@ -26,9 +26,11 @@ import java.util.TreeSet;
import message.Message;
import module.decode.DecoderType;
import module.decode.config.DecodeConfiguration;
import module.decode.event.CallEvent;
import module.decode.event.CallEvent.CallEventType;
import module.decode.mpt1327.MPT1327Message.IdentType;
import module.decode.state.ChangeChannelTimeoutEvent;
import module.decode.state.ChangedAttribute;
import module.decode.state.DecoderState;
import module.decode.state.DecoderStateEvent;
@@ -41,6 +43,8 @@ import org.slf4j.LoggerFactory;
import alias.Alias;
import alias.AliasList;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.channel.Channel.ChannelType;
import controller.channel.map.ChannelMap;
@@ -60,15 +64,18 @@ public class MPT1327DecoderState extends DecoderState
private ChannelMap mChannelMap;
private long mFrequency = 0;
private long mCallTimeout;
public MPT1327DecoderState( AliasList aliasList, ChannelMap channelMap,
ChannelType channelType )
public MPT1327DecoderState( AliasList aliasList,
ChannelMap channelMap,
ChannelType channelType,
long callTimeout )
{
super( aliasList );
mChannelMap = channelMap;
mChannelType = channelType;
mCallTimeout = callTimeout;
}
@Override
@@ -252,8 +259,42 @@ public class MPT1327DecoderState extends DecoderState
/* Traffic Channel Events */
case CLEAR:
mChannelNumber = mpt.getChannel();
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
break;
case MAINT:
broadcast( new DecoderStateEvent( this, Event.END, State.END ) );
if( mChannelType == ChannelType.STANDARD )
{
/**
* When we receive a MAINT message and we're configured
* as a standard channel, we need to apply the call
* timeout specified by the user. Otherwise we'll
* be using the shorter default call timeout
*/
broadcast( new ChangeChannelTimeoutEvent( this,
mChannelType, mCallTimeout ) );
if( mCurrentCallEvent == null )
{
mCurrentCallEvent = new MPT1327CallEvent.Builder( CallEventType.CALL )
.aliasList( getAliasList() )
.channel( String.valueOf( mChannelNumber ) )
.details( "MONITORED TRAFFIC CHANNEL" )
.frequency( mFrequency )
.to( mpt.getToID() )
.build();
broadcast( mCurrentCallEvent );
}
broadcast(new Metadata(MetadataType.TO,
mpt.getToID(), mpt.getToIDAlias(), true));
broadcast( new DecoderStateEvent( this, Event.START, State.CALL ) );
setToTalkgroup( mpt.getToID() );
}
break;
default:
break;
@@ -276,6 +317,25 @@ public class MPT1327DecoderState extends DecoderState
mToTalkgroup = null;
broadcast( ChangedAttribute.TO_TALKGROUP );
/**
* If this is a standard channel, reset the fade timeout to the default
* timeout. Once processing is underway, if we get a MAINT message,
* this indicates we're processing a traffic channel as a standard
* channel, so we'll issue a different call timeout then.
*/
if( mChannelType == ChannelType.STANDARD )
{
broadcast( new ChangeChannelTimeoutEvent( this, mChannelType,
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000 ) );
if( mCurrentCallEvent != null )
{
mCurrentCallEvent.end();
broadcast( mCurrentCallEvent );
mCurrentCallEvent = null;
}
}
}
public String getSite()

View File

@@ -42,6 +42,8 @@ import module.decode.p25.message.ldu.lc.TelephoneInterconnectVoiceChannelUser;
import module.decode.p25.message.ldu.lc.UnitToUnitVoiceChannelUser;
import module.decode.p25.message.pdu.PDUMessage;
import module.decode.p25.message.pdu.confirmed.PDUConfirmedMessage;
import module.decode.p25.message.pdu.confirmed.PDUTypeUnknown;
import module.decode.p25.message.pdu.confirmed.PacketData;
import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextAccept;
import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextReject;
import module.decode.p25.message.pdu.confirmed.SNDCPActivateTDSContextRequest;
@@ -118,7 +120,6 @@ import module.decode.p25.message.tsbk.osp.voice.UnitToUnitVoiceChannelGrantUpdat
import module.decode.p25.reference.IPProtocol;
import module.decode.p25.reference.LinkControlOpcode;
import module.decode.p25.reference.Response;
import module.decode.p25.reference.Service;
import module.decode.p25.reference.Vendor;
import module.decode.state.ChangedAttribute;
import module.decode.state.DecoderState;
@@ -406,12 +407,12 @@ public class P25DecoderState extends DecoderState
{
if( hdu.isValid() )
{
State state = hdu.isEncrypted() ? State.ENCRYPTED : State.CALL;
broadcast( new DecoderStateEvent( this, Event.START, state ) );
broadcast( new DecoderStateEvent( this, Event.START, State.CALL ) );
String to = hdu.getToID();
updateTo( to );
if( mCurrentCallEvent == null )
{
mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.CALL )
@@ -425,7 +426,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent );
}
updateTo( to );
}
}
@@ -437,7 +437,7 @@ public class P25DecoderState extends DecoderState
{
if( tdulc.getOpcode() == LinkControlOpcode.CALL_TERMINATION_OR_CANCELLATION )
{
broadcast( new DecoderStateEvent( this, Event.END, State.END ) );
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
if( mCurrentCallEvent != null )
{
@@ -615,12 +615,14 @@ public class P25DecoderState extends DecoderState
module.decode.p25.message.tdu.lc.GroupVoiceChannelUser gvcuser =
(module.decode.p25.message.tdu.lc.GroupVoiceChannelUser)tdulc;
broadcast( new DecoderStateEvent( this, Event.CONTINUATION,
( gvcuser.isEncrypted() ? State.ENCRYPTED : State.CALL ) ) );
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CALL ) );
String from = gvcuser.getSourceAddress();
String to = gvcuser.getGroupAddress();
updateFrom( from );
updateTo( to );
if( mCurrentCallEvent == null )
{
mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.GROUP_CALL )
@@ -636,9 +638,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent );
}
updateFrom( from );
updateTo( to );
}
break;
case MESSAGE_UPDATE:
@@ -845,6 +844,8 @@ public class P25DecoderState extends DecoderState
String to = tivcu.getAddress();
updateTo( to );
if( mCurrentCallEvent == null )
{
mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.TELEPHONE_INTERCONNECT )
@@ -858,8 +859,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent );
}
updateTo( to );
}
break;
case UNIT_AUTHENTICATION_COMMAND:
@@ -922,7 +921,10 @@ public class P25DecoderState extends DecoderState
(module.decode.p25.message.tdu.lc.UnitToUnitVoiceChannelUser)tdulc;
String from = uuvcu.getSourceAddress();
updateFrom( from );
String to = uuvcu.getTargetAddress();
updateTo( to );
if( mCurrentCallEvent != null )
{
@@ -936,9 +938,6 @@ public class P25DecoderState extends DecoderState
.to( to )
.build();
}
updateFrom( from );
updateTo( to );
}
break;
default:
@@ -950,35 +949,14 @@ public class P25DecoderState extends DecoderState
* Log optional vendor format messages that we don't yet support so that we
* can understand those messages and eventually add support.
*/
private void logAlternateVendorMessage( P25Message p25 )
private void logAlternateVendorMessage( P25Message message )
{
// if( p25 instanceof TDULinkControlMessage )
// if( message.isValid() )
// {
// TDULinkControlMessage tdulc = (TDULinkControlMessage)p25;
//
// mLog.debug( "TDULC - VENDOR [" + tdulc.getVendor() +
// "] SPECIFIC FORMAT FOR " + tdulc.getVendorOpcode().getLabel() +
// " " + tdulc.getBinaryMessage() );
// }
// else if( p25 instanceof LDU1Message )
// {
// LDU1Message ldu1 = (LDU1Message)p25;
//
// mLog.debug( "LDU1 - VENDOR [" + ldu1.getVendor() +
// "] SPECIFIC FORMAT FOR " + ldu1.getVendorOpcode().getLabel() +
// " " + ldu1.getBinaryMessage() );
// }
// else if( p25 instanceof PDUMessage )
// {
// PDUMessage pdu = (PDUMessage)p25;
//
// mLog.debug( "PDU - VENDOR [" + pdu.getVendor() +
// "] SPECIFIC FORMAT FOR " + pdu.getVendorOpcode().getLabel() +
// " " + pdu.getBinaryMessage() );
// }
// else
// {
// mLog.debug( "P25 - ALTERNATE VENDOR MESSAGE [" + p25.getClass() + "]" );
// mLog.info( "PLEASE NOTIFY DEVELOPER - UNRECOGNIZED P25 VENDOR FORMAT -"
// + " DUID:" + message.getDUID()
// + " MSG:" + message.getBinaryMessage()
// + " CLASS:" + message.getClass() );
// }
}
@@ -989,15 +967,11 @@ public class P25DecoderState extends DecoderState
*/
private void processLDU( LDUMessage ldu )
{
State state = ( ldu.isValid() && ldu.isEncrypted() ) ?
State.ENCRYPTED : State.CALL;
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, state ) );
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CALL ) );
if( mCurrentCallEvent == null )
{
mCurrentCallEvent = new P25CallEvent.Builder(
( state == State.CALL ) ? CallEventType.CALL : CallEventType.ENCRYPTED_CALL )
mCurrentCallEvent = new P25CallEvent.Builder( CallEventType.CALL )
.aliasList( getAliasList() )
.channel( mCurrentChannel )
.frequency( mCurrentChannelFrequency )
@@ -1057,7 +1031,7 @@ public class P25DecoderState extends DecoderState
}
break;
case CALL_TERMINATION_OR_CANCELLATION:
broadcast( new DecoderStateEvent( this, Event.END, State.END ) );
broadcast( new DecoderStateEvent( this, Event.END, State.FADE ) );
mCurrentCallEvent = null;
if( !( ldu instanceof CallTermination ) )
@@ -1115,42 +1089,43 @@ public class P25DecoderState extends DecoderState
}
break;
case GROUP_VOICE_CHANNEL_UPDATE:
if( mChannelType == ChannelType.STANDARD &&
ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate )
if( ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate )
{
module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate gvcu =
(module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate)ldu;
(module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdate)ldu;
String userA = gvcu.getGroupAddressA();
if( !mCallDetectTalkgroups.contains( userA ) )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcu.getChannelA() )
.details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcu.getDownlinkFrequencyA() )
.to( userA )
.build() );
mCallDetectTalkgroups.add( userA );
}
String userB = gvcu.getGroupAddressB();
if( userB != null &&
!userB.contentEquals( "0000" ) &&
!mCallDetectTalkgroups.contains( userB ) )
if( mChannelType == ChannelType.STANDARD )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcu.getChannelB() )
.details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcu.getDownlinkFrequencyB() )
.to( userB )
.build() );
if( !mCallDetectTalkgroups.contains( userA ) )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcu.getChannelA() )
.details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcu.getDownlinkFrequencyA() )
.to( userA )
.build() );
mCallDetectTalkgroups.add( userA );
}
mCallDetectTalkgroups.add( userB );
if( userB != null &&
!userB.contentEquals( "0000" ) &&
!mCallDetectTalkgroups.contains( userB ) )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcu.getChannelB() )
.details( ( gvcu.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcu.getDownlinkFrequencyB() )
.to( userB )
.build() );
mCallDetectTalkgroups.add( userB );
}
}
}
else
@@ -1159,25 +1134,27 @@ public class P25DecoderState extends DecoderState
}
break;
case GROUP_VOICE_CHANNEL_UPDATE_EXPLICIT:
if( mChannelType == ChannelType.STANDARD &&
ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit )
if( ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit )
{
module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit gvcue =
(module.decode.p25.message.ldu.lc.GroupVoiceChannelUpdateExplicit)ldu;
String group = gvcue.getGroupAddress();
if( !mCallDetectTalkgroups.contains( group ) )
if( mChannelType == ChannelType.STANDARD )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcue.getTransmitChannel() )
.details( ( gvcue.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcue.getDownlinkFrequency() )
.to( group )
.build() );
mCallDetectTalkgroups.add( group );
if( !mCallDetectTalkgroups.contains( group ) )
{
broadcast( new P25CallEvent.Builder( CallEventType.CALL_DETECT )
.aliasList( getAliasList() )
.channel( gvcue.getTransmitChannel() )
.details( ( gvcue.isEncrypted() ? "ENCRYPTED" : "" ) )
.frequency( gvcue.getDownlinkFrequency() )
.to( group )
.build() );
mCallDetectTalkgroups.add( group );
}
}
}
else
@@ -1187,29 +1164,32 @@ public class P25DecoderState extends DecoderState
break;
case GROUP_VOICE_CHANNEL_USER:
/* Indicates the current user of the current channel */
if( mChannelType == ChannelType.STANDARD &&
ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUser )
if( ldu instanceof module.decode.p25.message.ldu.lc.GroupVoiceChannelUser )
{
module.decode.p25.message.ldu.lc.GroupVoiceChannelUser gvcuser =
(module.decode.p25.message.ldu.lc.GroupVoiceChannelUser)ldu;
if( mCurrentCallEvent.getCallEventType() != CallEventType.GROUP_CALL )
{
mCurrentCallEvent.setCallEventType( CallEventType.GROUP_CALL );
broadcast( mCurrentCallEvent );
}
if( mCurrentCallEvent.getDetails() == null )
{
mCurrentCallEvent.setDetails(
( gvcuser.isEncrypted() ? "ENCRYPTED " : "" ) +
( gvcuser.isEmergency() ? "EMERGENCY " : "" ) );
broadcast( mCurrentCallEvent );
}
updateFrom( gvcuser.getSourceAddress() );
updateTo( gvcuser.getGroupAddress() );
if( mChannelType == ChannelType.STANDARD )
{
if( mCurrentCallEvent.getCallEventType() != CallEventType.GROUP_CALL )
{
mCurrentCallEvent.setCallEventType( CallEventType.GROUP_CALL );
broadcast( mCurrentCallEvent );
}
if( mCurrentCallEvent.getDetails() == null )
{
mCurrentCallEvent.setDetails(
( gvcuser.isEncrypted() ? "ENCRYPTED " : "" ) +
( gvcuser.isEmergency() ? "EMERGENCY " : "" ) );
broadcast( mCurrentCallEvent );
}
}
}
else
{
@@ -1405,6 +1385,8 @@ public class P25DecoderState extends DecoderState
TelephoneInterconnectVoiceChannelUser tivcu =
(TelephoneInterconnectVoiceChannelUser)ldu;
updateTo( tivcu.getAddress() );
if( mCurrentCallEvent.getCallEventType() != CallEventType.TELEPHONE_INTERCONNECT )
{
mCurrentCallEvent.setCallEventType( CallEventType.TELEPHONE_INTERCONNECT );
@@ -1419,8 +1401,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent );
}
updateTo( tivcu.getAddress() );
}
else
{
@@ -1486,6 +1466,10 @@ public class P25DecoderState extends DecoderState
UnitToUnitVoiceChannelUser uuvcu =
(UnitToUnitVoiceChannelUser)ldu;
updateFrom( uuvcu.getSourceAddress() );
updateTo( uuvcu.getTargetAddress() );
if( mCurrentCallEvent.getCallEventType() != CallEventType.UNIT_TO_UNIT_CALL )
{
mCurrentCallEvent.setCallEventType( CallEventType.UNIT_TO_UNIT_CALL );
@@ -1500,9 +1484,6 @@ public class P25DecoderState extends DecoderState
broadcast( mCurrentCallEvent );
}
updateFrom( uuvcu.getSourceAddress() );
updateTo( uuvcu.getTargetAddress() );
}
else
{
@@ -1521,7 +1502,7 @@ public class P25DecoderState extends DecoderState
private void processTSBK( TSBKMessage tsbk )
{
/* Trunking Signalling Block Messages - indicates Control Channel */
broadcast( new DecoderStateEvent( this, Event.START, State.CONTROL ) );
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.CONTROL ) );
if( tsbk.getVendor() == Vendor.STANDARD )
{
@@ -1650,7 +1631,11 @@ public class P25DecoderState extends DecoderState
*/
private void processPDU( PDUMessage pdu )
{
if( pdu instanceof PDUConfirmedMessage )
if( pdu instanceof PacketData || pdu instanceof PDUTypeUnknown )
{
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.DATA ) );
}
else if( pdu instanceof PDUConfirmedMessage )
{
broadcast( new DecoderStateEvent( this, Event.CONTINUATION, State.DATA ) );
@@ -1658,7 +1643,7 @@ public class P25DecoderState extends DecoderState
switch( pduc.getPDUType() )
{
case SN_ACTIVATE_TDS_CONTEXT_ACCEPT:
case SNDCP_ACTIVATE_TDS_CONTEXT_ACCEPT:
SNDCPActivateTDSContextAccept satca =
(SNDCPActivateTDSContextAccept)pduc;
@@ -1670,7 +1655,7 @@ public class P25DecoderState extends DecoderState
.to( satca.getLogicalLinkID() )
.build() );
break;
case SN_ACTIVATE_TDS_CONTEXT_REJECT:
case SNDCP_ACTIVATE_TDS_CONTEXT_REJECT:
SNDCPActivateTDSContextReject satcr =
(SNDCPActivateTDSContextReject)pduc;
@@ -1695,7 +1680,7 @@ public class P25DecoderState extends DecoderState
.from( satcreq.getLogicalLinkID() )
.build() );
break;
case SN_DEACTIVATE_TDS_CONTEXT_ACCEPT:
case SNDCP_DEACTIVATE_TDS_CONTEXT_ACCEPT:
SNDCPDeactivateTDSContext sdtca =
(SNDCPDeactivateTDSContext)pduc;
@@ -1707,7 +1692,7 @@ public class P25DecoderState extends DecoderState
.from( sdtca.getLogicalLinkID() )
.build() );
break;
case SN_DEACTIVATE_TDS_CONTEXT_REQUEST:
case SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST:
SNDCPDeactivateTDSContext sdtcreq =
(SNDCPDeactivateTDSContext)pduc;
@@ -1719,7 +1704,7 @@ public class P25DecoderState extends DecoderState
.from( sdtcreq.getLogicalLinkID() )
.build() );
break;
case SN_RF_CONFIRMED_DATA:
case SNDCP_RF_CONFIRMED_DATA:
SNDCPUserData sud = (SNDCPUserData)pduc;
StringBuilder sbFrom = new StringBuilder();
@@ -1748,8 +1733,10 @@ public class P25DecoderState extends DecoderState
.to( pduc.getLogicalLinkID() )
.build() );
break;
case SNDCP_RF_UNCONFIRMED_DATA:
break;
default:
mLog.debug( "PDUC - Unrecognized Message: " + pduc.toString() );
// mLog.debug( "PDUC - Unrecognized Message: " + pduc.toString() );
break;
}
}
@@ -3100,16 +3087,9 @@ public class P25DecoderState extends DecoderState
if( mToTalkgroup == null || !mToTalkgroup.contentEquals( to ) )
{
mToTalkgroup = to;
broadcast( ChangedAttribute.TO_TALKGROUP );
if( mCurrentCallEvent != null &&
( mCurrentCallEvent.getToID() == null ||
!mCurrentCallEvent.getToID().contentEquals( to ) ) )
{
mCurrentCallEvent.setToID( to );
broadcast( mCurrentCallEvent );
}
if( hasAliasList() )
{
mToAlias = getAliasList().getTalkgroupAlias( mToTalkgroup );
@@ -3118,12 +3098,20 @@ public class P25DecoderState extends DecoderState
{
mToAlias = null;
}
broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS );
}
/* Send audio metadata update */
broadcast( new Metadata( MetadataType.TO, mToTalkgroup, mToAlias, true ) );
broadcast( ChangedAttribute.TO_TALKGROUP_ALIAS );
/* Send audio metadata update */
broadcast( new Metadata( MetadataType.TO, mToTalkgroup, mToAlias, true ) );
if( mCurrentCallEvent != null &&
( mCurrentCallEvent.getToID() == null ||
!mCurrentCallEvent.getToID().contentEquals( to ) ) )
{
mCurrentCallEvent.setToID( to );
broadcast( mCurrentCallEvent );
}
}
}
}
@@ -3139,14 +3127,6 @@ public class P25DecoderState extends DecoderState
mFromTalkgroup = from;
broadcast( ChangedAttribute.FROM_TALKGROUP );
if( mCurrentCallEvent != null &&
mCurrentCallEvent.getFromID() == null ||
!mCurrentCallEvent.getFromID().contentEquals( from ) )
{
mCurrentCallEvent.setFromID( from );
broadcast( mCurrentCallEvent );
}
if( hasAliasList() )
{
mFromAlias = getAliasList().getTalkgroupAlias( mFromTalkgroup );
@@ -3157,10 +3137,19 @@ public class P25DecoderState extends DecoderState
}
broadcast( ChangedAttribute.FROM_TALKGROUP_ALIAS );
/* Send audio metadata update */
broadcast( new Metadata( MetadataType.FROM, mFromTalkgroup, mFromAlias, true ) );
if( mCurrentCallEvent != null &&
mCurrentCallEvent.getFromID() == null ||
!mCurrentCallEvent.getFromID().contentEquals( from ) )
{
mCurrentCallEvent.setFromID( from );
broadcast( mCurrentCallEvent );
}
}
/* Send audio metadata update */
broadcast( new Metadata( MetadataType.FROM, mFromTalkgroup, mFromAlias, true ) );
}
}

View File

@@ -87,6 +87,8 @@ public class P25Message extends Message
sb.append( " " );
sb.append( getDUID().getLabel() );
sb.append( " DATA UNIT ID:" + mMessage.getInt( DUID ) );
return sb.toString();
}

View File

@@ -1,5 +1,6 @@
package module.decode.p25.message.pdu;
import module.decode.p25.message.pdu.confirmed.PacketData;
import module.decode.p25.message.pdu.osp.control.AdjacentStatusBroadcastExtended;
import module.decode.p25.message.pdu.osp.control.CallAlertExtended;
import module.decode.p25.message.pdu.osp.control.GroupAffiliationQueryExtended;
@@ -121,7 +122,8 @@ public class PDUMessageFactory
}
return new PDUMessage( message, duid, aliasList );
case PACKET_DATA:
return new PacketData( message, aliasList );
case UNCONFIRMED_MULTI_BLOCK_TRUNKING_CONTROL:
return new PDUMessage( message, duid, aliasList );
default:

View File

@@ -48,19 +48,27 @@ public class PDUConfirmedMessageFactory
case UNENCRYPTED_USER_DATA:
switch( message.getPDUType() )
{
case SN_ACTIVATE_TDS_CONTEXT_ACCEPT:
case SNDCP_ACTIVATE_TDS_CONTEXT_ACCEPT:
return new SNDCPActivateTDSContextAccept( message );
case SN_ACTIVATE_TDS_CONTEXT_REJECT:
case SNDCP_ACTIVATE_TDS_CONTEXT_REJECT:
return new SNDCPActivateTDSContextReject( message );
case SN_ACTIVATE_TDS_CONTEXT_REQUEST:
return new SNDCPActivateTDSContextRequest( message );
case SN_DEACTIVATE_TDS_CONTEXT_ACCEPT:
case SN_DEACTIVATE_TDS_CONTEXT_REQUEST:
case SNDCP_DEACTIVATE_TDS_CONTEXT_ACCEPT:
case SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST:
return new SNDCPDeactivateTDSContext( message );
case SN_RF_CONFIRMED_DATA:
case SNDCP_RF_CONFIRMED_DATA:
return new SNDCPUserData( message );
case SN_RF_UNCONFIRMED_DATA:
case SNDCP_RF_UNCONFIRMED_DATA:
break;
case PDU_TYPE_6:
case PDU_TYPE_7:
case PDU_TYPE_8:
case PDU_TYPE_9:
case PDU_TYPE_10:
case PDU_TYPE_11:
return new PDUTypeUnknown( message );
case UNKNOWN:
default:
break;
}

View File

@@ -0,0 +1,55 @@
package module.decode.p25.message.pdu.confirmed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PDUTypeUnknown extends PDUConfirmedMessage
{
public final static Logger mLog =
LoggerFactory.getLogger( PDUTypeUnknown.class );
public PDUTypeUnknown( PDUConfirmedMessage message )
{
super( message );
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "NAC:" );
sb.append( getNAC() );
sb.append( " LLID:" );
sb.append( getLogicalLinkID() );
sb.append( " PACKET DATA UNIT CONFIRMED - " );
sb.append( getPDUType().getLabel() );
sb.append( " CRC[" );
sb.append( getErrorStatus() );
sb.append( "]" );
sb.append( " PACKET #" );
sb.append( getPacketSequenceNumber() );
if( isFinalFragment() && getFragmentSequenceNumber() == 0 )
{
sb.append( ".C" );
}
else
{
sb.append( "." );
sb.append( getFragmentSequenceNumber() );
if( isFinalFragment() )
{
sb.append( "C" );
}
}
sb.append( " " );
sb.append( mMessage.toString() );
return sb.toString();
}
}

View File

@@ -0,0 +1,66 @@
package module.decode.p25.message.pdu.confirmed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import alias.AliasList;
import bits.BinaryMessage;
public class PacketData extends PDUConfirmedMessage
{
public final static Logger mLog =
LoggerFactory.getLogger( PacketData.class );
public static final int DATA_BLOCK_START = 176;
public PacketData( BinaryMessage message, AliasList aliasList )
{
super( message, aliasList );
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "NAC:" );
sb.append( getNAC() );
sb.append( " PDUC LLID:" );
sb.append( getLogicalLinkID() );
sb.append( " PACKET DATA" );
sb.append( " CRC[" );
sb.append( getErrorStatus() );
sb.append( "]" );
if( !mMessage.get( FINAL_FRAGMENT_FLAG ) )
{
sb.append( " RESENDING" );
}
sb.append( " PACKET #" );
sb.append( getPacketSequenceNumber() );
if( isFinalFragment() && getFragmentSequenceNumber() == 0 )
{
sb.append( ".C" );
}
else
{
sb.append( "." );
sb.append( getFragmentSequenceNumber() );
if( isFinalFragment() )
{
sb.append( "C" );
}
}
sb.append( " " );
sb.append( mMessage.toString() );
return sb.toString();
}
}

View File

@@ -49,7 +49,7 @@ public class SNDCPDeactivateTDSContext extends PDUConfirmedMessage
public String getDeactivationType()
{
if( getPDUType() == PDUType.SN_DEACTIVATE_TDS_CONTEXT_REQUEST )
if( getPDUType() == PDUType.SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST )
{
return mMessage.getInt( DEACTIVATION_TYPE ) == 0 ?
"ALL NSAPIS" : "THIS NSAPI";

View File

@@ -1,5 +1,7 @@
package module.decode.p25.message.tsbk;
import alias.AliasList;
import bits.BinaryMessage;
import module.decode.p25.message.tsbk.motorola.ControlChannelBaseStationIdentification;
import module.decode.p25.message.tsbk.motorola.MotorolaOpcode;
import module.decode.p25.message.tsbk.motorola.MotorolaTSBKMessage;
@@ -19,6 +21,7 @@ import module.decode.p25.message.tsbk.osp.control.ExtendedFunctionCommand;
import module.decode.p25.message.tsbk.osp.control.GroupAffiliationQuery;
import module.decode.p25.message.tsbk.osp.control.GroupAffiliationResponse;
import module.decode.p25.message.tsbk.osp.control.IdentifierUpdateNonVUHF;
import module.decode.p25.message.tsbk.osp.control.IdentifierUpdateTDMA;
import module.decode.p25.message.tsbk.osp.control.IdentifierUpdateVUHF;
import module.decode.p25.message.tsbk.osp.control.LocationRegistrationResponse;
import module.decode.p25.message.tsbk.osp.control.MessageUpdate;
@@ -56,8 +59,6 @@ import module.decode.p25.message.tsbk.osp.voice.UnitToUnitVoiceChannelGrantUpdat
import module.decode.p25.reference.DataUnitID;
import module.decode.p25.reference.Opcode;
import module.decode.p25.reference.Vendor;
import alias.AliasList;
import bits.BinaryMessage;
public class TSBKMessageFactory
{
@@ -119,6 +120,8 @@ public class TSBKMessageFactory
case IDENTIFIER_UPDATE_NON_VUHF:
return new IdentifierUpdateNonVUHF( message, duid,
aliasList );
case IDENTIFIER_UPDATE_TDMA:
return new IdentifierUpdateTDMA( message, duid, aliasList );
case IDENTIFIER_UPDATE_VHF_UHF_BANDS:
return new IdentifierUpdateVUHF( message, duid,
aliasList );

View File

@@ -0,0 +1,159 @@
package module.decode.p25.message.tsbk.osp.control;
import module.decode.p25.reference.DataUnitID;
import module.decode.p25.reference.Opcode;
import alias.AliasList;
import bits.BinaryMessage;
public class IdentifierUpdateTDMA extends IdentifierUpdate
{
public static final int[] CHANNEL_TYPE = { 84,85,86,87 };
public static final int TRANSMIT_OFFSET_SIGN = 88;
public static final int[] TRANSMIT_OFFSET = { 89,90,91,92,93,94,95,96,97,98,
99,100,101 };
public IdentifierUpdateTDMA( BinaryMessage message,
DataUnitID duid,
AliasList aliasList )
{
super( message, duid, aliasList );
}
@Override
public String getEventType()
{
return Opcode.IDENTIFIER_UPDATE_TDMA.getDescription();
}
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( getMessageStub() );
sb.append( " IDEN:" + getIdentifier() );
sb.append( " BASE:" + getBaseFrequency() );
sb.append( " BW:" + getBandwidth() );
sb.append( " SPACING:" + getChannelSpacing() );
sb.append( " OFFSET:" + getTransmitOffset() );
sb.append( " TYPE:" + getChannelType().toString() );
return sb.toString();
}
public ChannelType getChannelType()
{
return ChannelType.fromValue( mMessage.getInt( CHANNEL_TYPE) );
}
/**
* Channel bandwidth in hertz
*/
public int getBandwidth()
{
return getChannelType().getBandwidth();
}
/**
* Transmit offset in hertz
*/
@Override
public long getTransmitOffset()
{
long offset = mMessage.getLong( TRANSMIT_OFFSET ) *
getChannelSpacing();
if( mMessage.get( TRANSMIT_OFFSET_SIGN ) )
{
return offset;
}
else
{
return -offset;
}
}
public enum ChannelType
{
TYPE_0( AccessType.FDMA, 12500, 1, Vocoder.HALF_RATE ),
TYPE_1( AccessType.FDMA, 12500, 1, Vocoder.FULL_RATE ),
TYPE_2( AccessType.FDMA, 6250, 1, Vocoder.HALF_RATE ),
TYPE_3( AccessType.TDMA, 12500, 2, Vocoder.HALF_RATE ),
TYPE_4( AccessType.TDMA, 25000, 4, Vocoder.HALF_RATE ),
TYPE_5( AccessType.TDMA, 12500, 2, Vocoder.HALF_RATE ), //HD8PSK simulcast
UNKNOWN( AccessType.UNKNOWN, 0, 0, Vocoder.HALF_RATE );
private AccessType mAccessType;
private int mBandwidth;
private int mSlotsPerCarrier;
private Vocoder mVocoder;
private ChannelType( AccessType accessType,
int bandwidth,
int slots,
Vocoder vocoder )
{
mAccessType = accessType;
mBandwidth = bandwidth;
mSlotsPerCarrier = slots;
mVocoder = vocoder;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
if( this.equals( TYPE_5 ) )
{
sb.append("H-D8PSK SIMULCAST " );
}
sb.append( mAccessType.name() );
sb.append( " BW:" + mBandwidth );
sb.append( " SLOT/CARRIER:" + mSlotsPerCarrier );
sb.append( " VOCODER:" + mVocoder.name() );
return sb.toString();
}
public static ChannelType fromValue( int value )
{
if( 0 <= value && value <= 5 )
{
return ChannelType.values()[ value ];
}
return ChannelType.UNKNOWN;
}
public AccessType getAccessType()
{
return mAccessType;
}
public int getBandwidth()
{
return mBandwidth;
}
public int getSlotsPerCarrier()
{
return mSlotsPerCarrier;
}
public Vocoder getVocoder()
{
return mVocoder;
}
}
public enum AccessType
{
FDMA,TDMA,UNKNOWN;
}
public enum Vocoder
{
HALF_RATE, FULL_RATE;
}
}

View File

@@ -1,8 +1,10 @@
package module.decode.p25.message.tsbk.osp.control;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import module.decode.p25.message.tsbk.TSBKMessage;
@@ -12,20 +14,21 @@ import alias.AliasList;
import bits.BinaryMessage;
/**
* Sync Broadcast - used to broadcast FDMA-TDMA synchronization timestamp so
* that a subscriber unit that is time synchronized on an FDMA control channel
* is also synchronized with a TDMA traffic channel.
* Sync Broadcast - used to broadcast FDMA-TDMA timing synchronization
* information so that a subscriber unit that is time synchronized on an FDMA
* control channel is also time synchronized with all TDMA traffic channels at
* the site.
*/
public class SyncBroadcast extends TSBKMessage
{
public static final int US = 92;
public static final int IST = 93;
public static final int MMU = 94;
public static final int[] MC = { 95,96 };
public static final int VL = 97;
public static final int[] RESERVED = { 80,81,82,83,84,85,86,87,88,89,90,91,92 };
public static final int SYSTEM_TIME_NOT_LOCKED_TO_EXTERNAL_REFERENCE = 93;
public static final int MICRO_SLOTS_TO_MINUTE_ROLLOVER_UNLOCKED = 94;
public static final int[] LEAP_SECOND_CORRECTION = { 95,96 };
public static final int LOCAL_TIME_OFFSET_VALID = 97;
public static final int LOCAL_TIME_OFFSET_SIGN = 98;
public static final int[] LOCAL_TIME_OFFSET = { 99,100,101,102 };
public static final int LOCAL_TIME_OFFSET_HALF = 103;
public static final int[] LOCAL_TIME_OFFSET_HOURS = { 99,100,101,102 };
public static final int LOCAL_TIME_OFFSET_HALF_HOUR = 103;
public static final int[] YEAR = { 104,105,106,107,108,109,110 };
public static final int[] MONTH = { 111,112,113,114 };
@@ -36,7 +39,10 @@ public class SyncBroadcast extends TSBKMessage
139,140,141,142,143 };
public static final int[] TSBK_CRC = { 144,145,146,147,148,149,150,151,152,
153,154,155,156,157,158,159 };
private static final DateFormat TIME_FORMATTER = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss.SSS Z" );
private static final TimeZone NO_TIME_ZONE = new SimpleTimeZone( 0, "NONE" );
public SyncBroadcast( BinaryMessage message,
DataUnitID duid,
AliasList aliasList )
@@ -56,24 +62,41 @@ public class SyncBroadcast extends TSBKMessage
sb.append( getMessageStub() );
sb.append( " " + new Date( getTime() ).toString() );
sb.append( " TIME ZONE OFFSET:" + getTimeZone().toUpperCase() );
sb.append( " US:" + isUS() );
sb.append( " IST:" + isIST() );
sb.append( " MMU:" + isMMU() );
sb.append( " VL:" + isVL() );
sb.append( " SYSTEM TIME" );
if( isSystemTimeNotLockedToExternalReference() )
{
sb.append( " UNLOCKED" );
}
sb.append( ":" );
TIME_FORMATTER.setTimeZone( getTimeZone() );
sb.append( " " + TIME_FORMATTER.format( new Date( getSystemTime() ) ) );
sb.append( " LEAP-SECOND CORRECTION:" + getLeapSecondCorrection() + "mS" );
if( isMicroslotsLockedToMinuteRollover() )
{
sb.append( " MICROSLOT-MINUTE ROLLOVER:LOCKED" );
}
else
{
sb.append( " MICROSLOT-MINUTE ROLLOVER:UNLOCKED" );
}
return sb.toString();
}
/**
* Returns the time sync in java time (ie milliseconds since epoch )
* System Time (UTC) in milliseconds since java epoch
*/
public long getTime()
public long getSystemTime()
{
Calendar cal = new GregorianCalendar();
Calendar cal = Calendar.getInstance( TimeZone.getTimeZone( "UTC" ) );
cal.clear();
cal.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
cal.set( Calendar.YEAR, getYear() );
cal.set( Calendar.MONTH, getMonth() - 1 );
cal.set( Calendar.DAY_OF_MONTH, getDay() );
@@ -84,102 +107,165 @@ public class SyncBroadcast extends TSBKMessage
return cal.getTimeInMillis();
}
public boolean isUS()
/**
* System Time not locked to external time reference indicator
*/
public boolean isSystemTimeNotLockedToExternalReference()
{
return mMessage.get( US );
return mMessage.get( SYSTEM_TIME_NOT_LOCKED_TO_EXTERNAL_REFERENCE );
}
/**
* Daylight Savings Time
* Micro-slot-to-Minute rollover boundary lock indicator.
*
* Note: we invert the message value so that true indicates that the micro
* slots and minute rollovers are locked, whereas the ICD uses false to
* indicate a lock.
*
* @return true if the microslots rollover at each minute interval is locked
* to the system time minute rollover.
*
* @return false if the microslots and minutes rollover are not locked. This
* indicates that the microslots minute rollover is free rolling.
*
*/
public boolean isIST()
public boolean isMicroslotsLockedToMinuteRollover()
{
return mMessage.get( IST );
return !mMessage.get( MICRO_SLOTS_TO_MINUTE_ROLLOVER_UNLOCKED );
}
/**
* No idea ...
* Leap second correction value that should be applied to the system time.
*
* The leap-second correction field indicates the number of 2.5 millisecond
* units that must be added to the system time to account for the insertion
* of leap seconds when the system time is represented as UTC universal time.
*
* @return leap-second correction value in milliseconds 0.0-10.0
*/
public boolean isMMU()
public double getLeapSecondCorrection()
{
return mMessage.get( MMU );
return (double)mMessage.getInt( LEAP_SECOND_CORRECTION ) * 2.5d;
}
/**
* Indicates the local time offset fields contain valid information.
*/
public boolean isValidLocalTimeOffset()
{
return !mMessage.get( LOCAL_TIME_OFFSET_VALID );
}
/**
* Millenium ...?
* Local time zone offset from UTC universal time (zulu) when the VALID
* LOCAL TIME OFFSET flag indicates a valid offset. Otherwise, this method
* returns a static +00:00 indicating no local time offset.
*/
public int getMC()
public TimeZone getTimeZone()
{
return mMessage.getInt( MC );
if( isValidLocalTimeOffset() )
{
int offset = 0;
offset += mMessage.getInt( LOCAL_TIME_OFFSET_HOURS ) * 3600000;
offset += mMessage.get( LOCAL_TIME_OFFSET_HALF_HOUR ) ? 1800000 : 0;
offset = mMessage.get( LOCAL_TIME_OFFSET_SIGN ) ? -offset : offset;
return new SimpleTimeZone( offset, "LOCAL" );
}
else
{
return NO_TIME_ZONE;
}
}
/**
* No idea ....
* System time - year.
*
* @return year in range 2000-2127
*/
public boolean isVL()
{
return mMessage.get( VL );
}
/**
* Calculates the time zone from the local time offset field.
*/
public String getTimeZone()
{
StringBuilder sb = new StringBuilder();
sb.append( mMessage.get( LOCAL_TIME_OFFSET_SIGN ) ? "-" : "+" );
int hours = mMessage.getInt( LOCAL_TIME_OFFSET );
sb.append( ( hours < 10 ? "0" : "" ) + String.valueOf( hours ) );
sb.append( ":" );
sb.append( mMessage.get( LOCAL_TIME_OFFSET_HALF ) ? "30" : "00" );
if( mMessage.get( IST ) )
{
sb.append( " DST" );
}
return sb.toString();
}
public int getYear()
{
return 2000 + mMessage.getInt( YEAR );
}
/**
* System time - month
*
* @return month 1-12 where 1=January and 12=December
*/
public int getMonth()
{
return mMessage.getInt( MONTH );
}
/**
* System time - day of month
*
* @return day of month in range 1-31
*/
public int getDay()
{
return mMessage.getInt( DAY );
}
/**
* System time - hours
*
* @return hours in range 0 - 23
*/
public int getHours()
{
return mMessage.getInt( HOURS );
}
/**
* System time - minutes
*
* @return minutes in range 0 - 59
*/
public int getMinutes()
{
return mMessage.getInt( MINUTES );
}
/**
* System time - milli-seconds
*
* Note: sub-millisecond values are rounded to nearest millisecond unit to
* conform to java Date internal milli-second precision level.
*
* @return milli-seconds in range 0 - 59999;
*/
public int getMilliSeconds()
{
return (int)( (double)getMicroSlots() * 7.5 );
}
/**
* Micro Slots - each uslot = 7.5 milliseconds. The value ranges 0 - 7999
* and represents 60,000 milliseconds ( 8000 x 7.5 ms). Superframes occur
* every 360 ms (48 micro-slots) on FDMA and Ultraframes occur every 4
* super frames on TDMA.
* TDMA Micro Slots.
*
* Number of 7.5 millisecond micro slots since the last minute rollover, or
* since the last micro-slot counter rollover when the the micro-slot to
* minute rollover is unlocked.
*
* @see isMicroslotsLockedToMinuteRollover() method.
*
* This value supports mobile subscriber timing alignment with a TDMA traffic
* channel when a channel is granted from the control channels, so that when
* the mobile changes from the control channel to the traffic channel, it is
* already aligned with the traffic channel TDMA timing.
*
* Super frames occur every 360 ms (48 micro-slots) on FDMA and Ultra frames
* occur every 4 super frames on TDMA.
*
* Valid micro-slot values range 0-7999 and represent 60,000
* milliseconds ( 8000 x 7.5 ms).
*
* @return number of 7.5 mS micro-slots since last minute or micro-slot
* rollover
*/
public int getMicroSlots()
{

View File

@@ -10,9 +10,11 @@ public enum DataUnitID
HDU ( 0, 712, false, "HDU ", "Header Data Unit" ),
TDU ( 3, 64, false, "TDU ", "Simple Terminator Data Unit" ),
LDU1 ( 5, 1632, true, "LDU1 ", "Logical Link Data Unit 1" ),
VSELP1( 6, 1680, false, "VSEL1", "Motorola VSELP Unit 1" ),
TSBK1( 7, 260, false, "TSBK1", "Trunking Signaling Block" ),
TSBK2( 7, 260, false, "TSBK2", "Trunking Signaling Block" ),
TSBK3( 7, 260, false, "TSBK3", "Trunking Signaling Block" ),
VSELP2( 9, 1680, false, "VSEL1", "Motorola VSELP Unit 1" ),
LDU2 ( 10, 1632, true, "LDU2 ", "Logical Link Data Unit 2" ),
PDU0 ( 12, 260, false, "PDU0 ", "Packet Header Data Unit" ),
PDU1 ( 12, 356, false, "PDU1 ", "Packet Data Unit-Unconfirmed" ),

View File

@@ -53,7 +53,7 @@ public enum Opcode
TDMA_SYNC_BROADCAST( "TDMA_SYNC_BCST ", "FDMA-TDMA Sync Broadcast", 48 ),
RESERVED_31( "RESERVED_31 ", "Reserved", 49 ),
RESERVED_32( "RESERVED_32 ", "Reserved", 50 ),
RESERVED_33( "RESERVED_33 ", "Reserved", 51 ),
IDENTIFIER_UPDATE_TDMA( "IDEN_UPDATE_TDMA", "Identifier Update for TDMA", 51 ),
IDENTIFIER_UPDATE_VHF_UHF_BANDS( "IDEN_UPDATE_VUHF", "Identifier Update for VHF/UHF Bands", 52 ),
TIME_DATE_ANNOUNCEMENT( "TIME_DATE_ANNOUN", "Time and Date Announcement", 53 ),
ROAMING_ADDRESS_COMMAND( "ROAM_ADDR_CMD ", "Roaming Address Command", 54 ),

View File

@@ -3,12 +3,18 @@ package module.decode.p25.reference;
public enum PDUType
{
/* Outbound */
SN_ACTIVATE_TDS_CONTEXT_ACCEPT( "SN-ACTIVATE TDS CONTEXT ACCEPT", 0 ),
SN_DEACTIVATE_TDS_CONTEXT_ACCEPT( "SN-DEACTIVATE TDS CONTEXT ACCEPT", 1 ),
SN_DEACTIVATE_TDS_CONTEXT_REQUEST( "SN-DEACTIVATE TDS CONTEXT REQUEST", 2 ),
SN_ACTIVATE_TDS_CONTEXT_REJECT( "SN-ACTIVATE TDS CONTEXT REJECT", 3 ),
SN_RF_UNCONFIRMED_DATA( "SN-RF UNCONFIRMED DATA", 4 ),
SN_RF_CONFIRMED_DATA( "SN-RF CONFIRMED DATA", 5 ),
SNDCP_ACTIVATE_TDS_CONTEXT_ACCEPT( "SNDCP-ACTIVATE TDS CONTEXT ACCEPT", 0 ),
SNDCP_DEACTIVATE_TDS_CONTEXT_ACCEPT( "SNDCP-DEACTIVATE TDS CONTEXT ACCEPT", 1 ),
SNDCP_DEACTIVATE_TDS_CONTEXT_REQUEST( "SNDCP-DEACTIVATE TDS CONTEXT REQUEST", 2 ),
SNDCP_ACTIVATE_TDS_CONTEXT_REJECT( "SNDCP-ACTIVATE TDS CONTEXT REJECT", 3 ),
SNDCP_RF_UNCONFIRMED_DATA( "SNDCP-RF UNCONFIRMED DATA", 4 ),
SNDCP_RF_CONFIRMED_DATA( "SNDCP-RF CONFIRMED DATA", 5 ),
PDU_TYPE_6( "PDU TYPE 6 UNKNOWN", 6 ),
PDU_TYPE_7( "PDU TYPE 7 UNKNOWN", 7 ),
PDU_TYPE_8( "PDU TYPE 8 UNKNOWN", 8 ),
PDU_TYPE_9( "PDU TYPE 9 UNKNOWN", 9 ),
PDU_TYPE_10( "PDU TYPE 10 UNKNOWN", 10 ),
PDU_TYPE_11( "PDU TYPE 11 UNKNOWN", 11 ),
/* Inbound */
SN_ACTIVATE_TDS_CONTEXT_REQUEST( "SN-ACTIVATE TDS CONTEXT REQUEST", 0 ),
@@ -36,7 +42,7 @@ public enum PDUType
public static PDUType fromValue( int value, boolean outbound )
{
if( outbound && 0 <= value && value <= 5 )
if( outbound && 0 <= value && value <= 11 )
{
return values()[ value ];
}

View File

@@ -2,7 +2,7 @@ package module.decode.p25.reference;
public enum ServiceAccessPoint
{
UNENCRYPTED_USER_DATA( "USER DATA", 0 ),
UNENCRYPTED_USER_DATA( "UNENCRYPTED USER DATA", 0 ),
ENCRYPTED_USER_DATA( "ENCRYPTED USER DATA", 1 ),
CIRCUIT_DATA( "CIRCUIT DATA", 2 ),
CIRCUIT_DATA_CONTROL( "CIRCUIT DATA CONTROL", 3 ),

View File

@@ -103,6 +103,7 @@ public class PassportDecoder extends Decoder
@Override
public void reset()
{
mPassportMessageFramer.reset();
}
@Override

View File

@@ -0,0 +1,29 @@
package module.decode.state;
import controller.channel.Channel.ChannelType;
/**
* Allows decoders to change the call timeout setting in the channel state
*/
public class ChangeChannelTimeoutEvent extends DecoderStateEvent
{
private ChannelType mChannelType;
private long mCallTimeout;
public ChangeChannelTimeoutEvent( Object source, ChannelType channelType, long timeout )
{
super( source, Event.CHANGE_CALL_TIMEOUT, State.IDLE );
mChannelType = channelType;
mCallTimeout = timeout;
}
public ChannelType getChannelType()
{
return mChannelType;
}
public long getCallTimeout()
{
return mCallTimeout;
}
}

View File

@@ -128,10 +128,13 @@ public class ChannelList extends JPanel implements ChannelEventListener
public void setSelectedChannel( Channel channel )
{
/* Send channel selection to each channel panel */
for( ChannelCollectionPanel panel: mDisplayedPanels.values() )
synchronized( mDisplayedPanels )
{
panel.setSelectedChannel( channel );
/* Send channel selection to each channel panel */
for( ChannelCollectionPanel panel: mDisplayedPanels.values() )
{
panel.setSelectedChannel( channel );
}
}
}
@@ -250,20 +253,6 @@ public class ChannelList extends JPanel implements ChannelEventListener
idleMenu.add( new ColorSettingResetMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_IDLE ) );
JMenu noTunerMenu = new JMenu( "No Tuner" );
channelsMenu.add( noTunerMenu );
noTunerMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER ) );
noTunerMenu.add( new ColorSettingResetMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_MIDDLE_NO_TUNER ) );
noTunerMenu.add( new JSeparator() );
noTunerMenu.add( new ColorSettingMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_NO_TUNER ) );
noTunerMenu.add( new ColorSettingResetMenuItem( mSettingsManager,
ColorSettingName.CHANNEL_STATE_GRADIENT_TOP_NO_TUNER ) );
JMenu labelMenu = new JMenu( "Labels" );
colorMenu.add( labelMenu );

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
* Copyright (C) 2014-2016 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
@@ -17,20 +17,14 @@
******************************************************************************/
package module.decode.state;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import module.Module;
import module.decode.config.DecodeConfiguration;
import module.decode.event.CallEvent;
import module.decode.event.ICallEventProvider;
import module.decode.state.DecoderStateEvent.Event;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import audio.metadata.IMetadataProvider;
import audio.metadata.Metadata;
import audio.metadata.MetadataReset;
@@ -40,38 +34,13 @@ import audio.squelch.SquelchState;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
import controller.channel.Channel.ChannelType;
import module.Module;
import module.decode.config.DecodeConfiguration;
import module.decode.event.CallEvent;
import module.decode.event.ICallEventProvider;
import module.decode.state.DecoderStateEvent.Event;
import sample.Listener;
/**
* Channel state tracks the overall state of all processing modules and decoders
* configured for the channel and provides squelch control events.
*
* Primary channel state provides a state machine for tracking decode events
* during a voice or data call through it's life cycle of CALL/DATA, FADE, END,
* and then back to an IDLE state. This class also provides squelch control
* to support any audio processing.
*
* State Descriptions:
* IDLE No voice or data call activity
* CALL/DATA A voice or data call has started, or is continuing
* CONTROL Control channel
* FADE The phase after a voice or data call when either an explicit
* call end has been received, or when no new signalling updates
* have been received, and the fade timer has expired. This phase
* allows for gui updates to signal to the user that the call is
* ended, while continuing to display the call details for the user
* END The phase after the voice or data call FADE, when the call is
* now over, and the gui should be reset to IDLE.
*
* When a call is started, invoke mTimerService.startFadeTimer() to start the
* call fade monitoring timer process that will automatically signal a call end
* once no more call signalling has been detected after the fade timeout has
* occurred, by invoking the fade() method.
*
* When a call ends, invoke the mTimerService.stopFadeTimer() and then invoke
* the mTimerService.startResetTimer(). Once the reset timeout occurs,
* the reset() method will be called, allowing the channel state to perform
* any reset actions, like clearing status and id labels on any view panels.
*/
public class ChannelState extends Module implements ICallEventProvider,
IChangedAttributeProvider, IDecoderStateEventListener,
IDecoderStateEventProvider, IMetadataProvider, ISquelchStateProvider
@@ -83,8 +52,6 @@ public class ChannelState extends Module implements ICallEventProvider,
public static final long RESET_TIMEOUT_DELAY = 2000;
private State mState = State.IDLE;
private long mTrafficChannelTimeout =
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS;
private Listener<CallEvent> mCallEventListener;
private Listener<ChangedAttribute> mChangedAttributeListener;
@@ -97,12 +64,15 @@ public class ChannelState extends Module implements ICallEventProvider,
private boolean mSquelchLocked = false;
private boolean mSelected = false;
private long mStandardChannelFadeTimeout = FADE_TIMEOUT_DELAY;
private long mTrafficChannelFadeTimeout =
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS * 1000;
private long mFadeTimeout;
private long mResetTimeout;
private long mEndTimeout;
private StateMonitor mStateMonitor = new StateMonitor();
private AtomicBoolean mMonitoring = new AtomicBoolean();
private ScheduledFuture<?> mRunningMonitor;
private ScheduledFuture<?> mStateMonitorFuture;
private ThreadPoolManager mThreadPoolManager;
private ChannelType mChannelType;
@@ -110,32 +80,97 @@ public class ChannelState extends Module implements ICallEventProvider,
private TrafficChannelStatusListener mTrafficChannelStatusListener;
private CallEvent mTrafficChannelCallEvent;
/**
* Channel state tracks the overall state of all processing modules and decoders
* configured for the channel and provides squelch control and decoder state
* reset events.
*
* Uses a state enumeration that defines allowable channel state transitions in
* order to track a call or data decode event from start to finish. Uses a
* timer to monitor for inactivity and to provide a FADE period that indicates
* to the user that the activity has stopped while continuing to provide details
* about the call, before the state is reset to IDLE.
*
* Since this class is multi-threaded between decoder events and the internal
* timer that monitors for inactivity, this class uses the mState object for
* thread synchronization.
*
* State Descriptions:
* IDLE Normal state. No voice or data call activity
* CALL/DATA/ENCRYPTED/CONTROL
* Decoding states.
* FADE The phase after a voice or data call when either an explicit
* call end has been received, or when no new signalling updates
* have been received, and the fade timer has expired. This phase
* allows for gui updates to signal to the user that the call is
* ended, while continuing to display the call details for the user
* TEARDOWN Indicates a traffic channel that will be torn down for reuse.
*/
public ChannelState( ThreadPoolManager manager, ChannelType channelType )
{
mThreadPoolManager = manager;
mChannelType = channelType;
}
/**
* Resets this channel state and prepares it for reuse.
*/
@Override
public void reset()
{
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
broadcast( new MetadataReset() );
mState = State.IDLE;
broadcast( ChangedAttribute.CHANNEL_STATE );
}
@Override
public void start()
{
//Start a monitoring task to check for FADE and END event timeouts
if( mMonitoring.compareAndSet( false, true ) )
{
if( mStateMonitorFuture == null && mStateMonitor != null )
{
try
{
mStateMonitorFuture = mThreadPoolManager.scheduleFixedRate(
ThreadType.DECODER, mStateMonitor, 20, TimeUnit.MILLISECONDS );
}
catch( RejectedExecutionException ree )
{
mLog.error( "state monitor scheduled task rejected", ree );
mMonitoring.set( false );
}
}
}
}
@Override
public void stop()
{
mSquelchLocked = false;
//Shutdown the state montoring task
if( mMonitoring.compareAndSet( true, false ) )
{
if( mStateMonitorFuture != null )
{
boolean success = mThreadPoolManager.cancel( mStateMonitorFuture );
if( !success )
{
mLog.error("Couldn't stop monitoring scheduled future" );
}
}
mStateMonitorFuture = null;
}
}
public void dispose()
{
stopMonitor();
mCallEventListener = null;
mChangedAttributeListener = null;
mDecoderStateListener = null;
@@ -144,9 +179,34 @@ public class ChannelState extends Module implements ICallEventProvider,
mStateMonitor = null;
}
private boolean isStandardChannel()
{
return mChannelType == ChannelType.STANDARD;
}
private boolean isTrafficChannel()
{
return mChannelType == ChannelType.TRAFFIC;
}
public void setStandardChannelTimeout( long milliseconds )
{
mStandardChannelFadeTimeout = milliseconds;
if( mChannelType == ChannelType.STANDARD )
{
mFadeTimeout = mStandardChannelFadeTimeout;
}
}
public void setTrafficChannelTimeout( long milliseconds )
{
mTrafficChannelTimeout = milliseconds;
mTrafficChannelFadeTimeout = milliseconds;
if( mChannelType == ChannelType.TRAFFIC )
{
mFadeTimeout = mTrafficChannelFadeTimeout;
}
}
public void setSelected( boolean selected )
@@ -164,71 +224,18 @@ public class ChannelState extends Module implements ICallEventProvider,
return mState;
}
private boolean isIdleOrEndingState()
{
return mState == State.FADE ||
mState == State.END ||
mState == State.IDLE;
}
/**
* Starts the state monitor running when we're not in IDLE state. Calls to
* start an already started monitor are ignored.
*/
private void startMonitor()
{
if( mMonitoring.compareAndSet( false, true ) )
{
if( mRunningMonitor == null && mStateMonitor != null )
{
mRunningMonitor = mThreadPoolManager.scheduleFixedRate(
ThreadType.DECODER, mStateMonitor, 20, TimeUnit.MILLISECONDS );
}
else
{
throw new RuntimeException( "Channel state monitor's scheduled "
+ "future pointer was not null" );
}
}
}
/**
* Stops the state monitor when we're in an IDLE state. Calls to stop an
* already stopped monitor are ignored.
*/
private void stopMonitor()
{
if( mMonitoring.compareAndSet( true, false ) )
{
if( mRunningMonitor != null )
{
mThreadPoolManager.cancel( mRunningMonitor );
}
mRunningMonitor = null;
}
}
/**
* Updates the fade timeout threshold to the current time plus delay
*/
private void updateFadeTimeout()
{
/* Standard channel uses the FADE_TIMEOUT_DELAY and traffic channels
* use the TRAFFIC_FADE_TIMEOUT_DELAY. Some traffic channels (e.g. MPT)
* don't have signaling on the traffic channel that can be used to
* update the fade timeout, so we set the timeout value larger to ensure
* the channel doesn't fade before the end of the call. */
long current = System.currentTimeMillis();
long timeout = current +
( mChannelType == ChannelType.STANDARD ? FADE_TIMEOUT_DELAY :
mTrafficChannelTimeout );
if( timeout > mFadeTimeout )
if( isTrafficChannel() )
{
mFadeTimeout = timeout;
mFadeTimeout = System.currentTimeMillis() + mTrafficChannelFadeTimeout;
}
else
{
mFadeTimeout = System.currentTimeMillis() + mStandardChannelFadeTimeout;
}
}
@@ -237,13 +244,13 @@ public class ChannelState extends Module implements ICallEventProvider,
*/
private void updateResetTimeout()
{
if( mChannelType == ChannelType.TRAFFIC )
if( isTrafficChannel() )
{
mResetTimeout = System.currentTimeMillis();
mEndTimeout = System.currentTimeMillis();
}
else
{
mResetTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY;
mEndTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY;
}
}
@@ -285,120 +292,120 @@ public class ChannelState extends Module implements ICallEventProvider,
*/
protected void setState( State state )
{
if( mState == state )
synchronized( mState )
{
switch( state )
if( state == mState )
{
case CALL:
case CONTROL:
case DATA:
case ENCRYPTED:
updateFadeTimeout();
break;
default:
break;
}
}
else if( mState.canChangeTo( state ) )
{
switch( state )
{
case CONTROL:
//Don't allow traffic channels to be control channels,
//otherwise they won't get call tear down
if( mChannelType != ChannelType.TRAFFIC )
{
broadcast( SquelchState.SQUELCH );
updateFadeTimeout();
startMonitor();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
}
break;
case DATA:
case ENCRYPTED:
broadcast( SquelchState.SQUELCH );
updateFadeTimeout();
startMonitor();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
case CALL:
broadcast( SquelchState.UNSQUELCH );
updateFadeTimeout();
startMonitor();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
case FADE:
broadcast( SquelchState.SQUELCH );
updateResetTimeout();
startMonitor();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
case END:
broadcast( SquelchState.SQUELCH );
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
broadcast( new MetadataReset() );
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
case IDLE:
broadcast( SquelchState.SQUELCH );
stopMonitor();
if( mChannelType == ChannelType.TRAFFIC )
{
if( mTrafficChannelStatusListener != null )
{
mTrafficChannelStatusListener.callEnd();
mTrafficChannelStatusListener = null;
}
if( mTrafficChannelCallEvent != null )
{
mTrafficChannelCallEvent.end();
broadcast( mTrafficChannelCallEvent );
mTrafficChannelCallEvent = null;
}
}
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
default:
break;
}
}
}
public class StateMonitor implements Runnable
{
@Override
public void run()
{
if( getState() == State.FADE )
{
if( mResetTimeout <= System.currentTimeMillis() )
if( State.CALL_STATES.contains( state ) )
{
setState( State.END );
updateFadeTimeout();
}
}
else if( getState() == State.END )
else if( state != mState && mState.canChangeTo( state ) )
{
setState( State.IDLE );
switch( state )
{
case CONTROL:
//Don't allow traffic channels to be control channels,
//otherwise they can't transition to call tear down
if( isStandardChannel() )
{
broadcast( SquelchState.SQUELCH );
updateFadeTimeout();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
}
break;
case DATA:
case ENCRYPTED:
broadcast( SquelchState.SQUELCH );
updateFadeTimeout();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
case CALL:
broadcast( SquelchState.UNSQUELCH );
updateFadeTimeout();
mState = state;
broadcast( ChangedAttribute.CHANNEL_STATE );
break;
case FADE:
processFadeState();
break;
case IDLE:
processIdleState();
break;
case TEARDOWN:
processTeardownState();
break;
case RESET:
mState = State.IDLE;
break;
default:
break;
}
}
else
{
if( mFadeTimeout <= System.currentTimeMillis() )
{
setState( State.FADE );
}
// mLog.debug( "Can't change from [" + mState + "] to [" + state + "]" );
}
}
}
/**
* Sets the state and processes related actions
*/
private void processFadeState()
{
updateResetTimeout();
mState = State.FADE;
broadcast( SquelchState.SQUELCH );
broadcast( ChangedAttribute.CHANNEL_STATE );
}
private void processIdleState()
{
broadcast( SquelchState.SQUELCH );
if( mState == State.FADE )
{
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
broadcast( new MetadataReset() );
}
mState = State.IDLE;
broadcast( ChangedAttribute.CHANNEL_STATE );
}
private void processTeardownState()
{
broadcast( SquelchState.SQUELCH );
mState = State.TEARDOWN;
broadcast( ChangedAttribute.CHANNEL_STATE );
if( mTrafficChannelCallEvent != null )
{
mTrafficChannelCallEvent.end();
broadcast( mTrafficChannelCallEvent );
mTrafficChannelCallEvent = null;
}
if( mTrafficChannelStatusListener != null )
{
mTrafficChannelStatusListener.callEnd();
mTrafficChannelStatusListener = null;
}
else
{
mLog.error( "Traffic channel status listener was null - couldn't "
+ "teardown the channel");
}
}
/**
* Broadcasts the call event to the registered listener
@@ -590,35 +597,95 @@ public class ChannelState extends Module implements ICallEventProvider,
broadcast( SquelchState.UNSQUELCH );
mSquelchLocked = true;
break;
case CHANGE_CALL_TIMEOUT:
if( event instanceof ChangeChannelTimeoutEvent )
{
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
if( timeout.getChannelType() == ChannelType.STANDARD )
{
setStandardChannelTimeout( timeout.getCallTimeout() );
}
else
{
setTrafficChannelTimeout( timeout.getCallTimeout() );
}
}
case CONTINUATION:
if( isIdleOrEndingState() )
{
setState( event.getState() );
}
else
{
updateFadeTimeout();
}
break;
case DECODE:
if( getState() == State.IDLE )
case START:
if( State.CALL_STATES.contains( event.getState() ) )
{
setState( event.getState() );
}
break;
case END:
setState( State.FADE );
if( isTrafficChannel() )
{
setState( State.TEARDOWN );
}
else
{
setState( State.FADE );
}
break;
case RESET:
/* Channel State does not respond to reset events */
break;
case START:
setState( event.getState() );
break;
default:
break;
}
}
}
}
/**
* Monitors decoder state events to automatically transition the channel
* state to IDLE (standard channel) or to TEARDOWN (traffic channel) when
* decoding stops or the monitored channel returns to a no signal state.
*
* Provides a FADE transition state to allow for momentary decoding dropouts
* and to allow the user access to call details for a fade period upon
* call end.
*/
public class StateMonitor implements Runnable
{
@Override
public void run()
{
try
{
synchronized( mState )
{
if( State.CALL_STATES.contains( mState ) &&
mFadeTimeout <= System.currentTimeMillis() )
{
mState = State.FADE;
processFadeState();
}
else if( mState == State.FADE &&
mEndTimeout <= System.currentTimeMillis() )
{
if( isTrafficChannel() )
{
processTeardownState();
}
else
{
processIdleState();
}
}
}
}
catch( Exception e )
{
mLog.error( "Exception thrown while state monitor is running " +
"- state [" + getState() +
"] current [" + System.currentTimeMillis() +
"] mResetTimeout [" + mEndTimeout +
"] mFadeTimeout [" + mFadeTimeout +
"]", e );
}
}
}
}

View File

@@ -14,12 +14,13 @@ import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JPanel;
import module.decode.DecoderFactory;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import controller.channel.Channel;
import controller.channel.ChannelUtils;
import module.decode.DecoderFactory;
import net.miginfocom.swing.MigLayout;
import playlist.PlaylistManager;
import record.RecorderType;
import record.config.RecordConfiguration;
@@ -29,8 +30,6 @@ import settings.ColorSetting.ColorSettingName;
import settings.Setting;
import settings.SettingChangeListener;
import settings.SettingsManager;
import controller.channel.Channel;
import controller.channel.ChannelUtils;
public class ChannelStatePanel extends JPanel
implements Listener<ChangedAttribute>, SettingChangeListener
@@ -236,26 +235,37 @@ public class ChannelStatePanel extends JPanel
Graphics2D g2 = (Graphics2D)g.create();
Paint p = null;
final Channel channel = mChannel;
switch( mChannel.getChannelState().getState() )
final ChannelState channelState = ( channel == null ? null : channel.getChannelState() );
if( channelState != null )
{
case CALL:
p = getGradient( mColorTopCall, mColorMiddleCall );
break;
case CONTROL:
p = getGradient( mColorTopControl, mColorMiddleControl );
break;
case DATA:
p = getGradient( mColorTopData, mColorMiddleData );
break;
case FADE:
p = getGradient( mColorTopFade, mColorMiddleFade );
break;
case END:
case IDLE:
default:
p = getGradient( mColorTopIdle, mColorMiddleIdle );
break;
switch( channelState.getState() )
{
case CALL:
p = getGradient( mColorTopCall, mColorMiddleCall );
break;
case CONTROL:
p = getGradient( mColorTopControl, mColorMiddleControl );
break;
case DATA:
p = getGradient( mColorTopData, mColorMiddleData );
break;
case FADE:
p = getGradient( mColorTopFade, mColorMiddleFade );
break;
case IDLE:
default:
p = getGradient( mColorTopIdle, mColorMiddleIdle );
break;
}
}
if( p == null )
{
p = getGradient( mColorTopIdle, mColorMiddleIdle );
}
g2.setPaint( p );

View File

@@ -56,6 +56,7 @@ public class DecoderStateEvent
public enum Event
{
ALWAYS_UNSQUELCH,
CHANGE_CALL_TIMEOUT,
CONTINUATION,
DECODE,
END,

View File

@@ -1,5 +1,10 @@
package module.decode.state;
import java.util.EnumSet;
/**
* Details the set of states for a channel and the allowable transition states
*/
public enum State
{
IDLE( "IDLE" )
@@ -7,7 +12,8 @@ public enum State
@Override
public boolean canChangeTo( State state )
{
return true;
return state != TEARDOWN &&
state != RESET;
}
},
CALL( "CALL" )
@@ -15,11 +21,11 @@ public enum State
@Override
public boolean canChangeTo( State state )
{
return state == CALL ||
state == CONTROL ||
return state == CONTROL ||
state == DATA ||
state == ENCRYPTED ||
state == FADE;
state == FADE ||
state == TEARDOWN;
}
},
DATA( "DATA" )
@@ -29,9 +35,9 @@ public enum State
{
return state == CALL ||
state == CONTROL ||
state == DATA ||
state == State.ENCRYPTED ||
state == FADE;
state == ENCRYPTED ||
state == FADE ||
state == TEARDOWN;
}
},
ENCRYPTED( "ENCRYPTED" )
@@ -39,7 +45,7 @@ public enum State
@Override
public boolean canChangeTo( State state )
{
return state == FADE;
return state == FADE || state == TEARDOWN;
}
},
CONTROL( "CONTROL" )
@@ -47,7 +53,8 @@ public enum State
@Override
public boolean canChangeTo( State state )
{
return true;
return state == IDLE ||
state == FADE;
}
},
FADE( "FADE" )
@@ -55,20 +62,36 @@ public enum State
@Override
public boolean canChangeTo( State state )
{
return state != FADE; //All states except fade allowed
}
return state != FADE &&
state != RESET;
}
},
END( "END" )
RESET( "RESET" )
{
@Override
public boolean canChangeTo( State state )
{
return state != FADE; //Fade is only disallowed state
}
};
@Override
public boolean canChangeTo(State state)
{
return state == IDLE;
}
},
TEARDOWN( "TEARDOWN" )
{
@Override
public boolean canChangeTo(State state)
{
return state == RESET;
}
};
private String mDisplayValue;
public static final EnumSet<State> CALL_STATES =
EnumSet.of( CALL, CONTROL, DATA, ENCRYPTED );
public static final EnumSet<State> IDLE_STATES =
EnumSet.of( IDLE, FADE );
private State( String displayValue )
{
mDisplayValue = displayValue;

View File

@@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.config.RecordConfiguration;
import sample.Broadcaster;
import sample.Listener;
import source.config.SourceConfigTuner;
import source.tuner.TunerChannel;
@@ -96,10 +97,21 @@ public class TrafficChannelManager extends Module
mPreviousDoNotMonitorCallEvent = null;
}
/**
* Provides a channel by either reusing an existing channel or constructing
* a new one, limited by the total number of constructed channels allowed.
*
* Note: you must enforce thread safety on the mTrafficChannelsInUse
* external to this method.
*
* @param channelNumber
* @param tunerChannel
* @return
*/
private Channel getChannel( String channelNumber, TunerChannel tunerChannel )
{
Channel channel = null;
if( !mTrafficChannelsInUse.containsKey( channelNumber ) )
{
for( Channel configuredChannel: mTrafficChannelPool )
@@ -154,7 +166,7 @@ public class TrafficChannelManager extends Module
}
}
}
return channel;
}
@@ -164,72 +176,79 @@ public class TrafficChannelManager extends Module
*/
private void process( TrafficChannelAllocationEvent event )
{
if( mCallEventListener != null )
{
synchronized( mCallEventListener )
{
CallEvent callEvent = event.getCallEvent();
CallEvent callEvent = event.getCallEvent();
/* Check for duplicate events and suppress */
if( mTrafficChannelsInUse.containsKey( callEvent.getChannel() ) )
/* Check for duplicate events and suppress */
synchronized( mTrafficChannelsInUse )
{
if( mTrafficChannelsInUse.containsKey( callEvent.getChannel() ) )
{
return;
}
long frequency = callEvent.getFrequency();
/* Check the from/to aliases for do not monitor priority */
if( isDoNotMonitor( callEvent ) )
{
callEvent.setCallEventType( CallEventType.CALL_DO_NOT_MONITOR );
if( isSameCallEvent( mPreviousDoNotMonitorCallEvent, callEvent ) )
{
return;
}
long frequency = callEvent.getFrequency();
/* Check the from/to aliases for do not monitor priority */
if( isDoNotMonitor( callEvent ) )
else
{
callEvent.setCallEventType( CallEventType.CALL_DO_NOT_MONITOR );
if( isSameCallEvent( mPreviousDoNotMonitorCallEvent, callEvent ) )
{
return;
}
else
{
mPreviousDoNotMonitorCallEvent = callEvent;
}
mPreviousDoNotMonitorCallEvent = callEvent;
}
else if( frequency > 0 )
{
Channel channel = getChannel( callEvent.getChannel(),
new TunerChannel( Type.TRAFFIC, frequency,
mDecodeConfiguration.getDecoderType()
.getChannelBandwidth() ) );
}
else if( frequency > 0 )
{
Channel channel = getChannel( callEvent.getChannel(),
new TunerChannel( Type.TRAFFIC, frequency,
mDecodeConfiguration.getDecoderType()
.getChannelBandwidth() ) );
if( channel != null )
if( channel != null )
{
if( channel.isProcessing() )
{
if( channel.isProcessing() )
{
ChannelState state = channel.getChannelState();
/* Register as a listener to be notified when the call
* is completed */
state.configureAsTrafficChannel(
new TrafficChannelStatusListener( this,
callEvent.getChannel() ), event );
/* Set state to call so that audio squelch state is correct */
state.setState( State.CALL );
}
else
{
callEvent.setCallEventType( CallEventType.CALL_DETECT );
}
callEvent.setDetails( "TRAFFIC CHANNEL ALLOCATED" );
ChannelState state = channel.getChannelState();
/* Register as a listener to be notified when the call
* is completed */
state.configureAsTrafficChannel(
new TrafficChannelStatusListener( this,
callEvent.getChannel() ), event );
/* Set state to call so that audio squelch state is correct */
state.setState( State.CALL );
}
else
{
callEvent.setCallEventType( CallEventType.CALL_DETECT );
callEvent.setDetails( "CHANNEL PROCESSING ERROR" );
}
}
else
{
callEvent.setCallEventType( CallEventType.CALL_DETECT );
callEvent.setDetails( "NO TUNER AVAILABLE" );
}
mCallEventListener.receive( callEvent );
}
else
{
callEvent.setCallEventType( CallEventType.CALL_DETECT );
callEvent.setDetails( "UNKNOWN FREQUENCY" );
}
final Listener<CallEvent> listener = mCallEventListener;
if( listener != null )
{
listener.receive( callEvent );
}
}
}
@@ -370,14 +389,18 @@ public class TrafficChannelManager extends Module
*/
public void callEnd( String channelNumber )
{
if( channelNumber != null && mTrafficChannelsInUse.containsKey( channelNumber ) )
synchronized( mTrafficChannelsInUse )
{
Channel channel = mTrafficChannelsInUse.get( channelNumber );
mTrafficChannelsInUse.remove( channelNumber );
if( channelNumber != null &&
mTrafficChannelsInUse.containsKey( channelNumber ) )
{
Channel channel = mTrafficChannelsInUse.get( channelNumber );
mTrafficChannelsInUse.remove( channelNumber );
/* Disable the channel and broadcast a notification */
channel.setEnabled( false, true );
/* Disable the channel and broadcast a notification */
channel.setEnabled( false, true );
}
}
}

View File

@@ -22,10 +22,9 @@ import java.awt.image.BufferedImage;
import java.beans.DesignMode;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import org.jdesktop.swingx.mapviewer.GeoPosition;
@@ -126,13 +125,13 @@ public class JXMapViewer extends JPanel implements DesignMode
// make a dummy loading image
try
{
URL url = JXMapViewer.class.getResource("mapviewer/resources/loading.png");
this.setLoadingImage(ImageIO.read(url));
ImageIcon imageIcon = new ImageIcon( "images/loading.png" );
this.setLoadingImage( imageIcon.getImage() );
}
catch (Throwable ex)
{
mLog.error( "JXMapViewer could not load 'loading.png'" );
mLog.error( "could not load 'loading.png'" );
BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = img.createGraphics();

View File

@@ -100,7 +100,7 @@ public class PlaylistManager
}
catch( Exception e )
{
mLog.error( "PlaylistManager - couldn't create file to save "
mLog.error( "couldn't create file to save "
+ "playlist [" + filePath.toString() + "]" );
}
@@ -122,14 +122,12 @@ public class PlaylistManager
}
catch ( JAXBException e )
{
mLog.error( "PlaylistManager - jaxb exception while saving " +
"playlist: ", e );
mLog.error( "jaxb exception while saving playlist: ", e );
}
}
catch ( Exception e )
{
mLog.error( "PlaylistManager - coulcn't open outputstream to " +
"save playlist [" + filePath.toString() + "]", e );
mLog.error( "coulcn't open outputstream to save playlist [" + filePath.toString() + "]", e );
}
finally
{
@@ -155,8 +153,7 @@ public class PlaylistManager
{
if( Files.exists( playlistPath ) )
{
mLog.info( "PlaylistManager - loading playlist file [" +
playlistPath.toString() + "]" );
mLog.info( "loading playlist file [" + playlistPath.toString() + "]" );
JAXBContext context = null;
@@ -176,14 +173,12 @@ public class PlaylistManager
}
catch ( JAXBException e )
{
mLog.error( "PlaylistManager - jaxb exception while loading " +
"playlist: ", e );
mLog.error( "jaxb exception while loading playlist: ", e );
}
}
catch ( Exception e )
{
mLog.error( "PlaylistManager - coulcn't open inputstream to " +
"load playlist [" + playlistPath.toString() + "]", e );
mLog.error( "coulcn't open inputstream to load playlist [" + playlistPath.toString() + "]", e );
}
finally
{
@@ -195,7 +190,7 @@ public class PlaylistManager
}
catch ( IOException e )
{
mLog.error( "PlaylistManager - exception while closing " +
mLog.error( "exception while closing " +
"the playlist file inputstream reader", e );
}
}
@@ -203,8 +198,7 @@ public class PlaylistManager
}
else
{
mLog.info( "PlaylistManager - playlist does not exist [" +
playlistPath.toString() + "]" );
mLog.info( "playlist does not exist [" + playlistPath.toString() + "]" );
}
if( mPlaylist == null )

View File

@@ -82,18 +82,13 @@ public class SystemProperties
{
out = new FileOutputStream( propsPath.toString() );
String comments =
"SDRTrunk - SDR Trunking Decoder Application Settings";
String comments = "SDRTrunk - SDR Trunking Decoder Application Settings";
mProperties.store( out, comments );
mLog.info( "SystemProperties - saved current properties [" +
propsPath.toString() + "]" );
}
catch( Exception e )
{
mLog.error( "SystemProperties - exception while saving " +
"application properties", e );
mLog.error( "exception while saving application properties", e );
}
finally
{
@@ -146,8 +141,7 @@ public class SystemProperties
}
catch ( IOException e )
{
mLog.error( "SystemProperties - exception while creating " +
"app folder [" + folder + "]", e );
mLog.error( "exception while creating app folder [" + folder + "]", e );
}
}
@@ -158,11 +152,11 @@ public class SystemProperties
{
if( mPropertiesPath == null )
{
mLog.info( "SystemProperties - no properties file loaded - using defaults" );
mLog.info( "no properties file loaded - using defaults" );
}
else
{
mLog.info( "SystemProperties - application properties loaded [" + mPropertiesPath.toString() + "]" );
mLog.info( "application properties loaded [" + mPropertiesPath.toString() + "]" );
}
}
@@ -184,7 +178,7 @@ public class SystemProperties
}
catch ( FileNotFoundException e )
{
mLog.error( "SDRTrunk - exception while opening inputstream on " +
mLog.error( "exception while opening inputstream on " +
"application properties file", e );
}
@@ -196,7 +190,7 @@ public class SystemProperties
}
catch ( IOException e )
{
mLog.error( "SDRTrunk - exception while loading properties " +
mLog.error( "exception while loading properties " +
"inputstream into SystemProperties", e );
}
finally
@@ -212,8 +206,7 @@ public class SystemProperties
}
}
mLog.info( "SystemProperties - loaded [" +
propertiesPath.toString() + "]" );
mLog.info( "loaded [" + propertiesPath.toString() + "]" );
}
/**

View File

@@ -123,7 +123,7 @@ public class ColorSetting extends Setting
"Control Gradient Middle", "Channel Control State Gradient Middle" ),
CHANNEL_STATE_GRADIENT_TOP_DATA( Color.BLACK,
"Data Gradient Top", "Channel Data State Gradient Top" ),
CHANNEL_STATE_GRADIENT_MIDDLE_DATA( Color.BLUE,
CHANNEL_STATE_GRADIENT_MIDDLE_DATA( new Color( 0xCC00CC ),
"Data Gradient Middle", "Channel Data State Gradient Middle" ),
CHANNEL_STATE_GRADIENT_TOP_FADE( Color.BLACK,
"Fade Gradient Top", "Channel Fade State Gradient Top" ),

View File

@@ -431,8 +431,7 @@ public class SettingsManager
}
/* Something happened ... the above should always return an icon */
mLog.error( "SettingsManager - couldn't return an icon named [" +
name + "] of heigh [" + height + "]" );
mLog.error( "couldn't return an icon named [" + name + "] of heigh [" + height + "]" );
}
return null;
@@ -630,8 +629,7 @@ public class SettingsManager
return r820TConfig;
default:
throw new IllegalArgumentException( "TunerConfiguration"
+ "Directory - tuner type is unrecognized [" +
throw new IllegalArgumentException( "tuner type is unrecognized [" +
type.toString() + "]" );
}
}
@@ -705,8 +703,7 @@ public class SettingsManager
}
catch( Exception e )
{
mLog.error( "SettingsManager - couldn't create file to save "
+ "settings [" + filePath.toString() + "]", e );
mLog.error( "couldn't create file to save settings [" + filePath.toString() + "]", e );
}
OutputStream out = null;
@@ -727,14 +724,12 @@ public class SettingsManager
}
catch ( JAXBException e )
{
mLog.error( "SettingsManager - jaxb exception while saving " +
"settings", e );
mLog.error( "jaxb exception while saving settings", e );
}
}
catch ( Exception e )
{
mLog.error( "SettingsManager - coulcn't open outputstream to " +
"save settings [" + filePath.toString() + "]" );
mLog.error( "couldn't open outputstream to save settings [" + filePath.toString() + "]" );
}
finally
{
@@ -760,8 +755,7 @@ public class SettingsManager
{
if( Files.exists( settingsPath ) )
{
mLog.info( "SettingsManager - loading settings file [" +
settingsPath.toString() + "]" );
mLog.info( "loading settings file [" + settingsPath.toString() + "]" );
JAXBContext context = null;
@@ -781,13 +775,13 @@ public class SettingsManager
}
catch ( JAXBException e )
{
mLog.error( "SettingsManager - jaxb exception while loading " +
mLog.error( "jaxb exception while loading " +
"settings", e );
}
}
catch ( Exception e )
{
mLog.error( "SettingsManager - coulcn't open inputstream to " +
mLog.error( "coulcn't open inputstream to " +
"load settings [" + settingsPath.toString() + "]", e );
}
finally
@@ -800,7 +794,7 @@ public class SettingsManager
}
catch ( IOException e )
{
mLog.error( "SettingsManager - exception while closing " +
mLog.error( "exception while closing " +
"the settings file inputstream reader", e );
}
}
@@ -808,8 +802,7 @@ public class SettingsManager
}
else
{
mLog.info( "SettingsManager - settings does not exist [" +
settingsPath.toString() + "]" );
mLog.info( "settings does not exist [" + settingsPath.toString() + "]" );
}
if( mSettings == null )

View File

@@ -23,6 +23,14 @@ public class MixerChannelConfiguration
return mMixerChannel;
}
public boolean matches( String mixer, String channels )
{
return mixer != null &&
channels != null &&
mMixer.getMixerInfo().getName().contentEquals( mixer ) &&
mMixerChannel.name().contentEquals( channels );
}
public String toString()
{
StringBuilder sb = new StringBuilder();

View File

@@ -143,8 +143,7 @@ public class RecordingSourceManager
ArrayList<RecordingConfiguration> recordingConfigurations =
settingsManager.getRecordingConfigurations();
mLog.info( "RecordingSourceManager - discovered [" +
recordingConfigurations.size() + "] recording configurations" );
mLog.info( "discovered [" + recordingConfigurations.size() + "] recording configurations" );
for( RecordingConfiguration config: recordingConfigurations )
{

View File

@@ -97,9 +97,13 @@ public class TunerChannel implements Comparable<TunerChannel>
}
@Override
public int compareTo( TunerChannel otherLock )
public int compareTo( TunerChannel otherTunerChannel )
{
if( mFrequency < otherLock.getFrequency() )
if( this.equals( otherTunerChannel ) )
{
return 0;
}
else if( mFrequency < otherTunerChannel.getFrequency() )
{
return -1;
}

View File

@@ -17,8 +17,8 @@
******************************************************************************/
package source.tuner;
import java.util.ArrayList;
import java.util.Collections;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.RejectedExecutionException;
import org.slf4j.Logger;
@@ -36,8 +36,7 @@ public abstract class TunerController implements Tunable
LoggerFactory.getLogger( TunerController.class );
/* List of currently tuned channels being served to demod channels */
protected ArrayList<TunerChannel> mTunedChannels =
new ArrayList<TunerChannel>();
private SortedSet<TunerChannel> mTunedChannels = new ConcurrentSkipListSet<>();
protected FrequencyController mFrequencyController;
private int mMiddleUnusable;
private double mUsableBandwidthPercentage;
@@ -147,13 +146,9 @@ public abstract class TunerController implements Tunable
}
else
{
//Sort the existing locks and get the min/max locked frequencies
Collections.sort( mTunedChannels );
int usableBandwidth = getUsableBandwidth();
long minLockedFrequency = mTunedChannels.get( 0 ).getMinFrequency();
long maxLockedFrequency = mTunedChannels
.get( mTunedChannels.size() - 1 ).getMaxFrequency();
long minLockedFrequency = mTunedChannels.first().getMinFrequency();
long maxLockedFrequency = mTunedChannels.last().getMaxFrequency();
//Requested channel is within current locked channel frequency range
if( minLockedFrequency <= channel.getMinFrequency() &&
@@ -251,10 +246,6 @@ public abstract class TunerController implements Tunable
{
mTunedChannels.remove( tunerChannelSource.getTunerChannel() );
}
else
{
mLog.error( "Request to release channel that is not in current tuned channels set" );
}
}
/**
@@ -282,8 +273,6 @@ public abstract class TunerController implements Tunable
*/
private void updateLOFrequency() throws SourceException
{
Collections.sort( mTunedChannels );
long frequency = getFrequency();
boolean frequencyValid = true;
@@ -291,13 +280,12 @@ public abstract class TunerController implements Tunable
//If there is only 1 channel, position it to the right of center
if( mTunedChannels.size() == 1 )
{
frequency = mTunedChannels.get( 0 ).getMinFrequency() - mMiddleUnusable;
frequency = mTunedChannels.first().getMinFrequency() - mMiddleUnusable;
}
else
{
long minLockedFrequency = mTunedChannels.get( 0 ).getMinFrequency();
long maxLockedFrequency = mTunedChannels
.get( mTunedChannels.size() - 1 ).getMaxFrequency();
long minLockedFrequency = mTunedChannels.first().getMinFrequency();
long maxLockedFrequency = mTunedChannels.last().getMaxFrequency();
//Start by placing the highest frequency channel at the high end of
//the spectrum

View File

@@ -30,6 +30,7 @@ import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import properties.SystemProperties;
import sample.Buffer;
import sample.Listener;
import sample.SampleType;
@@ -61,6 +62,8 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
private ScheduledExecutorService mScheduler = Executors
.newScheduledThreadPool( 1, new NamingThreadFactory( "spectrum dft" ) );
public static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate";
private DFTSize mDFTSize = DFTSize.FFT04096;
private DFTSize mNewDFTSize = DFTSize.FFT04096;
@@ -89,7 +92,16 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
public DFTProcessor( SampleType sampleType )
{
setSampleType( sampleType );
setFrameRate( 20 );
loadSettings();
}
private void loadSettings()
{
int frameRate = SystemProperties.getInstance()
.get( DFTProcessor.FRAME_RATE_PROPERTY, 20 );
setFrameRate( frameRate );
}
public void dispose()
@@ -171,6 +183,10 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
mFrameRate = framesPerSecond;
SystemProperties properties = SystemProperties.getInstance();
properties.set( FRAME_RATE_PROPERTY, String.valueOf( mFrameRate ) );
properties.save();
calculateConsumptionRate();
restart();

View File

@@ -55,6 +55,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import playlist.PlaylistManager;
import properties.SystemProperties;
import sample.Listener;
import sample.SampleType;
import sample.complex.ComplexBuffer;
@@ -99,8 +100,10 @@ public class SpectralDisplayPanel extends JPanel
private static DecimalFormat sCURSOR_FORMAT = new DecimalFormat( "000.00000" );
public static final String FFT_SIZE_PROPERTY = "spectral.display.dft.size";
public static final int NO_ZOOM = 0;
public static final int MAX_ZOOM = 6;
private DFTSize mDFTSize = DFTSize.FFT04096;
private int mZoom = 0;
@@ -150,6 +153,36 @@ public class SpectralDisplayPanel extends JPanel
}
init();
loadSettings();
}
private void loadSettings()
{
SystemProperties properties = SystemProperties.getInstance();
String rawSize = properties.get( FFT_SIZE_PROPERTY, DFTSize.FFT04096.name() );
DFTSize size = null;
if( rawSize != null )
{
try
{
size = DFTSize.valueOf( rawSize );
}
catch( Exception e )
{
//Do nothing, we couldn't parse the stored value
}
}
if( size == null )
{
size = DFTSize.FFT04096;
}
setDFTSize( size );
}
public void dispose()
@@ -194,6 +227,10 @@ public class SpectralDisplayPanel extends JPanel
mDFTProcessor.setDFTSize( size );
mOverlayPanel.setDFTSize( size );
mDFTSize = size;
SystemProperties properties = SystemProperties.getInstance();
properties.set( FFT_SIZE_PROPERTY, size.name() );
properties.save();
setZoom( 0, 0, 0 );
}

View File

@@ -121,6 +121,8 @@ public class WaterfallPanel extends JPanel implements DFTResultsListener,
mMemoryImageSource.setAnimated( true );
mWaterfallImage = createImage( mMemoryImageSource );
repaint();
}
/**