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
@@ -154,7 +154,7 @@ public class SDRTrunk implements Listener<TunerEvent>
mSourceManager = new SourceManager(tunerModel, mSettingsManager, mUserPreferences);
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, recorderManager,
mSourceManager, aliasModel);
mSourceManager, aliasModel, mUserPreferences);
mChannelModel.addListener(mChannelProcessingManager);
mChannelProcessingManager.addChannelEventListener(mChannelModel);

View File

@@ -0,0 +1,494 @@
/*
* ******************************************************************************
* 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.gui.control;
import net.miginfocom.swing.MigLayout;
import org.kordamp.ikonli.fontawesome.FontAwesome;
import org.kordamp.ikonli.swing.FontIcon;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.AbstractAction;
import javax.swing.DefaultCellEditor;
import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.text.DefaultFormatterFactory;
import javax.swing.text.NumberFormatter;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
/**
* Multiple frequency editor control for editing an ordered list of frequencies. Frequency values are persisted as
* long values in hertz, but presented to the user as double values in megahertz.
*
*/
public class MultipleFrequencyEditor extends JPanel
{
private final static Logger mLog = LoggerFactory.getLogger(MultipleFrequencyEditor.class);
private static final Long DEFAULT_FREQUENCY = 0l;
private DecimalFormat mDecimalFormat = new DecimalFormat("0.000000");
private FrequencyModel mFrequencyModel = new FrequencyModel();
private JTable mFrequencyTable;
private JButton mAddButton;
private JButton mRemoveButton;
private JButton mMoveUpButton;
private JButton mMoveDownButton;
/**
* Constructs the editor
* @param minimum allowable frequency value
* @param maximum allowable frequency value
*/
public MultipleFrequencyEditor(double minimum, double maximum)
{
setLayout(new MigLayout("insets 0 0 0 0", "[]1[grow,fill]1[]", "[]1[]1[top,grow,fill]"));
mMoveUpButton = new JButton();
mMoveUpButton.setIcon(FontIcon.of(FontAwesome.ARROW_UP, 10));
mMoveUpButton.setEnabled(false);
mMoveUpButton.addActionListener(e -> {
int selectedRow = mFrequencyTable.getSelectedRow();
int index = mFrequencyTable.convertRowIndexToModel(selectedRow);
mFrequencyModel.moveUp(index);
mFrequencyTable.getSelectionModel().setLeadSelectionIndex(index - 1);
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(selectedRow, 0, true)));
updateButtons();
});
add(mMoveUpButton);
mFrequencyTable = new JTable(mFrequencyModel);
//testing ...
mFrequencyTable.setPreferredScrollableViewportSize(new Dimension(100, 65));
mFrequencyTable.setFillsViewportHeight(true);
mFrequencyTable.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
mFrequencyTable.getSelectionModel().addListSelectionListener(new SelectionListener());
mFrequencyTable.setDefaultEditor(Long.class, new FrequencyEditor(minimum, maximum));
mFrequencyTable.setDefaultRenderer(Long.class, new FrequencyRenderer());
JScrollPane listScrollPane = new JScrollPane(mFrequencyTable);
add(listScrollPane, "span 1 3");
mAddButton = new JButton();
mAddButton.setIcon(FontIcon.of(FontAwesome.PLUS, 10));
mAddButton.addActionListener(e -> {
int selectedRow = mFrequencyTable.getSelectedRow();
if(selectedRow >= 0)
{
int index = mFrequencyTable.convertRowIndexToModel(selectedRow);
int nextIndex = index + 1;
mFrequencyModel.add(DEFAULT_FREQUENCY, nextIndex);
mFrequencyTable.requestFocus();
mFrequencyTable.setRowSelectionInterval(nextIndex, nextIndex);
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(nextIndex, 0, true)));
mFrequencyTable.editCellAt(nextIndex, 0);
}
else
{
mFrequencyModel.add(DEFAULT_FREQUENCY);
int lastIndex = mFrequencyModel.getRowCount() - 1;
mFrequencyTable.setRowSelectionInterval(lastIndex, lastIndex);
mFrequencyTable.editCellAt(lastIndex, 0);
}
mFrequencyTable.setSurrendersFocusOnKeystroke(true);
mFrequencyTable.getEditorComponent().requestFocus();
updateButtons();
});
add(mAddButton, "wrap");
mMoveDownButton = new JButton();
mMoveDownButton.setIcon(FontIcon.of(FontAwesome.ARROW_DOWN, 10));
mMoveDownButton.setEnabled(false);
mMoveDownButton.addActionListener(e -> {
int selectedRow = mFrequencyTable.getSelectedRow();
int index = mFrequencyTable.convertRowIndexToModel(selectedRow);
mFrequencyModel.moveDown(index);
mFrequencyTable.getSelectionModel().setLeadSelectionIndex(index + 1);
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(selectedRow, 0, true)));
updateButtons();
});
add(mMoveDownButton);
mRemoveButton = new JButton();
mRemoveButton.setIcon(FontIcon.of(FontAwesome.MINUS, 10));
mRemoveButton.setEnabled(false);
mRemoveButton.addActionListener(e -> {
int selectedRow = mFrequencyTable.getSelectedRow();
int selectedIndex = mFrequencyTable.convertRowIndexToModel(selectedRow);
mFrequencyModel.remove(selectedIndex);
if(selectedIndex < mFrequencyModel.getRowCount())
{
int reselectedRow = mFrequencyTable.convertRowIndexToView(selectedIndex);
mFrequencyTable.setRowSelectionInterval(reselectedRow, reselectedRow);
mFrequencyTable.scrollRectToVisible(new Rectangle(mFrequencyTable.getCellRect(reselectedRow, 0, true)));
}
updateButtons();
});
add(mRemoveButton);
}
public FrequencyModel getFrequencyModel()
{
return mFrequencyModel;
}
private void updateButtons()
{
int index = mFrequencyTable.getSelectedRow();
if(index >= 0)
{
mMoveUpButton.setEnabled(index > 0);
mMoveDownButton.setEnabled(index < mFrequencyModel.getRowCount() - 1);
mRemoveButton.setEnabled(true);
}
else
{
mRemoveButton.setEnabled(false);
mMoveUpButton.setEnabled(false);
mMoveDownButton.setEnabled(false);
}
}
/**
* Formats the frequency value as MHz
*/
private String format(long frequency)
{
return mDecimalFormat.format(frequency / 1e6d);
}
public class FrequencyModel extends AbstractTableModel
{
private List<Long> mFrequencies = new ArrayList<>();
@Override
public String getColumnName(int column)
{
return "Frequencies (MHz)";
}
@Override
public int getRowCount()
{
return mFrequencies.size();
}
@Override
public int getColumnCount()
{
return 1;
}
@Override
public Object getValueAt(int rowIndex, int columnIndex)
{
return mFrequencies.get(rowIndex);
}
public void setValueAt(int index, Long value)
{
mFrequencies.set(index, value);
fireTableRowsUpdated(index, index);
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex)
{
return true;
}
@Override
public Class<?> getColumnClass(int columnIndex)
{
return Long.class;
}
/**
* Adds a frequency to the model
*/
public void add(long frequency, int index)
{
mFrequencies.add(index, frequency);
fireTableRowsInserted(index, index);
}
/**
* Adds a frequency to the model
*/
public void add(long frequency)
{
mFrequencies.add(frequency);
fireTableRowsInserted(mFrequencies.size() - 1, mFrequencies.size() - 1);
}
/**
* Removes an indexed value from the model
*/
public Long remove(int index)
{
Long value = mFrequencies.remove(index);
fireTableRowsDeleted(index, index);
return value;
}
/**
* Removes all frequencies from the model
*/
public void clear()
{
mFrequencies.clear();
fireTableDataChanged();
}
/**
* Moves the frequency up in the list
*/
public void moveUp(int index)
{
if(index > 0 && index < mFrequencies.size())
{
Long valueA = mFrequencies.get(index);
Long valueB = mFrequencies.get(index - 1);
mFrequencies.set(index, valueB);
mFrequencies.set(index - 1, valueA);
fireTableRowsUpdated(index - 1, index);
}
}
/**
* Moves the frequency down in the list
*/
public void moveDown(int index)
{
if(index >= 0 && index + 1 < mFrequencies.size())
{
Long valueA = mFrequencies.get(index);
Long valueB = mFrequencies.get(index + 1);
mFrequencies.set(index, valueB);
mFrequencies.set(index + 1, valueA);
fireTableRowsUpdated(index, index + 1);
}
}
/**
* Sets the frequencies for the model
*/
public void setFrequencies(List<Long> frequencies)
{
mFrequencies.clear();
mFrequencies.addAll(frequencies);
fireTableDataChanged();
}
/**
* List of long frequency values contained in the model
*/
public List<Long> getFrequencies()
{
List<Long> frequencies = new ArrayList<>();
frequencies.addAll(mFrequencies);
return frequencies;
}
}
public class SelectionListener implements ListSelectionListener
{
@Override
public void valueChanged(ListSelectionEvent event)
{
updateButtons();
}
}
public class FrequencyEditor extends DefaultCellEditor
{
private JFormattedTextField mTextField;
private NumberFormat mNumberFormat;
private Double mMinimum;
private Double mMaximum;
public FrequencyEditor(double minValue, double maxValue)
{
super(new JFormattedTextField());
mTextField = (JFormattedTextField)getComponent();
mMinimum = minValue;
mMaximum = maxValue;
mNumberFormat = NumberFormat.getNumberInstance();
mNumberFormat.setMinimumFractionDigits(1);
mNumberFormat.setMaximumFractionDigits(6);
mNumberFormat.setGroupingUsed(false);
NumberFormatter numberFormatter = new NumberFormatter(mNumberFormat);
numberFormatter.setFormat(mNumberFormat);
numberFormatter.setMinimum(mMinimum);
numberFormatter.setMaximum(mMaximum);
numberFormatter.setAllowsInvalid(false);
mTextField.setFormatterFactory(new DefaultFormatterFactory(numberFormatter));
mTextField.setValue(mMinimum);
mTextField.setHorizontalAlignment(JTextField.TRAILING);
mTextField.setFocusLostBehavior(JFormattedTextField.PERSIST);
mTextField.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "check");
mTextField.getActionMap().put("check", new AbstractAction()
{
@Override
public void actionPerformed(ActionEvent event)
{
//This should always be true since we use formatter.setAllowsInvalid(false) above.
if(mTextField.isEditValid())
{
int selectedRow = mFrequencyTable.getSelectedRow();
int selectedIndex = mFrequencyTable.convertRowIndexToModel(selectedRow);
try
{
Double frequencyMHz = Double.parseDouble(mTextField.getText());
long frequency = (long)(frequencyMHz * 1e6d);
mFrequencyModel.setValueAt(selectedIndex, frequency);
}
catch(Exception err)
{
mLog.error("Error parsing frequency: " + mTextField.getText(), err);
}
fireEditingStopped();
}
}
});
}
//Override to invoke setValue on the formatted text field.
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
{
JFormattedTextField ftf = (JFormattedTextField)super.getTableCellEditorComponent(table, value, isSelected, row, column);
if(value instanceof Long)
{
ftf.setValue(((Long)value).doubleValue() / 1e6d);
}
else
{
ftf.setValue(value);
}
ftf.requestFocus();
ftf.selectAll();
ftf.setCaretPosition(0);
return ftf;
}
//Override to ensure that the value remains a Double.
public Object getCellEditorValue()
{
JFormattedTextField ftf = (JFormattedTextField)getComponent();
Object o = ftf.getValue();
if(o instanceof Double)
{
return o;
}
else if(o instanceof Number)
{
return ((Number)o).doubleValue();
}
else
{
try
{
return Double.parseDouble(o.toString());
}
catch(Exception exc)
{
return new Double(0);
}
}
}
}
/**
* Table cell renderer to display long hertz values as double megahertz values
*/
public class FrequencyRenderer extends DefaultTableCellRenderer
{
public FrequencyRenderer()
{
super.setHorizontalAlignment(JLabel.RIGHT);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
JLabel cell = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if(value instanceof Long)
{
cell.setText(format((long)value));
}
else
{
cell.setText("??");
}
return cell;
}
}
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
final JFrame frame = new JFrame();
frame.setSize(new Dimension(400, 400));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MultipleFrequencyEditor(1.0, 6000.0));
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}

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
@@ -21,6 +21,7 @@
package io.github.dsheirer.gui.preference;
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
import io.github.dsheirer.preference.UserPreferences;
import javafx.scene.Node;
@@ -38,9 +39,11 @@ public class PreferenceEditorFactory
return new DecodeEventViewPreferenceEditor(userPreferences);
case DIRECTORY:
return new DirectoryPreferenceEditor(userPreferences);
case SOURCE_CHANNEL_MULTIPLE_FREQUENCY:
return new ChannelMultipleFrequencyPreferenceEditor(userPreferences);
case TALKGROUP_FORMAT:
return new TalkgroupFormatPreferenceEditor(userPreferences);
case TUNER:
case SOURCE_TUNER_CHANNELIZER:
return new TunerPreferenceEditor(userPreferences);
}

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
@@ -27,8 +27,9 @@ public enum PreferenceEditorType
{
CHANNEL_EVENT("Channel Events"),
DIRECTORY("Directories"),
TALKGROUP_FORMAT("Talkgroups"),
TUNER("Tuners");
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
TALKGROUP_FORMAT("Talkgroups");
private String mLabel;

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
@@ -201,7 +201,8 @@ public class PreferencesEditor extends Application
storageItem.setExpanded(true);
TreeItem<String> sourceItem = new TreeItem<>("Source");
sourceItem.getChildren().add(new TreeItem(PreferenceEditorType.TUNER));
sourceItem.getChildren().add(new TreeItem(PreferenceEditorType.SOURCE_CHANNEL_MULTIPLE_FREQUENCY));
sourceItem.getChildren().add(new TreeItem(PreferenceEditorType.SOURCE_TUNER_CHANNELIZER));
treeRoot.getChildren().add(sourceItem);
sourceItem.setExpanded(true);

View File

@@ -0,0 +1,123 @@
/*
* ******************************************************************************
* 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.gui.preference.tuner;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.source.ChannelMultiFrequencyPreference;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
/**
* Preference settings for channel source configurations containing multiple frequencies
*/
public class ChannelMultipleFrequencyPreferenceEditor extends HBox
{
private static final String HELP_TEXT = "Rotation delay determines how long a decoder will wait before rotating " +
"to the next frequency in a list for a rotating control channel configuration.";
private static final Insets MARGINS = new Insets(5,5,5,5);
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
private GridPane mEditorPane;
private Label mRotationDelayLabel;
private Spinner<Integer> mRotationDelaySpinner;
private Button mResetButton;
private Label mHelpText;
public ChannelMultipleFrequencyPreferenceEditor(UserPreferences userPreferences)
{
mChannelMultiFrequencyPreference = userPreferences.getChannelMultiFrequencyPreference();
getChildren().add(getEditorPane());
}
private GridPane getEditorPane()
{
if(mEditorPane == null)
{
mEditorPane = new GridPane();
GridPane.setMargin(getRotationDelaySpinner(), new Insets(5, 5, 5, 5));
mEditorPane.add(getRotationDelaySpinner(), 0, 0);
GridPane.setMargin(getRotationDelayLabel(), new Insets(5, 5, 5, 5));
mEditorPane.add(getRotationDelayLabel(), 1, 0);
GridPane.setMargin(getResetButton(), MARGINS);
mEditorPane.add(getResetButton(), 2, 0);
GridPane.setMargin(getHelpText(), MARGINS);
mEditorPane.add(getHelpText(), 0, 1, 3, 1);
}
return mEditorPane;
}
public Spinner<Integer> getRotationDelaySpinner()
{
if(mRotationDelaySpinner == null)
{
int currentDelay = (int)(mChannelMultiFrequencyPreference.getRotationDelay() / 1000);
mRotationDelaySpinner = new Spinner<>(1, 60, currentDelay, 1);
mRotationDelaySpinner.setEditable(true);
mRotationDelaySpinner.valueProperty().addListener((observable, oldValue, newValue) -> {
mChannelMultiFrequencyPreference.setRotationDelay(newValue * 1000);
});
}
return mRotationDelaySpinner;
}
public Label getRotationDelayLabel()
{
if(mRotationDelayLabel == null)
{
mRotationDelayLabel = new Label("Rotation Delay (seconds)");
}
return mRotationDelayLabel;
}
public Button getResetButton()
{
if(mResetButton == null)
{
mResetButton = new Button("Reset");
mResetButton.setOnAction(event -> {
mChannelMultiFrequencyPreference.resetRotationDelay();
int currentDelay = (int)(mChannelMultiFrequencyPreference.getRotationDelay() / 1000);
getRotationDelaySpinner().getValueFactory().setValue(currentDelay);
});
}
return mResetButton;
}
public Label getHelpText()
{
if(mHelpText == null)
{
mHelpText = new Label(HELP_TEXT);
mHelpText.setWrapText(true);
}
return mHelpText;
}
}

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
@@ -21,8 +21,8 @@
package io.github.dsheirer.gui.preference.tuner;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.tuner.ChannelizerType;
import io.github.dsheirer.preference.tuner.TunerPreference;
import io.github.dsheirer.preference.source.ChannelizerType;
import io.github.dsheirer.preference.source.TunerPreference;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;