#1304 P25 Phase 1/2 decoder enhancements. (#1898)

* 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:
Denny Sheirer
2024-04-26 13:55:17 -04:00
committed by GitHub
parent f5938fa8e2
commit 7e709d9d6f
430 changed files with 30098 additions and 13300 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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