mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
and constraints on scheduled thread pool saturation. Adds a new scheduled thread pool for each processing chain that the components in the chain can share, versus the single shared pool exposed by the threadpoolmanager class.
689 lines
17 KiB
Java
689 lines
17 KiB
Java
/*******************************************************************************
|
|
* 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.state;
|
|
|
|
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;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import audio.metadata.IMetadataProvider;
|
|
import audio.metadata.Metadata;
|
|
import audio.metadata.MetadataReset;
|
|
import audio.metadata.MetadataType;
|
|
import audio.squelch.ISquelchStateProvider;
|
|
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;
|
|
|
|
public class ChannelState extends Module implements ICallEventProvider,
|
|
IChangedAttributeProvider, IDecoderStateEventListener,
|
|
IDecoderStateEventProvider, IMetadataProvider, ISquelchStateProvider
|
|
{
|
|
private final static Logger mLog =
|
|
LoggerFactory.getLogger( ChannelState.class );
|
|
|
|
public static final long FADE_TIMEOUT_DELAY = 1200;
|
|
public static final long RESET_TIMEOUT_DELAY = 2000;
|
|
|
|
private State mState = State.IDLE;
|
|
|
|
private Listener<CallEvent> mCallEventListener;
|
|
private Listener<ChangedAttribute> mChangedAttributeListener;
|
|
private Listener<DecoderStateEvent> mDecoderStateListener;
|
|
private Listener<Metadata> mMetadataListener;
|
|
private Listener<SquelchState> mSquelchStateListener;
|
|
private DecoderStateEventReceiver mDecoderStateEventReceiver =
|
|
new DecoderStateEventReceiver();
|
|
|
|
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 mEndTimeout;
|
|
|
|
private StateMonitor mStateMonitor = new StateMonitor();
|
|
private AtomicBoolean mMonitoring = new AtomicBoolean();
|
|
private ScheduledFuture<?> mStateMonitorFuture;
|
|
|
|
private ChannelType mChannelType;
|
|
|
|
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( ChannelType channelType )
|
|
{
|
|
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( ScheduledExecutorService executor )
|
|
{
|
|
//Start a monitoring task to check for FADE and END event timeouts
|
|
if( mMonitoring.compareAndSet( false, true ) )
|
|
{
|
|
if( mStateMonitorFuture == null && mStateMonitor != null )
|
|
{
|
|
try
|
|
{
|
|
mStateMonitorFuture = executor.scheduleAtFixedRate(
|
|
mStateMonitor, 0, 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 = mStateMonitorFuture.cancel( true );
|
|
|
|
if( !success )
|
|
{
|
|
mLog.error("Couldn't stop monitoring scheduled future" );
|
|
}
|
|
}
|
|
|
|
mStateMonitorFuture = null;
|
|
}
|
|
}
|
|
|
|
public void dispose()
|
|
{
|
|
mCallEventListener = null;
|
|
mChangedAttributeListener = null;
|
|
mDecoderStateListener = null;
|
|
mSquelchStateListener = null;
|
|
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 )
|
|
{
|
|
mTrafficChannelFadeTimeout = milliseconds;
|
|
|
|
if( mChannelType == ChannelType.TRAFFIC )
|
|
{
|
|
mFadeTimeout = mTrafficChannelFadeTimeout;
|
|
}
|
|
}
|
|
|
|
public void setSelected( boolean selected )
|
|
{
|
|
mSelected = selected;
|
|
}
|
|
|
|
public boolean isSelected()
|
|
{
|
|
return mSelected;
|
|
}
|
|
|
|
public State getState()
|
|
{
|
|
return mState;
|
|
}
|
|
|
|
/**
|
|
* Updates the fade timeout threshold to the current time plus delay
|
|
*/
|
|
private void updateFadeTimeout()
|
|
{
|
|
if( isTrafficChannel() )
|
|
{
|
|
mFadeTimeout = System.currentTimeMillis() + mTrafficChannelFadeTimeout;
|
|
}
|
|
else
|
|
{
|
|
mFadeTimeout = System.currentTimeMillis() + mStandardChannelFadeTimeout;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the reset timeout threshold to the current time plus delay
|
|
*/
|
|
private void updateResetTimeout()
|
|
{
|
|
if( isTrafficChannel() )
|
|
{
|
|
mEndTimeout = System.currentTimeMillis();
|
|
}
|
|
else
|
|
{
|
|
mEndTimeout = System.currentTimeMillis() + RESET_TIMEOUT_DELAY;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Broadcasts the squelch state to the registered listener
|
|
*/
|
|
protected void broadcast( SquelchState state )
|
|
{
|
|
if( mSquelchStateListener != null && !mSquelchLocked )
|
|
{
|
|
mSquelchStateListener.receive( state );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the squelch state listener
|
|
*/
|
|
@Override
|
|
public void setSquelchStateListener( Listener<SquelchState> listener )
|
|
{
|
|
mSquelchStateListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Removes the squelch state listener
|
|
*/
|
|
@Override
|
|
public void removeSquelchStateListener()
|
|
{
|
|
mSquelchStateListener = null;
|
|
}
|
|
|
|
/**
|
|
* Sets the channel state to the specified state, or updates the timeout
|
|
* values so that the state monitor will not change state. Broadcasts a
|
|
* squelch event when the state changes and the audio squelch state should
|
|
* change. Also broadcasts changed attribute and decoder state events so
|
|
* that external processes can maintain sync with this channel state.
|
|
*/
|
|
protected void setState( State state )
|
|
{
|
|
synchronized( mState )
|
|
{
|
|
if( state == mState )
|
|
{
|
|
if( State.CALL_STATES.contains( state ) )
|
|
{
|
|
updateFadeTimeout();
|
|
}
|
|
}
|
|
else if( state != mState && mState.canChangeTo( state ) )
|
|
{
|
|
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
|
|
{
|
|
// mLog.debug( "Can't change from [" + mState + "] to [" + state + "]" );
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the state and processes related actions
|
|
*/
|
|
private void processFadeState()
|
|
{
|
|
updateResetTimeout();
|
|
mState = State.FADE;
|
|
|
|
broadcast( SquelchState.SQUELCH );
|
|
broadcast( ChangedAttribute.CHANNEL_STATE );
|
|
}
|
|
|
|
private void processIdleState()
|
|
{
|
|
broadcast( SquelchState.SQUELCH );
|
|
|
|
if( mState == State.FADE )
|
|
{
|
|
broadcast( new DecoderStateEvent( this, Event.RESET, State.IDLE ) );
|
|
broadcast( new MetadataReset() );
|
|
}
|
|
|
|
mState = State.IDLE;
|
|
|
|
broadcast( ChangedAttribute.CHANNEL_STATE );
|
|
}
|
|
|
|
private void processTeardownState()
|
|
{
|
|
broadcast( SquelchState.SQUELCH );
|
|
|
|
mState = State.TEARDOWN;
|
|
|
|
broadcast( ChangedAttribute.CHANNEL_STATE );
|
|
|
|
if( mTrafficChannelCallEvent != null )
|
|
{
|
|
mTrafficChannelCallEvent.end();
|
|
|
|
broadcast( mTrafficChannelCallEvent );
|
|
mTrafficChannelCallEvent = null;
|
|
}
|
|
|
|
if( mTrafficChannelStatusListener != null )
|
|
{
|
|
mTrafficChannelStatusListener.callEnd();
|
|
mTrafficChannelStatusListener = null;
|
|
}
|
|
else
|
|
{
|
|
mLog.error( "Traffic channel status listener was null - couldn't "
|
|
+ "teardown the channel");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Broadcasts the call event to the registered listener
|
|
*/
|
|
protected void broadcast( CallEvent event )
|
|
{
|
|
if( mCallEventListener != null )
|
|
{
|
|
mCallEventListener.receive( event );
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void addCallEventListener( Listener<CallEvent> listener )
|
|
{
|
|
mCallEventListener = listener;
|
|
}
|
|
|
|
@Override
|
|
public void removeCallEventListener( Listener<CallEvent> listener )
|
|
{
|
|
mCallEventListener = null;
|
|
}
|
|
|
|
/**
|
|
* Broadcasts the channel state attribute change event to all registered
|
|
* listeners
|
|
*/
|
|
protected void broadcast( ChangedAttribute attribute )
|
|
{
|
|
if( mChangedAttributeListener != null )
|
|
{
|
|
mChangedAttributeListener.receive( attribute );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the listener to receive channel state attribute change events
|
|
*/
|
|
@Override
|
|
public void setChangedAttributeListener( Listener<ChangedAttribute> listener )
|
|
{
|
|
mChangedAttributeListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Removes the listener to receive channel state attribute change events
|
|
*/
|
|
@Override
|
|
public void removeChangedAttributeListener()
|
|
{
|
|
mChangedAttributeListener = null;
|
|
}
|
|
|
|
/**
|
|
* Broadcasts a channel state event to any registered listeners
|
|
*/
|
|
protected void broadcast( DecoderStateEvent event )
|
|
{
|
|
if( mDecoderStateListener != null )
|
|
{
|
|
mDecoderStateListener.receive( event );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a decoder state event listener
|
|
*/
|
|
@Override
|
|
public void setDecoderStateListener( Listener<DecoderStateEvent> listener )
|
|
{
|
|
mDecoderStateListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Removes the decoder state event listener
|
|
*/
|
|
@Override
|
|
public void removeDecoderStateListener()
|
|
{
|
|
mDecoderStateListener = null;
|
|
}
|
|
|
|
@Override
|
|
public Listener<DecoderStateEvent> getDecoderStateListener()
|
|
{
|
|
return mDecoderStateEventReceiver;
|
|
}
|
|
|
|
/**
|
|
* Broadcasts metadata to the registered listener
|
|
*/
|
|
protected void broadcast( Metadata metadata )
|
|
{
|
|
if( mMetadataListener != null )
|
|
{
|
|
mMetadataListener.receive( metadata );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds a metadata listener
|
|
*/
|
|
@Override
|
|
public void setMetadataListener( Listener<Metadata> listener )
|
|
{
|
|
mMetadataListener = listener;
|
|
}
|
|
|
|
/**
|
|
* Removes the decoder state event listener
|
|
*/
|
|
@Override
|
|
public void removeMetadataListener()
|
|
{
|
|
mMetadataListener = null;
|
|
}
|
|
|
|
/**
|
|
* Registers a listener to be notified when a traffic channel call event is
|
|
* completed, so that the listener can perform call tear-down
|
|
*/
|
|
public void configureAsTrafficChannel( TrafficChannelStatusListener listener,
|
|
TrafficChannelAllocationEvent allocationEvent )
|
|
{
|
|
mTrafficChannelStatusListener = listener;
|
|
mTrafficChannelCallEvent = allocationEvent.getCallEvent();
|
|
|
|
/* Broadcast the call event details as metadata for the audio manager */
|
|
String channel = mTrafficChannelCallEvent.getChannel();
|
|
|
|
if( channel != null )
|
|
{
|
|
broadcast( new Metadata( MetadataType.CHANNEL_NUMBER, channel, true ) );
|
|
}
|
|
|
|
broadcast( new Metadata( MetadataType.PROTOCOL,
|
|
mTrafficChannelCallEvent.getDecoderType().getDisplayString(), true ) );
|
|
|
|
String details = mTrafficChannelCallEvent.getDetails();
|
|
|
|
if( details != null )
|
|
{
|
|
broadcast( new Metadata( MetadataType.DETAILS, details, true ) );
|
|
}
|
|
|
|
String from = mTrafficChannelCallEvent.getFromID();
|
|
|
|
if( from != null )
|
|
{
|
|
broadcast( new Metadata( mTrafficChannelCallEvent.getFromIDType(),
|
|
from, mTrafficChannelCallEvent.getFromIDAlias(), true ) );
|
|
}
|
|
|
|
String to = mTrafficChannelCallEvent.getToID();
|
|
|
|
if( to != null )
|
|
{
|
|
broadcast( new Metadata( mTrafficChannelCallEvent.getToIDType(), to,
|
|
mTrafficChannelCallEvent.getToIDAlias(), true ) );
|
|
}
|
|
|
|
long frequency = mTrafficChannelCallEvent.getFrequency();
|
|
|
|
if( frequency > 0 )
|
|
{
|
|
broadcast( new Metadata( MetadataType.FREQUENCY,
|
|
String.valueOf( frequency ), true ) );
|
|
}
|
|
|
|
/* Rebroadcast the allocation event so that the internal decoder states
|
|
* can self-configure with the call event details */
|
|
broadcast( allocationEvent );
|
|
}
|
|
|
|
/**
|
|
* DecoderStateEvent receiver wrapper
|
|
*/
|
|
public class DecoderStateEventReceiver implements Listener<DecoderStateEvent>
|
|
{
|
|
@Override
|
|
public void receive( DecoderStateEvent event )
|
|
{
|
|
if( event.getSource() != this )
|
|
{
|
|
switch( event.getEvent() )
|
|
{
|
|
case ALWAYS_UNSQUELCH:
|
|
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:
|
|
case DECODE:
|
|
case START:
|
|
if( State.CALL_STATES.contains( event.getState() ) )
|
|
{
|
|
setState( event.getState() );
|
|
}
|
|
break;
|
|
case END:
|
|
if( isTrafficChannel() )
|
|
{
|
|
setState( State.TEARDOWN );
|
|
}
|
|
else
|
|
{
|
|
setState( State.FADE );
|
|
}
|
|
break;
|
|
case RESET:
|
|
/* Channel State does not respond to reset events */
|
|
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 );
|
|
}
|
|
}
|
|
}
|
|
} |