mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 10:07:26 -05:00
* Popout configuration panel (top-left of display) * Themed color support * Reworked signal averating/smoothing * Optional decibel overlay with corrected decibel calculations * Integrated noise floor slider in channel view * Traffic channel overlays * Popup channel information when hovering over playlist channels. * Removes legacy swing components. Co-authored-by: denny <dennis.sheirer@gmail.com>
484 lines
19 KiB
Java
484 lines
19 KiB
Java
/*
|
|
* *****************************************************************************
|
|
* Copyright (C) 2014-2026 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.channel;
|
|
|
|
import io.github.dsheirer.controller.channel.Channel;
|
|
import io.github.dsheirer.dsp.filter.channelizer.PolyphaseChannelSource;
|
|
import io.github.dsheirer.gui.power.SignalPowerView;
|
|
import io.github.dsheirer.gui.spectrum.ChannelSpectralDisplay;
|
|
import io.github.dsheirer.gui.spectrum.DftSpectralDisplay;
|
|
import io.github.dsheirer.gui.squelch.NoiseSquelchView;
|
|
import io.github.dsheirer.gui.symbol.SymbolView;
|
|
import io.github.dsheirer.gui.theme.ThemeManager;
|
|
import io.github.dsheirer.module.ProcessingChain;
|
|
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
|
import io.github.dsheirer.module.decode.PrimaryDecoder;
|
|
import io.github.dsheirer.module.decode.am.AMDecoder;
|
|
import io.github.dsheirer.module.decode.nbfm.NBFMDecoder;
|
|
import io.github.dsheirer.playlist.PlaylistManager;
|
|
import io.github.dsheirer.sample.Listener;
|
|
import io.github.dsheirer.sample.complex.ComplexSamplesToNativeBufferModule;
|
|
import io.github.dsheirer.source.ChannelFrequencyCorrectionStatusNotification;
|
|
import io.github.dsheirer.source.Source;
|
|
import io.github.dsheirer.source.SourceEvent;
|
|
import io.github.dsheirer.source.tuner.channel.HalfBandTunerChannelSource;
|
|
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
|
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
|
import java.awt.Component;
|
|
import java.awt.EventQueue;
|
|
import java.net.URL;
|
|
import java.text.DecimalFormat;
|
|
import java.util.List;
|
|
import javafx.application.Platform;
|
|
import javafx.embed.swing.JFXPanel;
|
|
import javafx.scene.Scene;
|
|
import net.miginfocom.swing.MigLayout;
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import javax.swing.JButton;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JSplitPane;
|
|
|
|
/**
|
|
* Display for channel FFT and squelch details
|
|
*/
|
|
public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingChain>
|
|
{
|
|
private static final Logger LOGGER = LoggerFactory.getLogger(ChannelSpectrumPanel.class);
|
|
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
|
|
private static final DecimalFormat PPM_FORMAT = new DecimalFormat("0.00000");
|
|
private static final String LABEL_PREFIX_PPM = "Tuner PPM: ";
|
|
private static final String LABEL_PREFIX_TUNER = "Tuner:";
|
|
private static final String LABEL_PREFIX_CHANNEL = "Channel:";
|
|
private static final String LABEL_PREFIX_DECODER = "Decoder:";
|
|
private final PlaylistManager mPlaylistManager;
|
|
private ProcessingChain mProcessingChain;
|
|
private final ComplexSamplesToNativeBufferModule mSampleStreamTapModule = new ComplexSamplesToNativeBufferModule();
|
|
private final JFXPanel mSpectralPanel = new JFXPanel();
|
|
//Created on the JavaFX thread - guarded by this. Channel state is kept here until the display exists.
|
|
private ChannelSpectralDisplay mChannelSpectralDisplay;
|
|
private long mChannelFrequency;
|
|
private int mChannelSampleRate;
|
|
private int mChannelBandwidth;
|
|
private Long mCarrierOffset;
|
|
private final SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
|
private final JLabel mTunerPPMLabel;
|
|
private final JLabel mTunerCorrectionLabel;
|
|
private final JLabel mChannelCorrectionLabel;
|
|
private final JLabel mDecoderCorrectionLabel;
|
|
private final JLabel mDummyLabel = new JLabel(" ");
|
|
private boolean mPanelVisible = false;
|
|
private final NoiseSquelchView mNoiseSquelchView;
|
|
private final SignalPowerView mSignalPowerView;
|
|
private final SymbolView mSymbolView = new SymbolView();
|
|
private final JFXPanel mNoiseSquelchPanel;
|
|
private final JFXPanel mSymbolPanel;
|
|
private JSplitPane mSplitPane;
|
|
private FeedbackDecoder mFeedbackDecoder;
|
|
|
|
/**
|
|
* Constructs an instance.
|
|
*/
|
|
public ChannelSpectrumPanel(PlaylistManager playlistManager)
|
|
{
|
|
mPlaylistManager = playlistManager;
|
|
mNoiseSquelchView = new NoiseSquelchView(mPlaylistManager);
|
|
mSignalPowerView = new SignalPowerView(mPlaylistManager);
|
|
|
|
//Register the three swappable right-pane components with the theme manager. Only one is
|
|
//attached to the split pane at a time, so the LAF-toggle walk of Window.getWindows() can
|
|
//miss the detached ones - they get refreshed explicitly via registerSwing.
|
|
ThemeManager.getInstance().registerSwing(mSignalPowerView);
|
|
setLayout(new MigLayout("insets 0", "[grow,fill]", "[grow,fill]"));
|
|
|
|
JPanel fftPanel = new JPanel();
|
|
fftPanel.setLayout(new MigLayout("insets 0", "[grow,fill]", "[][grow,fill]"));
|
|
|
|
JPanel labelPanel = new JPanel();
|
|
labelPanel.setLayout(new MigLayout("insets 2", "[grow,fill][][][][][grow,fill]", ""));
|
|
|
|
mTunerPPMLabel = new JLabel(LABEL_PREFIX_PPM);
|
|
mTunerPPMLabel.setEnabled(false);
|
|
labelPanel.add(mTunerPPMLabel);
|
|
|
|
labelPanel.add(new JLabel("Corrections (Hz) "));
|
|
|
|
mTunerCorrectionLabel = new JLabel(LABEL_PREFIX_TUNER);
|
|
mTunerCorrectionLabel.setEnabled(false);
|
|
labelPanel.add(mTunerCorrectionLabel);
|
|
|
|
mChannelCorrectionLabel = new JLabel(LABEL_PREFIX_CHANNEL);
|
|
mChannelCorrectionLabel.setEnabled(false);
|
|
labelPanel.add(mChannelCorrectionLabel);
|
|
|
|
mDecoderCorrectionLabel = new JLabel(LABEL_PREFIX_DECODER);
|
|
mDecoderCorrectionLabel.setEnabled(false);
|
|
labelPanel.add(mDecoderCorrectionLabel);
|
|
|
|
labelPanel.add(mDummyLabel);
|
|
|
|
JButton logIndexesButton = new JButton("Log Settings");
|
|
logIndexesButton.addActionListener(e -> {
|
|
if(mProcessingChain != null)
|
|
{
|
|
Source source = mProcessingChain.getSource();
|
|
|
|
if(source instanceof PolyphaseChannelSource pcs)
|
|
{
|
|
List<Integer> indexes = pcs.getOutputProcessorIndexes();
|
|
double sampleRate = pcs.getSampleRate();
|
|
long indexCenterFrequency = pcs.getIndexCenterFrequency();
|
|
long appliedFrequencyOffset = pcs.getFrequencyOffset();
|
|
long requestedCenterFrequency = pcs.getFrequency();
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("Polyphase Channel - BW: ").append(FREQUENCY_FORMAT.format(sampleRate / 1E6d));
|
|
sb.append(" Center/Requested/Mixer: ").append(FREQUENCY_FORMAT.format(indexCenterFrequency / 1E6d));
|
|
sb.append("/").append(FREQUENCY_FORMAT.format(requestedCenterFrequency / 1E6d));
|
|
sb.append("/").append(FREQUENCY_FORMAT.format(appliedFrequencyOffset / 1E6d));
|
|
sb.append(" Polyphase Indexes: ").append(indexes);
|
|
sb.append(" Tuner SR:").append(FREQUENCY_FORMAT.format(pcs.getTunerSampleRate() / 1E6d));
|
|
sb.append(" CF:").append(FREQUENCY_FORMAT.format(pcs.getTunerCenterFrequency() / 1E6d));
|
|
LOGGER.info(sb.toString());
|
|
LOGGER.info("Output Processor: " + pcs.getStateDescription());
|
|
}
|
|
else if(source instanceof HalfBandTunerChannelSource<?> hbtcs)
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append("Heterodyne Channel - CF:").append(FREQUENCY_FORMAT.format(hbtcs.getFrequency() / 1E6d));
|
|
sb.append(" SR:").append(FREQUENCY_FORMAT.format(hbtcs.getSampleRate() / 1E6d));
|
|
sb.append(" Mixer:").append(FREQUENCY_FORMAT.format(hbtcs.getMixerFrequency() / 1E6d));
|
|
LOGGER.info(sb.toString());
|
|
}
|
|
else
|
|
{
|
|
LOGGER.info("Unsupported channel type: " + (source != null ? source.getClass() : " null"));
|
|
}
|
|
}
|
|
});
|
|
//This is a debug button to log the current settings to the app log.
|
|
// labelPanel.add(mLogIndexesButton);
|
|
|
|
fftPanel.add(labelPanel, "wrap");
|
|
|
|
fftPanel.add(mSpectralPanel);
|
|
ThemeManager.getInstance().registerSwing(mSpectralPanel);
|
|
|
|
Platform.runLater(() -> {
|
|
ChannelSpectralDisplay display = new ChannelSpectralDisplay();
|
|
Scene scene = new Scene(display);
|
|
ThemeManager.getInstance().register(scene);
|
|
mSpectralPanel.setScene(scene);
|
|
displayCreated(display);
|
|
});
|
|
|
|
mNoiseSquelchPanel = new JFXPanel();
|
|
mSymbolPanel = new JFXPanel();
|
|
ThemeManager.getInstance().registerSwing(mNoiseSquelchPanel);
|
|
ThemeManager.getInstance().registerSwing(mSymbolPanel);
|
|
|
|
//Spin noise squelch panel construction off onto the JavafX UI thread.
|
|
Platform.runLater(() -> {
|
|
Scene scene = new Scene(mNoiseSquelchView);
|
|
ThemeManager.getInstance().register(scene);
|
|
mNoiseSquelchPanel.setScene(scene);
|
|
Scene scene2 = new Scene(mSymbolView);
|
|
URL resource = getClass().getResource("/sdrtrunk_style.css");
|
|
|
|
if(resource != null)
|
|
{
|
|
scene2.getStylesheets().add(resource.toExternalForm());
|
|
}
|
|
else
|
|
{
|
|
LOGGER.warn("Can't find stylesheet resource for sdrtrunk");
|
|
}
|
|
|
|
ThemeManager.getInstance().register(scene2);
|
|
//Use the same color theme as the channel spectral display
|
|
mSymbolView.followTheme(DftSpectralDisplay.getSharedTheme());
|
|
mSymbolPanel.setScene(scene2);
|
|
});
|
|
|
|
mSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
|
|
mSplitPane.add(fftPanel, JSplitPane.LEFT);
|
|
mSplitPane.add(mNoiseSquelchPanel, JSplitPane.RIGHT);
|
|
add(mSplitPane);
|
|
// mSplitPane.setDividerLocation(0.5);
|
|
|
|
}
|
|
|
|
/**
|
|
* Stores the spectral display once it's created on the JavaFX thread and applies the current channel state.
|
|
*/
|
|
private synchronized void displayCreated(ChannelSpectralDisplay display)
|
|
{
|
|
mChannelSpectralDisplay = display;
|
|
|
|
if(mChannelSampleRate > 0)
|
|
{
|
|
display.setChannel(mChannelFrequency, mChannelSampleRate, mChannelBandwidth);
|
|
}
|
|
|
|
if(mCarrierOffset != null)
|
|
{
|
|
display.setCarrierOffset(mCarrierOffset);
|
|
}
|
|
|
|
updateFFTProcessing();
|
|
}
|
|
|
|
private synchronized void setChannel(long frequency, int sampleRate, int bandwidth)
|
|
{
|
|
mChannelFrequency = frequency;
|
|
mChannelSampleRate = sampleRate;
|
|
mChannelBandwidth = bandwidth;
|
|
mCarrierOffset = null;
|
|
|
|
if(mChannelSpectralDisplay != null)
|
|
{
|
|
mChannelSpectralDisplay.setChannel(frequency, sampleRate, bandwidth);
|
|
}
|
|
}
|
|
|
|
private synchronized void setCarrierOffset(long offset)
|
|
{
|
|
mCarrierOffset = offset;
|
|
|
|
if(mChannelSpectralDisplay != null)
|
|
{
|
|
mChannelSpectralDisplay.setCarrierOffset(offset);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pads the value to a fixed width and returns the previx and padded value.
|
|
* @param prefix of the label
|
|
* @param value to pad with spaces
|
|
* @return padded, prefixed label
|
|
*/
|
|
private String getPaddedLabel(String prefix, Number value)
|
|
{
|
|
return prefix + StringUtils.leftPad(String.valueOf(value), 7);
|
|
}
|
|
|
|
/**
|
|
* Signals this panel to indicate if this panel is visible to turn on the FFT processor when the panel is visible
|
|
* and turn off the FFT processor when it's not.
|
|
*
|
|
* Note: this method is intended to be called by the Swing event thread to ensure that only a single thread is
|
|
* invoking either this method, or the receive() method, since there is no thread synchronization between these
|
|
* two methods and they each depend on stable access to the mPanelVisible variable.
|
|
*
|
|
* @param visible true to indicate that this panel is showing/visible.
|
|
*/
|
|
public void setPanelVisible(boolean visible)
|
|
{
|
|
mPanelVisible = visible;
|
|
updateFFTProcessing();
|
|
mNoiseSquelchView.setShowing(visible);
|
|
mSymbolView.setShowing(visible);
|
|
}
|
|
|
|
/**
|
|
* Updates processing state for the spectral display. Turns on DFT processing when we have a processing chain and
|
|
* when the user has this tab selected and visible. Otherwise, turns off DFT processing.
|
|
*/
|
|
private synchronized void updateFFTProcessing()
|
|
{
|
|
if(mChannelSpectralDisplay == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
boolean active = mPanelVisible && mProcessingChain != null;
|
|
|
|
if(active)
|
|
{
|
|
mSampleStreamTapModule.setListener(mChannelSpectralDisplay.getSampleListener());
|
|
}
|
|
else
|
|
{
|
|
mSampleStreamTapModule.removeListener();
|
|
}
|
|
|
|
mChannelSpectralDisplay.setActive(active);
|
|
}
|
|
|
|
private void broadcast(SourceEvent sourceEvent)
|
|
{
|
|
if(mProcessingChain != null)
|
|
{
|
|
mProcessingChain.broadcast(sourceEvent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets controls when changing processing chain source. Note: this must be called on the Swing
|
|
* dispatch thread because it directly invokes swing components.
|
|
*/
|
|
private void reset()
|
|
{
|
|
mTunerPPMLabel.setText(LABEL_PREFIX_PPM);
|
|
mTunerPPMLabel.setEnabled(false);
|
|
mTunerCorrectionLabel.setText(LABEL_PREFIX_TUNER);
|
|
mTunerCorrectionLabel.setEnabled(false);
|
|
mChannelCorrectionLabel.setText(LABEL_PREFIX_CHANNEL);
|
|
mChannelCorrectionLabel.setEnabled(false);
|
|
mDecoderCorrectionLabel.setText(LABEL_PREFIX_DECODER);
|
|
mDecoderCorrectionLabel.setEnabled(false);
|
|
setChannel(0, 0, 0);
|
|
}
|
|
|
|
/**
|
|
* Receive notifications of request to provide display of processing chain details.
|
|
*/
|
|
@Override
|
|
public void receive(ProcessingChain processingChain)
|
|
{
|
|
//Disconnect the previous processing chain.
|
|
if(mProcessingChain != null)
|
|
{
|
|
mNoiseSquelchView.setController(null);
|
|
mSignalPowerView.setProcessingChain(null);
|
|
mSymbolView.removeSymbolProvider();
|
|
mSymbolView.setProtocol("");
|
|
mProcessingChain.removeSourceEventListener(mSourceEventProcessor);
|
|
mProcessingChain.removeModule(mSampleStreamTapModule);
|
|
mFeedbackDecoder = null;
|
|
}
|
|
|
|
//Invoking reset - we're on the Swing dispatch thread here
|
|
reset();
|
|
|
|
mProcessingChain = processingChain;
|
|
|
|
if(mProcessingChain != null)
|
|
{
|
|
mProcessingChain.addSourceEventListener(mSourceEventProcessor);
|
|
|
|
PrimaryDecoder primaryDecoder = mProcessingChain.getPrimaryDecoder();
|
|
if(primaryDecoder instanceof NBFMDecoder nbfmDecoder)
|
|
{
|
|
setRightComponent(mNoiseSquelchPanel);
|
|
mNoiseSquelchView.setController(nbfmDecoder);
|
|
}
|
|
else if(primaryDecoder instanceof AMDecoder)
|
|
{
|
|
setRightComponent(mSignalPowerView);
|
|
mSignalPowerView.setProcessingChain(mProcessingChain);
|
|
}
|
|
else if(primaryDecoder instanceof FeedbackDecoder feedbackDecoder)
|
|
{
|
|
setRightComponent(mSymbolPanel);
|
|
mSymbolView.setSymbolProvider(feedbackDecoder);
|
|
mSymbolView.setProtocol(feedbackDecoder.getProtocolDescription());
|
|
mFeedbackDecoder = feedbackDecoder;
|
|
}
|
|
|
|
mProcessingChain.addModule(mSampleStreamTapModule);
|
|
Source source = mProcessingChain.getSource();
|
|
|
|
int bandwidth = 0;
|
|
Channel channel = mPlaylistManager.getChannelProcessingManager().getChannel(mProcessingChain);
|
|
|
|
if(channel != null)
|
|
{
|
|
List<TunerChannel> tunerChannels = channel.getTunerChannels();
|
|
|
|
if(!tunerChannels.isEmpty())
|
|
{
|
|
bandwidth = tunerChannels.getFirst().getBandwidth();
|
|
}
|
|
}
|
|
|
|
if(source instanceof TunerChannelSource tcs)
|
|
{
|
|
setChannel(tcs.getFrequency(), (int)tcs.getSampleRate(), bandwidth);
|
|
}
|
|
}
|
|
|
|
updateFFTProcessing();
|
|
}
|
|
|
|
/**
|
|
* Shows the component on the right side of the split pane.
|
|
* @param component to show.
|
|
*/
|
|
private void setRightComponent(Component component)
|
|
{
|
|
Component rightComponent = mSplitPane.getRightComponent();
|
|
|
|
if(rightComponent != component)
|
|
{
|
|
mSplitPane.remove(rightComponent);
|
|
mSplitPane.setRightComponent(component);
|
|
mSplitPane.setDividerLocation(0.4);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processor for source event stream to capture power level and squelch related source events.
|
|
*/
|
|
private class SourceEventProcessor implements Listener<SourceEvent>
|
|
{
|
|
@Override
|
|
public void receive(SourceEvent sourceEvent)
|
|
{
|
|
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_STATUS)
|
|
{
|
|
if(sourceEvent instanceof ChannelFrequencyCorrectionStatusNotification status)
|
|
{
|
|
//Note: we flip the sign on the error measurement because the value represents the amount of offset the PLL
|
|
//has to apply to move the signal to center/baseband
|
|
EventQueue.invokeLater(() -> {
|
|
mTunerPPMLabel.setText(LABEL_PREFIX_PPM + PPM_FORMAT.format(status.getTunerPPM()));
|
|
mTunerPPMLabel.setEnabled(true);
|
|
|
|
if(status.isAutoPPM())
|
|
{
|
|
mTunerCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_TUNER, status.getTunerCorrection()));
|
|
}
|
|
else
|
|
{
|
|
mTunerCorrectionLabel.setText(LABEL_PREFIX_TUNER + " disabled");
|
|
}
|
|
mTunerCorrectionLabel.setEnabled(true);
|
|
mChannelCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_CHANNEL, status.getChannelCorrection()));
|
|
mChannelCorrectionLabel.setEnabled(true);
|
|
mDecoderCorrectionLabel.setText(getPaddedLabel(LABEL_PREFIX_DECODER, status.getDecoderCorrection()));
|
|
mDecoderCorrectionLabel.setEnabled(true);
|
|
|
|
//We se the offset cursor line to negative so that it aligns with where the signal is located
|
|
//instead of the correction that's needed to bring the signal to zero.
|
|
setCarrierOffset(-status.getDecoderCorrection());
|
|
});
|
|
}
|
|
}
|
|
|
|
//TODO: is this still needed?
|
|
mSignalPowerView.receive(sourceEvent);
|
|
}
|
|
}
|
|
}
|