mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
Closes #7 NXDN Decoder * Implements: NXDN4800, NXDN9600, Type-C and Type-D decoding and trunk tracking. * Redesigns tuner and channel frequency error management.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* 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
|
||||
@@ -33,6 +33,7 @@ import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamplesToNativeBufferModule;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
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;
|
||||
@@ -75,6 +76,11 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
{
|
||||
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();
|
||||
@@ -83,7 +89,11 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
private final FrequencyOverlayPanel mFrequencyOverlayPanel;
|
||||
private final SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
|
||||
private final SpinnerNumberModel mNoiseFloorSpinnerModel;
|
||||
private final JLabel mEstimatedCarrierOffsetFrequencyValueLabel;
|
||||
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 boolean mDftProcessing = false;
|
||||
private final NoiseSquelchView mNoiseSquelchView;
|
||||
@@ -92,6 +102,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
private final JFXPanel mNoiseSquelchPanel;
|
||||
private final JFXPanel mSymbolPanel;
|
||||
private JSplitPane mSplitPane;
|
||||
private FeedbackDecoder mFeedbackDecoder;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
@@ -107,12 +118,27 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
fftPanel.setLayout(new MigLayout("insets 0", "[grow,fill]", "[][grow,fill]"));
|
||||
|
||||
JPanel labelPanel = new JPanel();
|
||||
labelPanel.setLayout(new MigLayout("insets 2", "[grow,fill][grow,fill,left][right][][]", ""));
|
||||
labelPanel.add(new JLabel("Channel Spectrum "));
|
||||
labelPanel.setLayout(new MigLayout("insets 2", "[grow,fill][][][][][grow,fill][][]", ""));
|
||||
|
||||
mEstimatedCarrierOffsetFrequencyValueLabel = new JLabel(getPaddedCarrierOffsetLabel(0));
|
||||
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
|
||||
labelPanel.add(mEstimatedCarrierOffsetFrequencyValueLabel);
|
||||
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);
|
||||
|
||||
mNoiseFloorSpinnerModel = new SpinnerNumberModel(18, 8, 36, 1);
|
||||
mNoiseFloorSpinnerModel.addChangeListener(e -> {
|
||||
@@ -120,8 +146,8 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
mSpectrumPanel.setSampleSize(number.doubleValue());
|
||||
});
|
||||
JSpinner noiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel);
|
||||
labelPanel.add(new JLabel("Noise Floor:"));
|
||||
labelPanel.add(noiseFloorSpinner);
|
||||
labelPanel.add(new JLabel("Noise Floor"));
|
||||
|
||||
JButton logIndexesButton = new JButton("Log Settings");
|
||||
logIndexesButton.addActionListener(e -> {
|
||||
@@ -229,11 +255,15 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
mSpectrumPanel.clearSpectrum();
|
||||
}
|
||||
|
||||
private String getPaddedCarrierOffsetLabel(long value)
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
String paddedValue = StringUtils.leftPad(String.valueOf(value), 5);
|
||||
String paddedText = StringUtils.rightPad(paddedValue + " Hz", 20);
|
||||
return "Carrier Offset: " + paddedText;
|
||||
return prefix + StringUtils.leftPad(String.valueOf(value), 7);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,22 +327,6 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the CarrierOffsetProcessor's current carrier offset tracking frequency
|
||||
* @param carrierOffsetFrequency that is currently measured/estimated.
|
||||
*/
|
||||
private void updateEstimatedCarrierOffsetFrequency(long carrierOffsetFrequency)
|
||||
{
|
||||
//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(() -> {
|
||||
mEstimatedCarrierOffsetFrequencyValueLabel.setText(getPaddedCarrierOffsetLabel(carrierOffsetFrequency));
|
||||
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(true);
|
||||
});
|
||||
|
||||
mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(carrierOffsetFrequency);
|
||||
}
|
||||
|
||||
private void broadcast(SourceEvent sourceEvent)
|
||||
{
|
||||
if(mProcessingChain != null)
|
||||
@@ -327,8 +341,14 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
*/
|
||||
private void reset()
|
||||
{
|
||||
mEstimatedCarrierOffsetFrequencyValueLabel.setText(getPaddedCarrierOffsetLabel(0));
|
||||
mEstimatedCarrierOffsetFrequencyValueLabel.setEnabled(false);
|
||||
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);
|
||||
mFrequencyOverlayPanel.process(SourceEvent.frequencyChange(null, 0));
|
||||
mFrequencyOverlayPanel.process(SourceEvent.sampleRateChange(0));
|
||||
mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(0);
|
||||
@@ -350,6 +370,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
mSymbolView.setProtocol("");
|
||||
mProcessingChain.removeSourceEventListener(mSourceEventProcessor);
|
||||
mProcessingChain.removeModule(mSampleStreamTapModule);
|
||||
mFeedbackDecoder = null;
|
||||
}
|
||||
|
||||
//Invoking reset - we're on the Swing dispatch thread here
|
||||
@@ -377,6 +398,7 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
setRightComponent(mSymbolPanel);
|
||||
mSymbolView.setSymbolProvider(feedbackDecoder);
|
||||
mSymbolView.setProtocol(feedbackDecoder.getProtocolDescription());
|
||||
mFeedbackDecoder = feedbackDecoder;
|
||||
}
|
||||
|
||||
mProcessingChain.addModule(mSampleStreamTapModule);
|
||||
@@ -428,11 +450,38 @@ public class ChannelSpectrumPanel extends JPanel implements Listener<ProcessingC
|
||||
@Override
|
||||
public void receive(SourceEvent sourceEvent)
|
||||
{
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED)
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_STATUS)
|
||||
{
|
||||
updateEstimatedCarrierOffsetFrequency(sourceEvent.getValue().longValue());
|
||||
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.
|
||||
mFrequencyOverlayPanel.setEstimatedCarrierOffsetFrequency(-status.getDecoderCorrection());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: is this still needed?
|
||||
mSignalPowerView.receive(sourceEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* 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
|
||||
@@ -744,7 +744,6 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
dmrMenu.getItems().add(new AddRadioIdRangeItem(Protocol.DMR));
|
||||
dmrMenu.getItems().add(new AddTonesItem("AMBE Audio Tones"));
|
||||
|
||||
|
||||
Menu fleetsyncMenu = new ProtocolMenu(Protocol.FLEETSYNC);
|
||||
fleetsyncMenu.getItems().add(new AddTalkgroupItem(Protocol.FLEETSYNC));
|
||||
fleetsyncMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.FLEETSYNC));
|
||||
@@ -766,6 +765,13 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM));
|
||||
nbfmMenu.getItems().add(new AddDcsItem());
|
||||
|
||||
Menu nxdnMenu = new ProtocolMenu(Protocol.NXDN);
|
||||
nxdnMenu.getItems().add(new AddTalkgroupItem(Protocol.NXDN));
|
||||
nxdnMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NXDN));
|
||||
nxdnMenu.getItems().add(new AddRadioIdItem(Protocol.NXDN));
|
||||
nxdnMenu.getItems().add(new AddRadioIdRangeItem(Protocol.NXDN));
|
||||
nxdnMenu.getItems().add(new AddTonesItem("AMBE Audio Tones"));
|
||||
|
||||
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
|
||||
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
|
||||
passportMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.PASSPORT));
|
||||
@@ -779,7 +785,7 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
lojackMenu.getItems().add(new AddLojackItem());
|
||||
|
||||
mAddIdentifierButton.getItems().addAll(amMenu, p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu,
|
||||
nbfmMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
|
||||
nbfmMenu, nxdnMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
|
||||
}
|
||||
|
||||
return mAddIdentifierButton;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* 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
|
||||
@@ -25,6 +25,8 @@ import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.HPos;
|
||||
@@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Editor for talkgroup alias identifiers
|
||||
*/
|
||||
@@ -244,6 +243,10 @@ public class RadioIdEditor extends IdentifierEditor<Radio>
|
||||
"Format: 0 - 16777215"));
|
||||
mRadioDetails.add(new RadioDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
|
||||
"Format: 0 - FFFFFF"));
|
||||
mRadioDetails.add(new RadioDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
|
||||
"Format: 0 - 65,535"));
|
||||
mRadioDetails.add(new RadioDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
|
||||
"Format: 0 - FFFF"));
|
||||
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
|
||||
"Format: 0 - 8388607"));
|
||||
mRadioDetails.add(new RadioDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* 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
|
||||
@@ -25,6 +25,8 @@ import io.github.dsheirer.gui.control.IntegerFormatter;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.HPos;
|
||||
@@ -36,9 +38,6 @@ import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Editor for radio ID range alias identifiers
|
||||
*/
|
||||
@@ -293,6 +292,10 @@ public class RadioIdRangeEditor extends IdentifierEditor<RadioRange>
|
||||
new IntegerFormatter(0,0xFFFFFF), "Format: 0 - 16777215"));
|
||||
details.add(new RadioIdDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFFFF),
|
||||
new HexFormatter(0,0xFFFFFF), "Format: 0 - FFFFFF"));
|
||||
details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
|
||||
new IntegerFormatter(0,0xFFFF),"Format: 0 - 65,535"));
|
||||
details.add(new RadioIdDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
|
||||
new IntegerFormatter(0,0xFFFF),"Format: 0 - FFFF"));
|
||||
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0x7FFFFF),
|
||||
new IntegerFormatter(0,0x7FFFFF), "Format: 0 - 8388607"));
|
||||
details.add(new RadioIdDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0x7FFFFF),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* 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
|
||||
@@ -263,6 +263,10 @@ public class TalkgroupEditor extends IdentifierEditor<Talkgroup>
|
||||
"Format: 1 - 65535"));
|
||||
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
|
||||
"Format: 1 - FFFF"));
|
||||
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF),
|
||||
"Format: 1 - 65,535"));
|
||||
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
|
||||
"Format: 1 - FFFF"));
|
||||
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
|
||||
"Format: 0 - 65535"));
|
||||
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* 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
|
||||
@@ -298,6 +298,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
|
||||
new IntegerFormatter(0,65535), "Format: 0 - 65535"));
|
||||
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
|
||||
new HexFormatter(0,65535), "Format: 0 - FFFF"));
|
||||
details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,16777215),
|
||||
new IntegerFormatter(1,16777215), "Format: 1 - 16777215"));
|
||||
details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0XFFFFFF),
|
||||
new HexFormatter(1, 0xFFFFFF), "Format: 0 - FFFFFF"));
|
||||
details.add(new TalkgroupDetail(Protocol.FLEETSYNC, IntegerFormat.FORMATTED,
|
||||
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
|
||||
"Format: PPP-IIII = Prefix (0-127), Ident (0-8191)"));
|
||||
@@ -314,6 +318,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
|
||||
new IntegerFormatter(0,65535), "Format 0 - 65535"));
|
||||
details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535),
|
||||
new IntegerFormatter(0,65535), "Format 0 - FFFF"));
|
||||
details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF),
|
||||
new IntegerFormatter(1,0xFFFF),"Format: 1 - 65,535"));
|
||||
details.add(new TalkgroupDetail(Protocol.NXDN, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
|
||||
new IntegerFormatter(1,0xFFFF),"Format: 1 - FFFF"));
|
||||
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
|
||||
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
|
||||
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),
|
||||
@@ -324,10 +332,6 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
|
||||
new IntegerFormatter(0,16777215), "Format: 0 - FFFFFF"));
|
||||
details.add(new TalkgroupDetail(Protocol.UNKNOWN, IntegerFormat.HEXADECIMAL, new HexFormatter(0,16777215),
|
||||
new HexFormatter(0,16777215), "Format: 0 - FFFFFF"));
|
||||
details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.DECIMAL, new IntegerFormatter(1,16777215),
|
||||
new IntegerFormatter(1,16777215), "Format: 1 - 16777215"));
|
||||
details.add(new TalkgroupDetail(Protocol.DMR, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0XFFFFFF),
|
||||
new HexFormatter(1, 0xFFFFFF), "Format: 0 - FFFFFF"));
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* 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
|
||||
@@ -55,14 +55,16 @@ public class ChannelConfigurationEditorFactory
|
||||
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case DMR:
|
||||
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case NBFM:
|
||||
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case LTR_NET:
|
||||
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case LTR:
|
||||
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case MPT1327:
|
||||
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case NBFM:
|
||||
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case NXDN:
|
||||
return new NXDNConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case PASSPORT:
|
||||
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case P25_PHASE1:
|
||||
|
||||
@@ -0,0 +1,718 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.playlist.channel;
|
||||
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.record.RecordConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.source.FrequencyEditor;
|
||||
import io.github.dsheirer.gui.playlist.source.FrequencyField;
|
||||
import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||
import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
|
||||
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
|
||||
import io.github.dsheirer.module.decode.nxdn.channel.ObservableChannelFrequency;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.proprietary.Encoding;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* NXDN channel configuration editor
|
||||
*/
|
||||
public class NXDNConfigurationEditor extends ChannelConfigurationEditor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(NXDNConfigurationEditor.class);
|
||||
private TitledPane mDecoderPane;
|
||||
private TitledPane mEventLogPane;
|
||||
private TitledPane mRecordPane;
|
||||
private TitledPane mSourcePane;
|
||||
private SourceConfigurationEditor mSourceConfigurationEditor;
|
||||
private EventLogConfigurationEditor mEventLogConfigurationEditor;
|
||||
private RecordConfigurationEditor mRecordConfigurationEditor;
|
||||
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
|
||||
private TableView<ObservableChannelFrequency> mChannelMapTable;
|
||||
private IntegerTextField mChannelField;
|
||||
private FrequencyField mDownlinkFrequencyField;
|
||||
private Button mAddTimeslotFrequencyButton;
|
||||
private Button mDeleteButton;
|
||||
private Spinner<Integer> mChannelRotationDelaySpinner;
|
||||
private SegmentedButton mTransmissionModeButton;
|
||||
private ComboBox<Encoding> mEncodingComboBox;
|
||||
private ToggleSwitch mIgnoreDataCallsToggle;
|
||||
private ToggleSwitch mIgnoreEncryptedCallsToggle;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for playlists
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for preferences
|
||||
*/
|
||||
public NXDNConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
||||
UserPreferences userPreferences, IFilterProcessor filterProcessor)
|
||||
{
|
||||
super(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
||||
getTitledPanesBox().getChildren().add(getRecordPane());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return DecoderType.NXDN;
|
||||
}
|
||||
|
||||
private TitledPane getSourcePane()
|
||||
{
|
||||
if(mSourcePane == null)
|
||||
{
|
||||
mSourcePane = new TitledPane("Source", getSourceConfigurationEditor());
|
||||
mSourcePane.setExpanded(true);
|
||||
}
|
||||
|
||||
return mSourcePane;
|
||||
}
|
||||
|
||||
private TitledPane getDecoderPane()
|
||||
{
|
||||
if(mDecoderPane == null)
|
||||
{
|
||||
mDecoderPane = new TitledPane();
|
||||
mDecoderPane.setText("Decoder: NXDN");
|
||||
mDecoderPane.setExpanded(true);
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setPadding(new Insets(10,10,10,10));
|
||||
gridPane.setHgap(10);
|
||||
gridPane.setVgap(10);
|
||||
|
||||
int row = 0;
|
||||
|
||||
Label mode = new Label("Mode:");
|
||||
GridPane.setHalignment(mode, HPos.RIGHT);
|
||||
GridPane.setConstraints(mode, 0, row);
|
||||
gridPane.getChildren().add(mode);
|
||||
|
||||
GridPane.setConstraints(getTransmissionModeButton(), 1, row);
|
||||
gridPane.getChildren().add(getTransmissionModeButton());
|
||||
|
||||
Label encoding = new Label("Talker Alias Encoding:");
|
||||
GridPane.setHalignment(encoding, HPos.RIGHT);
|
||||
GridPane.setConstraints(encoding, 2, row);
|
||||
gridPane.getChildren().add(encoding);
|
||||
|
||||
GridPane.setConstraints(getEncodingComboBox(), 3, row);
|
||||
gridPane.getChildren().add(getEncodingComboBox());
|
||||
|
||||
Label poolSizeLabel = new Label("Max Traffic Channels");
|
||||
GridPane.setHalignment(poolSizeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(poolSizeLabel, 4, row);
|
||||
gridPane.getChildren().add(poolSizeLabel);
|
||||
|
||||
GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 5, row);
|
||||
gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner());
|
||||
|
||||
Label ignoreLabel = new Label("Ignore:");
|
||||
GridPane.setHalignment(ignoreLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(ignoreLabel, 0, ++row);
|
||||
gridPane.getChildren().add(ignoreLabel);
|
||||
|
||||
GridPane.setConstraints(getIgnoreDataCallsToggle(), 1, row);
|
||||
gridPane.getChildren().add(getIgnoreDataCallsToggle());
|
||||
|
||||
GridPane.setConstraints(getIgnoreEncryptedCallsToggle(), 2, row, 2, 1);
|
||||
gridPane.getChildren().add(getIgnoreEncryptedCallsToggle());
|
||||
|
||||
Label channelMapTableLabel = new Label("Optional Channel Number (LCN) to Frequency Map for Channel-Mode systems. Ignore for Direct Frequency Access (DFA) systems.");
|
||||
GridPane.setHalignment(channelMapTableLabel, HPos.LEFT);
|
||||
GridPane.setConstraints(channelMapTableLabel, 0, ++row, 6, 1);
|
||||
gridPane.getChildren().add(channelMapTableLabel);
|
||||
|
||||
GridPane.setConstraints(getChannelMapTable(), 0, ++row, 6, 3);
|
||||
gridPane.getChildren().add(getChannelMapTable());
|
||||
|
||||
VBox buttonsBox = new VBox();
|
||||
buttonsBox.setAlignment(Pos.CENTER);
|
||||
buttonsBox.setSpacing(10);
|
||||
buttonsBox.getChildren().addAll(getAddTimeslotFrequencyButton(), getDeleteButton());
|
||||
|
||||
GridPane.setConstraints(buttonsBox, 6, row, 1, 3);
|
||||
gridPane.getChildren().addAll(buttonsBox);
|
||||
|
||||
row += 3;
|
||||
|
||||
HBox editorBox = new HBox();
|
||||
editorBox.setAlignment(Pos.CENTER_LEFT);
|
||||
editorBox.setSpacing(5);
|
||||
|
||||
Label lcnLabel = new Label("Channel Number");
|
||||
editorBox.getChildren().addAll(lcnLabel, getChannelField());
|
||||
|
||||
Label downlinkLabel = new Label("Downlink Frequency (MHz)");
|
||||
downlinkLabel.setPadding(new Insets(0,0,0,5));
|
||||
editorBox.getChildren().addAll(downlinkLabel,getDownlinkFrequencyField());
|
||||
|
||||
GridPane.setConstraints(editorBox, 0, row, 4, 1);
|
||||
gridPane.getChildren().add(editorBox);
|
||||
|
||||
mDecoderPane.setContent(gridPane);
|
||||
}
|
||||
|
||||
return mDecoderPane;
|
||||
}
|
||||
|
||||
private TitledPane getEventLogPane()
|
||||
{
|
||||
if(mEventLogPane == null)
|
||||
{
|
||||
mEventLogPane = new TitledPane("Logging", getEventLogConfigurationEditor());
|
||||
mEventLogPane.setExpanded(false);
|
||||
}
|
||||
|
||||
return mEventLogPane;
|
||||
}
|
||||
|
||||
private TitledPane getRecordPane()
|
||||
{
|
||||
if(mRecordPane == null)
|
||||
{
|
||||
mRecordPane = new TitledPane();
|
||||
mRecordPane.setText("Recording");
|
||||
mRecordPane.setExpanded(false);
|
||||
|
||||
Label notice = new Label("Note: use aliases to control call audio recording");
|
||||
notice.setPadding(new Insets(10, 10, 0, 10));
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.getChildren().addAll(getRecordConfigurationEditor(), notice);
|
||||
|
||||
mRecordPane.setContent(vBox);
|
||||
}
|
||||
|
||||
return mRecordPane;
|
||||
}
|
||||
|
||||
private SourceConfigurationEditor getSourceConfigurationEditor()
|
||||
{
|
||||
if(mSourceConfigurationEditor == null)
|
||||
{
|
||||
mSourceConfigurationEditor = new FrequencyEditor(mTunerManager,
|
||||
DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MINIMUM_MS,
|
||||
DecodeConfigDMR.CHANNEL_ROTATION_DELAY_MAXIMUM_MS,
|
||||
DecodeConfigDMR.CHANNEL_ROTATION_DELAY_DEFAULT_MS);
|
||||
|
||||
//Add a listener so that we can push change notifications up to this editor
|
||||
mSourceConfigurationEditor.modifiedProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mSourceConfigurationEditor;
|
||||
}
|
||||
|
||||
private EventLogConfigurationEditor getEventLogConfigurationEditor()
|
||||
{
|
||||
if(mEventLogConfigurationEditor == null)
|
||||
{
|
||||
List<EventLogType> types = new ArrayList<>();
|
||||
types.add(EventLogType.CALL_EVENT);
|
||||
types.add(EventLogType.DECODED_MESSAGE);
|
||||
types.add(EventLogType.TRAFFIC_CALL_EVENT);
|
||||
types.add(EventLogType.TRAFFIC_DECODED_MESSAGE);
|
||||
|
||||
mEventLogConfigurationEditor = new EventLogConfigurationEditor(types);
|
||||
mEventLogConfigurationEditor.setPadding(new Insets(5,5,5,5));
|
||||
mEventLogConfigurationEditor.modifiedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mEventLogConfigurationEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talker alias encoding combo box
|
||||
* @return encoding combo box.
|
||||
*/
|
||||
private ComboBox<Encoding> getEncodingComboBox()
|
||||
{
|
||||
if(mEncodingComboBox == null)
|
||||
{
|
||||
mEncodingComboBox = new ComboBox<>(FXCollections.observableArrayList(Arrays.stream(Encoding.values()).toList()));
|
||||
mEncodingComboBox.getSelectionModel().select(0);
|
||||
mEncodingComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
mEncodingComboBox.setTooltip(new Tooltip("Talker Alias Encoding: UTF-8:Latin/English, BIG5:Taiwan"));
|
||||
}
|
||||
|
||||
return mEncodingComboBox;
|
||||
}
|
||||
|
||||
private TableView<ObservableChannelFrequency> getChannelMapTable()
|
||||
{
|
||||
if(mChannelMapTable == null)
|
||||
{
|
||||
mChannelMapTable = new TableView<>(FXCollections.observableArrayList(ObservableChannelFrequency.extractor()));
|
||||
mChannelMapTable.setPrefHeight(100.0);
|
||||
|
||||
TableColumn<ObservableChannelFrequency,Number> channelColumn = new TableColumn<>("Channel");
|
||||
channelColumn.setPrefWidth(100);
|
||||
channelColumn.setCellValueFactory(cellData -> cellData.getValue().channelProperty());
|
||||
mChannelMapTable.getColumns().add(channelColumn);
|
||||
mChannelMapTable.getSortOrder().add(channelColumn);
|
||||
|
||||
TableColumn<ObservableChannelFrequency,Number> downlinkColumn = new TableColumn<>("Downlink Frequency (MHz)");
|
||||
downlinkColumn.setCellValueFactory(cellData -> cellData.getValue().downlinkProperty());
|
||||
downlinkColumn.setPrefWidth(200);
|
||||
mChannelMapTable.getColumns().add(downlinkColumn);
|
||||
|
||||
mChannelMapTable.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> setChannelFrequency(newValue));
|
||||
}
|
||||
|
||||
return mChannelMapTable;
|
||||
}
|
||||
|
||||
private SegmentedButton getTransmissionModeButton()
|
||||
{
|
||||
if(mTransmissionModeButton == null)
|
||||
{
|
||||
mTransmissionModeButton = new SegmentedButton();
|
||||
mTransmissionModeButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
|
||||
mTransmissionModeButton.setDisable(true);
|
||||
|
||||
ToggleButton tb4800 = new ToggleButton(TransmissionMode.M4800.getLabel());
|
||||
tb4800.setUserData(TransmissionMode.M4800);
|
||||
mTransmissionModeButton.getButtons().add(tb4800);
|
||||
|
||||
ToggleButton tb9600 = new ToggleButton(TransmissionMode.M9600.getLabel());
|
||||
tb9600.setUserData(TransmissionMode.M9600);
|
||||
mTransmissionModeButton.getButtons().add(tb9600);
|
||||
|
||||
ToggleButton tbTypeD = new ToggleButton(TransmissionMode.TYPE_D.getLabel());
|
||||
tbTypeD.setUserData(TransmissionMode.TYPE_D);
|
||||
mTransmissionModeButton.getButtons().add(tbTypeD);
|
||||
|
||||
mTransmissionModeButton.getToggleGroup().selectToggle(tb4800);
|
||||
|
||||
mTransmissionModeButton.getToggleGroup().selectedToggleProperty()
|
||||
.addListener((observable, oldValue, newValue) -> {
|
||||
//Only trigger change detection when both values are not null and not equal to each other
|
||||
if(oldValue != null && newValue != null && !oldValue.equals(newValue))
|
||||
{
|
||||
//Only set modified if the toggle changed from one to the other
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mTransmissionModeButton;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Sets the specified timeslot frequency into the editor
|
||||
*/
|
||||
private void setChannelFrequency(ObservableChannelFrequency channelFrequency)
|
||||
{
|
||||
//Preserve the current modified flag state since setting values in the editor will change it.
|
||||
boolean modified = modifiedProperty().get();
|
||||
|
||||
getChannelField().setDisable(channelFrequency == null);
|
||||
getDownlinkFrequencyField().setDisable(channelFrequency == null);
|
||||
getDeleteButton().setDisable(channelFrequency == null);
|
||||
|
||||
if(channelFrequency != null)
|
||||
{
|
||||
getChannelField().set(channelFrequency.channelProperty().get());
|
||||
getDownlinkFrequencyField().set(channelFrequency.downlinkProperty().get());
|
||||
}
|
||||
else
|
||||
{
|
||||
getChannelField().set(0);
|
||||
getDownlinkFrequencyField().set(0);
|
||||
}
|
||||
|
||||
modifiedProperty().set(modified);
|
||||
}
|
||||
|
||||
private Button getAddTimeslotFrequencyButton()
|
||||
{
|
||||
if(mAddTimeslotFrequencyButton == null)
|
||||
{
|
||||
mAddTimeslotFrequencyButton = new Button("Add");
|
||||
mAddTimeslotFrequencyButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mAddTimeslotFrequencyButton.setOnAction(event -> addChannelFrequency());
|
||||
}
|
||||
|
||||
return mAddTimeslotFrequencyButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new channel frequency entry
|
||||
*/
|
||||
private void addChannelFrequency()
|
||||
{
|
||||
ObservableChannelFrequency ocf = new ObservableChannelFrequency();
|
||||
getChannelMapTable().getItems().add(ocf);
|
||||
getChannelMapTable().scrollTo(ocf);
|
||||
getChannelMapTable().getSelectionModel().select(ocf);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
|
||||
private Button getDeleteButton()
|
||||
{
|
||||
if(mDeleteButton == null)
|
||||
{
|
||||
mDeleteButton = new Button("Delete");
|
||||
mDeleteButton.setDisable(true);
|
||||
mDeleteButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mDeleteButton.setOnAction(event -> {
|
||||
ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
getChannelMapTable().getItems().remove(selected);
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mDeleteButton;
|
||||
}
|
||||
|
||||
private IntegerTextField getChannelField()
|
||||
{
|
||||
if(mChannelField == null)
|
||||
{
|
||||
mChannelField = new IntegerTextField();
|
||||
mChannelField.setDisable(true);
|
||||
mChannelField.setPrefWidth(65);
|
||||
mChannelField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
Integer value = mChannelField.get();
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
selected.channelProperty().set(value);
|
||||
}
|
||||
}
|
||||
|
||||
modifiedProperty().set(true);
|
||||
});
|
||||
}
|
||||
|
||||
return mChannelField;
|
||||
}
|
||||
|
||||
private FrequencyField getDownlinkFrequencyField()
|
||||
{
|
||||
if(mDownlinkFrequencyField == null)
|
||||
{
|
||||
mDownlinkFrequencyField = new FrequencyField();
|
||||
mDownlinkFrequencyField.setDisable(true);
|
||||
mDownlinkFrequencyField.textProperty().addListener(new ChangeListener<String>()
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue)
|
||||
{
|
||||
ObservableChannelFrequency selected = getChannelMapTable().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
selected.downlinkProperty().set(mDownlinkFrequencyField.get());
|
||||
}
|
||||
|
||||
modifiedProperty().set(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mDownlinkFrequencyField;
|
||||
}
|
||||
|
||||
private Spinner<Integer> getTrafficChannelPoolSizeSpinner()
|
||||
{
|
||||
if(mTrafficChannelPoolSizeSpinner == null)
|
||||
{
|
||||
mTrafficChannelPoolSizeSpinner = new Spinner();
|
||||
mTrafficChannelPoolSizeSpinner.setDisable(true);
|
||||
mTrafficChannelPoolSizeSpinner.setTooltip(
|
||||
new Tooltip("Maximum number of traffic channels that can be created by the decoder"));
|
||||
mTrafficChannelPoolSizeSpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
|
||||
SpinnerValueFactory<Integer> svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 50);
|
||||
mTrafficChannelPoolSizeSpinner.setValueFactory(svf);
|
||||
mTrafficChannelPoolSizeSpinner.getValueFactory().valueProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mTrafficChannelPoolSizeSpinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle switch for ignore data calls.
|
||||
*/
|
||||
private ToggleSwitch getIgnoreDataCallsToggle()
|
||||
{
|
||||
if(mIgnoreDataCallsToggle == null)
|
||||
{
|
||||
mIgnoreDataCallsToggle = new ToggleSwitch("Data Calls");
|
||||
mIgnoreDataCallsToggle.setDisable(true);
|
||||
mIgnoreDataCallsToggle.selectedProperty().addListener((o, ov, nv) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mIgnoreDataCallsToggle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle switch for ignore encrypted calls.
|
||||
*/
|
||||
private ToggleSwitch getIgnoreEncryptedCallsToggle()
|
||||
{
|
||||
if(mIgnoreEncryptedCallsToggle == null)
|
||||
{
|
||||
mIgnoreEncryptedCallsToggle = new ToggleSwitch("Encrypted Calls");
|
||||
mIgnoreEncryptedCallsToggle.setDisable(true);
|
||||
mIgnoreEncryptedCallsToggle.selectedProperty().addListener((o, ov, nv) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mIgnoreEncryptedCallsToggle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel rotation monitor delay value. This dictates how long the decoder will remain on each frequency before
|
||||
* rotating to the next frequency in the list
|
||||
* @return spinner
|
||||
*/
|
||||
private Spinner<Integer> getChannelRotationDelaySpinner()
|
||||
{
|
||||
if(mChannelRotationDelaySpinner == null)
|
||||
{
|
||||
mChannelRotationDelaySpinner = new Spinner();
|
||||
mChannelRotationDelaySpinner.setDisable(true);
|
||||
mChannelRotationDelaySpinner.setTooltip(
|
||||
new Tooltip("Delay on each frequency before rotating to next when seeking to next active channel frequency"));
|
||||
mChannelRotationDelaySpinner.getStyleClass().add(Spinner.STYLE_CLASS_SPLIT_ARROWS_HORIZONTAL);
|
||||
SpinnerValueFactory<Integer> svf = new SpinnerValueFactory.IntegerSpinnerValueFactory(200, 2000, 200, 50);
|
||||
mChannelRotationDelaySpinner.setValueFactory(svf);
|
||||
mChannelRotationDelaySpinner.getValueFactory().valueProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mChannelRotationDelaySpinner;
|
||||
}
|
||||
|
||||
private RecordConfigurationEditor getRecordConfigurationEditor()
|
||||
{
|
||||
if(mRecordConfigurationEditor == null)
|
||||
{
|
||||
List<RecorderType> types = new ArrayList<>();
|
||||
types.add(RecorderType.BASEBAND);
|
||||
types.add(RecorderType.DEMODULATED_BIT_STREAM);
|
||||
types.add(RecorderType.MBE_CALL_SEQUENCE);
|
||||
types.add(RecorderType.TRAFFIC_BASEBAND);
|
||||
types.add(RecorderType.TRAFFIC_DEMODULATED_BIT_STREAM);
|
||||
types.add(RecorderType.TRAFFIC_MBE_CALL_SEQUENCE);
|
||||
mRecordConfigurationEditor = new RecordConfigurationEditor(types);
|
||||
mRecordConfigurationEditor.setDisable(true);
|
||||
mRecordConfigurationEditor.modifiedProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mRecordConfigurationEditor;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setDecoderConfiguration(DecodeConfiguration config)
|
||||
{
|
||||
getTransmissionModeButton().setDisable(config == null);
|
||||
getTrafficChannelPoolSizeSpinner().setDisable(config == null);
|
||||
getChannelMapTable().getItems().clear();
|
||||
getChannelMapTable().setDisable(config == null);
|
||||
getAddTimeslotFrequencyButton().setDisable(config == null);
|
||||
getDeleteButton().setDisable(true);
|
||||
getChannelField().set(0);
|
||||
getChannelField().setDisable(true);
|
||||
getDownlinkFrequencyField().set(0);
|
||||
getDownlinkFrequencyField().setDisable(true);
|
||||
getChannelRotationDelaySpinner().setDisable(config == null);
|
||||
getIgnoreDataCallsToggle().setDisable(config == null);
|
||||
getIgnoreEncryptedCallsToggle().setDisable(config == null);
|
||||
|
||||
if(config instanceof DecodeConfigNXDN configNXDN)
|
||||
{
|
||||
for(ToggleButton toggle: getTransmissionModeButton().getButtons())
|
||||
{
|
||||
if(toggle.getUserData() == configNXDN.getTransmissionMode())
|
||||
{
|
||||
toggle.setSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(configNXDN.getTrafficChannelPoolSize());
|
||||
|
||||
for(ChannelFrequency channelFrequency: configNXDN.getChannelMap())
|
||||
{
|
||||
getChannelMapTable().getItems().add(new ObservableChannelFrequency(channelFrequency));
|
||||
}
|
||||
|
||||
getIgnoreDataCallsToggle().setSelected(configNXDN.isIgnoreDataCalls());
|
||||
getIgnoreEncryptedCallsToggle().setSelected(configNXDN.isIgnoreEncryptedCalls());
|
||||
}
|
||||
else
|
||||
{
|
||||
getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(0);
|
||||
getChannelRotationDelaySpinner().getValueFactory().setValue(200);
|
||||
getIgnoreDataCallsToggle().setSelected(false);
|
||||
getIgnoreEncryptedCallsToggle().setSelected(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveDecoderConfiguration()
|
||||
{
|
||||
DecodeConfigNXDN config = null;
|
||||
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigNXDN configNXDN)
|
||||
{
|
||||
config = configNXDN;
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new DecodeConfigNXDN(TransmissionMode.M4800);
|
||||
}
|
||||
|
||||
TransmissionMode selected = (TransmissionMode)getTransmissionModeButton().getToggleGroup().selectedToggleProperty().get().getUserData();
|
||||
config.setTransmissionMode(selected);
|
||||
|
||||
config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
|
||||
|
||||
List<ChannelFrequency> channelFrequencies = new ArrayList<>();
|
||||
|
||||
for(ObservableChannelFrequency ocf: getChannelMapTable().getItems())
|
||||
{
|
||||
channelFrequencies.add(ocf.getChannel());
|
||||
}
|
||||
|
||||
config.setChannelMap(channelFrequencies);
|
||||
config.setIgnoreDataCalls(getIgnoreDataCallsToggle().isSelected());
|
||||
config.setIgnoreEncryptedCalls(getIgnoreEncryptedCallsToggle().isSelected());
|
||||
getItem().setDecodeConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setEventLogConfiguration(EventLogConfiguration config)
|
||||
{
|
||||
getEventLogConfigurationEditor().setItem(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveEventLogConfiguration()
|
||||
{
|
||||
getEventLogConfigurationEditor().save();
|
||||
|
||||
if(getEventLogConfigurationEditor().getItem().getLoggers().isEmpty())
|
||||
{
|
||||
getItem().setEventLogConfiguration(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
getItem().setEventLogConfiguration(getEventLogConfigurationEditor().getItem());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setAuxDecoderConfiguration(AuxDecodeConfiguration config)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAuxDecoderConfiguration()
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setRecordConfiguration(RecordConfiguration config)
|
||||
{
|
||||
getRecordConfigurationEditor().setDisable(config == null);
|
||||
getRecordConfigurationEditor().setItem(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveRecordConfiguration()
|
||||
{
|
||||
getRecordConfigurationEditor().save();
|
||||
RecordConfiguration config = getRecordConfigurationEditor().getItem();
|
||||
getItem().setRecordConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setSourceConfiguration(SourceConfiguration config)
|
||||
{
|
||||
getSourceConfigurationEditor().setSourceConfiguration(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveSourceConfiguration()
|
||||
{
|
||||
getSourceConfigurationEditor().save();
|
||||
SourceConfiguration sourceConfiguration = getSourceConfigurationEditor().getSourceConfiguration();
|
||||
getItem().setSourceConfiguration(sourceConfiguration);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* 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
|
||||
@@ -35,7 +35,7 @@ public enum ModeDecoderType
|
||||
DSTAR("d-star", null),
|
||||
FM("fm", DecoderType.NBFM),
|
||||
FMN("fmn", DecoderType.NBFM),
|
||||
NXDN("nxdn", null),
|
||||
NXDN("nxdn", DecoderType.NXDN),
|
||||
TELM("telm", null),
|
||||
USB("usb", null),
|
||||
LSB("lsb", null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* 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
|
||||
@@ -25,7 +25,10 @@ import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.UnknownTalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
|
||||
import io.github.dsheirer.module.decode.mpt1327.identifier.MPT1327Talkgroup;
|
||||
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
|
||||
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkgroupIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
|
||||
import io.github.dsheirer.module.decode.passport.identifier.PassportTalkgroup;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
@@ -146,10 +149,14 @@ public class RadioReferenceDecoder
|
||||
{
|
||||
case APCO25:
|
||||
return APCO25Talkgroup.create(value);
|
||||
case DMR:
|
||||
return DMRTalkgroup.create(value);
|
||||
case LTR:
|
||||
return LTRTalkgroup.create(value);
|
||||
case MPT1327:
|
||||
return MPT1327Talkgroup.createTo(value);
|
||||
case NXDN:
|
||||
return NXDNTalkgroupIdentifier.createTo(value);
|
||||
case PASSPORT:
|
||||
return PassportTalkgroup.create(value);
|
||||
default:
|
||||
@@ -385,6 +392,55 @@ public class RadioReferenceDecoder
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the specified site is an NXDN site that has site frequencies that can be converted
|
||||
* to channel frequency map
|
||||
* @param systemInformation to determine the system type
|
||||
* @param site containing site frequencies
|
||||
*/
|
||||
public boolean hasChannelMap(SystemInformation systemInformation, Site site)
|
||||
{
|
||||
Type type = getType(systemInformation);
|
||||
return type != null && type.getName().contains("NXDN") && !site.getSiteFrequencies().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel frequency map listing from the radio reference site's list of site frequencies for the site
|
||||
* @param systemInformation to detect the system type (NXDN)
|
||||
* @param site containing 1 or more site frequencies
|
||||
* @return list of timeslot frequency mappings or an empty list.
|
||||
*/
|
||||
public List<ChannelFrequency> getChannelMap(SystemInformation systemInformation, Site site)
|
||||
{
|
||||
if(hasChannelMap(systemInformation, site))
|
||||
{
|
||||
List<ChannelFrequency> frequencies = new ArrayList<>();
|
||||
|
||||
for(SiteFrequency siteFrequency: site.getSiteFrequencies())
|
||||
{
|
||||
int lcn = siteFrequency.getLogicalChannelNumber();
|
||||
|
||||
if(siteFrequency.getChannelId() != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
lcn = Integer.parseInt(siteFrequency.getChannelId());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//Do nothing, we couldn't parse the LSN from the channel ID value
|
||||
}
|
||||
}
|
||||
|
||||
frequencies.add(new ChannelFrequency(lcn, (long)(siteFrequency.getFrequency() * 1E6), 0));
|
||||
}
|
||||
|
||||
return frequencies;
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the sdrtrunk protocol used by the system.
|
||||
* @param system to identify
|
||||
@@ -430,6 +486,7 @@ public class RadioReferenceDecoder
|
||||
}
|
||||
break;
|
||||
case "NXDN":
|
||||
return Protocol.NXDN;
|
||||
case "EDACS":
|
||||
case "TETRA":
|
||||
case "Midland CMS":
|
||||
@@ -440,6 +497,8 @@ public class RadioReferenceDecoder
|
||||
default:
|
||||
}
|
||||
|
||||
// mLog.info("Type:" + type + " Flavor:" + flavor + " Voice:" + voice);
|
||||
|
||||
return Protocol.UNKNOWN;
|
||||
}
|
||||
|
||||
@@ -503,6 +562,7 @@ public class RadioReferenceDecoder
|
||||
}
|
||||
break;
|
||||
case "NXDN":
|
||||
return DecoderType.NXDN;
|
||||
|
||||
case "EDACS":
|
||||
case "TETRA":
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* 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
|
||||
@@ -29,6 +29,9 @@ import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
|
||||
import io.github.dsheirer.module.decode.nxdn.channel.ChannelFrequency;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.Modulation;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
|
||||
@@ -90,7 +93,6 @@ public class SiteEditor extends GridPane
|
||||
private static final String PRIMARY_CONTROL_CHANNEL = "d";
|
||||
private static final String TOGGLE_BUTTON_CONTROL = "Control";
|
||||
private static final String TOGGLE_BUTTON_P25_VOICE = "All P25 Voice";
|
||||
private static final String PHASE_2_TDMA_MODULATION = "TDMA";
|
||||
private static final String PHASE_2_FLAVOR = "Phase II";
|
||||
|
||||
private UserPreferences mUserPreferences;
|
||||
@@ -250,6 +252,39 @@ public class SiteEditor extends GridPane
|
||||
.getTimeslotFrequencies(systemInformation, site);
|
||||
dmr.setTimeslotMap(timeslotFrequencies);
|
||||
return dmr;
|
||||
case NXDN:
|
||||
Flavor flavor = mRadioReferenceDecoder.getFlavor(systemInformation);
|
||||
|
||||
TransmissionMode mode = TransmissionMode.M4800;
|
||||
|
||||
/**
|
||||
* 9600 bps Flavors
|
||||
* ----------------------------
|
||||
* 29 - NEXEDGE 9600
|
||||
* 45 - Conventional Networked
|
||||
*
|
||||
* 4800 bps Flavors
|
||||
* ---------------------------
|
||||
* 11 - Narrowband Networked
|
||||
* 42 - NEXEDGE 4800
|
||||
* 44 - Icom IDAS Type C
|
||||
*
|
||||
* Type-D (also 4800 bps) Flavors
|
||||
* ---------------------------
|
||||
* 36 - Icom IDAS Type D
|
||||
* 47 - Kenwood Type D
|
||||
*/
|
||||
mode = switch(flavor.getName())
|
||||
{
|
||||
case "NEXEDGE 9600", "Conventional Networked" -> TransmissionMode.M9600;
|
||||
case "Icom IDAS Type D", "Kenwood Type D" -> TransmissionMode.TYPE_D;
|
||||
default -> TransmissionMode.M4800;
|
||||
};
|
||||
|
||||
DecodeConfigNXDN nxdn = new DecodeConfigNXDN(mode);
|
||||
List<ChannelFrequency> channelMap = mRadioReferenceDecoder.getChannelMap(systemInformation, site);
|
||||
nxdn.setChannelMap(channelMap);
|
||||
return nxdn;
|
||||
case P25_PHASE1:
|
||||
DecodeConfiguration p1config = DecoderFactory.getDecodeConfiguration(decoderType);
|
||||
|
||||
@@ -399,7 +434,7 @@ public class SiteEditor extends GridPane
|
||||
getCreateChannelConfigurationButton().setVisible(true);
|
||||
getGoToChannelEditorCheckBox().setVisible(true);
|
||||
getSystemTextField().setText(system.getName());
|
||||
getSiteTextField().setText(mCurrentSite.getCountyName());
|
||||
getSiteTextField().setText(mCurrentSite.getDescription());
|
||||
getNameTextField().setText("Control");
|
||||
|
||||
if(!siteFrequencies.isEmpty())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* 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
|
||||
@@ -115,6 +115,7 @@ public class TunerPreferenceEditor extends HBox
|
||||
Label label = new Label("Please restart the application for this change to take effect");
|
||||
label.setWrapText(true);
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setHeaderText("Restart Required");
|
||||
alert.getDialogPane().setContent(label);
|
||||
alert.initOwner(((Node)getChannelizerTypeChoiceBox()).getScene().getWindow());
|
||||
alert.show();
|
||||
@@ -183,6 +184,7 @@ public class TunerPreferenceEditor extends HBox
|
||||
Label label = new Label("Please restart the application for this change to take effect");
|
||||
label.setWrapText(true);
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setHeaderText("Restart Required");
|
||||
alert.getDialogPane().setContent(label);
|
||||
alert.initOwner(((Node)getRspDuoTunerModeChoiceBox()).getScene().getWindow());
|
||||
alert.show();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* 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
|
||||
@@ -468,6 +468,10 @@ public class DmrViewer extends VBox
|
||||
mSelectFileButton = new Button("Select ...");
|
||||
mSelectFileButton.onActionProperty().set(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
|
||||
FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
|
||||
fileChooser.getExtensionFilters().addAll(f1, f2);
|
||||
fileChooser.setSelectedExtensionFilter(f1);
|
||||
fileChooser.setTitle("Select DMR .bits Recording");
|
||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||
if(lastDirectory != null)
|
||||
@@ -478,7 +482,6 @@ public class DmrViewer extends VBox
|
||||
fileChooser.setInitialDirectory(file);
|
||||
}
|
||||
}
|
||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
|
||||
File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||
|
||||
if(selected != null)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* 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
|
||||
@@ -38,14 +38,35 @@ public class MessagePackage
|
||||
private List<DecodeEventSnapshot> mDecodeEvents = new ArrayList<>();
|
||||
private ChannelStartProcessingRequest mChannelStartProcessingRequest;
|
||||
private AudioSegment mAudioSegment;
|
||||
private int mBitCounter;
|
||||
private int mElapsed;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param message for this instance
|
||||
* @param bitCounter at the time this instance was created
|
||||
*/
|
||||
public MessagePackage(IMessage message)
|
||||
public MessagePackage(IMessage message, int bitCounter, int elapsed)
|
||||
{
|
||||
mMessage = message;
|
||||
mBitCounter = bitCounter;
|
||||
mElapsed = elapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit counter value for this package
|
||||
*/
|
||||
public int getBitCounter()
|
||||
{
|
||||
return mBitCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elapsed bit count from last package
|
||||
*/
|
||||
public int getElapsed()
|
||||
{
|
||||
return mElapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* 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
|
||||
@@ -34,6 +34,8 @@ import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
public class MessagePackager
|
||||
{
|
||||
private MessagePackage mMessagePackage;
|
||||
private int mBitCounter;
|
||||
private int mLastBitCount;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -42,6 +44,15 @@ public class MessagePackager
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current bit count
|
||||
* @param bitCount
|
||||
*/
|
||||
public void updateBitCount(int bitCount)
|
||||
{
|
||||
mBitCounter = bitCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an audio segment.
|
||||
* @param audioSegment to add
|
||||
@@ -61,7 +72,9 @@ public class MessagePackager
|
||||
*/
|
||||
public void add(IMessage message)
|
||||
{
|
||||
mMessagePackage = new MessagePackage(message);
|
||||
int elapsed = mBitCounter - mLastBitCount;
|
||||
mMessagePackage = new MessagePackage(message, mBitCounter, elapsed);
|
||||
mLastBitCount = mBitCounter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* 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
|
||||
@@ -42,7 +42,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* Utility application to load and view .bits recording file with the messages fully parsed.
|
||||
*
|
||||
* Supported Protocols: DMR, APCO25 Phase 1 and Phase 2.
|
||||
* Supported Protocols: DMR, NXDN, and APCO25 Phase 1 and Phase 2.
|
||||
*/
|
||||
public class MessageRecordingViewer extends VBox
|
||||
{
|
||||
@@ -50,6 +50,7 @@ public class MessageRecordingViewer extends VBox
|
||||
private MenuBar mMenuBar;
|
||||
private TabPane mTabPane;
|
||||
private int mTabCounterDmr = 1;
|
||||
private int mTabCounterNxdn = 1;
|
||||
private int mTabCounterP25P1 = 1;
|
||||
private int mTabCounterP25P2 = 1;
|
||||
private UserPreferences mUserPreferences = new UserPreferences();
|
||||
@@ -77,6 +78,12 @@ public class MessageRecordingViewer extends VBox
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem nxdnMenuItem = new MenuItem("NXDN");
|
||||
nxdnMenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("NXDN-" + mTabCounterNxdn++, new NxdnViewer(mUserPreferences));
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1");
|
||||
p25p1MenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences));
|
||||
@@ -89,7 +96,7 @@ public class MessageRecordingViewer extends VBox
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem);
|
||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, nxdnMenuItem, p25p1MenuItem, p25p2MenuItem);
|
||||
|
||||
MenuItem exitMenu = new MenuItem("Exit");
|
||||
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
|
||||
@@ -110,6 +117,7 @@ public class MessageRecordingViewer extends VBox
|
||||
mTabPane = new TabPane();
|
||||
mTabPane.setMaxHeight(Double.MAX_VALUE);
|
||||
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
|
||||
mTabPane.getTabs().add(new LabeledTab("NXDN-" + mTabCounterNxdn++, new NxdnViewer(mUserPreferences)));
|
||||
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer(mUserPreferences)));
|
||||
mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer()));
|
||||
}
|
||||
|
||||
762
src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java
Normal file
762
src/main/java/io/github/dsheirer/gui/viewer/NxdnViewer.java
Normal file
@@ -0,0 +1,762 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.viewer;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.nxdn.DecodeConfigNXDN;
|
||||
import io.github.dsheirer.module.decode.nxdn.NXDNDecoderState;
|
||||
import io.github.dsheirer.module.decode.nxdn.NXDNMessageFramer;
|
||||
import io.github.dsheirer.module.decode.nxdn.NXDNMessageProcessor;
|
||||
import io.github.dsheirer.module.decode.nxdn.NXDNTrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.call.IPacketHeader;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.data.GPS;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.type.TransmissionMode;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.binary.BinaryReader;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.prefs.Preferences;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TablePosition;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* NXDN Viewer
|
||||
*/
|
||||
public class NxdnViewer extends VBox
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(NxdnViewer.class);
|
||||
private static final KeyCodeCombination KEY_CODE_COPY = new KeyCodeCombination(KeyCode.C, KeyCombination.CONTROL_ANY);
|
||||
private static final String LAST_SELECTED_DIRECTORY = "last.selected.directory.nxdn";
|
||||
private static final String LAST_SELECTED_FILE = "last.selected.file.nxdn";
|
||||
private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(NxdnViewer.class);
|
||||
private Button mSelectFileButton;
|
||||
private Label mSelectedFileLabel;
|
||||
private TableView<MessagePackage> mMessagePackageTableView;
|
||||
private ObservableList<MessagePackage> mMessagePackages = FXCollections.observableArrayList();
|
||||
private FilteredList<MessagePackage> mFilteredMessagePackages = new FilteredList<>(mMessagePackages);
|
||||
private TextField mSearchText;
|
||||
private TextField mFindText;
|
||||
private Button mFindButton;
|
||||
private Button mFindNextButton;
|
||||
private ProgressIndicator mLoadingIndicator;
|
||||
private MessagePackageViewer mMessagePackageViewer;
|
||||
private StringProperty mLoadedFile = new SimpleStringProperty();
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
public NxdnViewer(UserPreferences userPreferences)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
setPadding(new Insets(5));
|
||||
setSpacing(5);
|
||||
|
||||
HBox fileBox = new HBox();
|
||||
fileBox.setMaxWidth(Double.MAX_VALUE);
|
||||
fileBox.setAlignment(Pos.CENTER_LEFT);
|
||||
fileBox.setSpacing(5);
|
||||
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
|
||||
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
|
||||
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
|
||||
|
||||
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
|
||||
|
||||
HBox filterBox = new HBox();
|
||||
filterBox.setMaxWidth(Double.MAX_VALUE);
|
||||
filterBox.setAlignment(Pos.BASELINE_CENTER);
|
||||
filterBox.setSpacing(5);
|
||||
|
||||
Label searchLabel = new Label("Message Filter:");
|
||||
HBox.setMargin(searchLabel, new Insets(0,0,0,15));
|
||||
Label findLabel = new Label("Find:");
|
||||
|
||||
HBox.setHgrow(getFindText(), Priority.ALWAYS);
|
||||
HBox.setHgrow(getSearchText(), Priority.ALWAYS);
|
||||
|
||||
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
|
||||
getSearchText());
|
||||
|
||||
VBox.setVgrow(fileBox, Priority.NEVER);
|
||||
VBox.setVgrow(filterBox, Priority.NEVER);
|
||||
VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
|
||||
|
||||
getChildren().addAll(fileBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the recording file and loads the content into the viewer
|
||||
*
|
||||
* Note: invoke this method off of the UI thread in a thread pool executor and the results will be loaded into the
|
||||
* message table back on the JavaFX UI thread.
|
||||
*
|
||||
* @param file containing a .bits recording of decoded DMR data.
|
||||
*/
|
||||
private void load(File file)
|
||||
{
|
||||
if(file != null && file.exists())
|
||||
{
|
||||
mLoadedFile.set(file.toString());
|
||||
mMessagePackages.clear();
|
||||
getLoadingIndicator().setVisible(true);
|
||||
getSelectedFileLabel().setText("Loading ...");
|
||||
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
DecodeConfigNXDN config = new DecodeConfigNXDN();
|
||||
List<MessagePackage> messagePackages = new ArrayList<>();
|
||||
NXDNMessageProcessor messageProcessor = new NXDNMessageProcessor(config);
|
||||
|
||||
Broadcaster<IMessage> messageBroadcaster = new Broadcaster<>();
|
||||
messageProcessor.setMessageListener(messageBroadcaster);
|
||||
|
||||
Channel empty = new Channel("Empty");
|
||||
TransmissionMode transmissionMode = mLoadedFile.get().contains("4800") ? TransmissionMode.M4800 : TransmissionMode.M9600;
|
||||
config.setTransmissionMode(transmissionMode);
|
||||
empty.setDecodeConfiguration(new DecodeConfigNXDN());
|
||||
NXDNMessageFramer messageFramer = new NXDNMessageFramer(messageProcessor, transmissionMode);
|
||||
|
||||
MessagePackager messagePackager = new MessagePackager();
|
||||
|
||||
//Setup a temporary event bus to capture channel start processing requests
|
||||
EventBus eventBus = new EventBus("debug");
|
||||
eventBus.register(messagePackager);
|
||||
|
||||
NXDNTrafficChannelManager trafficChannelManager = new NXDNTrafficChannelManager(empty);
|
||||
trafficChannelManager.setInterModuleEventBus(eventBus);
|
||||
|
||||
//Register to receive events
|
||||
trafficChannelManager.addDecodeEventListener(messagePackager::add);
|
||||
NXDNDecoderState decoderState = new NXDNDecoderState(empty, trafficChannelManager);
|
||||
|
||||
Broadcaster<DecoderStateEvent> decoderStateEventBroadcaster = new Broadcaster<>();
|
||||
decoderState.setDecoderStateListener(decoderStateEventBroadcaster);
|
||||
|
||||
decoderStateEventBroadcaster.addListener(messagePackager::add);
|
||||
decoderState.addDecodeEventListener(messagePackager::add);
|
||||
decoderState.start();
|
||||
//
|
||||
long frequency = getFrequencyFromFile(mLoadedFile.get());
|
||||
|
||||
if(frequency > 0)
|
||||
{
|
||||
trafficChannelManager.setCurrentControlFrequency(frequency, empty);
|
||||
FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
|
||||
decoderState.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
|
||||
IdentifierUpdateNotification.Operation.ADD, 0));
|
||||
}
|
||||
|
||||
messageProcessor.setMessageListener(message -> {
|
||||
//Add the initial message to the packager so that it can be combined with any decoder state events.
|
||||
if(message.toString().contains("FROM:166"))
|
||||
{
|
||||
int a = 0;
|
||||
}
|
||||
messagePackager.add(message);
|
||||
decoderState.receive(message);
|
||||
|
||||
//Collect the packaged message with events
|
||||
messagePackages.add(messagePackager.getMessageWithEvents());
|
||||
});
|
||||
|
||||
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
|
||||
{
|
||||
int bitCount = 0;
|
||||
|
||||
while(reader.hasNext())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
for(byte value : buffer.array())
|
||||
{
|
||||
for(int x = 0; x <= 3; x++)
|
||||
{
|
||||
messageFramer.processWithHardSyncDetection(Dibit.parse(value, x));
|
||||
bitCount += 2;
|
||||
messagePackager.updateBitCount(bitCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
|
||||
// audioModule.stop();
|
||||
// decoderState.stop();
|
||||
// singleChannelState.stop();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getLoadingIndicator().setVisible(false);
|
||||
getSelectedFileLabel().setText(file.getName());
|
||||
mMessagePackages.addAll(messagePackages);
|
||||
getMessagePackageTableView().scrollTo(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the channel frequency value from the bits file name to broadcast as the current frequency for each of
|
||||
* the decoder states.
|
||||
* @param file name to parse
|
||||
* @return parsed frequency or zero.
|
||||
*/
|
||||
private static long getFrequencyFromFile(String file)
|
||||
{
|
||||
if(file == null || file.isEmpty())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(file.matches(FILE_FREQUENCY_REGEX))
|
||||
{
|
||||
Pattern p = Pattern.compile(FILE_FREQUENCY_REGEX);
|
||||
Matcher m = p.matcher(file);
|
||||
if(m.find())
|
||||
{
|
||||
try
|
||||
{
|
||||
String raw = m.group(1);
|
||||
return Long.parseLong(raw);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't parse frequency from bits file [" + file + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the filter(s) applies to the list of messages
|
||||
*/
|
||||
private void updateFilters()
|
||||
{
|
||||
String filterText = getSearchText().getText();
|
||||
|
||||
if(filterText != null && !filterText.isEmpty())
|
||||
{
|
||||
Predicate<MessagePackage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
|
||||
mFilteredMessagePackages.setPredicate(textPredicate);
|
||||
}
|
||||
else
|
||||
{
|
||||
mFilteredMessagePackages.setPredicate(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and selects the first row containing the text argument.
|
||||
* @param text to search for.
|
||||
*/
|
||||
private void find(String text)
|
||||
{
|
||||
if(text != null && !text.isEmpty())
|
||||
{
|
||||
for(MessagePackage messagePackage: mFilteredMessagePackages)
|
||||
{
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and selects the first row containing the text argument, after the currently selected row.
|
||||
* @param text to search for.
|
||||
*/
|
||||
private void findNext(String text)
|
||||
{
|
||||
if(text != null && !text.isEmpty())
|
||||
{
|
||||
MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected == null)
|
||||
{
|
||||
find(text);
|
||||
return;
|
||||
}
|
||||
|
||||
int row = mFilteredMessagePackages.indexOf(selected);
|
||||
|
||||
for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
|
||||
{
|
||||
if(x < mFilteredMessagePackages.size())
|
||||
{
|
||||
MessagePackage messagePackage = mFilteredMessagePackages.get(x);
|
||||
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MessagePackageViewer getMessagePackageViewer()
|
||||
{
|
||||
if(mMessagePackageViewer == null)
|
||||
{
|
||||
mMessagePackageViewer = new MessagePackageViewer();
|
||||
mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
//Register for table selection events to display the selected value.
|
||||
getMessagePackageTableView().getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> getMessagePackageViewer().set(newValue));
|
||||
}
|
||||
|
||||
return mMessagePackageViewer;
|
||||
}
|
||||
|
||||
/**
|
||||
* List view control with messages
|
||||
*/
|
||||
private TableView<MessagePackage> getMessagePackageTableView()
|
||||
{
|
||||
if(mMessagePackageTableView == null)
|
||||
{
|
||||
mMessagePackageTableView = new TableView<>();
|
||||
mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
mMessagePackageTableView.setPlaceholder(getLoadingIndicator());
|
||||
SortedList<MessagePackage> sortedList = new SortedList<>(mFilteredMessagePackages);
|
||||
sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty());
|
||||
mMessagePackageTableView.setItems(sortedList);
|
||||
|
||||
mMessagePackageTableView.setOnKeyPressed(event ->
|
||||
{
|
||||
if(KEY_CODE_COPY.match(event))
|
||||
{
|
||||
final Set<Integer> rows = new TreeSet<>();
|
||||
for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells())
|
||||
{
|
||||
rows.add(tablePosition.getRow());
|
||||
}
|
||||
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
boolean firstRow = true;
|
||||
for (final Integer row : rows)
|
||||
{
|
||||
if(firstRow)
|
||||
{
|
||||
firstRow = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
boolean firstCol = true;
|
||||
|
||||
for (final TableColumn<?, ?> column : mMessagePackageTableView.getColumns())
|
||||
{
|
||||
if(firstCol)
|
||||
{
|
||||
firstCol = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append('\t');
|
||||
}
|
||||
|
||||
final Object cellData = column.getCellData(row);
|
||||
sb.append(cellData == null ? "" : cellData.toString());
|
||||
}
|
||||
}
|
||||
final ClipboardContent clipboardContent = new ClipboardContent();
|
||||
clipboardContent.putString(sb.toString());
|
||||
Clipboard.getSystemClipboard().setContent(clipboardContent);
|
||||
}
|
||||
});
|
||||
|
||||
TableColumn timestampColumn = new TableColumn();
|
||||
timestampColumn.setPrefWidth(110);
|
||||
timestampColumn.setText("Time");
|
||||
timestampColumn.setCellValueFactory(new PropertyValueFactory<>("timestamp"));
|
||||
|
||||
TableColumn bitCountColumn = new TableColumn();
|
||||
bitCountColumn.setPrefWidth(90);
|
||||
bitCountColumn.setText("Bit Counter");
|
||||
bitCountColumn.setCellValueFactory(new PropertyValueFactory<>("bitCounter"));
|
||||
|
||||
TableColumn elapsedColumn = new TableColumn();
|
||||
elapsedColumn.setPrefWidth(70);
|
||||
elapsedColumn.setText("Elapsed");
|
||||
elapsedColumn.setCellValueFactory(new PropertyValueFactory<>("elapsed"));
|
||||
|
||||
TableColumn validColumn = new TableColumn();
|
||||
validColumn.setPrefWidth(50);
|
||||
validColumn.setText("Valid");
|
||||
validColumn.setCellValueFactory(new PropertyValueFactory<>("valid"));
|
||||
|
||||
TableColumn timeslotColumn = new TableColumn();
|
||||
timeslotColumn.setPrefWidth(35);
|
||||
timeslotColumn.setText("TS");
|
||||
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
|
||||
|
||||
TableColumn messageColumn = new TableColumn();
|
||||
messageColumn.setPrefWidth(900);
|
||||
messageColumn.setText("Message");
|
||||
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
|
||||
SimpleStringProperty property = new SimpleStringProperty();
|
||||
if(param.getValue() instanceof MessagePackage messagePackage)
|
||||
{
|
||||
property.set(messagePackage.toString());
|
||||
}
|
||||
|
||||
return property;
|
||||
});
|
||||
|
||||
TableColumn decodeEventCountColumn = new TableColumn();
|
||||
decodeEventCountColumn.setPrefWidth(50);
|
||||
decodeEventCountColumn.setText("Events");
|
||||
decodeEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decodeEventCount"));
|
||||
|
||||
TableColumn decoderStateEventCountColumn = new TableColumn();
|
||||
decoderStateEventCountColumn.setPrefWidth(50);
|
||||
decoderStateEventCountColumn.setText("States");
|
||||
decoderStateEventCountColumn.setCellValueFactory(new PropertyValueFactory<>("decoderStateEventCount"));
|
||||
|
||||
TableColumn channelStartCountColumn = new TableColumn();
|
||||
channelStartCountColumn.setPrefWidth(50);
|
||||
channelStartCountColumn.setText("Starts");
|
||||
channelStartCountColumn.setCellValueFactory(new PropertyValueFactory<>("channelStartProcessingRequestCount"));
|
||||
|
||||
TableColumn audioSegmentCountColumn = new TableColumn();
|
||||
audioSegmentCountColumn.setPrefWidth(50);
|
||||
audioSegmentCountColumn.setText("Audio");
|
||||
audioSegmentCountColumn.setCellValueFactory(new PropertyValueFactory<>("audioSegmentCount"));
|
||||
|
||||
mMessagePackageTableView.getColumns().addAll(timestampColumn, bitCountColumn, elapsedColumn, validColumn,
|
||||
timeslotColumn, messageColumn, decodeEventCountColumn, decoderStateEventCountColumn,
|
||||
channelStartCountColumn, audioSegmentCountColumn);
|
||||
}
|
||||
|
||||
return mMessagePackageTableView;
|
||||
}
|
||||
|
||||
/**
|
||||
* File selection button
|
||||
* @return button
|
||||
*/
|
||||
private Button getSelectFileButton()
|
||||
{
|
||||
if(mSelectFileButton == null)
|
||||
{
|
||||
mSelectFileButton = new Button("Select ...");
|
||||
mSelectFileButton.onActionProperty().set(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Select NXDN .bits Recording");
|
||||
FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
|
||||
FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
|
||||
fileChooser.getExtensionFilters().addAll(f1, f2);
|
||||
fileChooser.setSelectedExtensionFilter(f1);
|
||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||
if(lastDirectory != null)
|
||||
{
|
||||
File file = new File(lastDirectory);
|
||||
if(file.exists() && file.isDirectory())
|
||||
{
|
||||
fileChooser.setInitialDirectory(file);
|
||||
|
||||
String lastFile = mPreferences.get(LAST_SELECTED_FILE, null);
|
||||
|
||||
if(lastFile != null)
|
||||
{
|
||||
fileChooser.initialFileNameProperty().set(lastFile);
|
||||
// fileChooser.setInitialFileName(lastFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
|
||||
mPreferences.put(LAST_SELECTED_FILE, selected.getName());
|
||||
load(selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mSelectFileButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spinny loading icon to show over the message table view
|
||||
*/
|
||||
private ProgressIndicator getLoadingIndicator()
|
||||
{
|
||||
if(mLoadingIndicator == null)
|
||||
{
|
||||
mLoadingIndicator = new ProgressIndicator();
|
||||
mLoadingIndicator.setProgress(-1);
|
||||
mLoadingIndicator.setVisible(false);
|
||||
}
|
||||
|
||||
return mLoadingIndicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selected file path label.
|
||||
*/
|
||||
private Label getSelectedFileLabel()
|
||||
{
|
||||
if(mSelectedFileLabel == null)
|
||||
{
|
||||
mSelectedFileLabel = new Label(" ");
|
||||
}
|
||||
|
||||
return mSelectedFileLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search text filter box
|
||||
* @return text control for entering search text
|
||||
*/
|
||||
private TextField getSearchText()
|
||||
{
|
||||
if(mSearchText == null)
|
||||
{
|
||||
mSearchText = new TextField();
|
||||
mSearchText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
|
||||
}
|
||||
|
||||
return mSearchText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Text box for find text
|
||||
*/
|
||||
private TextField getFindText()
|
||||
{
|
||||
if(mFindText == null)
|
||||
{
|
||||
mFindText = new TextField();
|
||||
mFindText.setOnKeyPressed(event -> {
|
||||
if(event.getCode().equals(KeyCode.ENTER))
|
||||
{
|
||||
getFindButton().fire();
|
||||
}
|
||||
});
|
||||
mFindText.textProperty().addListener((observable, oldValue, newValue) -> updateFilters());
|
||||
}
|
||||
|
||||
return mFindText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find button to search for the text in the find text box.
|
||||
* @return button
|
||||
*/
|
||||
private Button getFindButton()
|
||||
{
|
||||
if(mFindButton == null)
|
||||
{
|
||||
mFindButton = new Button("Find");
|
||||
mFindButton.setOnAction(event -> find(getFindText().getText()));
|
||||
}
|
||||
|
||||
return mFindButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find next button to search for the text in the find text box.
|
||||
* @return button
|
||||
*/
|
||||
private Button getFindNextButton()
|
||||
{
|
||||
if(mFindNextButton == null)
|
||||
{
|
||||
mFindNextButton = new Button("Next");
|
||||
mFindNextButton.setOnAction(event -> findNext(getFindText().getText()));
|
||||
}
|
||||
|
||||
return mFindNextButton;
|
||||
}
|
||||
|
||||
public static class A implements Listener<IMessage>
|
||||
{
|
||||
private Path mOutput;
|
||||
private int mTarget;
|
||||
|
||||
public A(Path output, int target)
|
||||
{
|
||||
mOutput = output;
|
||||
mTarget = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(IMessage message)
|
||||
{
|
||||
if(message instanceof GPS gps)
|
||||
{
|
||||
System.out.println(gps);
|
||||
|
||||
IPacketHeader header = gps.getPacketSequence().getHeader();
|
||||
|
||||
int id = header.getSource().getValue();
|
||||
|
||||
if(mTarget != 0 && id != mTarget)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Files.writeString(mOutput, gps + "\n", StandardOpenOption.APPEND);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void main() throws IOException
|
||||
{
|
||||
Path directory = Paths.get("/home/denny/SDRTrunk/recordings");
|
||||
int target = 0;
|
||||
Path output = directory.resolve(target == 0 ? "GPS.txt" : "GPS_" + target + ".txt");
|
||||
A a = new A(output, target);
|
||||
|
||||
if(Files.exists(output))
|
||||
{
|
||||
Files.delete(output);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
output.toFile().createNewFile();
|
||||
|
||||
File[] files = directory.toFile().listFiles();
|
||||
|
||||
if(files == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<File> fileList = Arrays.asList(files);
|
||||
Collections.sort(fileList);
|
||||
for(File file: fileList)
|
||||
{
|
||||
if(file.getName().endsWith(".bits"))
|
||||
{
|
||||
if(file != null && file.exists())
|
||||
{
|
||||
DecodeConfigNXDN config = new DecodeConfigNXDN();
|
||||
NXDNMessageProcessor messageProcessor = new NXDNMessageProcessor(config);
|
||||
|
||||
Channel empty = new Channel("Empty");
|
||||
TransmissionMode transmissionMode = file.getName().contains("4800") ? TransmissionMode.M4800 : TransmissionMode.M9600;
|
||||
config.setTransmissionMode(transmissionMode);
|
||||
empty.setDecodeConfiguration(new DecodeConfigNXDN());
|
||||
NXDNMessageFramer messageFramer = new NXDNMessageFramer(messageProcessor, transmissionMode);
|
||||
messageProcessor.setMessageListener(a);
|
||||
|
||||
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
|
||||
{
|
||||
while(reader.hasNext())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
for(byte value : buffer.array())
|
||||
{
|
||||
for(int x = 0; x <= 3; x++)
|
||||
{
|
||||
messageFramer.processWithHardSyncDetection(Dibit.parse(value, x));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* 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
|
||||
@@ -508,6 +508,10 @@ public class P25P1Viewer extends VBox
|
||||
mSelectFileButton = new Button("Select ...");
|
||||
mSelectFileButton.onActionProperty().set(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
|
||||
FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
|
||||
fileChooser.getExtensionFilters().addAll(f1, f2);
|
||||
fileChooser.setSelectedExtensionFilter(f1);
|
||||
fileChooser.setTitle("Select P25 Phase 1 .bits Recording");
|
||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||
if(lastDirectory != null)
|
||||
@@ -518,7 +522,6 @@ public class P25P1Viewer extends VBox
|
||||
fileChooser.setInitialDirectory(file);
|
||||
}
|
||||
}
|
||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
|
||||
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||
|
||||
if(selected != null)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* 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
|
||||
@@ -594,6 +594,10 @@ public class P25P2Viewer extends VBox
|
||||
mSelectFileButton = new Button("Select ...");
|
||||
mSelectFileButton.onActionProperty().set(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser.ExtensionFilter f1 = new FileChooser.ExtensionFilter("Demodulated Bitstream Recording (*.bits)", "*.bits");
|
||||
FileChooser.ExtensionFilter f2 = new FileChooser.ExtensionFilter("All Files *.*", "*.*");
|
||||
fileChooser.getExtensionFilters().addAll(f1, f2);
|
||||
fileChooser.setSelectedExtensionFilter(f1);
|
||||
fileChooser.setTitle("Select P25 Phase 2 .bits Recording");
|
||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||
if(lastDirectory != null)
|
||||
@@ -604,7 +608,6 @@ public class P25P2Viewer extends VBox
|
||||
fileChooser.setInitialDirectory(file);
|
||||
}
|
||||
}
|
||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
|
||||
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||
|
||||
if(selected != null)
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.viewer.sync;
|
||||
|
||||
import io.github.dsheirer.util.SwingUtils;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.Scene;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.JFrame;
|
||||
|
||||
/**
|
||||
* Utility for viewing sync detection results
|
||||
*/
|
||||
public class FMSyncResultsViewer implements ISyncResultsListener
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FMSyncResultsViewer.class.getName());
|
||||
private ISyncResultsListener mListener;
|
||||
|
||||
public FMSyncResultsViewer()
|
||||
{
|
||||
initUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* View sync detection results.
|
||||
* @param symbols that were detected
|
||||
* @param sync that was detected
|
||||
* @param samples for the symbols
|
||||
* @param syncIntervals symbol timing interval pointers into the I/Q sample arrays
|
||||
* @param label to display to the user
|
||||
* @param release latch to release via the UI when done viewing these results
|
||||
*/
|
||||
@Override
|
||||
public void receive(float[] symbols, float[] sync, float[] samples, float[] syncIntervals, float equalizerBalance,
|
||||
float equalizerGain, String label, CountDownLatch release)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(symbols, samples, sync, syncIntervals, equalizerBalance, equalizerGain, label, release);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void symbol(float symbol)
|
||||
{
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.symbol(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the viewer UI
|
||||
*/
|
||||
private void initUI()
|
||||
{
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
final JFXPanel fxPanel = new JFXPanel();
|
||||
Platform.runLater(() -> {
|
||||
Platform.setImplicitExit(false);
|
||||
FMSyncVisualizer syncVisualizer = new FMSyncVisualizer();
|
||||
mListener = syncVisualizer;
|
||||
|
||||
Scene scene = new Scene(syncVisualizer, 1400, 1000);
|
||||
|
||||
// URL resource = getClass().getResource("/sdrtrunk_style.css");
|
||||
//
|
||||
// if(resource != null)
|
||||
// {
|
||||
// scene.getStylesheets().add(resource.toExternalForm());
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// LOGGER.warn("Can't find stylesheet resource for sdrtrunk");
|
||||
// }
|
||||
|
||||
fxPanel.setScene(scene);
|
||||
fxPanel.setVisible(true);
|
||||
|
||||
JFrame frame = new JFrame();
|
||||
frame.setTitle("Sync Results Viewer");
|
||||
frame.setContentPane(fxPanel);
|
||||
frame.setSize(1400, 1400);
|
||||
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
frame.setLocationRelativeTo(null);
|
||||
SwingUtils.run(() -> {
|
||||
frame.setVisible(true);
|
||||
latch.countDown();
|
||||
});
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
latch.await();
|
||||
}
|
||||
catch(InterruptedException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.viewer.sync;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.ScatterChart;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* JavaFX viewer application for visualizing sample buffers and detected sync patterns
|
||||
*/
|
||||
public class FMSyncVisualizer extends VBox implements ISyncResultsListener
|
||||
{
|
||||
private static final int SYMBOL_CHART_SERIES_COUNT = 2000;
|
||||
private static final float SYMBOL = (float) (Math.PI / 4.0);
|
||||
private final NumberAxis mConstellationI = new NumberAxis();
|
||||
private final NumberAxis mConstellationQ = new NumberAxis();
|
||||
private final NumberAxis mSampleTiming = new NumberAxis();
|
||||
private final NumberAxis mSamplePhase = new NumberAxis(-4.0, 4.0, .1);
|
||||
private final NumberAxis mSymbolTiming = new NumberAxis();
|
||||
private final NumberAxis mSymbolPhase = new NumberAxis(-Math.PI, Math.PI, Math.PI / 2);
|
||||
private final XYChart.Series<Number, Number> mSymbolSeries = new XYChart.Series<>();
|
||||
private final XYChart.Series<Number, Number> mSymbolSeriesMarker = new XYChart.Series<>();
|
||||
private final ScatterChart<Number, Number> mConstellationChart = new ScatterChart<>(mConstellationI, mConstellationQ);
|
||||
private final XYChart.Series<Number, Number> mConstellationSymbols = new XYChart.Series<>();
|
||||
private static final int CONSTELLATION_SYMBOL_COUNT = 50;
|
||||
private int mConstellationSymbolPointer = 0;
|
||||
|
||||
private final LineChart<Number, Number> mSampleChart = new LineChart<>(mSampleTiming, mSamplePhase);
|
||||
private final ScatterChart<Number, Number> mSymbolChart = new ScatterChart<>(mSymbolTiming, mSymbolPhase);
|
||||
|
||||
private final NumberAxis mEqualizerHistoryAxis = new NumberAxis();
|
||||
private final NumberAxis mEqualizerValueAxis = new NumberAxis(-14.0, 14.0, .25);
|
||||
private final LineChart<Number, Number> mEqualizerChart = new LineChart<>(mEqualizerHistoryAxis, mEqualizerValueAxis);
|
||||
private int mEqualizerPointer = 0;
|
||||
private int mEqualizerHistorySize = 40;
|
||||
private float[] mEqualizerBalanceHistory = new float[mEqualizerHistorySize];
|
||||
private float[] mEqualizerGainHistory = new float[mEqualizerHistorySize];
|
||||
|
||||
private final Button mReleaseButton = new Button("Next Sync");
|
||||
private final Label mPatternLabel = new Label();
|
||||
private CountDownLatch mRelease;
|
||||
private int mSymbolPointer = 0;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public FMSyncVisualizer()
|
||||
{
|
||||
setPadding(new Insets(5));
|
||||
mConstellationI.setLabel("Constellation - Inphase");
|
||||
mConstellationQ.setLabel("Quadrature");
|
||||
mSampleTiming.setLabel("Sample Timing");
|
||||
mSamplePhase.setLabel("Sample Phase (+/- PI)");
|
||||
|
||||
mReleaseButton.setOnAction(e -> {
|
||||
if(mRelease != null)
|
||||
{
|
||||
mRelease.countDown();
|
||||
mReleaseButton.setDisable(true);
|
||||
}
|
||||
});
|
||||
|
||||
mConstellationChart.setPrefSize(400, 400);
|
||||
mConstellationChart.setAnimated(false);
|
||||
XYChart.Series<Number, Number> constellationIdeal = new XYChart.Series<>();
|
||||
constellationIdeal.setName("Ideal");
|
||||
float PI_4 = (float) (Math.PI / 4);
|
||||
constellationIdeal.getData().add(new XYChart.Data<>(PI_4, PI_4));
|
||||
constellationIdeal.getData().add(new XYChart.Data<>(PI_4, -PI_4));
|
||||
constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, PI_4));
|
||||
constellationIdeal.getData().add(new XYChart.Data<>(-PI_4, -PI_4));
|
||||
mConstellationChart.getData().add(constellationIdeal);
|
||||
mConstellationSymbols.setName("Symbols");
|
||||
|
||||
for(int x = 0; x < CONSTELLATION_SYMBOL_COUNT; x++)
|
||||
{
|
||||
mConstellationSymbols.getData().add(new XYChart.Data<>(0, 0));
|
||||
}
|
||||
|
||||
mConstellationChart.getData().add(mConstellationSymbols);
|
||||
|
||||
mSampleChart.setAnimated(false);
|
||||
mSymbolChart.setAnimated(false);
|
||||
mSymbolChart.setId("symbol-chart");
|
||||
mEqualizerChart.setAnimated(false);
|
||||
|
||||
mEqualizerValueAxis.setLabel("Value");
|
||||
mEqualizerHistoryAxis.setLabel("Equalizer");
|
||||
|
||||
HBox hbox = new HBox();
|
||||
hbox.getChildren().add(mConstellationChart);
|
||||
HBox.setHgrow(mEqualizerChart, Priority.ALWAYS);
|
||||
hbox.getChildren().add(mEqualizerChart);
|
||||
// VBox.setVgrow(hbox, Priority.ALWAYS);
|
||||
|
||||
VBox.setVgrow(mSampleChart, Priority.ALWAYS);
|
||||
VBox.setVgrow(mSymbolChart, Priority.ALWAYS);
|
||||
|
||||
HBox buttons = new HBox();
|
||||
buttons.setAlignment(Pos.BASELINE_LEFT);
|
||||
buttons.setSpacing(10);
|
||||
buttons.getChildren().addAll(mReleaseButton, mPatternLabel);
|
||||
VBox.setVgrow(buttons, Priority.NEVER);
|
||||
|
||||
//Load the symbol chart with symbols
|
||||
mSymbolSeries.setName("Symbols");
|
||||
for(int x = 0; x < SYMBOL_CHART_SERIES_COUNT; x++)
|
||||
{
|
||||
mSymbolSeries.getData().add(new XYChart.Data<>(x, 0));
|
||||
}
|
||||
mSymbolChart.getData().add(mSymbolSeries);
|
||||
float pi_4 = (float)Math.PI / 4f;
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 3f * pi_4));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, pi_4));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, 0));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -pi_4));
|
||||
mSymbolSeriesMarker.getData().add(new XYChart.Data<>(0, -3 * pi_4));
|
||||
mSymbolChart.getData().add(mSymbolSeriesMarker);
|
||||
getChildren().addAll(buttons, hbox, mSampleChart, mSymbolChart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void symbol(float symbol)
|
||||
{
|
||||
for(XYChart.Data<Number, Number> data : mSymbolSeriesMarker.getData())
|
||||
{
|
||||
data.setXValue(mSymbolPointer);
|
||||
}
|
||||
mSymbolSeries.getData().get(mSymbolPointer++).setYValue(symbol);
|
||||
mSymbolPointer %= SYMBOL_CHART_SERIES_COUNT;
|
||||
|
||||
XYChart.Data<Number,Number> element = mConstellationSymbols.getData().get(mConstellationSymbolPointer);
|
||||
element.setXValue(Math.cos(symbol));
|
||||
element.setYValue(Math.sin(symbol));
|
||||
mConstellationSymbolPointer++;
|
||||
mConstellationSymbolPointer %= CONSTELLATION_SYMBOL_COUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(float[] symbols, float[] samples, float[] sync, float[] syncIntervals, float equalizerBalance, float equalizerGain, String label, CountDownLatch latch)
|
||||
{
|
||||
mRelease = latch;
|
||||
mReleaseButton.setDisable(latch == null);
|
||||
|
||||
mEqualizerBalanceHistory[mEqualizerPointer] = equalizerBalance;
|
||||
mEqualizerGainHistory[mEqualizerPointer++] = (equalizerGain - 1.0f);
|
||||
mEqualizerPointer %= mEqualizerHistorySize;
|
||||
//Clear the next/upcoming values to give visual indication of where we're at.
|
||||
mEqualizerBalanceHistory[mEqualizerPointer] = 0f;
|
||||
mEqualizerGainHistory[mEqualizerPointer] = 0f;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
XYChart.Series<Number, Number> sampleSeries = new XYChart.Series<>();
|
||||
sampleSeries.setName("Demodulated Samples");
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
sampleSeries.getData().add(new XYChart.Data<>(x, samples[x]));
|
||||
}
|
||||
|
||||
mSampleChart.getData().clear();
|
||||
mSampleChart.getData().add(sampleSeries);
|
||||
|
||||
XYChart.Series<Number, Number> patternSeries = new XYChart.Series<>();
|
||||
patternSeries.setName("Ideal Sync Pattern");
|
||||
|
||||
for(int x = 0; x < sync.length; x++)
|
||||
{
|
||||
patternSeries.getData().add(new XYChart.Data<>(syncIntervals[x], sync[x]));
|
||||
}
|
||||
|
||||
mSampleChart.getData().add(patternSeries);
|
||||
|
||||
mPatternLabel.setText(label);
|
||||
mEqualizerChart.getData().clear();
|
||||
|
||||
XYChart.Series<Number, Number> balanceSeries = new XYChart.Series<>();
|
||||
balanceSeries.setName("Balance");
|
||||
XYChart.Series<Number, Number> gainSeries = new XYChart.Series<>();
|
||||
gainSeries.setName("Gain (Value minus 1.0)");
|
||||
|
||||
for(int x = 0; x < mEqualizerHistorySize; x++)
|
||||
{
|
||||
balanceSeries.getData().add(new XYChart.Data<>(x, mEqualizerBalanceHistory[x]));
|
||||
gainSeries.getData().add(new XYChart.Data<>(x, mEqualizerGainHistory[x]));
|
||||
}
|
||||
|
||||
mEqualizerChart.getData().add(balanceSeries);
|
||||
mEqualizerChart.getData().add(gainSeries);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user