/*******************************************************************************
* 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
******************************************************************************/
package controller.channel;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.bind.annotation.XmlTransient;
import message.Message;
import module.Module;
import module.ProcessingChain;
import module.decode.DecoderFactory;
import module.decode.config.AuxDecodeConfiguration;
import module.decode.config.DecodeConfigFactory;
import module.decode.config.DecodeConfiguration;
import module.decode.event.CallEventModel;
import module.decode.event.MessageActivityModel;
import module.decode.state.ChannelState;
import module.log.MessageEventLogger;
import module.log.config.EventLogConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import record.RecorderManager;
import record.RecorderType;
import record.config.RecordConfiguration;
import sample.Listener;
import source.Source;
import source.SourceException;
import source.SourceType;
import source.config.SourceConfigFactory;
import source.config.SourceConfigRecording;
import source.config.SourceConfigTuner;
import source.config.SourceConfiguration;
import source.tuner.TunerChannel;
import source.tuner.TunerChannel.Type;
import alias.AliasList;
import audio.metadata.Metadata;
import audio.metadata.MetadataType;
import controller.ResourceManager;
import controller.channel.ChannelEvent.Event;
import controller.config.Configuration;
import controller.site.Site;
import controller.system.System;
import filter.FilterSet;
@XmlSeeAlso( { Configuration.class } )
@XmlRootElement( name = "channel" )
public class Channel extends Configuration
{
private final static Logger mLog = LoggerFactory.getLogger( Channel.class );
private static final boolean ENABLED = true;
private static final boolean DISABLED = false;
private static final boolean BROADCAST_CHANGE = true;
public enum ChannelType
{
STANDARD,
TRAFFIC
};
private CopyOnWriteArrayList mChannelListeners =
new CopyOnWriteArrayList();
private CopyOnWriteArrayList> mMessageListeners =
new CopyOnWriteArrayList>();
private Map mTrafficChannels = new HashMap<>();
private DecodeConfiguration mDecodeConfiguration =
DecodeConfigFactory.getDefaultDecodeConfiguration();
private AuxDecodeConfiguration mAuxDecodeConfiguration =
new AuxDecodeConfiguration();
private SourceConfiguration mSourceConfiguration =
SourceConfigFactory.getDefaultSourceConfiguration();
private EventLogConfiguration mEventLogConfiguration =
new EventLogConfiguration();
private RecordConfiguration mRecordConfiguration = new RecordConfiguration();
private ResourceManager mResourceManager;
private String mAliasListName;
private System mSystem;
private Site mSite;
private String mName;
private boolean mEnabled;
private boolean mSelected;
private ChannelType mChannelType;
private CallEventModel mCallEventModel;
private MessageActivityModel mMessageActivityModel;
private MessageEventLogger mEventLogger;
private ProcessingChain mProcessingChain;
private ChannelState mChannelState;
private long mTrafficChannelTimeout =
DecodeConfiguration.DEFAULT_CALL_TIMEOUT_SECONDS;
/**
* Constructs a new standard channel with a default name of "New Channel"
*/
public Channel()
{
this( "New Channel", ChannelType.STANDARD );
}
/**
* Constructs a new standard channel with the specified channel name
*/
public Channel( String channelName )
{
this( channelName, ChannelType.STANDARD );
}
/**
* Constructs a new channel with the specified channel name and type.
*
* @param channelName
* @param channelType
*/
public Channel( String channelName, ChannelType channelType )
{
mName = channelName;
mChannelType = channelType;
}
/**
* Indicates if the channel has Automatic or Direct Frequency Control
*/
public boolean hasFrequencyControl()
{
return mProcessingChain != null &&
mProcessingChain.hasFrequencyCorrectionControl();
}
public long getFrequencyCorrection()
{
if( hasFrequencyControl() )
{
return mProcessingChain.getFrequencyCorrectionControl().getErrorCorrection();
}
return 0;
}
/**
* Sets the channel state traffic channel call end timer to the specified
* value.
*/
public void setTrafficChannelTimeout( long milliseconds )
{
mTrafficChannelTimeout = milliseconds;
if( mChannelState != null )
{
mChannelState.setTrafficChannelTimeout( milliseconds );
}
}
/**
* Sets the resource manager responsible for this channel and registers
* the channel manager as a listener to this channel. Fires channel add
* channel event to the newly added channel manager channel listener.
*
* Note: resource manager instance is applied after object construction
* since we construct the channel objects from the xml playlist
*/
public void setResourceManager( ResourceManager resourceManager )
{
mResourceManager = resourceManager;
/* Add system-wide channel listeners onto this channel */
addListeners( mResourceManager.getChannelManager().getChannelListeners() );
/* If we're enabled, fire enable changed event to get processing started */
if( mEnabled )
{
enableChanged();
}
}
@XmlTransient
public ResourceManager getResourceManager()
{
return mResourceManager;
}
/**
* Returns the processing chain for this channel. If the processing chain
* is not currently constructed and running, this method will return null.
*/
@XmlTransient
public ProcessingChain getProcessingChain()
{
return mProcessingChain;
}
/**
* Indicates if this channel has a processing chain constructed and it
* is processing.
*/
public boolean isProcessing()
{
return mProcessingChain != null && mProcessingChain.processing();
}
/**
* Indicates if this channel has been selected for audio output
*/
@XmlTransient
public boolean isSelected()
{
return mSelected;
}
public void setSelected( boolean selected )
{
mSelected = selected;
fireChannelEvent( Event.CHANGE_SELECTED );
}
@XmlTransient
public CallEventModel getCallEventModel()
{
return mCallEventModel;
}
/**
* Returns the MessageEventLogger for this processing chain
*/
@XmlTransient
public MessageEventLogger getEventLogger()
{
return mEventLogger;
}
/**
* Sets the MessageEventLogger for this processing chain
*/
public void setEventLogger( MessageEventLogger eventLogger )
{
mEventLogger = eventLogger;
}
@XmlTransient
public MessageActivityModel getMessageActivityModel()
{
return mMessageActivityModel;
}
@XmlTransient
public ChannelState getChannelState()
{
return mChannelState;
}
@XmlTransient
public ChannelType getChannelType()
{
return mChannelType;
}
/**
* Orderly shutdown method when this channel is going to be deleted.
*/
public void dispose()
{
setEnabled( DISABLED, BROADCAST_CHANGE );
/* Broadcast channel deleted event */
fireChannelEvent( Event.CHANNEL_DELETED );
mProcessingChain.dispose();
mProcessingChain = null;
}
/**
* Returns a full channel name containing the system, site and channel name.
*/
public String getChannelDisplayName()
{
StringBuilder sb = new StringBuilder();
if( mSystem != null )
{
sb.append( mSystem.getName() );
}
else
{
sb.append( "-" );
}
sb.append( "/" );
if( mSite != null )
{
sb.append( mSite.getName() );
}
else
{
sb.append( "-" );
}
sb.append( "/" );
sb.append( mName );
return sb.toString();
}
/**
* Returns the tuner channel requirement for this channel when the source
* configuration specifies a tuner and frequency as the source for this
* channel.
*
* If the source configuration is anything else, this method returns null.
*/
public TunerChannel getTunerChannel()
{
TunerChannel retVal = null;
if( mSourceConfiguration.getSourceType() == SourceType.TUNER )
{
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
retVal = new TunerChannel( Type.LOCKED, config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth() );
}
else if( mSourceConfiguration.getSourceType() == SourceType.RECORDING )
{
SourceConfigRecording config =
(SourceConfigRecording)mSourceConfiguration;
retVal = new TunerChannel( Type.LOCKED, config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth() );
}
return retVal;
}
/**
* Returns the owning system for this channel.
*/
public System getSystem()
{
return mSystem;
}
public boolean hasSystem()
{
return mSystem != null;
}
/**
* Sets the owning system for this channel and optionally broadcasts a
* system change channel event.
*/
public void setSystem( System system, boolean fireChangeEvent )
{
mSystem = system;
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_SYSTEM );
}
}
/**
* Returns the owning site for this channel.
*/
public Site getSite()
{
return mSite;
}
public boolean hasSite()
{
return mSite != null;
}
/**
* Sets the owning site for this channel and optionally broadcasts a site
* change channel event.
*/
public void setSite( Site site, boolean fireChangeEvent )
{
mSite = site;
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_SITE );
}
}
/**
* Default display string for this channel -- the channel name.
*/
public String toString()
{
return mName;
}
/**
* Indicates if this channel is enabled for processing.
*/
@XmlAttribute
public boolean getEnabled()
{
return mEnabled;
}
/**
* Enables/disables this channel for processing. Automatically starts or
* stops channel processing in response to this change.
*/
public void setEnabled( boolean enabled )
{
mEnabled = enabled;
enableChanged();
}
/**
* Enables/disables this channel for processing. Automatically starts or
* stops channel processing in response to this change. Optionally
* broadcasts a enabled change channel event.
*/
public void setEnabled( boolean enabled, boolean fireChannelEvent )
{
setEnabled( enabled );
if( fireChannelEvent )
{
if( enabled )
{
fireChannelEvent( Event.CHANNEL_ENABLED );
}
else
{
fireChannelEvent( Event.CHANNEL_DISABLED );
}
}
}
/**
* Returns the alias list that is used by this channel for looking up alias
* values for the various identifiers produced by the decoder.
*/
@XmlElement( name = "alias_list_name" )
public String getAliasListName()
{
return mAliasListName;
}
/**
* Sets the alias list to be used for looking up the alias values for the
* various identifiers produced by the decoder
*/
public void setAliasListName( String name )
{
mAliasListName = name;
}
/**
* Sets the alias list to be used for looking up the alias values for the
* various identifiers produced by the decoder. This method is the same
* as setAliasName(name) and optionally broadcasts an alias list change
* channel event.
*/
public void setAliasListName( String name, boolean fireChangeEvent )
{
mAliasListName = name;
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_ALIAS_LIST );
}
}
/**
* Gets the decoder configuration used by this channel
*/
@XmlElement( name = "decode_configuration" )
public DecodeConfiguration getDecodeConfiguration()
{
return mDecodeConfiguration;
}
/**
* Sets the decoder configuration used by this channel
*/
public void setDecodeConfiguration( DecodeConfiguration config )
{
mDecodeConfiguration = config;
}
/**
* Gets the aux decoder configuration used by this channel
*/
@XmlElement( name = "aux_decode_configuration" )
public AuxDecodeConfiguration getAuxDecodeConfiguration()
{
return mAuxDecodeConfiguration;
}
/**
* Sets the decoder configuration used by this channel
*/
public void setAuxDecodeConfiguration( AuxDecodeConfiguration config )
{
mAuxDecodeConfiguration = config;
}
/**
* Sets the decoder configuration used by this channel and optionally
* broadcasts a decoder change channel event.
*/
public void setDecodeConfiguration( DecodeConfiguration config,
boolean fireChangeEvent )
{
setDecodeConfiguration( config );
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_DECODER );
}
}
/**
* Returns the source configuration for this channel.
*/
@XmlElement( name = "source_configuration" )
public SourceConfiguration getSourceConfiguration()
{
return mSourceConfiguration;
}
/**
* Sets the source configuration for this channel.
*/
public void setSourceConfiguration( SourceConfiguration config )
{
mSourceConfiguration = config;
}
/**
* Sets the source configuration for this channel and optionally broadcasts
* a source change channel event.
*/
public void setSourceConfiguration( SourceConfiguration config,
boolean fireChangeEvent )
{
setSourceConfiguration( config );
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_SOURCE );
}
}
/**
* Returns the event logger configuration for this channel.
*/
@XmlElement( name = "event_log_configuration" )
public EventLogConfiguration getEventLogConfiguration()
{
return mEventLogConfiguration;
}
/**
* Sets the event logger configuration for this channel.
*/
public void setEventLogConfiguration( EventLogConfiguration config )
{
mEventLogConfiguration = config;
}
/**
* Sets the event logger configuration for this channel and optionally
* broadcasts an event logger change channel event.
*/
public void setEventLogConfiguration( EventLogConfiguration config,
boolean fireChangeEvent )
{
setEventLogConfiguration( config );
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_EVENT_LOGGER );
}
}
/**
* Returns the recorder configuration for this channel.
*/
@XmlElement( name = "record_configuration" )
public RecordConfiguration getRecordConfiguration()
{
return mRecordConfiguration;
}
/**
* Sets the recorder configuration for this channel.
*/
public void setRecordConfiguration( RecordConfiguration config )
{
mRecordConfiguration = config;
}
/**
* Sets the recorder configuration for this channel and optionally broadcasts
* a recorder configuration change channel event.
*/
public void setRecordConfiguration( RecordConfiguration config,
boolean fireChangeEvent )
{
setRecordConfiguration( config );
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_RECORDER );
}
}
/**
* Returns the name of this channel.
*/
@XmlAttribute
public String getName()
{
return mName;
}
/**
* Sets the name of this channel.
*/
public void setName( String name )
{
mName = name;
}
/**
* Sets the name of this channel and optionally broadcasts a name change
* channel event.
*/
public void setName( String name, boolean fireChangeEvent )
{
mName = name;
if( fireChangeEvent )
{
fireChannelEvent( Event.CHANGE_NAME );
}
}
/**
* Responds to changes in the enabled setting by starting the processing
* change if the channel is now enabled, or stopping the procesing chain
* if it was already started.
*/
private void enableChanged()
{
if( mEnabled )
{
start();
}
else
{
stop();
}
}
/**
* Starts the channel processing chain.
*/
private void start()
{
if( mEnabled && mResourceManager != null )
{
setupChannelState();
try
{
setup();
mProcessingChain.start();
fireChannelEvent( Event.CHANNEL_PROCESSING_STARTED );
}
catch( SourceException se )
{
//Do nothing, we couldn't get a source for the channel
}
}
}
private void setupChannelState()
{
if( mChannelState == null )
{
mChannelState = new ChannelState( mResourceManager
.getThreadPoolManager(), mChannelType );
mChannelState.setTrafficChannelTimeout( mTrafficChannelTimeout );
}
}
/**
* Sets up the channel and prepares to start processing. Sets up the
* processing chain and requests a source.
*
* @throws SourceException - if no source is available or if there is an
* error obtaining a source
*/
public void setup() throws SourceException
{
setupProcessingChain();
setupSource();
}
/**
* Setup recording options. Adds RecorderManager as a audio packet listener
* to receive and record decoded audio.
*/
private void setupRecording()
{
List recorders = mRecordConfiguration.getRecorders();
if( !recorders.isEmpty() )
{
if( recorders.contains( RecorderType.AUDIO ) )
{
mProcessingChain.addAudioPacketListener(
mResourceManager.getRecorderManager() );
}
/* Add baseband recorder */
if( ( recorders.contains( RecorderType.BASEBAND ) &&
mChannelType == ChannelType.STANDARD ) )
{
mProcessingChain.addModule( RecorderManager.getBasebandRecorder(
mResourceManager.getThreadPoolManager(), getChannelName() ) );
}
/* Add traffic channel baseband recorder */
if( recorders.contains( RecorderType.TRAFFIC_BASEBAND ) &&
mChannelType == ChannelType.TRAFFIC )
{
mProcessingChain.addModule( RecorderManager.getBasebandRecorder(
mResourceManager.getThreadPoolManager(), getChannelName() ) );
}
}
}
private String getChannelName()
{
StringBuilder sb = new StringBuilder();
sb.append( mSystem );
sb.append( "_" );
sb.append( mSite );
sb.append( "_" );
sb.append( mName );
return sb.toString();
}
private void setupLogging()
{
if( mProcessingChain != null )
{
String channelName = getChannelName();
List loggers = mResourceManager.getEventLogManager()
.getLoggers( mEventLogConfiguration, channelName );
if( !loggers.isEmpty() )
{
mProcessingChain.addModules( loggers );
}
}
}
private void setupProcessingChain()
{
if( mProcessingChain == null )
{
mProcessingChain = new ProcessingChain( getName() );
/* Get the optional alias list for the decode modules to use */
AliasList aliasList = mResourceManager.getPlaylistManager()
.getPlayist().getAliasDirectory().getAliasList( mAliasListName );
/* Add the channel state as a module */
mProcessingChain.addModule( mChannelState );
/* Processing Modules */
List modules = DecoderFactory.getModules( mChannelType,
mResourceManager, mDecodeConfiguration, mRecordConfiguration,
mAuxDecodeConfiguration, aliasList, mSystem,
mSite, mName );
mProcessingChain.addModules( modules );
/* Get message filters for the set of processing modules */
FilterSet messageFilter = DecoderFactory.getMessageFilters( modules );
/* Setup the message activity model and add message listeners */
mMessageActivityModel = new MessageActivityModel( messageFilter );
mProcessingChain.addMessageListener( mMessageActivityModel );
/* Add audio manager as listener for audio packets */
mProcessingChain.addAudioPacketListener(
mResourceManager.getAudioManager() );
/* Add system-wide message listeners */
mProcessingChain.addMessageListeners(
mResourceManager.getChannelManager().getMessageListeners() );
/* Add individual message listeners */
mProcessingChain.addMessageListeners( mMessageListeners );
/* Create the call event model to receive call events */
mCallEventModel = new CallEventModel();
mProcessingChain.addCallEventListener( mCallEventModel );
setupLogging();
setupRecording();
/* Inject channel metadata that will be inserted into audio packets
* for the recorder manager and streaming */
mProcessingChain.broadcast(
new Metadata( MetadataType.SYSTEM, mSystem.getName() ) );
mProcessingChain.broadcast(
new Metadata( MetadataType.SITE_ID, mSite.getName() ) );
mProcessingChain.broadcast(
new Metadata( MetadataType.CHANNEL_NAME, mName ) );
}
}
/**
* Attempts to obtain a source and apply it to the processing chain.
*
* @throws SourceException - if the processing chain is not setup or if
* there is an error produced while obtaining the source
*/
private void setupSource() throws SourceException
{
if( mProcessingChain != null )
{
Source source = mResourceManager.getSourceManager()
.getSource( getSourceConfiguration(), getDecodeConfiguration()
.getDecoderType().getChannelBandwidth() );
if( source != null )
{
mProcessingChain.setSource( source );
}
else
{
throw new SourceException( "Couldn't obtain tuner channel source" );
}
}
else
{
throw new SourceException( "Cannot obtain source for a null "
+ "processing chain - setup the processing chain first" );
}
}
/**
* Stops the channel processing chain
*/
private void stop()
{
if( !mEnabled && mProcessingChain != null )
{
fireChannelEvent( Event.CHANNEL_PROCESSING_STOPPED );
mProcessingChain.stop();
if( mChannelType == ChannelType.STANDARD )
{
mProcessingChain.dispose();
mProcessingChain = null;
}
mSelected = false;
}
}
/**
* Broadcasts a channel change event to all registered listeners
*/
public void fireChannelEvent( Event event )
{
ChannelEvent channelEvent = new ChannelEvent( this, event );
for( ChannelEventListener listener: mChannelListeners )
{
listener.channelChanged( channelEvent );
}
/* Send event to processing chain so modules can respond to channel
* selection events */
if( mProcessingChain != null )
{
mProcessingChain.getChannelEventListener().receive( channelEvent );
}
}
/**
* Adds a channel listener to receive all channel events from this channel
* and automatically sends the listener a channel add event.
*/
public void addListener( ChannelEventListener listener )
{
if( !mChannelListeners.contains( listener ) )
{
mChannelListeners.add( listener );
listener.channelChanged(
new ChannelEvent( this, Event.CHANNEL_ADDED ) );
}
else
{
mLog.error( "Channel - attempt to add already existing channel "
+ "listener [" + listener.getClass() + "]" );
}
}
/**
* Adds a list of channel listeners to receive all channel events from this
* channel and automatically sends each one a channel add event.
*/
public void addListeners( List listeners )
{
for( ChannelEventListener listener: listeners )
{
addListener( listener );
}
}
/**
* Removes a channel listener from receiving channel events from this channel
*/
public void removeListener( ChannelEventListener listener )
{
mChannelListeners.remove( listener );
}
public void addListener( Listener listener )
{
mMessageListeners.add( listener );
if( isProcessing() )
{
mProcessingChain.addMessageListener( listener );
}
}
public void removeListener( Listener listener )
{
mMessageListeners.remove( listener );
if( mProcessingChain != null )
{
mProcessingChain.removeMessageListener( listener );
}
}
/**
* Indicates if any part of this channel is contained within the
* minimum and maximum frequency values.
*/
public boolean isWithin( long minimum, long maximum )
{
TunerChannel tunerChannel = getTunerChannel();
return tunerChannel != null &&
tunerChannel.overlaps( minimum, maximum );
}
public void addTrafficChannel( String channelID, Channel channel )
{
mTrafficChannels.put( channelID, channel );
}
public boolean hasTrafficChannel( Channel channel )
{
return mTrafficChannels.containsValue( channel );
}
public void removeTrafficChannel( Channel channel )
{
String name = channel.getChannelDisplayName();
if( mTrafficChannels.containsKey( name ) )
{
mTrafficChannels.remove( name );
}
}
public Collection getTrafficChannels()
{
return Collections.unmodifiableCollection( mTrafficChannels.values() );
}
public boolean hasTrafficChannel( String name )
{
return mTrafficChannels.containsKey( name );
}
}