61 Rotating Control Channel Support - P25 Decoder (#475)

* Initial work on add support for rotating control channels.

* Creates multiple frequency editor control.

* Updates channel source editor to allow user to select multi-frequency
configuration and add multiple frequencies.

* Adds full support for rotating control channels to the P25 channelizer.
Users define multiple frequencies in the channel source configuration.
The decoder monitors the state of the channel for a CONTROL state.  If
the state does not stay in the CONTROL state for a user-specified delay
value, then the decoder rotates to the next frequency in the list.  This
behavior continues until the decoder state once again changes to CONTROL.

This is NOT a 'scanner' feature.
This commit is contained in:
Denny Sheirer
2019-01-06 06:59:04 -05:00
committed by GitHub
parent 3a39ae76ba
commit 5481fb7d4d
44 changed files with 2502 additions and 634 deletions

View File

@@ -1,7 +1,7 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* Copyright (C) 2014-2019 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
@@ -456,7 +456,7 @@ public class ProcessingChain implements Listener<ChannelEvent>
if(module instanceof IHeartbeatProvider)
{
((IHeartbeatProvider)module).getHeartbeatManager().addHeartbeatListener(mHeartbeatBroadcaster);
((IHeartbeatProvider)module).addHeartbeatListener(mHeartbeatBroadcaster);
}
if(module instanceof IMessageProvider)
@@ -528,7 +528,7 @@ public class ProcessingChain implements Listener<ChannelEvent>
if(module instanceof IHeartbeatProvider)
{
((IHeartbeatProvider)module).getHeartbeatManager().removeHeartbeatListener(mHeartbeatBroadcaster);
((IHeartbeatProvider)module).removeHeartbeatListener(mHeartbeatBroadcaster);
}
if(module instanceof IMessageProvider)
@@ -571,7 +571,7 @@ public class ProcessingChain implements Listener<ChannelEvent>
mSourceEventBroadcaster.broadcast(SourceEvent.sampleRateChange(mSource.getSampleRate(), "Processing Chain Startup"));
//Broadcast the source center frequency / sample rate so that each of the modules can self-configure
mSourceEventBroadcaster.broadcast(SourceEvent.frequencyChange(mSource.getFrequency(), "Processing Chain Startup"));
mSourceEventBroadcaster.broadcast(SourceEvent.frequencyChange(mSource, mSource.getFrequency(), "Processing Chain Startup"));
//Setup the channel state to monitor source overflow conditions
mSource.setOverflowListener(mChannelState);

View File

@@ -1,7 +1,7 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* Copyright (C) 2014-2019 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
@@ -24,6 +24,7 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.alias.action.AliasActionManager;
import io.github.dsheirer.audio.AudioModule;
import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.map.ChannelMap;
@@ -87,7 +88,10 @@ import io.github.dsheirer.module.decode.tait.Tait1200Decoder;
import io.github.dsheirer.module.decode.tait.Tait1200DecoderState;
import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule;
import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.tuner.channel.ChannelRotationMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -109,9 +113,10 @@ public class DecoderFactory
*
* @return list of configured decoders
*/
public static List<Module> getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel)
public static List<Module> getModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel,
UserPreferences userPreferences)
{
List<Module> modules = getPrimaryModules(channelMapModel, channel, aliasModel);
List<Module> modules = getPrimaryModules(channelMapModel, channel, aliasModel, userPreferences);
modules.addAll(getAuxiliaryDecoders(channel.getAuxDecodeConfiguration()));
return modules;
}
@@ -119,7 +124,8 @@ public class DecoderFactory
/**
* Constructs a primary decoder as specified in the decode configuration
*/
public static List<Module> getPrimaryModules(ChannelMapModel channelMapModel, Channel channel, AliasModel aliasModel)
public static List<Module> getPrimaryModules(ChannelMapModel channelMapModel, Channel channel,
AliasModel aliasModel, UserPreferences userPreferences)
{
List<Module> modules = new ArrayList<Module>();
@@ -239,6 +245,15 @@ public class DecoderFactory
}
modules.add(new P25AudioModule());
//Add a channel rotation monitor when we have multiple control channel frequencies specified
if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency &&
((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).hasMultipleFrequencies())
{
List<State> activeStates = new ArrayList<>();
activeStates.add(State.CONTROL);
modules.add(new ChannelRotationMonitor(activeStates, userPreferences));
}
break;
default:
throw new IllegalArgumentException("Unknown decoder type [" + decodeConfig.getDecoderType().toString() + "]");

View File

