#7 NXDN Decoder WIP

This commit is contained in:
sheirerd
2025-11-20 07:21:45 -05:00
committed by denny
parent a053315675
commit a79ab1c607
444 changed files with 31047 additions and 2075 deletions

View File

@@ -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)

View File

@@ -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;
}
/**

View File

@@ -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;
}
/**

View File

@@ -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()));
}

View 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();
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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);
}
}
}

View File

@@ -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);
});
}
}