Compare commits

...

64 Commits

Author SHA1 Message Date
denny
daf2b58cf2 Updates build to Version 0.2.0 Beta 8 2016-02-27 06:21:51 -05:00
denny
cf2c0ca07d Resolves significant memory usage issue - updates Airspy and HackRF with
the recent changes applied to RTL2832 Tuner controller for processing of
libusb transfer buffers.
2016-02-27 06:19:35 -05:00
denny
eead22ffad Issue #59 - more changes to improve handling of transfer buffers between
tuner start/stop iterations.
2016-02-26 14:52:31 -05:00
denny
1550c62782 Adds null checking to channel state panel. 2016-02-26 14:37:12 -05:00
denny
5f2d8c8fe6 Issue #59 - reorganizes management of the libusb transfers queue to
better handle processing and shutdowns.

-Reuses transfers between tuner start/stop invocations

-Moves libusb transfer status direct byte buffer allocation out of
buffer processing thread and reuses it, saving a lot of memory
allocations over time.
2016-02-26 14:35:34 -05:00
denny
22d3871d0b Restructures transfer buffer handling to better handle shutdown and
canceling of transfers across multiple threads.
2016-02-26 13:28:59 -05:00
denny
455e45269c Issue #58 - DFT processor queue - removed logging that indicates the
queue is full and samples are being purged.
2016-02-26 10:08:52 -05:00
denny
ca8406bdc0 Resolves #58 - DFT processor sample queue may fill up over time causing
the spectral display to be slow to reflect changes to tuner settings.

Changed the max size of the sample queue based on the highest rate tuner
sample rate 10 MHz (airspy) and the lowest available spectral update
frame rate (12).  Sized the buffer queue based on the maximum number of
buffers that should be available per DFT processing iteration.  If the
queue backs up, new buffers will be discarded until the DFT processor
consumes the queue and catches up.
2016-02-26 06:35:03 -05:00
denny
399d164bd0 Resolves #57 - default tuner configuration per tuner type was being
shared across multiple tuners of the same type, unless the user created
a new tuner configuration for each tuner. 

Updated code to create a default tuner configuration based on the tuner
type, but name the configuration as 'Default-UNIQUETUNERID', so that any
changes to that uniquely identified tuner will persist across sessions.

Note: this does not avoid the situation where multiple tuners of the
same type are all using the same default serial number (e.g. 0000001).
In this case, they will all still share the same configuraiton.  User
must change the serial number of the dongle in order for it to be
uniquely identifiable, so that we can correctly lookup the configuration
for the specific tuner.
2016-02-26 06:07:21 -05:00
denny
6f2a7e89a9 Resolves issue with RTL2832 Tuner cancelling transfer buffers that
haven't yet been initialized.  This occurs when there is a channel
allocated from the tuner that is the first/only listener and then is
immediately released.  Some of the buffers are in motion and the others
simply haven't yet been initialized.
2016-02-22 20:49:20 -05:00
denny
b821588e54 Removes debug message from trafficchannelmanager 2016-02-20 17:20:15 -05:00
denny
62aca27e66 Resolves issue with resetting audio metadata between traffic channel
reuse.
2016-02-20 10:26:50 -05:00
denny
cdcc487095 Resolves potential threading issue in tuner channel source. 2016-02-20 10:21:47 -05:00
denny
d5b6589e36 Updates build to 0.2.0 beta 7 2016-02-20 10:05:22 -05:00
denny
be7ab871d1 Resolves issue with audio metadata updates that was causing the audio
playback panel to not detect changes in FROM/TO values for the current
audio playback.
2016-02-20 09:55:39 -05:00
denny
294a830aaf Updates TDU message decoded message text. 2016-02-20 08:54:02 -05:00
denny
a7da60cd27 Resolves issue with audio playback where audio playback stops working.
Potential existed for the audio output class to become unsynchronized
with the underlying source data line and attempt to perform a blocking
write to the data line when the data line doesn't have the capacity to
accept the full buffer, and the data line is not started.  Thus,
playback could potentially stay in a persitent blocked state.

Refactored Mono and Stereo audio output implementations to push the
audio buffer queue processing into the parent abstract class.

AudioManager was updated to remove the last time active timestamp out 
of the audio manager and down into the audio output to remove duplicate
timestamp accounting.
2016-02-20 08:53:09 -05:00
denny
894ebeb040 Removes logging of C4FM filter coefficients. 2016-02-19 21:03:34 -05:00
denny
682fe0c779 Updates linux launch script to allow clean exit on program exit. 2016-02-15 21:06:20 -05:00
denny
88d3088ebc Resolves issue with concurrent access of channel panels in channel list 2016-02-15 14:58:07 -05:00
denny
dc3390460a Updates build to beta.6 2016-02-15 10:01:11 -05:00
denny
8db226cd7b Adds broadcasting of unknown P25 message types. If a P25 message passes
the NID crc check but uses an unrecognized data unit id, it will now be
broadcast as NID:UNKN, but allows for detection of new DUID types when
we have a large number of occurances of unknonwn duid types.
2016-02-15 10:00:29 -05:00
denny
b7a54598b1 Changes audio panel call timeout from 2 seconds to 1 second in order to
increase audio output cycle time.
2016-02-15 09:59:04 -05:00
denny
dd22397f75 Resolves issue with application memory growth and thread pool
saturation.

Each tunerchannelsource was sharing a thread pool and as the processing
load increased, the thread pool was not expanding and the processing
tasks were backing up, causing the buffers to grow with unprocessed
data.

Changed so that each tuner channel source is handed a thread pool to use
for processing incoming sample data.  Processing chain maintains the
thread pool and starts the channel source.  This allows traffic channels
to reuse the thread pool across multiple iterations.
2016-02-15 09:46:33 -05:00
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
95 changed files with 3701 additions and 3164 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.8"/>
<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>

2
scripts/run_sdrtrunk_linux.sh Normal file → Executable file
View File

@@ -1,2 +1,2 @@
#!/bin/bash
java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk&
java -XX:+UseG1GC -cp "*:libs/*" gui.SDRTrunk

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

@@ -1,8 +1,6 @@
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 +15,14 @@ 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/>
******************************************************************************/
package audio;
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;
@@ -52,8 +50,10 @@ 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 int AUDIO_TIMEOUT = 1000; //1 second
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,220 @@ 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
{
private static final int DISCONNECTED = -1;
private AudioOutput mAudioOutput;
private int mPriority = 0;
private int mSource = DISCONNECTED;
public AudioOutputConnection( AudioOutput audioOutput )
{
mAudioOutput = audioOutput;
}
public void receive( AudioPacket packet )
{
if( packet.hasAudioMetadata() &&
packet.getAudioMetadata().getSource() == mSource )
{
int priority = packet.getAudioMetadata().getPriority();
if( mPriority != priority )
{
mPriority = priority;
updateLowestPriorityAssignment();
}
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" );
}
}
}
/**
* 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;
mAudioOutput.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;
mPriority = 0;
}
/**
* Indicates if audio output is current inactive, meaning that the
* audio output hasn't recently processed any audio packets.
*/
public boolean isInactive()
{
return ( mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT ) < System.currentTimeMillis();
}
public int getPriority()
{
return mPriority;
}
}
}

View File

