mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 08:37:25 -05:00
* P25 Phase 2 now supports control channel decoding and decoder was enhanced to improve decodes. * Radio reference site editor updated for phase 2 sites to allow user to select FDMA or TDMA control. * Decode events are now minimized and de-duplicated. * Fully parsing all phase 1/2 messages from the latest ICD. * Vendor specific messaging recoveries (Moto & Harris). * Patch group management enhancements with stale patch detection. * Tuner editor now fully resets the min/max frequency values * Default traffic channel count increased to 20 (from 3). * ISSI roaming radio and talkgroup values now displayed and aliasable as fully qualified values (e.g. 1234(123.456.7890) * Moto emergency alarm activation messaging support * Expanded list of P25 encryption algorithms * Message (bits) viewer enhanced for P25 phase 1 and phase 2 * IPV4 addresses and UDP ports no longer classified as user values - won't display in event views. * New framework for describing message binary fields that should reduce overall memory footprint and enhance code readability Co-authored-by: Dennis Sheirer <dsheirer@github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
||||
import io.github.dsheirer.gui.viewer.RecordingViewer;
|
||||
import io.github.dsheirer.gui.viewer.MessageRecordingViewer;
|
||||
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.jmbe.JmbeEditor;
|
||||
@@ -84,7 +84,7 @@ public class JavaFxWindowManager extends Application
|
||||
private TunerManager mTunerManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
private UserPreferencesEditor mUserPreferencesEditor;
|
||||
private RecordingViewer mRecordingViewer;
|
||||
private MessageRecordingViewer mMessageRecordingViewer;
|
||||
|
||||
private Stage mChannelMapStage;
|
||||
private Stage mIconManagerStage;
|
||||
@@ -216,14 +216,14 @@ public class JavaFxWindowManager extends Application
|
||||
return mRecordingViewerStage;
|
||||
}
|
||||
|
||||
public RecordingViewer getRecordingViewer()
|
||||
public MessageRecordingViewer getRecordingViewer()
|
||||
{
|
||||
if(mRecordingViewer == null)
|
||||
if(mMessageRecordingViewer == null)
|
||||
{
|
||||
mRecordingViewer = new RecordingViewer();
|
||||
mMessageRecordingViewer = new MessageRecordingViewer();
|
||||
}
|
||||
|
||||
return mRecordingViewer;
|
||||
return mMessageRecordingViewer;
|
||||
}
|
||||
|
||||
public Stage getIconManagerStage()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -647,11 +647,13 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
public NewP25P2ChannelMenu()
|
||||
{
|
||||
setText(DecoderType.P25_PHASE2.getDisplayString());
|
||||
MenuItem trunked = new MenuItem("Trunked System");
|
||||
trunked.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
|
||||
MenuItem channel = new MenuItem("Individual Channel");
|
||||
MenuItem trunkedP1 = new MenuItem("Trunked System - FDMA Phase 1 Control Channel");
|
||||
trunkedP1.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE1));
|
||||
MenuItem trunkedP2 = new MenuItem("Trunked System - TDMA Phase 2 Control Channel");
|
||||
trunkedP2.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
|
||||
MenuItem channel = new MenuItem("Individual Phase 2 Channel");
|
||||
channel.setOnAction(event -> createNewChannel(DecoderType.P25_PHASE2));
|
||||
getItems().addAll(trunked, channel);
|
||||
getItems().addAll(trunkedP1, trunkedP2, channel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -113,7 +113,7 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
|
||||
if(mDecoderPane == null)
|
||||
{
|
||||
mDecoderPane = new TitledPane();
|
||||
mDecoderPane.setText("Decoder: P25 Phase 1 (or P25 Phase 2 Control Channel)");
|
||||
mDecoderPane.setText("Decoder: P25 Phase 1 (also used for P25 Phase 2 system with FDMA control channels)");
|
||||
mDecoderPane.setExpanded(true);
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
@@ -355,9 +355,9 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
|
||||
{
|
||||
DecodeConfigP25Phase1 config;
|
||||
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1)
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase1 p1)
|
||||
{
|
||||
config = (DecodeConfigP25Phase1)getItem().getDecodeConfiguration();
|
||||
config = p1;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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,9 +42,13 @@ import java.util.List;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -64,6 +68,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
private IntegerTextField mWacnTextField;
|
||||
private IntegerTextField mSystemTextField;
|
||||
private IntegerTextField mNacTextField;
|
||||
private ToggleSwitch mIgnoreDataCallsButton;
|
||||
private Spinner<Integer> mTrafficChannelPoolSizeSpinner;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -102,7 +108,7 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
if(mDecoderPane == null)
|
||||
{
|
||||
mDecoderPane = new TitledPane();
|
||||
mDecoderPane.setText("Decoder: P25 Phase 2");
|
||||
mDecoderPane.setText("Decoder: P25 Phase 2 for TDMA control or TDMA traffic channels.");
|
||||
mDecoderPane.setExpanded(true);
|
||||
|
||||
GridPane gridPane = new GridPane();
|
||||
@@ -112,6 +118,22 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
int row = 0;
|
||||
|
||||
Label poolSizeLabel = new Label("Max Traffic Channels");
|
||||
GridPane.setHalignment(poolSizeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(poolSizeLabel, 0, row);
|
||||
gridPane.getChildren().add(poolSizeLabel);
|
||||
|
||||
GridPane.setConstraints(getTrafficChannelPoolSizeSpinner(), 1, row);
|
||||
gridPane.getChildren().add(getTrafficChannelPoolSizeSpinner());
|
||||
|
||||
GridPane.setConstraints(getIgnoreDataCallsButton(), 2, row);
|
||||
gridPane.getChildren().add(getIgnoreDataCallsButton());
|
||||
|
||||
Label directionLabel = new Label("Ignore Data Calls");
|
||||
GridPane.setHalignment(directionLabel, HPos.LEFT);
|
||||
GridPane.setConstraints(directionLabel, 3, row);
|
||||
gridPane.getChildren().add(directionLabel);
|
||||
|
||||
Label wacnLabel = new Label("WACN");
|
||||
GridPane.setHalignment(wacnLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(wacnLabel, 0, ++row);
|
||||
@@ -122,20 +144,27 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 0, ++row);
|
||||
GridPane.setConstraints(systemLabel, 2, row);
|
||||
gridPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemTextField(), 1, row);
|
||||
GridPane.setConstraints(getSystemTextField(), 3, row);
|
||||
gridPane.getChildren().add(getSystemTextField());
|
||||
|
||||
Label nacLabel = new Label("NAC");
|
||||
GridPane.setHalignment(nacLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(nacLabel, 0, ++row);
|
||||
GridPane.setConstraints(nacLabel, 4, row);
|
||||
gridPane.getChildren().add(nacLabel);
|
||||
|
||||
GridPane.setConstraints(getNacTextField(), 1, row);
|
||||
GridPane.setConstraints(getNacTextField(), 5, row);
|
||||
gridPane.getChildren().add(getNacTextField());
|
||||
|
||||
Label noteLabel = new Label("Note: WACN/System/NAC values are auto-detected (ie not required) from " +
|
||||
"the control channel and are only required when decoding individual traffic channels");
|
||||
GridPane.setHalignment(noteLabel, HPos.LEFT);
|
||||
GridPane.setConstraints(noteLabel, 1, ++row, 6, 1);
|
||||
gridPane.getChildren().add(noteLabel);
|
||||
|
||||
|
||||
mDecoderPane.setContent(gridPane);
|
||||
}
|
||||
|
||||
@@ -203,6 +232,37 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
return mEventLogConfigurationEditor;
|
||||
}
|
||||
|
||||
private ToggleSwitch getIgnoreDataCallsButton()
|
||||
{
|
||||
if(mIgnoreDataCallsButton == null)
|
||||
{
|
||||
mIgnoreDataCallsButton = new ToggleSwitch();
|
||||
mIgnoreDataCallsButton.setDisable(true);
|
||||
mIgnoreDataCallsButton.selectedProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
}
|
||||
|
||||
return mIgnoreDataCallsButton;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private IntegerTextField getWacnTextField()
|
||||
{
|
||||
if(mWacnTextField == null)
|
||||
@@ -247,6 +307,9 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
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()
|
||||
@@ -259,9 +322,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
@Override
|
||||
protected void setDecoderConfiguration(DecodeConfiguration config)
|
||||
{
|
||||
if(config instanceof DecodeConfigP25Phase2)
|
||||
if(config instanceof DecodeConfigP25Phase2 decodeConfig)
|
||||
{
|
||||
DecodeConfigP25Phase2 decodeConfig = (DecodeConfigP25Phase2)config;
|
||||
getWacnTextField().setDisable(false);
|
||||
getSystemTextField().setDisable(false);
|
||||
getNacTextField().setDisable(false);
|
||||
@@ -280,6 +342,11 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
getSystemTextField().set(0);
|
||||
getNacTextField().set(0);
|
||||
}
|
||||
|
||||
getIgnoreDataCallsButton().setDisable(false);
|
||||
getIgnoreDataCallsButton().setSelected(decodeConfig.getIgnoreDataCalls());
|
||||
getTrafficChannelPoolSizeSpinner().setDisable(false);
|
||||
getTrafficChannelPoolSizeSpinner().getValueFactory().setValue(decodeConfig.getTrafficChannelPoolSize());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -289,6 +356,8 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
getWacnTextField().setDisable(true);
|
||||
getSystemTextField().setDisable(true);
|
||||
getNacTextField().setDisable(true);
|
||||
getIgnoreDataCallsButton().setDisable(true);
|
||||
getTrafficChannelPoolSizeSpinner().setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,20 +366,23 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
{
|
||||
DecodeConfigP25Phase2 config;
|
||||
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2)
|
||||
if(getItem().getDecodeConfiguration() instanceof DecodeConfigP25Phase2 p2)
|
||||
{
|
||||
config = (DecodeConfigP25Phase2)getItem().getDecodeConfiguration();
|
||||
config.setAutoDetectScrambleParameters(false);
|
||||
int wacn = getWacnTextField().get();
|
||||
int system = getSystemTextField().get();
|
||||
int nac = getNacTextField().get();
|
||||
config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
|
||||
config = p2;
|
||||
}
|
||||
else
|
||||
{
|
||||
config = new DecodeConfigP25Phase2();
|
||||
}
|
||||
|
||||
config.setAutoDetectScrambleParameters(false);
|
||||
int wacn = getWacnTextField().get();
|
||||
int system = getSystemTextField().get();
|
||||
int nac = getNacTextField().get();
|
||||
config.setScrambleParameters(new ScrambleParameters(wacn, system, nac));
|
||||
config.setIgnoreDataCalls(getIgnoreDataCallsButton().isSelected());
|
||||
config.setTrafficChannelPoolSize(getTrafficChannelPoolSizeSpinner().getValue());
|
||||
|
||||
getItem().setDecodeConfiguration(config);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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,13 +25,15 @@ import io.github.dsheirer.rrapi.type.Site;
|
||||
/**
|
||||
* Wrapper class to join a Site and a corresponding County Info
|
||||
*/
|
||||
public class EnrichedSite
|
||||
public class EnrichedSite implements Comparable<EnrichedSite>
|
||||
{
|
||||
private static final String PHASE_2_TDMA_MODULATION = "TDMA";
|
||||
private Site mSite;
|
||||
private CountyInfo mCountyInfo;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param site object
|
||||
* @param countyInfo optional
|
||||
*/
|
||||
@@ -41,8 +43,16 @@ public class EnrichedSite
|
||||
mCountyInfo = countyInfo;
|
||||
}
|
||||
|
||||
public static String format(int value)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(value).append(" (").append(Integer.toHexString(value).toUpperCase()).append(")");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Site instance
|
||||
*
|
||||
* @return site or null
|
||||
*/
|
||||
public Site getSite()
|
||||
@@ -52,6 +62,7 @@ public class EnrichedSite
|
||||
|
||||
/**
|
||||
* Sets the site instance
|
||||
*
|
||||
* @param site or null
|
||||
*/
|
||||
public void setSite(Site site)
|
||||
@@ -61,6 +72,7 @@ public class EnrichedSite
|
||||
|
||||
/**
|
||||
* County information
|
||||
*
|
||||
* @return county info or null
|
||||
*/
|
||||
public CountyInfo getCountyInfo()
|
||||
@@ -70,6 +82,7 @@ public class EnrichedSite
|
||||
|
||||
/**
|
||||
* Sets the county info
|
||||
*
|
||||
* @param countyInfo or null
|
||||
*/
|
||||
public void setCountyInfo(CountyInfo countyInfo)
|
||||
@@ -78,13 +91,26 @@ public class EnrichedSite
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional site number
|
||||
* Formatted system identity
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Integer getSiteNumber()
|
||||
public String getSystemFormatted()
|
||||
{
|
||||
if(mSite != null)
|
||||
{
|
||||
return mSite.getSiteNumber();
|
||||
//System number is stored in the zone number field.
|
||||
return format(mSite.getZoneNumber());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getSiteFormatted()
|
||||
{
|
||||
if(mSite != null)
|
||||
{
|
||||
return format(mSite.getSiteNumber());
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -93,11 +119,11 @@ public class EnrichedSite
|
||||
/**
|
||||
* Optional site RFSS value
|
||||
*/
|
||||
public Integer getRfss()
|
||||
public String getRfssFormatted()
|
||||
{
|
||||
if(mSite != null)
|
||||
{
|
||||
return mSite.getRfss();
|
||||
return format(mSite.getRfss());
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -128,4 +154,23 @@ public class EnrichedSite
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String system = getSystemFormatted();
|
||||
sb.append(system == null ? "-" : system).append(" ");
|
||||
String rfss = getRfssFormatted();
|
||||
sb.append(rfss == null ? "-" : rfss).append(" ");
|
||||
String site = getSiteFormatted();
|
||||
sb.append(site == null ? "-" : site);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(EnrichedSite o)
|
||||
{
|
||||
return this.toString().compareTo(o.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -449,6 +449,16 @@ public class RadioReferenceDecoder
|
||||
* @return decoder type or null.
|
||||
*/
|
||||
public DecoderType getDecoderType(System system)
|
||||
{
|
||||
return getDecoderType(system, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoder type for the specified system, if supported.
|
||||
* @param system requiring a decoder type
|
||||
* @return decoder type or null.
|
||||
*/
|
||||
public DecoderType getDecoderType(System system, Site site)
|
||||
{
|
||||
Type type = getType(system);
|
||||
Flavor flavor = getFlavor(system);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.type.Flavor;
|
||||
import io.github.dsheirer.rrapi.type.RadioNetwork;
|
||||
import io.github.dsheirer.rrapi.type.Site;
|
||||
import io.github.dsheirer.rrapi.type.SiteFrequency;
|
||||
@@ -42,6 +43,13 @@ import io.github.dsheirer.rrapi.type.System;
|
||||
import io.github.dsheirer.rrapi.type.SystemInformation;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import javafx.animation.RotateTransition;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -74,14 +82,6 @@ import org.controlsfx.control.SegmentedButton;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class SiteEditor extends GridPane
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(SiteEditor.class);
|
||||
@@ -90,6 +90,8 @@ 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;
|
||||
private PlaylistManager mPlaylistManager;
|
||||
@@ -115,6 +117,10 @@ public class SiteEditor extends GridPane
|
||||
private SystemInformation mCurrentSystemInformation;
|
||||
private ComboBox mAliasListNameComboBox;
|
||||
private Button mNewAliasListButton;
|
||||
private Label mP25ControlLabel;
|
||||
private SegmentedButton mP25ControlSegmentedButton;
|
||||
private ToggleButton mFdmaControlToggleButton;
|
||||
private ToggleButton mTdmaControlToggleButton;
|
||||
|
||||
public SiteEditor(UserPreferences userPreferences, PlaylistManager playlistManager)
|
||||
{
|
||||
@@ -163,6 +169,13 @@ public class SiteEditor extends GridPane
|
||||
GridPane.setConstraints(getConfigurationsSegmentedButton(), 2, row);
|
||||
getChildren().add(getConfigurationsSegmentedButton());
|
||||
|
||||
GridPane.setConstraints(getP25ControlLabel(), 1, ++row);
|
||||
GridPane.setHalignment(getP25ControlLabel(), HPos.RIGHT);
|
||||
getChildren().add(getP25ControlLabel());
|
||||
|
||||
GridPane.setConstraints(getP25ControlSegmentedButton(), 2, row);
|
||||
getChildren().add(getP25ControlSegmentedButton());
|
||||
|
||||
Label systemLabel = new Label("System");
|
||||
GridPane.setConstraints(systemLabel, 1, ++row);
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
@@ -206,7 +219,7 @@ public class SiteEditor extends GridPane
|
||||
createBox.setAlignment(Pos.CENTER_LEFT);
|
||||
createBox.setSpacing(10);
|
||||
createBox.getChildren().addAll(getCreateChannelConfigurationButton(), getGoToChannelEditorCheckBox());
|
||||
GridPane.setConstraints(createBox, 2, ++row + 1);
|
||||
GridPane.setConstraints(createBox, 2, ++row);
|
||||
getChildren().addAll(createBox);
|
||||
|
||||
//Note: the following label node is added to the same location as the buttons and visibility is toggled
|
||||
@@ -215,6 +228,13 @@ public class SiteEditor extends GridPane
|
||||
getChildren().add(getProtocolNotSupportedLabel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a channel decode configuration for the specified site.
|
||||
* @param decoderType to create
|
||||
* @param site information and frequencies
|
||||
* @param systemInformation with frequency mapping
|
||||
* @return decode configuration
|
||||
*/
|
||||
private DecodeConfiguration getDecodeConfiguration(DecoderType decoderType, Site site, SystemInformation systemInformation)
|
||||
{
|
||||
if(decoderType == null)
|
||||
@@ -298,7 +318,13 @@ public class SiteEditor extends GridPane
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the user selected site info into this editor.
|
||||
* @param site to load
|
||||
* @param system for the site
|
||||
* @param systemInformation for the site
|
||||
* @param decoder to lookup supplemental information
|
||||
*/
|
||||
public void setSite(EnrichedSite site, System system, SystemInformation systemInformation, RadioReferenceDecoder decoder)
|
||||
{
|
||||
mCurrentSite = site;
|
||||
@@ -346,6 +372,29 @@ public class SiteEditor extends GridPane
|
||||
}
|
||||
else
|
||||
{
|
||||
Flavor flavor = decoder.getFlavor(system);
|
||||
|
||||
if(flavor != null && flavor.getName() != null && flavor.getName().equals(PHASE_2_FLAVOR))
|
||||
{
|
||||
getP25ControlLabel().setVisible(true);
|
||||
getTdmaControlToggleButton().setVisible(true);
|
||||
getFdmaControlToggleButton().setVisible(true);
|
||||
if(site.getSite().getModulation() != null && site.getSite().getModulation().contains(PHASE_2_TDMA_MODULATION))
|
||||
{
|
||||
getTdmaControlToggleButton().setSelected(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
getFdmaControlToggleButton().setSelected(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
getP25ControlLabel().setVisible(false);
|
||||
getTdmaControlToggleButton().setVisible(false);
|
||||
getFdmaControlToggleButton().setVisible(false);
|
||||
}
|
||||
|
||||
getCreateChannelConfigurationButton().setVisible(true);
|
||||
getGoToChannelEditorCheckBox().setVisible(true);
|
||||
getSystemTextField().setText(system.getName());
|
||||
@@ -514,10 +563,13 @@ public class SiteEditor extends GridPane
|
||||
|
||||
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
|
||||
|
||||
//Change a phase 2 system to use the phase 1 control channel
|
||||
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
|
||||
if(decoderType == DecoderType.P25_PHASE2)
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
if(getFdmaControlToggleButton().isSelected())
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
}
|
||||
}
|
||||
|
||||
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(), mCurrentSystemInformation));
|
||||
@@ -587,10 +639,13 @@ public class SiteEditor extends GridPane
|
||||
|
||||
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
|
||||
|
||||
//Change a phase 2 system to use the phase 1 control channel
|
||||
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
|
||||
if(decoderType == DecoderType.P25_PHASE2)
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
if(getFdmaControlToggleButton().isSelected())
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
}
|
||||
}
|
||||
|
||||
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
|
||||
@@ -685,6 +740,16 @@ public class SiteEditor extends GridPane
|
||||
Channel channel = getChannelTemplate();
|
||||
|
||||
DecoderType decoderType = mRadioReferenceDecoder.getDecoderType(mCurrentSystem);
|
||||
|
||||
//Phase 2 - inspect the site modulation and use Phase 2 for TDMA control channel, otherwise Phase 1
|
||||
if(decoderType == DecoderType.P25_PHASE2)
|
||||
{
|
||||
if(getFdmaControlToggleButton().isSelected())
|
||||
{
|
||||
decoderType = DecoderType.P25_PHASE1;
|
||||
}
|
||||
}
|
||||
|
||||
channel.setDecodeConfiguration(getDecodeConfiguration(decoderType, mCurrentSite.getSite(),
|
||||
mCurrentSystemInformation));
|
||||
|
||||
@@ -750,6 +815,70 @@ public class SiteEditor extends GridPane
|
||||
return (long)(siteFrequency.getFrequency() * 1E6);
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 control channel label.
|
||||
*/
|
||||
private Label getP25ControlLabel()
|
||||
{
|
||||
if(mP25ControlLabel == null)
|
||||
{
|
||||
mP25ControlLabel = new Label("Control");
|
||||
mP25ControlLabel.setVisible(false);
|
||||
}
|
||||
|
||||
return mP25ControlLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 control channel type (TDMA vs FDMA) selection buttons.
|
||||
*/
|
||||
private SegmentedButton getP25ControlSegmentedButton()
|
||||
{
|
||||
if(mP25ControlSegmentedButton == null)
|
||||
{
|
||||
mP25ControlSegmentedButton = new SegmentedButton(getFdmaControlToggleButton(), getTdmaControlToggleButton());
|
||||
mP25ControlSegmentedButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
|
||||
mP25ControlSegmentedButton.getToggleGroup().selectedToggleProperty()
|
||||
.addListener((observable, oldValue, newValue) -> {
|
||||
//Ensure that one button is always selected
|
||||
if(newValue == null)
|
||||
{
|
||||
oldValue.setSelected(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mP25ControlSegmentedButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 with FDMA (phase 1) control channel selection button.
|
||||
*/
|
||||
private ToggleButton getFdmaControlToggleButton()
|
||||
{
|
||||
if(mFdmaControlToggleButton == null)
|
||||
{
|
||||
mFdmaControlToggleButton = new ToggleButton("FDMA Phase 1");
|
||||
mFdmaControlToggleButton.setVisible(false);
|
||||
}
|
||||
|
||||
return mFdmaControlToggleButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 with TDMA (phase 2) control channel selection button.
|
||||
*/
|
||||
private ToggleButton getTdmaControlToggleButton()
|
||||
{
|
||||
if(mTdmaControlToggleButton == null)
|
||||
{
|
||||
mTdmaControlToggleButton = new ToggleButton("TDMA Phase 2");
|
||||
mTdmaControlToggleButton.setVisible(false);
|
||||
}
|
||||
|
||||
return mTdmaControlToggleButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flashes the alias list combobox to let the user know that they must select an alias list
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
package io.github.dsheirer.gui.playlist.radioreference;
|
||||
|
||||
import com.google.common.collect.Ordering;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.rrapi.type.Flavor;
|
||||
@@ -42,6 +43,8 @@ import javafx.scene.layout.Priority;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -126,6 +129,7 @@ public class SystemSiteSelectionEditor extends GridPane
|
||||
getFlavorLabel().setText(flavor != null ? flavor.getName() : null);
|
||||
Voice voice = decoder.getVoice(system);
|
||||
getVoiceLabel().setText(voice != null ? voice.getName() : null);
|
||||
Collections.sort(sites, Ordering.natural());
|
||||
getSiteTableView().getItems().addAll(sites);
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -212,27 +216,33 @@ public class SystemSiteSelectionEditor extends GridPane
|
||||
mSiteTableView = new TableView<>();
|
||||
mSiteTableView.setPlaceholder(getPlaceholderLabel());
|
||||
|
||||
TableColumn numberColumn = new TableColumn();
|
||||
numberColumn.setText("Site");
|
||||
numberColumn.setCellValueFactory(new PropertyValueFactory<>("siteNumber"));
|
||||
numberColumn.setPrefWidth(75);
|
||||
TableColumn systemColumn = new TableColumn();
|
||||
systemColumn.setText("System");
|
||||
systemColumn.setCellValueFactory(new PropertyValueFactory<>("systemFormatted"));
|
||||
systemColumn.setPrefWidth(60);
|
||||
|
||||
TableColumn rfssColumn = new TableColumn();
|
||||
rfssColumn.setText("RFSS");
|
||||
rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfss"));
|
||||
rfssColumn.setCellValueFactory(new PropertyValueFactory<>("rfssFormatted"));
|
||||
rfssColumn.setPrefWidth(60);
|
||||
|
||||
TableColumn countyColumn = new TableColumn();
|
||||
countyColumn.setText("County");
|
||||
countyColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
|
||||
countyColumn.setPrefWidth(125);
|
||||
TableColumn siteColumn = new TableColumn();
|
||||
siteColumn.setText("Site");
|
||||
siteColumn.setCellValueFactory(new PropertyValueFactory<>("siteFormatted"));
|
||||
siteColumn.setPrefWidth(75);
|
||||
|
||||
TableColumn countyNameColumn = new TableColumn();
|
||||
countyNameColumn.setText("County");
|
||||
countyNameColumn.setCellValueFactory(new PropertyValueFactory<>("countyName"));
|
||||
countyNameColumn.setPrefWidth(125);
|
||||
|
||||
TableColumn descriptionColumn = new TableColumn();
|
||||
descriptionColumn.setText("Name");
|
||||
descriptionColumn.setCellValueFactory(new PropertyValueFactory<>("description"));
|
||||
descriptionColumn.setPrefWidth(400);
|
||||
|
||||
mSiteTableView.getColumns().addAll(numberColumn, rfssColumn, countyColumn, descriptionColumn);
|
||||
mSiteTableView.getColumns().addAll(systemColumn, rfssColumn, siteColumn, countyNameColumn,
|
||||
descriptionColumn);
|
||||
mSiteTableView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
mSiteTableView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, selected) ->
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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 io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
/**
|
||||
* Viewer for channel start processing requests
|
||||
*/
|
||||
public class ChannelStartProcessingRequestViewer extends HBox
|
||||
{
|
||||
private Label mChannelConfigLabel;
|
||||
private Label mChannelDescriptorLabel;
|
||||
private Label mTrafficChannelManagerLabel;
|
||||
private Label mPreloadDataContentLabel;
|
||||
private Label mParentDecodeHistoryLabel;
|
||||
private Label mChildDecodeHistoryLabel;
|
||||
private IdentifierCollectionViewer mIdentifierCollectionViewer;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public ChannelStartProcessingRequestViewer()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setVgap(5);
|
||||
gridPane.setHgap(5);
|
||||
|
||||
gridPane.add(new Label("Configuration:"), 0, 0);
|
||||
GridPane.setHgrow(getChannelConfigLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getChannelConfigLabel(), 1, 0);
|
||||
|
||||
gridPane.add(new Label("Channel:"), 0, 1);
|
||||
GridPane.setHgrow(getChannelDescriptorLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getChannelDescriptorLabel(), 1, 1);
|
||||
|
||||
gridPane.add(new Label("TCM:"), 0, 2);
|
||||
GridPane.setHgrow(getTrafficChannelManagerLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getTrafficChannelManagerLabel(), 1, 2);
|
||||
|
||||
gridPane.add(new Label("Preload Items:"), 0, 3);
|
||||
GridPane.setHgrow(getPreloadDataContentLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getPreloadDataContentLabel(), 1, 3);
|
||||
|
||||
gridPane.add(new Label("Parent History:"), 0, 4);
|
||||
GridPane.setHgrow(getParentDecodeHistoryLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getParentDecodeHistoryLabel(), 1, 4);
|
||||
|
||||
gridPane.add(new Label("Child History:"), 0, 5);
|
||||
GridPane.setHgrow(getChildDecodeHistoryLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getChildDecodeHistoryLabel(), 1, 5);
|
||||
|
||||
gridPane.add(new Label("Identifiers"), 0, 6);
|
||||
GridPane.setHgrow(getIdentifierCollectionViewer(), Priority.ALWAYS);
|
||||
getIdentifierCollectionViewer().setPrefHeight(120);
|
||||
gridPane.add(getIdentifierCollectionViewer(), 0, 7, 2, 1);
|
||||
|
||||
ColumnConstraints cc0 = new ColumnConstraints();
|
||||
cc0.setHalignment(HPos.RIGHT);
|
||||
gridPane.getColumnConstraints().add(cc0);
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
public void set(ChannelStartProcessingRequest request)
|
||||
{
|
||||
if(request != null)
|
||||
{
|
||||
if(request.getChannel() != null)
|
||||
{
|
||||
getChannelConfigLabel().setText(request.getChannel().toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
getChannelConfigLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getChannelDescriptor() != null)
|
||||
{
|
||||
getChannelDescriptorLabel().setText(request.getChannelDescriptor().toString() + " " +
|
||||
request.getChannelDescriptor().getDownlinkFrequency() + " MHz");
|
||||
}
|
||||
else
|
||||
{
|
||||
getChannelDescriptorLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getChildDecodeEventHistory() != null)
|
||||
{
|
||||
getChildDecodeHistoryLabel().setText(request.getChildDecodeEventHistory().getItems().size() + " items");
|
||||
}
|
||||
else
|
||||
{
|
||||
getChildDecodeHistoryLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getParentDecodeEventHistory() != null)
|
||||
{
|
||||
getParentDecodeHistoryLabel().setText(request.getParentDecodeEventHistory().getItems().size() + " items");
|
||||
}
|
||||
else
|
||||
{
|
||||
getParentDecodeHistoryLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getPreloadDataContents() != null)
|
||||
{
|
||||
getPreloadDataContentLabel().setText(request.getPreloadDataContents().size() + " items");
|
||||
}
|
||||
else
|
||||
{
|
||||
getPreloadDataContentLabel().setText("None");
|
||||
}
|
||||
|
||||
if(request.getTrafficChannelManager() != null)
|
||||
{
|
||||
getTrafficChannelManagerLabel().setText(request.getTrafficChannelManager().getClass().getName());
|
||||
}
|
||||
else
|
||||
{
|
||||
getTrafficChannelManagerLabel().setText("None:");
|
||||
}
|
||||
|
||||
if(request.getIdentifierCollection() != null)
|
||||
{
|
||||
getIdentifierCollectionViewer().set(request.getIdentifierCollection());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
getChannelDescriptorLabel().setText(null);
|
||||
getChildDecodeHistoryLabel().setText(null);
|
||||
getChannelConfigLabel().setText(null);
|
||||
getIdentifierCollectionViewer().set(null);
|
||||
getParentDecodeHistoryLabel().setText(null);
|
||||
getPreloadDataContentLabel().setText(null);
|
||||
getTrafficChannelManagerLabel().setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
public Label getChannelConfigLabel()
|
||||
{
|
||||
if(mChannelConfigLabel == null)
|
||||
{
|
||||
mChannelConfigLabel = new Label();
|
||||
}
|
||||
|
||||
return mChannelConfigLabel;
|
||||
}
|
||||
|
||||
public Label getChannelDescriptorLabel()
|
||||
{
|
||||
if(mChannelDescriptorLabel == null)
|
||||
{
|
||||
mChannelDescriptorLabel = new Label();
|
||||
}
|
||||
|
||||
return mChannelDescriptorLabel;
|
||||
}
|
||||
|
||||
private Label getTrafficChannelManagerLabel()
|
||||
{
|
||||
if(mTrafficChannelManagerLabel == null)
|
||||
{
|
||||
mTrafficChannelManagerLabel = new Label();
|
||||
|
||||
}
|
||||
|
||||
return mTrafficChannelManagerLabel;
|
||||
}
|
||||
|
||||
private Label getPreloadDataContentLabel()
|
||||
{
|
||||
if(mPreloadDataContentLabel == null)
|
||||
{
|
||||
mPreloadDataContentLabel = new Label();
|
||||
}
|
||||
|
||||
return mPreloadDataContentLabel;
|
||||
}
|
||||
|
||||
private Label getParentDecodeHistoryLabel()
|
||||
{
|
||||
if(mParentDecodeHistoryLabel == null)
|
||||
{
|
||||
mParentDecodeHistoryLabel = new Label();
|
||||
}
|
||||
|
||||
return mParentDecodeHistoryLabel;
|
||||
}
|
||||
|
||||
private Label getChildDecodeHistoryLabel()
|
||||
{
|
||||
if(mChildDecodeHistoryLabel == null)
|
||||
{
|
||||
mChildDecodeHistoryLabel = new Label();
|
||||
}
|
||||
|
||||
return mChildDecodeHistoryLabel;
|
||||
}
|
||||
|
||||
private IdentifierCollectionViewer getIdentifierCollectionViewer()
|
||||
{
|
||||
if(mIdentifierCollectionViewer == null)
|
||||
{
|
||||
mIdentifierCollectionViewer = new IdentifierCollectionViewer();
|
||||
}
|
||||
|
||||
return mIdentifierCollectionViewer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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 io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* JavaFX identifier collection viewer
|
||||
*/
|
||||
public class IdentifierCollectionViewer extends VBox
|
||||
{
|
||||
private TableView<Identifier> mIdentifierTableView;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public IdentifierCollectionViewer()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
GridPane.setHgrow(getIdentifierTableView(), Priority.ALWAYS);
|
||||
gridPane.add(getIdentifierTableView(), 0, 0);
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
public void set(IdentifierCollection identifierCollection)
|
||||
{
|
||||
getIdentifierTableView().getItems().clear();
|
||||
|
||||
if(identifierCollection != null)
|
||||
{
|
||||
getIdentifierTableView().getItems().addAll(identifierCollection.getIdentifiers());
|
||||
}
|
||||
}
|
||||
|
||||
public TableView<Identifier> getIdentifierTableView()
|
||||
{
|
||||
if(mIdentifierTableView == null)
|
||||
{
|
||||
mIdentifierTableView = new TableView<>();
|
||||
|
||||
TableColumn classColumn = new TableColumn();
|
||||
classColumn.setPrefWidth(110);
|
||||
classColumn.setText("Class");
|
||||
classColumn.setCellValueFactory(new PropertyValueFactory<>("identifierClass"));
|
||||
|
||||
TableColumn formColumn = new TableColumn();
|
||||
formColumn.setPrefWidth(160);
|
||||
formColumn.setText("Form");
|
||||
formColumn.setCellValueFactory(new PropertyValueFactory<>("form"));
|
||||
|
||||
TableColumn roleColumn = new TableColumn();
|
||||
roleColumn.setPrefWidth(110);
|
||||
roleColumn.setText("Role");
|
||||
roleColumn.setCellValueFactory(new PropertyValueFactory<>("role"));
|
||||
|
||||
TableColumn valueColumn = new TableColumn();
|
||||
valueColumn.setPrefWidth(160);
|
||||
valueColumn.setText("Value");
|
||||
valueColumn.setCellValueFactory(new PropertyValueFactory<>("value"));
|
||||
|
||||
mIdentifierTableView.getColumns().addAll(classColumn, formColumn, roleColumn, valueColumn);
|
||||
}
|
||||
|
||||
return mIdentifierTableView;
|
||||
}
|
||||
}
|
||||
159
src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
Normal file
159
src/main/java/io/github/dsheirer/gui/viewer/MessagePackage.java
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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 io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wrapper class that combines a single message with any decoder state events and decoded events that were produced by a
|
||||
* decoder state.
|
||||
*/
|
||||
public class MessagePackage
|
||||
{
|
||||
private IMessage mMessage;
|
||||
private List<DecoderStateEvent> mDecoderStateEvents = new ArrayList<>();
|
||||
private List<DecodeEventSnapshot> mDecodeEvents = new ArrayList<>();
|
||||
private ChannelStartProcessingRequest mChannelStartProcessingRequest;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param message for this instance
|
||||
*/
|
||||
public MessagePackage(IMessage message)
|
||||
{
|
||||
mMessage = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message for this instance
|
||||
*/
|
||||
public IMessage getMessage()
|
||||
{
|
||||
return mMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of decoder state events
|
||||
*/
|
||||
public List<DecoderStateEvent> getDecoderStateEvents()
|
||||
{
|
||||
return mDecoderStateEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decoder state event to this instance
|
||||
*/
|
||||
public void add(DecoderStateEvent event)
|
||||
{
|
||||
mDecoderStateEvents.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of decode event snapshots
|
||||
*/
|
||||
public List<DecodeEventSnapshot> getDecodeEvents()
|
||||
{
|
||||
return mDecodeEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decode event to this instance
|
||||
*/
|
||||
public void add(DecodeEventSnapshot event)
|
||||
{
|
||||
mDecodeEvents.add(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the channel start processing request
|
||||
*/
|
||||
public void add(ChannelStartProcessingRequest request)
|
||||
{
|
||||
mChannelStartProcessingRequest = request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message timeslot
|
||||
*/
|
||||
public int getTimeslot()
|
||||
{
|
||||
return getMessage().getTimeslot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Message timestamp
|
||||
*/
|
||||
public long getTimestamp()
|
||||
{
|
||||
return getMessage().getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Message validity flag
|
||||
*/
|
||||
public boolean isValid()
|
||||
{
|
||||
return getMessage().isValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Message string representation
|
||||
*/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getMessage().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of decode events
|
||||
*/
|
||||
public int getDecodeEventCount()
|
||||
{
|
||||
return mDecodeEvents.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of decoder state events
|
||||
*/
|
||||
public int getDecoderStateEventCount()
|
||||
{
|
||||
return mDecoderStateEvents.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count (0 or 1) of channel start processing requests.
|
||||
*/
|
||||
public int getChannelStartProcessingRequestCount()
|
||||
{
|
||||
return mChannelStartProcessingRequest == null ? 0 : 1;
|
||||
}
|
||||
|
||||
public ChannelStartProcessingRequest getChannelStartProcessingRequest()
|
||||
{
|
||||
return mChannelStartProcessingRequest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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 io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* JavaFX message package details viewer
|
||||
*/
|
||||
public class MessagePackageViewer extends VBox
|
||||
{
|
||||
private Label mMessageLabel;
|
||||
private TableView<DecoderStateEvent> mDecoderStateEventTableView;
|
||||
private TableView<DecodeEventSnapshot> mDecodeEventTableView;
|
||||
private IdentifierCollectionViewer mIdentifierCollectionViewer;
|
||||
private ChannelStartProcessingRequestViewer mChannelStartProcessingRequestViewer;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public MessagePackageViewer()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(5);
|
||||
gridPane.setVgap(5);
|
||||
Label messageLabel = new Label("Message:");
|
||||
gridPane.add(messageLabel, 0, 0);
|
||||
GridPane.setHgrow(getMessageLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getMessageLabel(), 1, 0);
|
||||
|
||||
gridPane.add(new Label("Decoder State Events"), 0, 1);
|
||||
getDecoderStateEventTableView().setPrefHeight(120);
|
||||
GridPane.setHgrow(getDecoderStateEventTableView(), Priority.NEVER);
|
||||
gridPane.add(getDecoderStateEventTableView(), 0, 2);
|
||||
|
||||
gridPane.add(new Label("Decode Events"), 1, 1);
|
||||
getDecodeEventTableView().setPrefHeight(120);
|
||||
GridPane.setHgrow(getDecodeEventTableView(), Priority.ALWAYS);
|
||||
gridPane.add(getDecodeEventTableView(), 1, 2);
|
||||
|
||||
gridPane.add(new Label("Channel Start Processing Request"), 0, 3);
|
||||
gridPane.add(getChannelStartProcessingRequestViewer(), 0, 4);
|
||||
|
||||
getIdentifierCollectionViewer().setPrefHeight(120);
|
||||
gridPane.add(new Label("Selected Decode Event Identifiers"), 1, 3);
|
||||
gridPane.add(getIdentifierCollectionViewer(), 1, 4);
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
public void set(MessagePackage messagePackage)
|
||||
{
|
||||
getMessageLabel().setText(null);
|
||||
getDecoderStateEventTableView().getItems().clear();
|
||||
getDecodeEventTableView().getItems().clear();
|
||||
getChannelStartProcessingRequestViewer().set(null);
|
||||
|
||||
if(messagePackage != null)
|
||||
{
|
||||
String message = messagePackage.toString();
|
||||
if(message.length() > 40)
|
||||
{
|
||||
message = message.substring(0, 40) + "...";
|
||||
}
|
||||
getMessageLabel().setText(message);
|
||||
getDecoderStateEventTableView().getItems().addAll(messagePackage.getDecoderStateEvents());
|
||||
getDecodeEventTableView().getItems().addAll(messagePackage.getDecodeEvents());
|
||||
getChannelStartProcessingRequestViewer().set(messagePackage.getChannelStartProcessingRequest());
|
||||
|
||||
if(getDecodeEventTableView().getItems().size() > 0)
|
||||
{
|
||||
getDecodeEventTableView().getSelectionModel().select(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IdentifierCollectionViewer getIdentifierCollectionViewer()
|
||||
{
|
||||
if(mIdentifierCollectionViewer == null)
|
||||
{
|
||||
mIdentifierCollectionViewer = new IdentifierCollectionViewer();
|
||||
}
|
||||
|
||||
return mIdentifierCollectionViewer;
|
||||
}
|
||||
|
||||
private Label getMessageLabel()
|
||||
{
|
||||
if(mMessageLabel == null)
|
||||
{
|
||||
mMessageLabel = new Label();
|
||||
mMessageLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
return mMessageLabel;
|
||||
}
|
||||
|
||||
public TableView<DecoderStateEvent> getDecoderStateEventTableView()
|
||||
{
|
||||
if(mDecoderStateEventTableView == null)
|
||||
{
|
||||
mDecoderStateEventTableView = new TableView<>();
|
||||
|
||||
TableColumn timeslotColumn = new TableColumn();
|
||||
timeslotColumn.setPrefWidth(110);
|
||||
timeslotColumn.setText("Timeslot");
|
||||
timeslotColumn.setCellValueFactory(new PropertyValueFactory<>("timeslot"));
|
||||
|
||||
TableColumn stateColumn = new TableColumn();
|
||||
stateColumn.setPrefWidth(110);
|
||||
stateColumn.setText("State");
|
||||
stateColumn.setCellValueFactory(new PropertyValueFactory<>("state"));
|
||||
|
||||
TableColumn eventColumn = new TableColumn();
|
||||
eventColumn.setPrefWidth(110);
|
||||
eventColumn.setText("Event");
|
||||
eventColumn.setCellValueFactory(new PropertyValueFactory<>("event"));
|
||||
|
||||
TableColumn frequencyColumn = new TableColumn();
|
||||
frequencyColumn.setPrefWidth(110);
|
||||
frequencyColumn.setText("Frequency");
|
||||
frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
|
||||
|
||||
mDecoderStateEventTableView.getColumns().addAll(timeslotColumn, stateColumn, eventColumn, frequencyColumn);
|
||||
}
|
||||
|
||||
return mDecoderStateEventTableView;
|
||||
}
|
||||
|
||||
private TableView<DecodeEventSnapshot> getDecodeEventTableView()
|
||||
{
|
||||
if(mDecodeEventTableView == null)
|
||||
{
|
||||
mDecodeEventTableView = new TableView<>();
|
||||
mDecodeEventTableView.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
TableColumn startTimeColumn = new TableColumn();
|
||||
startTimeColumn.setPrefWidth(110);
|
||||
startTimeColumn.setText("Start");
|
||||
startTimeColumn.setCellValueFactory(new PropertyValueFactory<>("timeStart"));
|
||||
|
||||
TableColumn durationColumn = new TableColumn();
|
||||
durationColumn.setPrefWidth(110);
|
||||
durationColumn.setText("Duration");
|
||||
durationColumn.setCellValueFactory(new PropertyValueFactory<>("duration"));
|
||||
|
||||
TableColumn typeColumn = new TableColumn();
|
||||
typeColumn.setPrefWidth(130);
|
||||
typeColumn.setText("Type");
|
||||
typeColumn.setCellValueFactory(new PropertyValueFactory<>("eventType"));
|
||||
|
||||
TableColumn channelDescriptorColumn = new TableColumn();
|
||||
channelDescriptorColumn.setPrefWidth(110);
|
||||
channelDescriptorColumn.setText("Channel");
|
||||
channelDescriptorColumn.setCellValueFactory(new PropertyValueFactory<>("channelDescriptor"));
|
||||
|
||||
TableColumn frequencyColumn = new TableColumn();
|
||||
frequencyColumn.setPrefWidth(110);
|
||||
frequencyColumn.setText("Frequency");
|
||||
frequencyColumn.setCellValueFactory(new PropertyValueFactory<>("frequency"));
|
||||
|
||||
TableColumn hashcodeColumn = new TableColumn();
|
||||
hashcodeColumn.setPrefWidth(100);
|
||||
hashcodeColumn.setText("Hash ID");
|
||||
hashcodeColumn.setCellValueFactory(new PropertyValueFactory<>("originalHashCode"));
|
||||
|
||||
TableColumn detailsColumn = new TableColumn();
|
||||
detailsColumn.setPrefWidth(500);
|
||||
detailsColumn.setText("Details");
|
||||
detailsColumn.setCellValueFactory(new PropertyValueFactory<>("details"));
|
||||
|
||||
mDecodeEventTableView.getColumns().addAll(startTimeColumn, durationColumn, typeColumn, channelDescriptorColumn,
|
||||
frequencyColumn, hashcodeColumn, detailsColumn);
|
||||
|
||||
mDecodeEventTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->
|
||||
{
|
||||
if(newValue != null)
|
||||
{
|
||||
getIdentifierCollectionViewer().set(newValue.getIdentifierCollection());
|
||||
}
|
||||
else
|
||||
{
|
||||
getIdentifierCollectionViewer().set(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mDecodeEventTableView;
|
||||
}
|
||||
|
||||
private ChannelStartProcessingRequestViewer getChannelStartProcessingRequestViewer()
|
||||
{
|
||||
if(mChannelStartProcessingRequestViewer == null)
|
||||
{
|
||||
mChannelStartProcessingRequestViewer = new ChannelStartProcessingRequestViewer();
|
||||
}
|
||||
|
||||
return mChannelStartProcessingRequestViewer;
|
||||
}
|
||||
}
|
||||
107
src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
Normal file
107
src/main/java/io/github/dsheirer/gui/viewer/MessagePackager.java
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.Subscribe;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventSnapshot;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEventListener;
|
||||
|
||||
/**
|
||||
* Utility for combining a message and decoder state events.
|
||||
*/
|
||||
public class MessagePackager
|
||||
{
|
||||
private MessagePackage mMessagePackage;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public MessagePackager()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the message and creates a new MessageWithEvents instance, wrapping the message, ready to also receive any
|
||||
* decode events and decoder state events. The previous message with events is overwritten.
|
||||
* @param message to wrap.
|
||||
*/
|
||||
public void add(IMessage message)
|
||||
{
|
||||
mMessagePackage = new MessagePackage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the current message with events.
|
||||
*/
|
||||
public MessagePackage getMessageWithEvents()
|
||||
{
|
||||
return mMessagePackage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decoder state event to the current message with events.
|
||||
* @param event to add
|
||||
*/
|
||||
public void add(DecoderStateEvent event)
|
||||
{
|
||||
if(mMessagePackage != null)
|
||||
{
|
||||
mMessagePackage.add(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the decode event to the current message with events.
|
||||
* @param event to add
|
||||
*/
|
||||
public void add(IDecodeEvent event)
|
||||
{
|
||||
if(mMessagePackage != null)
|
||||
{
|
||||
if(event instanceof DecodeEvent decodeEvent)
|
||||
{
|
||||
try
|
||||
{
|
||||
DecodeEventSnapshot snapshot = decodeEvent.getSnapshot();
|
||||
mMessagePackage.add(snapshot);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription to receive channel start processing requests via the event bus from the traffic channel manager
|
||||
* @param request sent from the traffic channel manager.
|
||||
*/
|
||||
@Subscribe
|
||||
public void process(ChannelStartProcessingRequest request)
|
||||
{
|
||||
mMessagePackage.add(request);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -41,20 +41,21 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* Utility application to load and view .bits recording file with the messages fully parsed.
|
||||
*
|
||||
* Supported Protocols: DMR and APCO25 Phase 1.
|
||||
* Supported Protocols: DMR, APCO25 Phase 1 and Phase 2.
|
||||
*/
|
||||
public class RecordingViewer extends VBox
|
||||
public class MessageRecordingViewer extends VBox
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(RecordingViewer.class);
|
||||
private static final Logger mLog = LoggerFactory.getLogger(MessageRecordingViewer.class);
|
||||
private MenuBar mMenuBar;
|
||||
private TabPane mTabPane;
|
||||
private int mTabCounterDmr = 1;
|
||||
private int mTabCounterP25P1 = 1;
|
||||
private int mTabCounterP25P2 = 1;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public RecordingViewer()
|
||||
public MessageRecordingViewer()
|
||||
{
|
||||
VBox.setVgrow(getTabPane(), Priority.ALWAYS);
|
||||
getChildren().addAll(getMenuBar(), getTabPane());
|
||||
@@ -74,13 +75,19 @@ public class RecordingViewer extends VBox
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem p25p1MenuItem = new MenuItem("P25P1");
|
||||
MenuItem p25p1MenuItem = new MenuItem("P25 Phase 1");
|
||||
p25p1MenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer());
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem);
|
||||
MenuItem p25p2MenuItem = new MenuItem("P25 Phase 2");
|
||||
p25p2MenuItem.onActionProperty().set(event -> {
|
||||
Tab tab = new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer());
|
||||
getTabPane().getTabs().add(tab);
|
||||
getTabPane().getSelectionModel().select(tab);
|
||||
});
|
||||
createNewViewerMenu.getItems().addAll(dmrMenuItem, p25p1MenuItem, p25p2MenuItem);
|
||||
|
||||
MenuItem exitMenu = new MenuItem("Exit");
|
||||
exitMenu.onActionProperty().set(event -> ((Stage)getScene().getWindow()).close());
|
||||
@@ -102,6 +109,7 @@ public class RecordingViewer extends VBox
|
||||
mTabPane.setMaxHeight(Double.MAX_VALUE);
|
||||
mTabPane.getTabs().add(new LabeledTab("DMR-" + mTabCounterDmr++, new DmrViewer()));
|
||||
mTabPane.getTabs().add(new LabeledTab("P25P1-" + mTabCounterP25P1++, new P25P1Viewer()));
|
||||
mTabPane.getTabs().add(new LabeledTab("P25P2-" + mTabCounterP25P2++, new P25P2Viewer()));
|
||||
}
|
||||
|
||||
return mTabPane;
|
||||
@@ -151,11 +159,6 @@ public class RecordingViewer extends VBox
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LabeledTab(String label)
|
||||
{
|
||||
this(label, null);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
@@ -165,7 +168,7 @@ public class RecordingViewer extends VBox
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws Exception
|
||||
{
|
||||
Scene scene = new Scene(new RecordingViewer(), 1100, 800);
|
||||
Scene scene = new Scene(new MessageRecordingViewer(), 1100, 800);
|
||||
primaryStage.setTitle("Message Recording Viewer (.bits)");
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.show();
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -19,8 +19,14 @@
|
||||
|
||||
package io.github.dsheirer.gui.viewer;
|
||||
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.message.StuffBitsMessage;
|
||||
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.DecodeConfigP25Phase1;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1DecoderState;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageFramer;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1MessageProcessor;
|
||||
import io.github.dsheirer.record.binary.BinaryReader;
|
||||
@@ -33,8 +39,11 @@ 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;
|
||||
@@ -71,17 +80,20 @@ public class P25P1Viewer extends VBox
|
||||
private static final Logger mLog = LoggerFactory.getLogger(P25P1Viewer.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.p25p1";
|
||||
private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(P25P1Viewer.class);
|
||||
private Button mSelectFileButton;
|
||||
private Label mSelectedFileLabel;
|
||||
private TableView<IMessage> mMessageTableView;
|
||||
private ObservableList<IMessage> mMessages = FXCollections.observableArrayList();
|
||||
private FilteredList<IMessage> mFilteredMessages = new FilteredList<>(mMessages);
|
||||
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();
|
||||
|
||||
public P25P1Viewer()
|
||||
{
|
||||
@@ -115,9 +127,11 @@ public class P25P1Viewer extends VBox
|
||||
|
||||
VBox.setVgrow(fileBox, Priority.NEVER);
|
||||
VBox.setVgrow(filterBox, Priority.NEVER);
|
||||
VBox.setVgrow(getMessageTableView(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
|
||||
|
||||
getChildren().addAll(fileBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
|
||||
|
||||
getChildren().addAll(fileBox, filterBox, getMessageTableView());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,19 +146,54 @@ public class P25P1Viewer extends VBox
|
||||
{
|
||||
if(file != null && file.exists())
|
||||
{
|
||||
mMessages.clear();
|
||||
mLoadedFile.set(file.toString());
|
||||
mMessagePackages.clear();
|
||||
getLoadingIndicator().setVisible(true);
|
||||
getSelectedFileLabel().setText("Loading ...");
|
||||
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
List<IMessage> messages = new ArrayList<>();
|
||||
List<MessagePackage> messagePackages = new ArrayList<>();
|
||||
P25P1MessageFramer messageFramer = new P25P1MessageFramer(null, 9600);
|
||||
P25P1MessageProcessor messageProcessor = new P25P1MessageProcessor();
|
||||
messageFramer.setListener(messageProcessor);
|
||||
|
||||
Channel empty = new Channel("Empty");
|
||||
empty.setDecodeConfiguration(new DecodeConfigP25Phase1());
|
||||
|
||||
MessagePackager messagePackager = new MessagePackager();
|
||||
|
||||
//Setup a temporary event bus to capture channel start processing requests
|
||||
EventBus eventBus = new EventBus("debug");
|
||||
eventBus.register(messagePackager);
|
||||
P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
|
||||
trafficChannelManager.setInterModuleEventBus(eventBus);
|
||||
|
||||
//Register to receive events
|
||||
trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
P25P1DecoderState decoderState = new P25P1DecoderState(empty, trafficChannelManager);
|
||||
decoderState.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
|
||||
decoderState.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
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, 1));
|
||||
}
|
||||
|
||||
messageProcessor.setMessageListener(message -> {
|
||||
if(!(message instanceof StuffBitsMessage))
|
||||
{
|
||||
messages.add(message);
|
||||
//Add the initial message to the packager so that it can be combined with any decoder state events.
|
||||
messagePackager.add(message);
|
||||
decoderState.receive(message);
|
||||
|
||||
//Collect the packaged message with events
|
||||
messagePackages.add(messagePackager.getMessageWithEvents());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -164,13 +213,47 @@ public class P25P1Viewer extends VBox
|
||||
Platform.runLater(() -> {
|
||||
getLoadingIndicator().setVisible(false);
|
||||
getSelectedFileLabel().setText(file.getName());
|
||||
mMessages.addAll(messages);
|
||||
getMessageTableView().scrollTo(0);
|
||||
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
|
||||
*/
|
||||
@@ -180,12 +263,12 @@ public class P25P1Viewer extends VBox
|
||||
|
||||
if(filterText != null && !filterText.isEmpty())
|
||||
{
|
||||
Predicate<IMessage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
|
||||
mFilteredMessages.setPredicate(textPredicate);
|
||||
Predicate<MessagePackage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
|
||||
mFilteredMessagePackages.setPredicate(textPredicate);
|
||||
}
|
||||
else
|
||||
{
|
||||
mFilteredMessages.setPredicate(null);
|
||||
mFilteredMessagePackages.setPredicate(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,12 +280,12 @@ public class P25P1Viewer extends VBox
|
||||
{
|
||||
if(text != null && !text.isEmpty())
|
||||
{
|
||||
for(IMessage message: mFilteredMessages)
|
||||
for(MessagePackage messagePackage: mFilteredMessagePackages)
|
||||
{
|
||||
if(message.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessageTableView().getSelectionModel().select(message);
|
||||
getMessageTableView().scrollTo(message);
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -217,7 +300,7 @@ public class P25P1Viewer extends VBox
|
||||
{
|
||||
if(text != null && !text.isEmpty())
|
||||
{
|
||||
IMessage selected = getMessageTableView().getSelectionModel().getSelectedItem();
|
||||
MessagePackage selected = getMessagePackageTableView().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(selected == null)
|
||||
{
|
||||
@@ -225,18 +308,18 @@ public class P25P1Viewer extends VBox
|
||||
return;
|
||||
}
|
||||
|
||||
int row = mFilteredMessages.indexOf(selected);
|
||||
int row = mFilteredMessagePackages.indexOf(selected);
|
||||
|
||||
for(int x = row + 1; x < mFilteredMessages.size(); x++)
|
||||
for(int x = row + 1; x < mFilteredMessagePackages.size(); x++)
|
||||
{
|
||||
if(x < mFilteredMessages.size())
|
||||
if(x < mFilteredMessagePackages.size())
|
||||
{
|
||||
IMessage message = mFilteredMessages.get(x);
|
||||
MessagePackage messagePackage = mFilteredMessagePackages.get(x);
|
||||
|
||||
if(message.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
if(messagePackage.toString().toLowerCase().contains(text.toLowerCase()))
|
||||
{
|
||||
getMessageTableView().getSelectionModel().select(message);
|
||||
getMessageTableView().scrollTo(message);
|
||||
getMessagePackageTableView().getSelectionModel().select(messagePackage);
|
||||
getMessagePackageTableView().scrollTo(messagePackage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -244,25 +327,40 @@ public class P25P1Viewer extends VBox
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List view control with DMR messages
|
||||
*/
|
||||
private TableView<IMessage> getMessageTableView()
|
||||
private MessagePackageViewer getMessagePackageViewer()
|
||||
{
|
||||
if(mMessageTableView == null)
|
||||
if(mMessagePackageViewer == null)
|
||||
{
|
||||
mMessageTableView = new TableView<>();
|
||||
mMessageTableView.setPlaceholder(getLoadingIndicator());
|
||||
SortedList<IMessage> sortedList = new SortedList<>(mFilteredMessages);
|
||||
sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty());
|
||||
mMessageTableView.setItems(sortedList);
|
||||
mMessagePackageViewer = new MessagePackageViewer();
|
||||
mMessagePackageViewer.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
mMessageTableView.setOnKeyPressed(event ->
|
||||
//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.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 : mMessageTableView.getSelectionModel().getSelectedCells())
|
||||
for (final TablePosition tablePosition : mMessagePackageTableView.getSelectionModel().getSelectedCells())
|
||||
{
|
||||
rows.add(tablePosition.getRow());
|
||||
}
|
||||
@@ -282,7 +380,7 @@ public class P25P1Viewer extends VBox
|
||||
|
||||
boolean firstCol = true;
|
||||
|
||||
for (final TableColumn<?, ?> column : mMessageTableView.getColumns())
|
||||
for (final TableColumn<?, ?> column : mMessagePackageTableView.getColumns())
|
||||
{
|
||||
if(firstCol)
|
||||
{
|
||||
@@ -313,23 +411,44 @@ public class P25P1Viewer extends VBox
|
||||
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(1000);
|
||||
messageColumn.setPrefWidth(900);
|
||||
messageColumn.setText("Message");
|
||||
messageColumn.setCellValueFactory((Callback<TableColumn.CellDataFeatures, ObservableValue>) param -> {
|
||||
SimpleStringProperty property = new SimpleStringProperty();
|
||||
if(param.getValue() instanceof IMessage message)
|
||||
if(param.getValue() instanceof MessagePackage messagePackage)
|
||||
{
|
||||
property.set(message.toString());
|
||||
property.set(messagePackage.toString());
|
||||
}
|
||||
|
||||
return property;
|
||||
});
|
||||
|
||||
mMessageTableView.getColumns().addAll(timestampColumn, validColumn, messageColumn);
|
||||
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"));
|
||||
|
||||
mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
|
||||
decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn);
|
||||
}
|
||||
|
||||
return mMessageTableView;
|
||||
return mMessagePackageTableView;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -343,7 +462,7 @@ public class P25P1Viewer extends VBox
|
||||
mSelectFileButton = new Button("Select ...");
|
||||
mSelectFileButton.onActionProperty().set(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Select DMR .bits Recording");
|
||||
fileChooser.setTitle("Select P25 Phase 1 .bits Recording");
|
||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||
if(lastDirectory != null)
|
||||
{
|
||||
|
||||
738
src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
Normal file
738
src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java
Normal file
@@ -0,0 +1,738 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.controller.channel.Channel;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupManager;
|
||||
import io.github.dsheirer.message.StuffBitsMessage;
|
||||
import io.github.dsheirer.module.decode.p25.P25FrequencyBandPreloadDataContent;
|
||||
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.P25FrequencyBand;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.DecodeConfigP25Phase2;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.P25P2DecoderState;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageFramer;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.P25P2MessageProcessor;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameters;
|
||||
import io.github.dsheirer.record.binary.BinaryReader;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
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.binding.Bindings;
|
||||
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.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
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;
|
||||
|
||||
/**
|
||||
* APCO25 Phase 2 viewer panel
|
||||
*/
|
||||
public class P25P2Viewer extends VBox
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(P25P2Viewer.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.p25p2";
|
||||
private static final String FILE_FREQUENCY_REGEX = ".*\\d{8}_\\d{6}_(\\d{9}).*";
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(P25P2Viewer.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 CheckBox mShowTS0;
|
||||
private CheckBox mShowTS1;
|
||||
private CheckBox mShowTS2;
|
||||
private TextField mSearchText;
|
||||
private TextField mFindText;
|
||||
private Button mFindButton;
|
||||
private Button mFindNextButton;
|
||||
private ProgressIndicator mLoadingIndicator;
|
||||
private IntegerTextField mWACNTextField;
|
||||
private IntegerTextField mSystemTextField;
|
||||
private IntegerTextField mNACTextField;
|
||||
private Button mReloadButton;
|
||||
private StringProperty mLoadedFile = new SimpleStringProperty();
|
||||
private MessagePackageViewer mMessagePackageViewer;
|
||||
|
||||
public P25P2Viewer()
|
||||
{
|
||||
setPadding(new Insets(5));
|
||||
setSpacing(5);
|
||||
|
||||
HBox fileBox = new HBox();
|
||||
fileBox.setMaxWidth(Double.MAX_VALUE);
|
||||
fileBox.setAlignment(Pos.CENTER_LEFT);
|
||||
fileBox.setSpacing(5);
|
||||
getSelectedFileLabel().setAlignment(Pos.BASELINE_CENTER);
|
||||
|
||||
HBox.setHgrow(getSelectFileButton(), Priority.NEVER);
|
||||
HBox.setHgrow(getSelectedFileLabel(), Priority.ALWAYS);
|
||||
fileBox.getChildren().addAll(getSelectFileButton(), getSelectedFileLabel());
|
||||
|
||||
HBox scrambleSettingsBox = new HBox();
|
||||
scrambleSettingsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||
scrambleSettingsBox.setSpacing(5);
|
||||
Label wacnLabel = new Label("WACN:");
|
||||
Label systemLabel = new Label("SYSTEM:");
|
||||
Label nacLabel = new Label("NAC:");
|
||||
scrambleSettingsBox.getChildren().addAll(wacnLabel, getWACNTextField(), systemLabel, getSystemTextField(),
|
||||
nacLabel, getNACTextField(), getReloadButton());
|
||||
|
||||
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);
|
||||
|
||||
Label showLabel = new Label("Show:");
|
||||
HBox.setMargin(showLabel, new Insets(0,0,0,15));
|
||||
|
||||
filterBox.getChildren().addAll(findLabel, getFindText(), getFindButton(), getFindNextButton(), searchLabel,
|
||||
getSearchText(), showLabel, getShowTS0(), getShowTS1(), getShowTS2());
|
||||
|
||||
VBox.setVgrow(fileBox, Priority.NEVER);
|
||||
VBox.setVgrow(filterBox, Priority.NEVER);
|
||||
VBox.setVgrow(scrambleSettingsBox, Priority.NEVER);
|
||||
VBox.setVgrow(getMessagePackageTableView(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getMessagePackageViewer(), Priority.NEVER);
|
||||
|
||||
getChildren().addAll(fileBox, scrambleSettingsBox, filterBox, getMessagePackageTableView(), getMessagePackageViewer());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 data.
|
||||
*/
|
||||
private void load(File file)
|
||||
{
|
||||
mLog.info("Loading File: " + file);
|
||||
if(file != null && file.exists())
|
||||
{
|
||||
mLoadedFile.set(file.toString());
|
||||
mMessagePackages.clear();
|
||||
getLoadingIndicator().setVisible(true);
|
||||
getSelectedFileLabel().setText("Loading ...");
|
||||
|
||||
int wacn = getWACNTextField().get();
|
||||
int system = getSystemTextField().get();
|
||||
int nac = getNACTextField().get();
|
||||
ScrambleParameters scrambleParameters = new ScrambleParameters(wacn, system, nac);
|
||||
|
||||
ThreadPool.CACHED.submit(() -> {
|
||||
List<MessagePackage> messages = new ArrayList<>();
|
||||
P25P2MessageFramer messageFramer = new P25P2MessageFramer(null, 9600);
|
||||
messageFramer.setScrambleParameters(scrambleParameters);
|
||||
P25P2MessageProcessor messageProcessor = new P25P2MessageProcessor();
|
||||
messageFramer.setListener(messageProcessor);
|
||||
Channel empty = new Channel("Empty");
|
||||
empty.setDecodeConfiguration(new DecodeConfigP25Phase2());
|
||||
|
||||
MessagePackager messagePackager = new MessagePackager();
|
||||
|
||||
//Setup a temporary event bus to capture channel start processing requests
|
||||
EventBus eventBus = new EventBus("debug");
|
||||
eventBus.register(messagePackager);
|
||||
P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(empty);
|
||||
trafficChannelManager.setInterModuleEventBus(eventBus);
|
||||
|
||||
//Register to receive events
|
||||
trafficChannelManager.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
PatchGroupManager patchGroupManager = new PatchGroupManager();
|
||||
P25P2DecoderState decoderState1 = new P25P2DecoderState(empty, 1, trafficChannelManager,
|
||||
patchGroupManager);
|
||||
decoderState1.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
|
||||
decoderState1.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
P25P2DecoderState decoderState2 = new P25P2DecoderState(empty, 2, trafficChannelManager,
|
||||
patchGroupManager);
|
||||
decoderState2.setDecoderStateListener(decoderStateEvent -> messagePackager.add(decoderStateEvent));
|
||||
decoderState2.addDecodeEventListener(decodeEvent -> messagePackager.add(decodeEvent));
|
||||
decoderState1.start();
|
||||
decoderState2.start();
|
||||
|
||||
long frequency = getFrequencyFromFile(mLoadedFile.get());
|
||||
|
||||
if(frequency > 0)
|
||||
{
|
||||
trafficChannelManager.setCurrentControlFrequency(frequency, empty);
|
||||
FrequencyConfigurationIdentifier id = FrequencyConfigurationIdentifier.create(frequency);
|
||||
decoderState1.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
|
||||
IdentifierUpdateNotification.Operation.ADD, 1));
|
||||
decoderState2.getConfigurationIdentifierListener().receive(new IdentifierUpdateNotification(id,
|
||||
IdentifierUpdateNotification.Operation.ADD, 2));
|
||||
}
|
||||
|
||||
//TODO: testing use - PCWIN TDMA Frequency Band as preload data
|
||||
//ID:2 OFFSET:-45000000 SPACING:12500 BASE:851012500 TDMA BW:12500 TIMESLOTS:2 VOCODER:HALF_RATE
|
||||
P25FrequencyBand band = new P25FrequencyBand(2, 851012500l, -45000000l, 12500, 12500, 2);
|
||||
P25FrequencyBandPreloadDataContent content = new P25FrequencyBandPreloadDataContent(Collections.singleton(band));
|
||||
messageProcessor.preload(content);
|
||||
|
||||
messageProcessor.setMessageListener(message -> {
|
||||
if(!(message instanceof StuffBitsMessage))
|
||||
{
|
||||
// System.out.println(message);
|
||||
//Add the initial message to the packager so that it can be combined with any decoder state events.
|
||||
messagePackager.add(message);
|
||||
if(message.getTimeslot() == P25P1Message.TIMESLOT_1)
|
||||
{
|
||||
decoderState1.receive(message);
|
||||
}
|
||||
else if(message.getTimeslot() == P25P1Message.TIMESLOT_2)
|
||||
{
|
||||
decoderState2.receive(message);
|
||||
}
|
||||
|
||||
//Collect the packaged message with events
|
||||
messages.add(messagePackager.getMessageWithEvents());
|
||||
}
|
||||
});
|
||||
|
||||
try(BinaryReader reader = new BinaryReader(file.toPath(), 200))
|
||||
{
|
||||
while(reader.hasNext())
|
||||
{
|
||||
ByteBuffer buffer = reader.next();
|
||||
// System.out.println("Processing Bytes " + buffer.capacity() + " / " + reader.getByteCounter());
|
||||
messageFramer.receive(buffer);
|
||||
}
|
||||
}
|
||||
catch(Exception ioe)
|
||||
{
|
||||
ioe.printStackTrace();
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getLoadingIndicator().setVisible(false);
|
||||
getSelectedFileLabel().setText(file.getName());
|
||||
mMessagePackages.addAll(messages);
|
||||
getMessagePackageTableView().scrollTo(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("Can't load file: " + file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the filter(s) applies to the list of messages
|
||||
*/
|
||||
private void updateFilters()
|
||||
{
|
||||
Predicate<MessagePackage> timeslotPredicate = message ->
|
||||
(getShowTS0().isSelected() && (message.getTimeslot() == 0)) ||
|
||||
(getShowTS1().isSelected() && (message.getTimeslot() == 1)) ||
|
||||
(getShowTS2().isSelected() && (message.getTimeslot() == 2));
|
||||
|
||||
String filterText = getSearchText().getText();
|
||||
|
||||
if(filterText == null || filterText.isEmpty())
|
||||
{
|
||||
mFilteredMessagePackages.setPredicate(timeslotPredicate);
|
||||
}
|
||||
else
|
||||
{
|
||||
Predicate<MessagePackage> textPredicate = message -> message.toString().toLowerCase().contains(filterText.toLowerCase());
|
||||
mFilteredMessagePackages.setPredicate(timeslotPredicate.and(textPredicate));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
private IntegerTextField getWACNTextField()
|
||||
{
|
||||
if(mWACNTextField == null)
|
||||
{
|
||||
mWACNTextField = new IntegerTextField();
|
||||
}
|
||||
|
||||
return mWACNTextField;
|
||||
}
|
||||
|
||||
private IntegerTextField getSystemTextField()
|
||||
{
|
||||
if(mSystemTextField == null)
|
||||
{
|
||||
mSystemTextField = new IntegerTextField();
|
||||
}
|
||||
|
||||
return mSystemTextField;
|
||||
}
|
||||
|
||||
private IntegerTextField getNACTextField()
|
||||
{
|
||||
if(mNACTextField == null)
|
||||
{
|
||||
mNACTextField = new IntegerTextField();
|
||||
}
|
||||
|
||||
return mNACTextField;
|
||||
}
|
||||
|
||||
private Button getReloadButton()
|
||||
{
|
||||
if(mReloadButton == null)
|
||||
{
|
||||
mReloadButton = new Button("Reload");
|
||||
mReloadButton.disableProperty().bind(Bindings.isNull(mLoadedFile));
|
||||
mReloadButton.setOnAction(event -> load(new File(mLoadedFile.get())));
|
||||
}
|
||||
|
||||
return mReloadButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* List view control with message packages
|
||||
*/
|
||||
private TableView<MessagePackage> getMessagePackageTableView()
|
||||
{
|
||||
if(mMessagePackageTableView == null)
|
||||
{
|
||||
mMessagePackageTableView = new TableView<>();
|
||||
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 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"));
|
||||
|
||||
mMessagePackageTableView.getColumns().addAll(timestampColumn, validColumn, timeslotColumn, messageColumn,
|
||||
decodeEventCountColumn, decoderStateEventCountColumn, channelStartCountColumn);
|
||||
}
|
||||
|
||||
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 P25 Phase 2 .bits Recording");
|
||||
String lastDirectory = mPreferences.get(LAST_SELECTED_DIRECTORY, null);
|
||||
if(lastDirectory != null)
|
||||
{
|
||||
File file = new File(lastDirectory);
|
||||
if(file.exists() && file.isDirectory())
|
||||
{
|
||||
fileChooser.setInitialDirectory(file);
|
||||
}
|
||||
}
|
||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("sdrtrunk bits recording", "*.bits"));
|
||||
final File selected = fileChooser.showOpenDialog(getScene().getWindow());
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check box to apply filter to show/hide TS0 messages
|
||||
* @return check box control
|
||||
*/
|
||||
private CheckBox getShowTS0()
|
||||
{
|
||||
if(mShowTS0 == null)
|
||||
{
|
||||
mShowTS0 = new CheckBox("TS0");
|
||||
mShowTS0.setSelected(true);
|
||||
mShowTS0.setOnAction(event -> updateFilters());
|
||||
}
|
||||
|
||||
return mShowTS0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check box to apply filter to show/hide TS0 messages
|
||||
* @return check box control
|
||||
*/
|
||||
private CheckBox getShowTS1()
|
||||
{
|
||||
if(mShowTS1 == null)
|
||||
{
|
||||
mShowTS1 = new CheckBox("TS1");
|
||||
mShowTS1.setSelected(true);
|
||||
mShowTS1.setOnAction(event -> updateFilters());
|
||||
}
|
||||
|
||||
return mShowTS1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check box to apply filter to show/hide TS0 messages
|
||||
* @return check box control
|
||||
*/
|
||||
private CheckBox getShowTS2()
|
||||
{
|
||||
if(mShowTS2 == null)
|
||||
{
|
||||
mShowTS2 = new CheckBox("TS2");
|
||||
mShowTS2.setSelected(true);
|
||||
mShowTS2.setOnAction(event -> updateFilters());
|
||||
}
|
||||
|
||||
return mShowTS2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user