@@ -1,20 +1,22 @@
/*******************************************************************************
* 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/>
******************************************************************************/
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 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 io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.controller.channel.Channel;
@@ -26,6 +28,7 @@ import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.source.SourceConfigurationEditor;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.config.SourceConfiguration;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
@@ -43,178 +46,176 @@ import java.awt.event.ActionListener;
public class P25DecoderEditor extends ValidatingEditor<Channel>
{
private final static Logger mLog = LoggerFactory.getLogger( P25DecoderEditor.class );
private final static Logger mLog = LoggerFactory.getLogger(P25DecoderEditor.class);
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = 1L;
private JComboBox<P25DecoderLSM.Modulation> mComboModulation;
private JCheckBox mIgnoreDataCalls;
private JLabel mTrafficChannelPoolSizeLabel;
private JSlider mTrafficChannelPoolSize;
public P25DecoderEditor()
{
init();
}
private void init()
{
setLayout( new MigLayout( "insets 0 0 0 0,wrap 3", "[right][grow,fill][]", "" ) );
mComboModulation = new JComboBox<P25DecoderLSM.Modulation>();
mComboModulation.setModel( new DefaultComboBoxModel<P25DecoderLSM.Modulation>(
P25DecoderLSM.Modulation.values() ) );
mComboModulation.setEnabled( false );
mComboModulation.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
setModified( true );
}
} );
add( new JLabel( "Modulation:" ) );
add( mComboModulation );
mIgnoreDataCalls = new JCheckBox( "Ignore Data Calls" );
mIgnoreDataCalls.setEnabled( false );
mIgnoreDataCalls.addActionListener( new ActionListener()
{
@Override
public void actionPerformed( ActionEvent e )
{
setModified( true );
}
} );
add( mIgnoreDataCalls );
mTrafficChannelPoolSize = new JSlider( JSlider.HORIZONTAL, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MINIMUM,
DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MAXIMUM, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT );
mTrafficChannelPoolSize.setEnabled( false );
mTrafficChannelPoolSize.setMajorTickSpacing( 10 );
mTrafficChannelPoolSize.setMinorTickSpacing( 5 );
mTrafficChannelPoolSize.setPaintTicks( true );
mTrafficChannelPoolSize.addChangeListener( new ChangeListener()
{
@Override
public void stateChanged( ChangeEvent e )
{
mTrafficChannelPoolSizeLabel.setText( "Traffic Pool: " + mTrafficChannelPoolSize.getValue() );
setModified( true );
}
} );
mTrafficChannelPoolSizeLabel = new JLabel( "Traffic Pool: " + mTrafficChannelPoolSize.getValue() + " " );
add( mTrafficChannelPoolSizeLabel );
add( mTrafficChannelPoolSize );
}
@Override
public void save()
{
if( hasItem() && isModified() )
{
DecodeConfigP25Phase1 p25 = new DecodeConfigP25Phase1();
p25.setModulation( (P25Decoder.Modulation)mComboModulation.getSelectedItem() );
p25.setIgnoreDataCalls( mIgnoreDataCalls.isSelected() );
p25.setTrafficChannelPoolSize( mTrafficChannelPoolSize.getValue() );
getItem().setDecodeConfiguration( p25 );
}
setModified( false );
{
init();
}
@Override
public void validate( Editor<Channel> editor ) throws EditorValidationException
{
/**
* Enforce CQPSK modulation uses a Tuner Source for I/Q sample data
*/
if( editor instanceof SourceConfigurationEditor &&
((P25Decoder.Modulation)mComboModulation.getSelectedItem()) == P25Decoder.Modulation.CQPSK )
{
SourceConfiguration config = ((SourceConfigurationEditor)editor).getSourceConfiguration();
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0,wrap 3", "[right][grow,fill][]", ""));
if( config != null && !( config instanceof SourceConfigTuner ) )
{
throw new EditorValidationException( editor,
"<html><body width='175'><h3>LSM Simulcast</h3>"
+ "<p>P25 LSM Simulcast decoder can only be used with "
+ "a tuner source. Please change the Source to use a tuner"
+ " or change the P25 Decoder to C4FM modulation</p>" );
}
}
if( editor instanceof AuxDecodeConfigurationEditor)
{
AuxDecodeConfiguration config = ((AuxDecodeConfigurationEditor)editor).getConfiguration();
if( config != null && !config.getAuxDecoders().isEmpty() )
{
throw new EditorValidationException( editor,
"<html><body width='175'><h3>Auxilary Decoders</h3>"
+ "<p>The auxiliary decoders work with analog audio and are not "
+ "compatible with the P25 Decoder. Please uncheck any auxiliary "
+ "decoders that you have selected for this channel</p>" );
}
}
}
mComboModulation = new JComboBox<P25DecoderLSM.Modulation>();
mComboModulation.setModel(new DefaultComboBoxModel<P25DecoderLSM.Modulation>(
P25DecoderLSM.Modulation.values()));
mComboModulation.setEnabled(false);
mComboModulation.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
private void setControlsEnabled( boolean enabled )
{
if( mComboModulation.isEnabled() != enabled )
{
mComboModulation.setEnabled( enabled );
}
if( mTrafficChannelPoolSize.isEnabled() != enabled )
{
mTrafficChannelPoolSize.setEnabled( enabled );
}
add(new JLabel("Modulation:"));
add(mComboModulation);
if( mIgnoreDataCalls.isEnabled() != enabled )
{
mIgnoreDataCalls.setEnabled( enabled );
}
}
mIgnoreDataCalls = new JCheckBox("Ignore Data Calls");
mIgnoreDataCalls.setEnabled(false);
mIgnoreDataCalls.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
setModified(true);
}
});
@Override
public void setItem( Channel item )
{
super.setItem( item );
if( hasItem() )
{
setControlsEnabled( true );
DecodeConfiguration config = getItem().getDecodeConfiguration();
if( config instanceof DecodeConfigP25Phase1 )
{
DecodeConfigP25Phase1 p25 = (DecodeConfigP25Phase1)config;
mComboModulation.setSelectedItem( p25.getModulation() );
mIgnoreDataCalls.setSelected( p25.getIgnoreDataCalls() );
mTrafficChannelPoolSize.setValue( p25.getTrafficChannelPoolSize() );
setModified( false );
}
else
{
mComboModulation.setSelectedItem( P25Decoder.Modulation.C4FM );
mIgnoreDataCalls.setSelected( false );
mTrafficChannelPoolSize.setValue( DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT );
setModified( true );
}
}
else
{
setControlsEnabled( false );
setModified( false );
}
}
add(mIgnoreDataCalls);
mTrafficChannelPoolSize = new JSlider(JSlider.HORIZONTAL, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MINIMUM,
DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_MAXIMUM, DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT);
mTrafficChannelPoolSize.setEnabled(false);
mTrafficChannelPoolSize.setMajorTickSpacing(10);
mTrafficChannelPoolSize.setMinorTickSpacing(5);
mTrafficChannelPoolSize.setPaintTicks(true);
mTrafficChannelPoolSize.addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent e)
{
mTrafficChannelPoolSizeLabel.setText("Traffic Pool: " + mTrafficChannelPoolSize.getValue());
setModified(true);
}
});
mTrafficChannelPoolSizeLabel = new JLabel("Traffic Pool: " + mTrafficChannelPoolSize.getValue() + " ");
add(mTrafficChannelPoolSizeLabel);
add(mTrafficChannelPoolSize);
}
@Override
public void save()
{
if(hasItem() && isModified())
{
DecodeConfigP25Phase1 p25 = new DecodeConfigP25Phase1();
p25.setModulation((P25Decoder.Modulation)mComboModulation.getSelectedItem());
p25.setIgnoreDataCalls(mIgnoreDataCalls.isSelected());
p25.setTrafficChannelPoolSize(mTrafficChannelPoolSize.getValue());
getItem().setDecodeConfiguration(p25);
}
setModified(false);
}
@Override
public void validate(Editor<Channel> editor) throws EditorValidationException
{
/**
* Enforce CQPSK modulation uses a Tuner Source for I/Q sample data
*/
if(editor instanceof SourceConfigurationEditor)
{
SourceConfiguration config = ((SourceConfigurationEditor)editor).getSourceConfiguration();
if(!(config instanceof SourceConfigTuner || config instanceof SourceConfigTunerMultipleFrequency))
{
throw new EditorValidationException(editor,
"<html><body width='175'><h3>LSM Simulcast</h3>"
+ "<p>P25 LSM Simulcast decoder can only be used with "
+ "a tuner source. Please change the Source to use a tuner"
+ " or change the P25 Decoder to C4FM modulation</p>");
}
}
else if(editor instanceof AuxDecodeConfigurationEditor)
{
AuxDecodeConfiguration config = ((AuxDecodeConfigurationEditor)editor).getConfiguration();
if(config != null && !config.getAuxDecoders().isEmpty())
{
throw new EditorValidationException(editor,
"<html><body width='175'><h3>Auxilary Decoders</h3>"
+ "<p>The auxiliary decoders work with analog audio and are not "
+ "compatible with the P25 Decoder. Please uncheck any auxiliary "
+ "decoders that you have selected for this channel</p>");
}
}
}
private void setControlsEnabled(boolean enabled)
{
if(mComboModulation.isEnabled() != enabled)
{
mComboModulation.setEnabled(enabled);
}
if(mTrafficChannelPoolSize.isEnabled() != enabled)
{
mTrafficChannelPoolSize.setEnabled(enabled);
}
if(mIgnoreDataCalls.isEnabled() != enabled)
{
mIgnoreDataCalls.setEnabled(enabled);
}
}
@Override
public void setItem(Channel item)
{
super.setItem(item);
if(hasItem())
{
setControlsEnabled(true);
DecodeConfiguration config = getItem().getDecodeConfiguration();
if(config instanceof DecodeConfigP25Phase1)
{
DecodeConfigP25Phase1 p25 = (DecodeConfigP25Phase1)config;
mComboModulation.setSelectedItem(p25.getModulation());
mIgnoreDataCalls.setSelected(p25.getIgnoreDataCalls());
mTrafficChannelPoolSize.setValue(p25.getTrafficChannelPoolSize());
setModified(false);
}
else
{
mComboModulation.setSelectedItem(P25Decoder.Modulation.C4FM);
mIgnoreDataCalls.setSelected(false);
mTrafficChannelPoolSize.setValue(DecodeConfiguration.TRAFFIC_CHANNEL_LIMIT_DEFAULT);
setModified(true);
}
}
else
{
setControlsEnabled(false);
setModified(false);
}
}
}