Files
sdrtrunk/src/module/decode/state/ChannelState.java

694 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.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 ThreadPoolManager mThreadPoolManager;
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( 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()
{
mCallEventListener = null;
mChangedAttributeListener = null;
mDecoderStateListener = null;
mSquelchStateListener = null;
mThreadPoolManager = 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 );
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
*/
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 );
}
}
}
}