@@ -64,6 +64,11 @@ public class AudioPacket
return mAudioData;
}
public boolean hasAudioBuffer()
{
return mAudioData != null;
}
public enum Type
{
AUDIO,

View File

@@ -1,19 +1,6 @@
package audio.metadata;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import alias.Alias;
import alias.priority.Priority;
/*******************************************************************************
* 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
@@ -28,15 +15,30 @@ import alias.priority.Priority;
* 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 audio.metadata;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Listener;
import alias.Alias;
import alias.priority.Priority;
public class AudioMetadata implements Listener<Metadata>
{
protected static final Logger mLog = LoggerFactory.getLogger( AudioMetadata.class );
private int mSource;
private List<Metadata> mMetadata = new ArrayList<>();
private Map<MetadataType,Metadata> mMetadata = new HashMap<>();
private boolean mUpdated = false;
/* Channel Selectect == highest priority */
/* Channel Selected == highest priority */
private boolean mSelected = false;
/* Call Priority */
@@ -92,7 +94,8 @@ public class AudioMetadata implements Listener<Metadata>
copy.mPriority = mPriority;
copy.mSelected = mSelected;
copy.mRecordable = mRecordable;
copy.mMetadata.addAll( mMetadata );
copy.mMetadata.putAll( mMetadata );
copy.mUpdated = mUpdated;
copy.mIdentifier = new String( mIdentifier );
@@ -106,27 +109,36 @@ public class AudioMetadata implements Listener<Metadata>
*/
public void reset()
{
Iterator<Metadata> it = mMetadata.iterator();
while( it.hasNext() )
{
if( it.next().isTemporal() )
{
it.remove();
}
}
/* Reset recordable state, identifier and audio call priority levels
* and reprocess the metadata */
mRecordable = mSourceRecordable;
setIdentifier( mSource );
mPriority = Priority.DEFAULT_PRIORITY;
for( Metadata metadata: mMetadata )
List<MetadataType> toRemove = new ArrayList<>();
for( MetadataType type: mMetadata.keySet() )
{
processMetadata( metadata );
Metadata metadata = mMetadata.get( type );
if( metadata != null )
{
if( metadata.isTemporal() )
{
toRemove.add( type );
}
else
{
processMetadata( metadata );
}
}
}
for( MetadataType type: toRemove )
{
mMetadata.remove( type );
}
mUpdated = true;
}
@@ -185,8 +197,14 @@ public class AudioMetadata implements Listener<Metadata>
}
else
{
mMetadata.add( metadata );
processMetadata( metadata );
Metadata existing = mMetadata.get( metadata.getMetadataType() );
if( existing == null || !existing.equals( metadata ) )
{
mMetadata.put( metadata.getMetadataType(), metadata );
processMetadata( metadata );
setUpdated( true );
}
}
mUpdated = true;
@@ -195,26 +213,17 @@ public class AudioMetadata implements Listener<Metadata>
/**
* Returns the complete set of metadata
*/
public List<Metadata> getMetadata()
public Map<MetadataType,Metadata> getMetadata()
{
return mMetadata;
}
/**
* Returns the first metadata object matching the type, or null if the
* metadata type is not found.
* Returns the metadata object matching the type or null
*/
public Metadata getMetadata( MetadataType type )
{
for( Metadata metadata: mMetadata )
{
if( metadata.getMetadataType() == type )
{
return metadata;
}
}
return null;
return mMetadata.get( type );
}
/**
@@ -296,11 +305,11 @@ public class AudioMetadata implements Listener<Metadata>
sb.append( getPriority() );
sb.append( " updated:" + isUpdated() );
sb.append( " " );
for( Metadata m: getMetadata() )
for( Entry<MetadataType,Metadata> entry: getMetadata().entrySet() )
{
sb.append( m.getKey() );
sb.append( entry.getValue().getKey() );
sb.append( ":" );
sb.append( m.getValue() );
sb.append( entry.getValue().getValue() );
sb.append( " " );
}

View File

@@ -21,7 +21,7 @@ public class Metadata
mValueAlias = alias;
mTemporal = temporal;
}
/**
* Constucts a metadata object with the temporal settings. Temporal
* indicates that this metadata will be removed at the end of a reset
@@ -103,4 +103,44 @@ public class Metadata
return sb.toString();
}
public boolean equals( Metadata other )
{
if( mMetadataType != other.getMetadataType() )
{
return false;
}
if( mTemporal != other.isTemporal() )
{
return false;
}
if( mValue != null && other.getValue() != null &&
!mValue.contentEquals( other.getValue() ))
{
return false;
}
if( ( mValue == null && other.getValue() != null ) ||
( mValue != null && other.getValue() == null ) )
{
return false;
}
if( mValueAlias != null && other.getAlias() != null &&
!mValueAlias.getName().contentEquals( other.getAlias().getName() ))
{
return false;
}
if( ( mValueAlias == null && other.getAlias() != null ) ||
( mValueAlias != null && other.getAlias() == null ) )
{
return false;
}
return true;
}
}

View File

@@ -1,34 +1,150 @@
package audio.output;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.Control;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sample.Broadcaster;
import sample.Listener;
import source.mixer.MixerChannel;
import audio.AudioEvent;
import audio.AudioEvent.Type;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import audio.metadata.AudioMetadata;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
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 LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private int mBufferStartThreshold;
private int mBufferStopThreshold;
private Listener<AudioMetadata> mAudioMetadataListener;
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
protected LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
protected ScheduledExecutorService mExecutorService;
protected boolean mCanProcessAudio;
protected long mLastActivity;
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mProcessorTask;
private SourceDataLine mOutput;
private Mixer mMixer;
private MixerChannel mMixerChannel;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private boolean mCanProcessAudio = false;
private long mLastActivity = System.currentTimeMillis();
/**
* Single audio channel playback with automatic starting and stopping of the
* underlying sourcedataline specified by the mixer and mixer channel
* arguments.
*
* Maintains an internal non-blocking audio packet queue and processes this
* queue 25 times a second (every 40 ms).
*
* @param threadPoolManager for assigning buffer processing schedule task
* @param mixer to obtain source data line
* @param mixerChannel either mono or left/right stereo
* @param audioFormat to use during playback
* @param lineInfo to use when obtaining the source data line
* @param requestedBufferSize of approximately 1 second of audio
*/
public AudioOutput( ThreadPoolManager threadPoolManager,
Mixer mixer,
MixerChannel mixerChannel,
AudioFormat audioFormat,
Line.Info lineInfo,
int requestedBufferSize )
{
mThreadPoolManager = threadPoolManager;
mMixer = mixer;
mMixerChannel = mixerChannel;
try
{
mOutput = (SourceDataLine)mMixer.getLine( lineInfo );
if( mOutput != null )
{
mOutput.open( audioFormat, requestedBufferSize );
//Start threshold: buffer is full with 10% or less of capacity remaining
mBufferStartThreshold = (int)( mOutput.getBufferSize() * 0.10 );
//Stop threshold: buffer is empty with 90% or more capacity available
mBufferStopThreshold = (int)( mOutput.getBufferSize() * 0.90 );
mOutput.addLineListener( this );
if( mOutput != null )
{
try
{
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
mGainControl = (FloatControl)gain;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" );
}
try
{
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
mMuteControl = (BooleanControl)mute;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MUTE control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]" );
}
/* Run the queue processor task every 40 milliseconds or 25 times a second */
mProcessorTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.AUDIO_PROCESSING, new BufferProcessor(),
40, TimeUnit.MILLISECONDS );
}
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
mCanProcessAudio = true;
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain audio source data line for "
+ "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]" );
}
}
public void reset()
{
broadcast( new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
@@ -39,52 +155,52 @@ public abstract class AudioOutput implements Listener<AudioPacket>
{
mCanProcessAudio = false;
mBuffer.clear();
if( mExecutorService != null )
if( mProcessorTask != null && mThreadPoolManager != null )
{
mExecutorService.shutdown();
mThreadPoolManager.cancel( mProcessorTask );
}
mThreadPoolManager = null;
mProcessorTask = null;
mBuffer.clear();
mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null;
mAudioMetadataListener = null;
}
public abstract String getChannelName();
public abstract void setMuted( boolean muted );
public abstract boolean isMuted();
public abstract FloatControl getGainControl();
public abstract boolean hasGainControl();
protected void broadcast( AudioMetadata metadata )
{
if( mAudioMetadataListener != null )
{
mAudioMetadataListener.receive( metadata );
}
}
protected void broadcast( AudioEvent audioEvent )
{
mAudioEventBroadcaster.broadcast( audioEvent );
}
public void setSquelched( boolean squelched )
{
mSquelched.set( squelched );
broadcast( new AudioEvent( squelched ? Type.AUDIO_SQUELCHED :
Type.AUDIO_UNSQUELCHED, getChannelName() ) );
if( mOutput != null )
{
mOutput.close();
}
mOutput = null;
mGainControl = null;
mMuteControl = null;
}
public boolean isSquelched()
/**
* Converts the audio packet data into a byte buffer format appropriate for
* the underlying source data line.
*/
protected abstract ByteBuffer convert( AudioPacket packet );
/**
* Audio output channel name
*/
public String getChannelName()
{
return mSquelched.get();
return mMixerChannel.getLabel();
}
/**
* Mixer Channel for this audio output
*/
protected MixerChannel getMixerChannel()
{
return mMixerChannel;
}
/**
* Registers a single listener to receive audio start and audio stop events
*/
@@ -98,6 +214,14 @@ public abstract class AudioOutput implements Listener<AudioPacket>
mAudioEventBroadcaster.removeListener( listener );
}
/**
* Broadcasts an audio event to the registered listener
*/
private void broadcast( AudioEvent audioEvent )
{
mAudioEventBroadcaster.broadcast( audioEvent );
}
/**
* Registers a single listener to receive the audio metadata from each
* audio packet
@@ -113,21 +237,200 @@ public abstract class AudioOutput implements Listener<AudioPacket>
}
/**
* Timestamp when most recent buffer was received
* Broadcasts audio metadata to the registered listener
*/
public long getLastBufferReceived()
private void broadcast( AudioMetadata metadata )
{
if( mAudioMetadataListener != null )
{
mAudioMetadataListener.receive( metadata );
}
}
/**
* Timestamp of either last buffer received or last buffer processed
*/
public long getLastActivityTimestamp()
{
return mLastActivity;
}
/**
* Updates the last activity timestamp to current system time
*/
public void updateTimestamp()
{
mLastActivity = System.currentTimeMillis();
}
@Override
public void receive( AudioPacket packet )
{
if( mCanProcessAudio )
{
mLastActivity = System.currentTimeMillis();
//Update the activity timestamp so that this audio output doesn't
//get disconnected before it starts processing the audio stream
updateTimestamp();
mBuffer.add( packet );
}
}
public class BufferProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
public BufferProcessor()
{
}
@Override
public void run()
{
try
{
/* The processing flag ensures that only one instance of the
* processor can run at any given time */
if( mProcessing.compareAndSet( false, true ) )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
for( AudioPacket packet: packets )
{
if( packet.getType() == Type.AUDIO )
{
broadcast( packet.getAudioMetadata() );
ByteBuffer buffer = convert( packet );
int wrote = 0;
if( !mOutput.isRunning() )
{
int toWrite = mOutput.available();
if( toWrite > buffer.array().length )
{
toWrite = buffer.array().length;
}
//Top off the buffer and check if we can start it
wrote += mOutput.write( buffer.array(), 0, toWrite );
checkStart();
}
if( mOutput.isRunning() && wrote < buffer.array().length )
{
//Blocking write
wrote += mOutput.write( buffer.array(), wrote,
buffer.array().length - wrote );
}
updateTimestamp();
}
}
checkStop();
mProcessing.set( false );
}
}
catch( Exception e )
{
mLog.error( "Error while processing audio buffers", e );
}
}
/**
* Starts audio playback once audio buffer is almost full and remaining
* capacity falls below the start threshold.
*/
private void checkStart()
{
if( mCanProcessAudio &&
!mOutput.isRunning() &&
mOutput.available() <= mBufferStartThreshold )
{
mOutput.start();
}
}
/**
* Stops audio playback and drains the audio buffer to empty when the
* audio buffer is mostly empty and the available buffer capacity
* exceeds the stop threshold
*/
private void checkStop()
{
if( mCanProcessAudio &&
mOutput.isRunning() &&
mOutput.available() >= mBufferStopThreshold )
{
mOutput.drain();
mOutput.stop();
}
}
}
/**
* Sets the mute state for this audio output channel
*/
public void setMuted( boolean muted )
{
if( mMuteControl != null )
{
mMuteControl.setValue( muted );
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
}
}
/**
* Current mute state for this audio output channel
*/
public boolean isMuted()
{
if( mMuteControl != null )
{
return mMuteControl.getValue();
}
return false;
}
/**
* Gain/volume control for this audio output channel, if one is available.
*/
public FloatControl getGainControl()
{
return mGainControl;
}
public boolean hasGainControl()
{
return mGainControl != null;
}
/**
* Monitors the source data line playback state and broadcasts audio events
* to the registered listener as the state changes
*/
@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

@@ -3,267 +3,55 @@ package audio.output;
import java.nio.ByteBuffer;
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.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.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.mixer.MixerChannel;
import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import audio.output.StereoAudioOutput.QueueProcessor;
import controller.NamingThreadFactory;
import controller.ThreadPoolManager;
/**
* Mono Audio output with automatic flow control based on the availability of
* audio data packets.
* Mono Audio output implementation
*/
public class MonoAudioOutput extends AudioOutput
{
private final static Logger mLog = LoggerFactory.getLogger( MonoAudioOutput.class );
/* Output line buffer size is set at one half second of audio at 48 kHz and
* two bytes per sample */
private final static int BUFFER_SIZE = 48000;
/* The queue processor will run every 20 milliseconds checking for inbound
* audio and automatically starting or stopping 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 */
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;
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent;
public MonoAudioOutput( Mixer mixer )
public MonoAudioOutput( ThreadPoolManager threadPoolManager, Mixer mixer )
{
super();
super( threadPoolManager,
mixer,
MixerChannel.MONO,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO,
AudioFormats.MONO_SOURCE_DATALINE_INFO,
BUFFER_SIZE );
}
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION,
getChannelName() );
try
/**
* Converts the audio packet data into mono audio frames.
*/
protected ByteBuffer convert( AudioPacket packet )
{
if( packet.hasAudioBuffer() )
{
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.MONO_SOURCE_DATALINE_INFO );
float[] samples = packet.getAudioBuffer().getSamples();
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_MONO, BUFFER_SIZE );
mCanProcessAudio = true;
mExecutorService = Executors.newSingleThreadScheduledExecutor(
new NamingThreadFactory( "audio (mono) output" ) );
/* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 2 )
.order( ByteOrder.LITTLE_ENDIAN );
/* Run the queue processor task every 40 milliseconds */
mExecutorService.scheduleAtFixedRate( new QueueProcessor(),
0, 40, TimeUnit.MILLISECONDS );
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain source data line for 48kHz PCM mono "
+ "audio output" );
}
if( mOutput != null )
{
try
ShortBuffer shortBuffer = buffer.asShortBuffer();
for( float sample: samples )
{
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
mGainControl = (FloatControl)gain;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MASTER GAIN control for mono line [" +
mixer.getMixerInfo().getName() + "]" );
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
try
{
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
mMuteControl = (BooleanControl)mute;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MUTE control for mono line [" +
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 );
}
}
@Override
public String getChannelName()
{
return MixerChannel.MONO.getLabel();
}
public void dispose()
{
super.dispose();
if( mOutput != null )
{
mOutput.close();
}
}
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 ) )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
broadcast( mAudioContinuationEvent );
for( AudioPacket packet: packets )
{
if( mCanProcessAudio )
{
if( isSquelched() )
{
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 )
{
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
mOutput.write( buffer.array(), 0,
buffer.array().length );
checkStart();
broadcast( packet.getAudioMetadata() );
mLastActivity = System.currentTimeMillis();
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
return buffer;
}
private void checkStart()
{
if( mCanProcessAudio && !isSquelched() &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@Override
public void setMuted( boolean muted )
{
if( mMuteControl != null )
{
mMuteControl.setValue( muted );
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
}
}
@Override
public boolean isMuted()
{
if( mMuteControl != null )
{
return mMuteControl.getValue();
}
return false;
}
@Override
public FloatControl getGainControl()
{
return mGainControl;
}
@Override
public boolean hasGainControl()
{
return mGainControl != null;
return null;
}
}

View File

@@ -3,283 +3,69 @@ package audio.output;
import java.nio.ByteBuffer;
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.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.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import source.mixer.MixerChannel;
import audio.AudioEvent;
import audio.AudioFormats;
import audio.AudioPacket;
import audio.AudioPacket.Type;
import controller.NamingThreadFactory;
import controller.ThreadPoolManager;
/**
* Mono Audio output in stereo format with automatic flow control based on the
* availability of audio data packets.
* Stereo audio output implementation.
*/
public class StereoAudioOutput extends AudioOutput
{
private final static Logger mLog = LoggerFactory.getLogger( StereoAudioOutput.class );
/* Output line buffer size is set at one half second of audio at 48 kHz and
* two bytes per sample for two channels */
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
* 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 */
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;
private SourceDataLine mOutput;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private AudioEvent mAudioContinuationEvent;
private MixerChannel mMixerChannel;
public StereoAudioOutput( Mixer mixer, MixerChannel channel )
public StereoAudioOutput( ThreadPoolManager threadPoolManager,
Mixer mixer, MixerChannel channel )
{
super();
assert( channel == MixerChannel.LEFT || channel == MixerChannel.RIGHT );
mMixerChannel = channel;
mAudioStartEvent = new AudioEvent( AudioEvent.Type.AUDIO_STARTED,
getChannelName() );
mAudioStopEvent = new AudioEvent( AudioEvent.Type.AUDIO_STOPPED,
getChannelName() );
mAudioContinuationEvent = new AudioEvent( AudioEvent.Type.AUDIO_CONTINUATION,
getChannelName() );
try
{
mOutput = (SourceDataLine)mixer.getLine(
AudioFormats.STEREO_SOURCE_DATALINE_INFO );
if( mOutput != null )
{
mOutput.open( AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO, BUFFER_SIZE );
mCanProcessAudio = true;
}
}
catch ( LineUnavailableException e )
{
mLog.error( "Couldn't obtain source data line for 48kHz PCM stereo "
+ "audio output - mixer [" + mixer.getMixerInfo().getName() +
"] channel [" + mMixerChannel.name() + "]" );
}
if( mOutput != null )
{
try
{
Control gain = mOutput.getControl( FloatControl.Type.MASTER_GAIN );
mGainControl = (FloatControl)gain;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MASTER GAIN control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + channel.name() + "]" );
}
try
{
Control mute = mOutput.getControl( BooleanControl.Type.MUTE );
mMuteControl = (BooleanControl)mute;
}
catch( IllegalArgumentException iae )
{
mLog.warn( "Couldn't obtain MUTE control for stereo line [" +
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 );
}
super( threadPoolManager,
mixer,
channel,
AudioFormats.PCM_SIGNED_48KHZ_16BITS_STEREO,
AudioFormats.STEREO_SOURCE_DATALINE_INFO,
BUFFER_SIZE );
}
@Override
public String getChannelName()
/**
* Converts the audio packet data into stereo audio frames with the mixer
* channel containing the audio and the other channel containing zero
* valued (silent) samples.
*/
protected ByteBuffer convert( AudioPacket packet )
{
return mMixerChannel.getLabel();
}
public void dispose()
{
super.dispose();
if( mOutput != null )
if( packet.hasAudioBuffer() )
{
mOutput.close();
}
}
private ByteBuffer convert( float[] samples )
{
/* Little-endian byte buffer */
ByteBuffer buffer =
ByteBuffer.allocate( samples.length * 4 )
float[] samples = packet.getAudioBuffer().getSamples();
/* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate( samples.length * 4 )
.order( ByteOrder.LITTLE_ENDIAN );
ShortBuffer shortBuffer = buffer.asShortBuffer();
if( mMixerChannel == MixerChannel.LEFT )
{
for( float sample: samples )
{
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
shortBuffer.put( (short)0 );
}
}
else
{
for( float sample: samples )
{
shortBuffer.put( (short)0 );
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
}
return buffer;
}
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 ) )
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo( packets );
if( packets.size() > 0 )
{
broadcast( mAudioContinuationEvent );
for( AudioPacket packet: packets )
{
if( mCanProcessAudio )
{
if( isSquelched() )
{
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();
}
}
}
}
else
{
checkStop();
}
mProcessing.set( false );
}
}
private void checkStart()
{
if( mCanProcessAudio && !isSquelched() &&
!mOutput.isRunning() && mOutput.available() <= START_THRESHOLD )
{
mOutput.start();
mAudioEventBroadcaster.broadcast( mAudioStartEvent );
}
}
private void checkStop()
{
if( mCanProcessAudio && mOutput.isRunning() &&
( mOutput.available() >= STOP_THRESHOLD || isSquelched() ) )
{
mOutput.drain();
mOutput.stop();
mAudioEventBroadcaster.broadcast( mAudioStopEvent );
}
}
}
@Override
public void setMuted( boolean muted )
{
if( mMuteControl != null )
{
mMuteControl.setValue( muted );
broadcast( new AudioEvent( muted ? AudioEvent.Type.AUDIO_MUTED :
AudioEvent.Type.AUDIO_UNMUTED, getChannelName() ) );
}
}
ShortBuffer shortBuffer = buffer.asShortBuffer();
@Override
public boolean isMuted()
{
if( mMuteControl != null )
{
return mMuteControl.getValue();
if( getMixerChannel() == MixerChannel.LEFT )
{
for( float sample: samples )
{
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
shortBuffer.put( (short)0 );
}
}
else
{
for( float sample: samples )
{
shortBuffer.put( (short)0 );
shortBuffer.put( (short)( sample * Short.MAX_VALUE ) );
}
}
return buffer;
}
return false;
}
@Override
public FloatControl getGainControl()
{
return mGainControl;
}
@Override
public boolean hasGainControl()
{
return mGainControl != null;
return null;
}
}

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

@@ -297,6 +297,9 @@ public class Channel extends Configuration
/* Broadcast channel deleted event */
fireChannelEvent( Event.CHANNEL_DELETED );
mProcessingChain.dispose();
mProcessingChain = null;
}
/**
@@ -809,8 +812,7 @@ public class Channel extends Configuration
{
if( mProcessingChain == null )
{
mProcessingChain = new ProcessingChain(
mResourceManager.getThreadPoolManager() );
mProcessingChain = new ProcessingChain( getName() );
/* Get the optional alias list for the decode modules to use */
AliasList aliasList = mResourceManager.getPlaylistManager()
@@ -901,6 +903,8 @@ public class Channel extends Configuration
{
if( !mEnabled && mProcessingChain != null )
{
fireChannelEvent( Event.CHANNEL_PROCESSING_STOPPED );
mProcessingChain.stop();
if( mChannelType == ChannelType.STANDARD )
@@ -910,8 +914,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

@@ -12,8 +12,9 @@ import source.Source;
public class InstrumentableProcessingChain extends ProcessingChain implements Instrumentable
{
public InstrumentableProcessingChain()
public InstrumentableProcessingChain( String name )
{
super( name );
}
public void setSource( Source source ) throws IllegalStateException

View File

@@ -99,7 +99,7 @@ public class DecoderViewFrame extends JInternalFrame
mDecodeConfig = decodeConfig;
mProcessingChain = new InstrumentableProcessingChain();
mProcessingChain = new InstrumentableProcessingChain( "instrumented" );
List<Module> modules = DecoderFactory
.getModules( ChannelType.STANDARD,

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

@@ -19,7 +19,8 @@ package module;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import message.IMessageListener;
@@ -61,7 +62,7 @@ import audio.metadata.Metadata;
import audio.squelch.ISquelchStateListener;
import audio.squelch.ISquelchStateProvider;
import audio.squelch.SquelchState;
import controller.ThreadPoolManager;
import controller.NamingThreadFactory;
import controller.channel.ChannelEvent;
import controller.channel.IChannelEventListener;
import controller.channel.IChannelEventProvider;
@@ -99,24 +100,19 @@ public class ProcessingChain implements IChannelEventListener
private Broadcaster<RealBuffer> mRealBufferBroadcaster = new Broadcaster<>();
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mBufferProcessingTask;
private ScheduledExecutorService mScheduledExecutorService;
private AtomicBoolean mRunning = new AtomicBoolean();
protected Source mSource;
private List<Module> mModules = new ArrayList<>();
private IFrequencyCorrectionController mFrequencyCorrectionController;
public ProcessingChain()
public ProcessingChain( String name )
{
this( new ThreadPoolManager() );
mScheduledExecutorService = Executors.newScheduledThreadPool( 1,
new NamingThreadFactory( "channel " + name ) );
}
public ProcessingChain( ThreadPoolManager threadManager )
{
mThreadPoolManager = threadManager;
}
public void dispose()
{
stop();
@@ -128,14 +124,9 @@ public class ProcessingChain implements IChannelEventListener
mModules.clear();
// mComplexReceiver.dispose();
// mComplexReceiver = null;
//
// mRealReceiver.dispose();
// mRealReceiver = null;
mScheduledExecutorService.shutdownNow();
mScheduledExecutorService = null;
mThreadPoolManager = null;
mBufferProcessingTask = null;
mFrequencyCorrectionController = null;
mAudioPacketBroadcaster.dispose();
@@ -447,9 +438,11 @@ public class ProcessingChain implements IChannelEventListener
* of the decoder state's */
if( mSource instanceof TunerChannelSource )
{
TunerChannelSource tunerChannelSource = (TunerChannelSource)mSource;
try
{
long frequency = ((TunerChannelSource)mSource).getFrequency();
long frequency = tunerChannelSource.getFrequency();
mDecoderStateEventBroadcaster.broadcast(
new DecoderStateEvent( this, Event.SOURCE_FREQUENCY,
@@ -459,6 +452,8 @@ public class ProcessingChain implements IChannelEventListener
{
mLog.error( "Error getting frequency from tuner channel source", e );
}
tunerChannelSource.start( mScheduledExecutorService );
}
}
else
@@ -496,12 +491,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

@@ -289,6 +289,7 @@ public class P25MessageFramer implements Listener<Dibit>
else
{
mComplete = true;
dispatch( new P25Message( mMessage.copy(), duid, mAliasList ) );
}
}
else

View File

@@ -77,7 +77,6 @@ public class P25_C4FMDecoder extends P25Decoder implements IRealBufferListener
/* Filter demodulated sample buffers */
float[] filter = FilterFactory.getLowPass( 48000, 2500, 4000, 80, WindowType.HANNING, true );
mLog.debug( "Demod Filter tap count:" + filter.length + " coefficients:" + Arrays.toString( filter ) );
mC4FMPreFilter = new RealFIRFilter_RB_RB( filter, 1.0f );
/* Issue tuned frequency correction commands, remotely controlled by the

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

@@ -18,4 +18,19 @@ public class TDUMessage extends P25Message
mCRC = new CRC[ 1 ];
mCRC[ 0 ] = CRC.PASSED;
}
@Override
public String getMessage()
{
StringBuilder sb = new StringBuilder();
sb.append( "NAC:" );
sb.append( getNAC() );
sb.append( " " );
sb.append( getDUID().getLabel() );
sb.append( " TERMINATOR DATA UNIT" );
return sb.toString();
}
}

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

@@ -22,7 +22,8 @@ import java.awt.Component;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.swing.JMenu;
import javax.swing.JPanel;
@@ -53,8 +54,8 @@ public class ChannelList extends JPanel implements ChannelEventListener
private final static Logger mLog = LoggerFactory.getLogger( ChannelList.class );
private HashMap<Channel,ChannelCollectionPanel> mDisplayedPanels =
new HashMap<Channel,ChannelCollectionPanel>();
private Map<Channel,ChannelCollectionPanel> mDisplayedPanels =
new ConcurrentHashMap<Channel,ChannelCollectionPanel>();
private PlaylistManager mPlaylistManager;
private SettingsManager mSettingsManager;
@@ -128,10 +129,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 +254,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,122 @@ 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 );
broadcast( new MetadataReset() );
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 +599,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
@@ -203,7 +202,7 @@ public class ChannelStatePanel extends JPanel
switch( changedAttribute )
{
case CHANNEL_STATE:
if( state.getState() != null )
if( state != null && state.getState() != null )
{
mStateLabel.setText( state.getState().getDisplayValue() );
}
@@ -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;
@@ -90,16 +91,35 @@ public class TrafficChannelManager extends Module
@Override
public void dispose()
{
for( Channel trafficChannel: mTrafficChannelPool )
{
trafficChannel.setEnabled( false );
trafficChannel.dispose();
}
mTrafficChannelPool.clear();
mCallEventListener = null;
mResourceManager = null;
mDecodeConfiguration = null;
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 +174,7 @@ public class TrafficChannelManager extends Module
}
}
}
return channel;
}
@@ -164,72 +184,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 +397,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;
@@ -558,11 +557,6 @@ public class SettingsManager
ArrayList<TunerConfiguration> configs = getSettings()
.getTunerConfigurations( type );
if( configs.isEmpty() )
{
configs.add( addNewTunerConfiguration( type, "Default" ) );
}
return configs;
}
@@ -630,8 +624,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 +698,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 +719,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 +750,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 +770,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 +789,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 +797,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

@@ -63,9 +63,8 @@ public class Recording implements Comparable<Recording>,
}
@Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel ) throws RejectedExecutionException,
SourceException
public TunerChannelSource getChannel( TunerChannel channel )
throws RejectedExecutionException, SourceException
{
// TODO Auto-generated method stub
return null;

View File

@@ -71,9 +71,7 @@ public class RecordingSourceManager
if( recording != null )
{
retVal = recording.getChannel(
mResourceManager.getThreadPoolManager(),
tunerChannel );
retVal = recording.getChannel( tunerChannel );
}
else
{
@@ -143,8 +141,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

@@ -30,10 +30,9 @@ import sample.complex.ComplexBuffer;
import source.SourceException;
import source.tuner.frequency.FrequencyChangeBroadcaster;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeListener;
import source.tuner.frequency.FrequencyChangeEvent.Event;
import source.tuner.frequency.FrequencyChangeListener;
import controller.ResourceManager;
import controller.ThreadPoolManager;
/**
* Tuner - provides tuner channel sources, representing a channel frequency
@@ -147,8 +146,8 @@ public abstract class Tuner implements FrequencyChangeBroadcaster,
*
* @return - source for 48k sample rate
*/
public abstract TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel ) throws RejectedExecutionException, SourceException;
public abstract TunerChannelSource getChannel( TunerChannel channel )
throws RejectedExecutionException, SourceException;
/**
* Releases the tuned channel resources

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

@@ -14,8 +14,8 @@ public interface TunerChannelProvider
*
* @return - source for 48k sample rate
*/
public abstract TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel ) throws RejectedExecutionException, SourceException;
public abstract TunerChannelSource getChannel( TunerChannel channel )
throws RejectedExecutionException, SourceException;
/**
* Releases the tuned channel resources

View File

@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -37,8 +38,6 @@ import source.SourceException;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Event;
import source.tuner.frequency.FrequencyChangeListener;
import controller.ThreadPoolManager;
import controller.ThreadPoolManager.ThreadType;
import dsp.filter.FilterFactory;
import dsp.filter.Window.WindowType;
import dsp.filter.cic.ComplexPrimeCICDecimate;
@@ -62,7 +61,6 @@ public class TunerChannelSource extends ComplexSource
private ComplexPrimeCICDecimate mDecimationFilter;
private Listener<ComplexBuffer> mListener;
private FrequencyChangeListener mFrequencyChangeListener;
private ThreadPoolManager mThreadPoolManager;
private ScheduledFuture<?> mTaskHandle;
private long mTunerFrequency = 0;
@@ -71,15 +69,11 @@ public class TunerChannelSource extends ComplexSource
private DecimationProcessor mDecimationProcessor = new DecimationProcessor();
public TunerChannelSource( ThreadPoolManager threadPoolManager,
Tuner tuner,
TunerChannel tunerChannel )
throws RejectedExecutionException, SourceException
public TunerChannelSource( Tuner tuner, TunerChannel tunerChannel )
throws RejectedExecutionException, SourceException
{
super( "Tuner Channel Source" );
mThreadPoolManager = threadPoolManager;
mTuner = tuner;
mTuner.addListener( (FrequencyChangeListener)this );
@@ -95,14 +89,17 @@ public class TunerChannelSource extends ComplexSource
/* Fire a sample rate change event to setup the decimation chain */
frequencyChanged( new FrequencyChangeEvent(
Event.SAMPLE_RATE_CHANGE_NOTIFICATION, mTuner.getSampleRate() ) );
/* Schedule the decimation task to run every 20 ms (50 iterations/second) */
mTaskHandle = mThreadPoolManager.scheduleFixedRate( ThreadType.DECIMATION,
mDecimationProcessor, 20, TimeUnit.MILLISECONDS );
}
public void start( ScheduledExecutorService scheduledExecutorService )
{
/* Schedule the decimation task to run every 10 ms (100 iterations/second) */
mTaskHandle = scheduledExecutorService.scheduleAtFixedRate(
mDecimationProcessor, 0, 10, TimeUnit.MILLISECONDS );
/* Finally, register to receive samples from the tuner */
mTuner.addListener( (Listener<ComplexBuffer>)this );
}
}
@Override
public void dispose()
@@ -129,14 +126,15 @@ public class TunerChannelSource extends ComplexSource
{
if( mTaskHandle != null )
{
mThreadPoolManager.cancel( mTaskHandle );
mThreadPoolManager = null;
mTaskHandle.cancel( true );
mTaskHandle = null;
}
mBuffer.clear();
mBuffer = null;
synchronized( mBuffer )
{
mBuffer.clear();
mBuffer = null;
}
mFrequencyChangeListener = null;
mListener = null;
@@ -160,7 +158,13 @@ public class TunerChannelSource extends ComplexSource
@Override
public void receive( ComplexBuffer buffer )
{
mBuffer.add( buffer );
synchronized( mBuffer )
{
if( mBuffer != null )
{
mBuffer.add( buffer );
}
}
}
public void setFrequencyChangeListener( FrequencyChangeListener listener )
@@ -290,7 +294,7 @@ public class TunerChannelSource extends ComplexSource
{
if( mBuffer != null )
{
mBuffer.drainTo( mSampleBuffers, 4 );
mBuffer.drainTo( mSampleBuffers, 8 );
for( Buffer buffer: mSampleBuffers )
{

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() &&
@@ -199,8 +194,8 @@ public abstract class TunerController implements Tunable
*
* @throws RejectedExecutionException if the decimation processor has an error
*/
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
Tuner tuner, TunerChannel channel ) throws RejectedExecutionException
public TunerChannelSource getChannel( Tuner tuner, TunerChannel channel )
throws RejectedExecutionException
{
TunerChannelSource source = null;
@@ -215,8 +210,7 @@ public abstract class TunerController implements Tunable
updateLOFrequency();
}
source = new TunerChannelSource( threadPoolManager,
tuner, channel );
source = new TunerChannelSource( tuner, channel );
}
catch( SourceException se )
{
@@ -251,10 +245,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 +272,6 @@ public abstract class TunerController implements Tunable
*/
private void updateLOFrequency() throws SourceException
{
Collections.sort( mTunedChannels );
long frequency = getFrequency();
boolean frequencyValid = true;
@@ -291,13 +279,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

@@ -100,8 +100,7 @@ public class TunerManager
try
{
retVal = tuner.getChannel(
mResourceManager.getThreadPoolManager(), tunerChannel );
retVal = tuner.getChannel( tunerChannel );
}
catch ( RejectedExecutionException ree )
{
@@ -545,16 +544,14 @@ public class TunerManager
}
}
}
else
{
config = configs.get( 0 );
}
//If we're still null at this point, create a default config
if( config == null )
{
String name = "Default-" + tuner.getUniqueID();
config = mResourceManager.getSettingsManager()
.addNewTunerConfiguration( tuner.getTunerType(), "Default" );
.addNewTunerConfiguration( tuner.getTunerType(), name );
}
//Persist the config as the selected tuner configuration. The method

View File

@@ -128,8 +128,8 @@ public class AirspyTuner extends Tuner
}
@Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel ) throws RejectedExecutionException, SourceException
public TunerChannelSource getChannel( TunerChannel channel )
throws RejectedExecutionException, SourceException
{
//TODO: this channel has a decimated sample rate of:
// 10.0 MSps = 10,000,000 / 208 = 48076.923
@@ -138,7 +138,7 @@ public class AirspyTuner extends Tuner
//Consider implementing a fractional resampler to get a correct 48 kHz
//output sample rate
return mController.getChannel( threadPoolManager, this, channel );
return mController.getChannel( this, channel );
}
@Override

View File

@@ -262,7 +262,30 @@ public class AirspyTunerController extends TunerController
}
}
@Override
public static String getTransferStatus( int status )
{
switch( status )
{
case 0:
return "TRANSFER COMPLETED (0)";
case 1:
return "TRANSFER ERROR (1)";
case 2:
return "TRANSFER TIMED OUT (2)";
case 3:
return "TRANSFER CANCELLED (3)";
case 4:
return "TRANSFER STALL (4)";
case 5:
return "TRANSFER NO DEVICE (5)";
case 6:
return "TRANSFER OVERFLOW (6)";
default:
return "UNKNOWN TRANSFER STATUS (" + status + ")";
}
}
@Override
public void apply( TunerConfiguration config ) throws SourceException
{
if( config instanceof AirspyTunerConfiguration )
@@ -1192,18 +1215,21 @@ public class AirspyTunerController extends TunerController
*/
public void addListener( Listener<ComplexBuffer> listener )
{
mComplexBufferBroadcaster.addListener( listener );
synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.addListener( listener );
if( mBufferProcessor == null || !mBufferProcessor.isRunning() )
{
mBufferProcessor = new BufferProcessor();
if( mBufferProcessor == null || !mBufferProcessor.isRunning() )
{
mBufferProcessor = new BufferProcessor();
Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true );
thread.setName( "Airspy Buffer Processor" );
Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true );
thread.setName( "Airspy Buffer Processor" );
thread.start();
}
thread.start();
}
}
}
/**
@@ -1212,12 +1238,15 @@ public class AirspyTunerController extends TunerController
*/
public void removeListener( Listener<ComplexBuffer> listener )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() )
{
mBufferProcessor.stop();
}
synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() )
{
mBufferProcessor.stop();
}
}
}
/**
@@ -1227,8 +1256,11 @@ public class AirspyTunerController extends TunerController
public class BufferProcessor implements Runnable, TransferCallback
{
private ScheduledFuture<?> mSampleDispatcherTask;
private CopyOnWriteArrayList<Transfer> mTransfers;
private LinkedTransferQueue<Transfer> mAvailableTransfers;
private LinkedTransferQueue<Transfer> mTransfersInProgress = new LinkedTransferQueue<>();
private AtomicBoolean mRunning = new AtomicBoolean();
private ByteBuffer mLibUsbHandlerStatus;
private boolean mCancel = false;
@Override
public void run()
@@ -1237,33 +1269,64 @@ public class AirspyTunerController extends TunerController
{
prepareTransfers();
for( Transfer transfer: mTransfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
break;
}
}
mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.SOURCE_SAMPLE_PROCESSING, new BufferDispatcher(),
20, TimeUnit.MILLISECONDS );
mLibUsbHandlerStatus = ByteBuffer.allocateDirect( 4 );
List<Transfer> transfers = new ArrayList<>();
mCancel = false;
while( mRunning.get() )
{
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
mAvailableTransfers.drainTo( transfers );
for( Transfer transfer: transfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result == LibUsb.SUCCESS )
{
mTransfersInProgress.add( transfer );
}
else
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
}
}
int result = LibUsb.handleEventsTimeoutCompleted(
null, USB_TIMEOUT_MS, completed.asIntBuffer() );
null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling events for libusb" );
}
transfers.clear();
mLibUsbHandlerStatus.rewind();
}
if( mCancel )
{
for( Transfer transfer: mTransfersInProgress )
{
LibUsb.cancelTransfer( transfer );
}
int result = LibUsb.handleEventsTimeoutCompleted( null,
USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling events for libusb" );
}
mLibUsbHandlerStatus.rewind();
}
}
}
@@ -1273,10 +1336,10 @@ public class AirspyTunerController extends TunerController
*/
public void stop()
{
mCancel = true;
if( mRunning.compareAndSet( true, false ) )
{
cancel();
if( mSampleDispatcherTask != null )
{
mSampleDispatcherTask.cancel( true );
@@ -1296,66 +1359,39 @@ public class AirspyTunerController extends TunerController
@Override
public void processTransfer( Transfer transfer )
{
if( transfer.status() == LibUsb.TRANSFER_COMPLETED )
{
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
mFilledBuffers.add( data );
if( !isRunning() )
{
LibUsb.cancelTransfer( transfer );
}
}
mTransfersInProgress.remove( transfer );
switch( transfer.status() )
{
case LibUsb.TRANSFER_COMPLETED:
/* resubmit the transfer */
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
case LibUsb.TRANSFER_STALL:
if( transfer.actualLength() > 0 )
{
mLog.error( "couldn't resubmit buffer transfer to tuner" );
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
if( isRunning() )
{
mFilledBuffers.add( data );
}
}
break;
case LibUsb.TRANSFER_CANCELLED:
/* free the transfer and remove it */
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
break;
default:
/* unexpected error */
mLog.error( "transfer error" );
mLog.error( "transfer error [" +
getTransferStatus( transfer.status() ) +
"] transferred actual: " + transfer.actualLength() );
}
}
private void cancel()
{
for( Transfer transfer: mTransfers )
{
LibUsb.cancelTransfer( transfer );
}
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
int result = LibUsb.handleEventsTimeoutCompleted(
null, USB_TIMEOUT_MS, completed.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling usb events during cancel [" +
LibUsb.errorName( result ) + "]" );
}
}
mAvailableTransfers.add( transfer );
}
/**
* Prepares (allocates) a set of transfer buffers for use in
@@ -1363,30 +1399,28 @@ public class AirspyTunerController extends TunerController
*/
private void prepareTransfers() throws LibUsbException
{
mTransfers = new CopyOnWriteArrayList<Transfer>();
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
if( mAvailableTransfers == null )
{
Transfer transfer = LibUsb.allocTransfer();
mAvailableTransfers = new LinkedTransferQueue<>();
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{
Transfer transfer = LibUsb.allocTransfer();
if( transfer == null )
{
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer =
ByteBuffer.allocateDirect( mBufferSize );
LibUsb.fillBulkTransfer( transfer,
mDeviceHandle,
USB_ENDPOINT,
buffer,
BufferProcessor.this,
"Buffer #" + x,
USB_TIMEOUT_MS );
mTransfers.add( transfer );
if( transfer == null )
{
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer =
ByteBuffer.allocateDirect( mBufferSize );
LibUsb.fillBulkTransfer( transfer, mDeviceHandle, USB_ENDPOINT,
buffer, BufferProcessor.this, "Buffer", USB_TIMEOUT_MS );
mAvailableTransfers.add( transfer );
}
}
}
}

View File

@@ -99,8 +99,8 @@ public class B100Tuner extends Tuner
}
@Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel ) throws RejectedExecutionException, SourceException
public TunerChannelSource getChannel( TunerChannel channel )
throws RejectedExecutionException, SourceException
{
//TODO: pass this call to the tuner controller
return null;

View File

@@ -112,11 +112,10 @@ public class FCDTuner extends MixerTuner
}
@Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel tunerChannel ) throws RejectedExecutionException,
SourceException
public TunerChannelSource getChannel( TunerChannel tunerChannel )
throws RejectedExecutionException, SourceException
{
return mController.getChannel( threadPoolManager, this, tunerChannel );
return mController.getChannel( this, tunerChannel );
}
/**

View File

@@ -106,10 +106,10 @@ public class HackRFTuner extends Tuner
}
@Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel ) throws RejectedExecutionException, SourceException
public TunerChannelSource getChannel( TunerChannel channel )
throws RejectedExecutionException, SourceException
{
return mController.getChannel( threadPoolManager, this, channel );
return mController.getChannel( this, channel );
}
@Override

View File

@@ -56,6 +56,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
@@ -91,7 +92,7 @@ public class HackRFTunerController extends TunerController
private final static Logger mLog =
LoggerFactory.getLogger( HackRFTunerController.class );
public final static long TIMEOUT_US = 1000000l; //uSeconds
public final static long USB_TIMEOUT_MS = 1000000l; //uSeconds
public static final byte USB_ENDPOINT = (byte)0x81;
public static final byte USB_INTERFACE = (byte)0x0;
@@ -394,7 +395,7 @@ public class HackRFTunerController extends TunerController
(short)value,
(short)index,
buffer,
TIMEOUT_US );
USB_TIMEOUT_MS );
if( transferred < 0 )
{
@@ -467,7 +468,7 @@ public class HackRFTunerController extends TunerController
(short)value,
(short)index,
buffer,
TIMEOUT_US );
USB_TIMEOUT_MS );
if( transferred < 0 )
{
@@ -886,18 +887,21 @@ public class HackRFTunerController extends TunerController
*/
public void addListener( Listener<ComplexBuffer> listener )
{
mComplexBufferBroadcaster.addListener( listener );
synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.addListener( listener );
if( mBufferProcessor == null || !mBufferProcessor.isRunning() )
{
mBufferProcessor = new BufferProcessor();
if( mBufferProcessor == null || !mBufferProcessor.isRunning() )
{
mBufferProcessor = new BufferProcessor();
Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true );
thread.setName( "HackRF Sample Processor" );
Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true );
thread.setName( "HackRF Sample Processor" );
thread.start();
}
thread.start();
}
}
}
/**
@@ -906,12 +910,15 @@ public class HackRFTunerController extends TunerController
*/
public void removeListener( Listener<ComplexBuffer> listener )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() )
{
mBufferProcessor.stop();
}
synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() )
{
mBufferProcessor.stop();
}
}
}
/**
@@ -921,45 +928,77 @@ public class HackRFTunerController extends TunerController
public class BufferProcessor implements Runnable, TransferCallback
{
private ScheduledFuture<?> mSampleDispatcherTask;
private CopyOnWriteArrayList<Transfer> mTransfers;
private LinkedTransferQueue<Transfer> mAvailableTransfers;
private LinkedTransferQueue<Transfer> mTransfersInProgress = new LinkedTransferQueue<>();
private AtomicBoolean mRunning = new AtomicBoolean();
private ByteBuffer mLibUsbHandlerStatus;
private boolean mCancel = false;
@Override
public void run()
{
if( mRunning.compareAndSet( false, true ) )
{
mLog.info( "starting HackRF sample fetch thread" );
prepareTransfers();
for( Transfer transfer: mTransfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
break;
}
}
mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate(
ThreadType.SOURCE_SAMPLE_PROCESSING, new BufferDispatcher(),
20, TimeUnit.MILLISECONDS );
mLibUsbHandlerStatus = ByteBuffer.allocateDirect( 4 );
List<Transfer> transfers = new ArrayList<>();
mCancel = false;
while( mRunning.get() )
{
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
mAvailableTransfers.drainTo( transfers );
for( Transfer transfer: transfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result == LibUsb.SUCCESS )
{
mTransfersInProgress.add( transfer );
}
else
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
}
}
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() );
null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling events for libusb" );
}
transfers.clear();
mLibUsbHandlerStatus.rewind();
}
if( mCancel )
{
for( Transfer transfer: mTransfersInProgress )
{
LibUsb.cancelTransfer( transfer );
}
int result = LibUsb.handleEventsTimeoutCompleted(
null, USB_TIMEOUT_MS, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error cancelling transfers in progress" );
}
mLibUsbHandlerStatus.rewind();
}
}
}
@@ -969,12 +1008,10 @@ public class HackRFTunerController extends TunerController
*/
public void stop()
{
mCancel = true;
if( mRunning.compareAndSet( true, false ) )
{
mLog.info( "stopping HackRF sample fetch thread" );
cancel();
if( mSampleDispatcherTask != null )
{
mSampleDispatcherTask.cancel( true );
@@ -994,41 +1031,29 @@ public class HackRFTunerController extends TunerController
@Override
public void processTransfer( Transfer transfer )
{
if( transfer.status() == LibUsb.TRANSFER_COMPLETED )
{
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
mFilledBuffers.add( data );
if( !isRunning() )
{
LibUsb.cancelTransfer( transfer );
}
}
mTransfersInProgress.remove( transfer );
switch( transfer.status() )
{
case LibUsb.TRANSFER_COMPLETED:
/* resubmit the transfer */
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
case LibUsb.TRANSFER_STALL:
if( transfer.actualLength() > 0 )
{
mLog.error( "couldn't resubmit buffer transfer to tuner" );
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
if( isRunning() )
{
mFilledBuffers.add( data );
}
}
break;
case LibUsb.TRANSFER_CANCELLED:
/* free the transfer and remove it */
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
break;
default:
/* unexpected error */
@@ -1036,26 +1061,9 @@ public class HackRFTunerController extends TunerController
getTransferStatus( transfer.status() ) +
"] transferred actual: " + transfer.actualLength() );
}
}
private void cancel()
{
for( Transfer transfer: mTransfers )
{
LibUsb.cancelTransfer( transfer );
}
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling usb events during cancel [" +
LibUsb.errorName( result ) + "]" );
}
}
mAvailableTransfers.add( transfer );
}
/**
* Prepares (allocates) a set of transfer buffers for use in
@@ -1063,30 +1071,28 @@ public class HackRFTunerController extends TunerController
*/
private void prepareTransfers() throws LibUsbException
{
mTransfers = new CopyOnWriteArrayList<Transfer>();
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
if( mAvailableTransfers == null )
{
Transfer transfer = LibUsb.allocTransfer();
mAvailableTransfers = new LinkedTransferQueue<>();
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{
Transfer transfer = LibUsb.allocTransfer();
if( transfer == null )
{
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer =
ByteBuffer.allocateDirect( mBufferSize );
LibUsb.fillBulkTransfer( transfer,
mDeviceHandle,
USB_ENDPOINT,
buffer,
BufferProcessor.this,
"Buffer #" + x,
TIMEOUT_US );
mTransfers.add( transfer );
if( transfer == null )
{
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer =
ByteBuffer.allocateDirect( mBufferSize );
LibUsb.fillBulkTransfer( transfer, mDeviceHandle,USB_ENDPOINT,
buffer, BufferProcessor.this, "Buffer", USB_TIMEOUT_MS );
mAvailableTransfers.add( transfer );
}
}
}
}

View File

@@ -37,7 +37,6 @@ import source.tuner.TunerType;
import source.tuner.frequency.FrequencyChangeListener;
import source.tuner.rtl.RTL2832TunerController.SampleRate;
import controller.ResourceManager;
import controller.ThreadPoolManager;
public class RTL2832Tuner extends Tuner
{
@@ -148,16 +147,14 @@ public class RTL2832Tuner extends Tuner
}
@Override
public TunerChannelSource getChannel( ThreadPoolManager threadPoolManager,
TunerChannel channel )
throws RejectedExecutionException,
SourceException
public TunerChannelSource getChannel( TunerChannel channel )
throws RejectedExecutionException, SourceException
{
TunerChannelSource source = null;
try
{
source = mController.getChannel( threadPoolManager, this, channel );
source = mController.getChannel( this, channel );
}
catch( Exception e )
{

View File

@@ -27,7 +27,7 @@ import java.nio.charset.Charset;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -55,8 +55,6 @@ import sample.complex.ComplexBuffer;
import source.SourceException;
import source.tuner.TunerController;
import source.tuner.TunerType;
import source.tuner.frequency.FrequencyChangeEvent;
import source.tuner.frequency.FrequencyChangeEvent.Event;
import buffer.FloatAveragingBuffer;
import controller.ResourceManager;
import controller.ThreadPoolManager;
@@ -73,7 +71,7 @@ public abstract class RTL2832TunerController extends TunerController
public final static int TWO_TO_22_POWER = 4194304;
public final static byte USB_INTERFACE = (byte)0x0;
public final static byte BULK_ENDPOINT_IN = (byte)0x81;
public final static byte USB_BULK_ENDPOINT = (byte)0x81;
public final static byte CONTROL_ENDPOINT_IN =
(byte)( LibUsb.ENDPOINT_IN | LibUsb.REQUEST_TYPE_VENDOR );
public final static byte CONTROL_ENDPOINT_OUT =
@@ -1342,21 +1340,24 @@ public abstract class RTL2832TunerController extends TunerController
*/
public void addListener( Listener<ComplexBuffer> listener )
{
mComplexBufferBroadcaster.addListener( listener );
synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.addListener( listener );
if( mBufferProcessor == null )
{
mBufferProcessor = new BufferProcessor( mThreadPoolManager );
}
if( !mBufferProcessor.isRunning() )
{
Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true );
thread.setName( "RTL2832 Sample Processor" );
if( mBufferProcessor == null )
{
mBufferProcessor = new BufferProcessor( mThreadPoolManager );
}
if( !mBufferProcessor.isRunning() )
{
Thread thread = new Thread( mBufferProcessor );
thread.setDaemon( true );
thread.setName( "RTL2832 Sample Processor" );
thread.start();
}
thread.start();
}
}
}
/**
@@ -1365,12 +1366,15 @@ public abstract class RTL2832TunerController extends TunerController
*/
public void removeListener( Listener<ComplexBuffer> listener )
{
mComplexBufferBroadcaster.removeListener( listener );
synchronized( mComplexBufferBroadcaster )
{
mComplexBufferBroadcaster.removeListener( listener );
if( !mComplexBufferBroadcaster.hasListeners() )
{
mBufferProcessor.stop();
}
if( !mComplexBufferBroadcaster.hasListeners() )
{
mBufferProcessor.stop();
}
}
}
/**
@@ -1380,9 +1384,12 @@ public abstract class RTL2832TunerController extends TunerController
public class BufferProcessor implements Runnable, TransferCallback
{
private ScheduledFuture<?> mSampleDispatcherTask;
private CopyOnWriteArrayList<Transfer> mTransfers;
private LinkedTransferQueue<Transfer> mAvailableTransfers;
private LinkedTransferQueue<Transfer> mTransfersInProgress = new LinkedTransferQueue<>();
private AtomicBoolean mRunning = new AtomicBoolean();
private ThreadPoolManager mThreadPoolManager;
private ByteBuffer mLibUsbHandlerStatus;
private boolean mCancel = false;
public BufferProcessor( ThreadPoolManager manager )
{
@@ -1409,18 +1416,6 @@ public abstract class RTL2832TunerController extends TunerController
prepareTransfers();
for( Transfer transfer: mTransfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
break;
}
}
try
{
mSampleDispatcherTask = mThreadPoolManager.scheduleFixedRate(
@@ -1431,20 +1426,66 @@ public abstract class RTL2832TunerController extends TunerController
{
mLog.error( "NPE! = tpm null: " + ( mThreadPoolManager == null ), npe );
}
while( mRunning.get() )
mLibUsbHandlerStatus = ByteBuffer.allocateDirect( 4 );
List<Transfer> transfers = new ArrayList<>();
mCancel = false;
while( mRunning.get() )
{
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
mAvailableTransfers.drainTo( transfers );
for( Transfer transfer: transfers )
{
int result = LibUsb.submitTransfer( transfer );
if( result == LibUsb.SUCCESS )
{
mTransfersInProgress.add( transfer );
}
else
{
mLog.error( "Error submitting transfer [" +
LibUsb.errorName( result ) + "]" );
}
}
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() );
null, TIMEOUT_US, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling events for libusb" );
}
transfers.clear();
mLibUsbHandlerStatus.rewind();
}
if( mCancel )
{
for( Transfer transfer: mTransfersInProgress )
{
LibUsb.cancelTransfer( transfer );
}
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, mLibUsbHandlerStatus.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling events for libusb" );
}
mLibUsbHandlerStatus.rewind();
}
}
else
{
mLog.error( "Couldn't start buffer processor - already running!" );
}
}
@@ -1453,10 +1494,10 @@ public abstract class RTL2832TunerController extends TunerController
*/
public void stop()
{
mCancel = true;
if( mRunning.compareAndSet( true, false ) )
{
cancel();
if( mSampleDispatcherTask != null )
{
mThreadPoolManager.cancel( mSampleDispatcherTask );
@@ -1477,43 +1518,29 @@ public abstract class RTL2832TunerController extends TunerController
@Override
public void processTransfer( Transfer transfer )
{
if( transfer.status() == LibUsb.TRANSFER_COMPLETED )
{
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
mFilledBuffers.add( data );
mSampleCounter.addAndGet( transfer.actualLength() );
if( !isRunning() )
{
LibUsb.cancelTransfer( transfer );
}
}
mTransfersInProgress.remove( transfer );
switch( transfer.status() )
{
case LibUsb.TRANSFER_COMPLETED:
/* resubmit the transfer */
int result = LibUsb.submitTransfer( transfer );
if( result != LibUsb.SUCCESS )
case LibUsb.TRANSFER_STALL:
if( transfer.actualLength() > 0 )
{
mLog.error( "couldn't resubmit buffer transfer to tuner" );
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
ByteBuffer buffer = transfer.buffer();
byte[] data = new byte[ transfer.actualLength() ];
buffer.get( data );
buffer.rewind();
if( isRunning() )
{
mFilledBuffers.add( data );
}
}
break;
case LibUsb.TRANSFER_CANCELLED:
/* free the transfer and remove it */
LibUsb.freeTransfer( transfer );
mTransfers.remove( transfer );
break;
default:
/* unexpected error */
@@ -1521,26 +1548,9 @@ public abstract class RTL2832TunerController extends TunerController
getTransferStatus( transfer.status() ) +
"] transferred actual: " + transfer.actualLength() );
}
}
private void cancel()
{
for( Transfer transfer: mTransfers )
{
LibUsb.cancelTransfer( transfer );
}
ByteBuffer completed = ByteBuffer.allocateDirect( 4 );
int result = LibUsb.handleEventsTimeoutCompleted(
null, TIMEOUT_US, completed.asIntBuffer() );
if( result != LibUsb.SUCCESS )
{
mLog.error( "error handling usb events during cancel [" +
LibUsb.errorName( result ) + "]" );
}
}
mAvailableTransfers.add( transfer );
}
/**
* Prepares (allocates) a set of transfer buffers for use in
@@ -1548,30 +1558,28 @@ public abstract class RTL2832TunerController extends TunerController
*/
private void prepareTransfers() throws LibUsbException
{
mTransfers = new CopyOnWriteArrayList<Transfer>();
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
if( mAvailableTransfers == null )
{
Transfer transfer = LibUsb.allocTransfer();
mAvailableTransfers = new LinkedTransferQueue<>();
for( int x = 0; x < TRANSFER_BUFFER_POOL_SIZE; x++ )
{
Transfer transfer = LibUsb.allocTransfer();
if( transfer == null )
{
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer =
ByteBuffer.allocateDirect( mBufferSize );
LibUsb.fillBulkTransfer( transfer,
mDeviceHandle,
BULK_ENDPOINT_IN,
buffer,
BufferProcessor.this,
"Buffer #" + x,
TIMEOUT_US );
mTransfers.add( transfer );
if( transfer == null )
{
throw new LibUsbException( "couldn't allocate transfer",
LibUsb.ERROR_NO_MEM );
}
final ByteBuffer buffer =
ByteBuffer.allocateDirect( mBufferSize );
LibUsb.fillBulkTransfer( transfer, mDeviceHandle, USB_BULK_ENDPOINT,
buffer, BufferProcessor.this, "Buffer", TIMEOUT_US );
mAvailableTransfers.add( transfer );
}
}
}
}

View File

@@ -34,7 +34,7 @@ public class R820TTunerConfiguration extends TunerConfiguration
private R820TLNAGain mLNAGain = R820TLNAGain.GAIN_222;
private R820TVGAGain mVGAGain = R820TVGAGain.GAIN_210;
private double mFrequencyCorrection = 0.0d;
private SampleRate mSampleRate = SampleRate.RATE_1_920MHZ;
private SampleRate mSampleRate = SampleRate.RATE_2_400MHZ;
/**
* Default constructor for JAXB

View File

@@ -20,6 +20,7 @@ package spectrum;
import java.util.Arrays;
import java.util.Iterator;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@@ -30,6 +31,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;
@@ -55,12 +57,18 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
private CopyOnWriteArrayList<DFTResultsConverter> mListeners =
new CopyOnWriteArrayList<DFTResultsConverter>();
private ArrayBlockingQueue<ComplexBuffer> mQueue =
new ArrayBlockingQueue<ComplexBuffer>( 200 );
//Fixed size sample queue. We set the size at 12 based on the highest
//sample rate tuner (10 MHz) producing 153 frames per second and the lowest
//DFT processing frame rate of 12 frames per second. This means that the
//maximum size needed is 153 / 12 = 11 frames per DFT processing iteration
//so we set it at 12.
private BlockingQueue<ComplexBuffer> mQueue = new ArrayBlockingQueue<>( 12 );
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 +97,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 +188,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();
@@ -230,15 +251,7 @@ public class DFTProcessor implements Listener<ComplexBuffer>,
@Override
public void receive( ComplexBuffer sampleBuffer )
{
if( !mQueue.offer( sampleBuffer ) )
{
mLog.error( "DFTProcessor - [" + mSampleType.toString()
+ "] queue is full, purging queue, "
+ "samples[" + sampleBuffer + "]" );
mQueue.clear();
mQueue.offer( sampleBuffer );
}
mQueue.offer( sampleBuffer );
}
private void getNextBuffer()

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