mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-15 10:07:26 -05:00
#2660 Talker alias cleanup across the decoders
This commit is contained in:
@@ -56,7 +56,7 @@ public class ChannelDetailView extends VBox implements Listener<ProcessingChain>
|
||||
|
||||
Button refreshButton = new Button("Refresh");
|
||||
refreshButton.setTooltip(new Tooltip("Update the channel details"));
|
||||
refreshButton.setOnAction(event -> receive(mProcessingChain));
|
||||
refreshButton.setOnAction(event -> refresh());
|
||||
|
||||
HBox labels = new HBox(5, new Label("System:"), grow(mSystemLabel), new Label("Site:"), grow(mSiteLabel),
|
||||
new Label("Channel Name:"), grow(mNameLabel), refreshButton);
|
||||
@@ -68,6 +68,14 @@ public class ChannelDetailView extends VBox implements Listener<ProcessingChain>
|
||||
getChildren().addAll(labels, mDetailTextArea);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the details for the currently selected channel. Call on the JavaFX application thread.
|
||||
*/
|
||||
public void refresh()
|
||||
{
|
||||
receive(mProcessingChain);
|
||||
}
|
||||
|
||||
private static Label grow(Label label)
|
||||
{
|
||||
label.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
@@ -42,6 +42,7 @@ import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -315,19 +316,15 @@ public class ChannelMetadataView extends VBox implements ChannelMetadataModel.Li
|
||||
|
||||
columns.add(NowPlayingCells.column("from", "From", null, Pos.CENTER,
|
||||
row -> {
|
||||
ChannelMetadata metadata = row.getChannelMetadata();
|
||||
String text = mTalkgroupFormatPreference.format(metadata.getFromIdentifier());
|
||||
String text = mTalkgroupFormatPreference.format(row.getChannelMetadata().getFromIdentifier());
|
||||
return text == null || text.isEmpty() ? EMPTY_IDENTIFIER : text;
|
||||
}, Row::revisionProperty));
|
||||
|
||||
if(text == null || text.isEmpty())
|
||||
{
|
||||
return EMPTY_IDENTIFIER;
|
||||
}
|
||||
else if(metadata.hasTalkerAliasIdentifier())
|
||||
{
|
||||
return text + " " + metadata.getTalkerAliasIdentifier();
|
||||
}
|
||||
|
||||
return text;
|
||||
//Talker alias sent over the air by the radio (e.g. DMR or P25)
|
||||
columns.add(NowPlayingCells.column("talker", "Talker", null, Pos.CENTER,
|
||||
row -> {
|
||||
Identifier talkerAlias = row.getChannelMetadata().getTalkerAliasIdentifier();
|
||||
return talkerAlias != null && talkerAlias.getValue() != null ? talkerAlias.getValue().toString() : null;
|
||||
}, Row::revisionProperty));
|
||||
|
||||
TableColumn<Row,AliasDisplay> fromAlias = NowPlayingCells.column("from.alias", "Alias", "From Alias",
|
||||
@@ -368,6 +365,20 @@ public class ChannelMetadataView extends VBox implements ChannelMetadataModel.Li
|
||||
column.setSortable(false);
|
||||
}
|
||||
|
||||
//Column numbers in the Swing version of the table, for its stored column widths. Talker is a new column.
|
||||
String[] swingColumns = {"status", "decoder", "from", "from.alias", "to", "to.alias", "channel", "frequency",
|
||||
"channel.name"};
|
||||
|
||||
for(TableColumn<Row,?> column: columns)
|
||||
{
|
||||
int swingColumn = Arrays.asList(swingColumns).indexOf(column.getId());
|
||||
|
||||
if(swingColumn >= 0)
|
||||
{
|
||||
TableColumnMonitor.setLegacyIndex(column, swingColumn);
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.filter.FilterSet;
|
||||
import io.github.dsheirer.gui.table.TableColumnMonitor;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
@@ -188,6 +189,10 @@ public class DecodeEventView extends HistoryTableView<IDecodeEvent,IDecodeEvent>
|
||||
row -> formatIdentifiers(row.getItem().getIdentifierCollection(), Role.FROM, userPreferences),
|
||||
Row::revisionProperty));
|
||||
|
||||
//Talker alias sent over the air by the radio (e.g. DMR, P25 or NXDN)
|
||||
columns.add(NowPlayingCells.column("talker", "Talker", null, Pos.CENTER,
|
||||
row -> getTalkerAlias(row.getItem().getIdentifierCollection()), Row::revisionProperty));
|
||||
|
||||
TableColumn<Row<IDecodeEvent>,AliasDisplay> fromAlias = NowPlayingCells.column("from.alias", "Alias",
|
||||
"From Alias", Pos.CENTER, row -> getAliases(row.getItem().getIdentifierCollection(), Role.FROM, aliasModel),
|
||||
Row::revisionProperty);
|
||||
@@ -220,9 +225,54 @@ public class DecodeEventView extends HistoryTableView<IDecodeEvent,IDecodeEvent>
|
||||
columns.add(NowPlayingCells.column("details", "Details", null, Pos.CENTER_LEFT,
|
||||
row -> row.getItem().getDetails(), Row::revisionProperty));
|
||||
|
||||
//Column numbers in the Swing version of the table, for its stored column widths. Talker is a new column.
|
||||
List<String> swingColumns = List.of("time", "duration", "event", "from", "from.alias", "to", "to.alias",
|
||||
"channel", "frequency", "details");
|
||||
|
||||
for(TableColumn<Row<IDecodeEvent>,?> column: columns)
|
||||
{
|
||||
if(swingColumns.contains(column.getId()))
|
||||
{
|
||||
TableColumnMonitor.setLegacyIndex(column, swingColumns.indexOf(column.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talker alias(es) in the identifier collection
|
||||
*/
|
||||
private static String getTalkerAlias(IdentifierCollection identifierCollection)
|
||||
{
|
||||
if(identifierCollection == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<String> aliases = new ArrayList<>();
|
||||
|
||||
for(Identifier identifier: identifierCollection.getIdentifiers(Form.TALKER_ALIAS))
|
||||
{
|
||||
if(identifier.getValue() != null)
|
||||
{
|
||||
aliases.add(identifier.getValue().toString());
|
||||
}
|
||||
}
|
||||
|
||||
return aliases.isEmpty() ? null : String.join(",", aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifiers with the role, without talker aliases, which have their own column
|
||||
*/
|
||||
private static List<Identifier> getIdentifiers(IdentifierCollection identifierCollection, Role role)
|
||||
{
|
||||
List<Identifier> identifiers = new ArrayList<>(identifierCollection.getIdentifiers(role));
|
||||
identifiers.removeIf(identifier -> identifier.getForm() == Form.TALKER_ALIAS);
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event timestamp formatter that uses the timestamp format preference.
|
||||
*/
|
||||
@@ -278,7 +328,7 @@ public class DecodeEventView extends HistoryTableView<IDecodeEvent,IDecodeEvent>
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Identifier> identifiers = identifierCollection.getIdentifiers(role);
|
||||
List<Identifier> identifiers = getIdentifiers(identifierCollection, role);
|
||||
|
||||
if(identifiers == null || identifiers.isEmpty())
|
||||
{
|
||||
@@ -319,7 +369,7 @@ public class DecodeEventView extends HistoryTableView<IDecodeEvent,IDecodeEvent>
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Identifier> identifiers = identifierCollection.getIdentifiers(role);
|
||||
List<Identifier> identifiers = getIdentifiers(identifierCollection, role);
|
||||
|
||||
if(identifiers == null || identifiers.isEmpty())
|
||||
{
|
||||
|
||||
@@ -62,14 +62,22 @@ public class NowPlayingView extends SplitPane
|
||||
mMessageActivityView = new MessageActivityView(userPreferences);
|
||||
mChannelSpectrumView = new ChannelSpectrumView(playlistManager);
|
||||
|
||||
Tab detailsTab = new Tab("Details", mChannelDetailView);
|
||||
Tab channelTab = new Tab("Channel", mChannelSpectrumView);
|
||||
mTabPane.getTabs().addAll(new Tab("Details", mChannelDetailView), new Tab("Events", mDecodeEventView),
|
||||
mTabPane.getTabs().addAll(detailsTab, new Tab("Events", mDecodeEventView),
|
||||
new Tab("Messages", mMessageActivityView), channelTab);
|
||||
mTabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||
|
||||
//Turn the channel spectrum processing on only while the channel tab is selected
|
||||
mTabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldTab, newTab) ->
|
||||
mChannelSpectrumView.setPanelVisible(newTab == channelTab));
|
||||
mTabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldTab, newTab) -> {
|
||||
mChannelSpectrumView.setPanelVisible(newTab == channelTab);
|
||||
|
||||
//Show current details each time the user opens the details tab
|
||||
if(newTab == detailsTab)
|
||||
{
|
||||
mChannelDetailView.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
mChannelMetadataView.addProcessingChainSelectionListener(mChannelDetailView);
|
||||
mChannelMetadataView.addProcessingChainSelectionListener(mDecodeEventView);
|
||||
|
||||
@@ -50,6 +50,11 @@ public class TableColumnMonitor<S>
|
||||
* column header text doesn't identify the column by itself (e.g. two columns titled 'Alias').
|
||||
*/
|
||||
public static final String CONFIGURATION_NAME = "column.configuration.name";
|
||||
/**
|
||||
* Column property that holds the column number of the column in the Swing version of the table, for the stored
|
||||
* Swing column widths.
|
||||
*/
|
||||
public static final String LEGACY_INDEX = "column.legacy.index";
|
||||
private static final String ORDER = ".column.order";
|
||||
private static final String HIDDEN = ".column.hidden";
|
||||
private static final String WIDTH = ".column.width.";
|
||||
@@ -186,6 +191,18 @@ public class TableColumnMonitor<S>
|
||||
column.getProperties().put(CONFIGURATION_NAME, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column number of the column in the Swing version of the table, for migrating the Swing column width.
|
||||
* When any column of a table has a legacy index, only those columns use Swing column widths. Otherwise, the
|
||||
* default column index is the Swing column number.
|
||||
* @param column to set
|
||||
* @param index of the column in the Swing table
|
||||
*/
|
||||
public static void setLegacyIndex(TableColumn<?,?> column, int index)
|
||||
{
|
||||
column.getProperties().put(LEGACY_INDEX, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the default column order and visibility and removes the stored order and visibility settings. The
|
||||
* column widths aren't changed.
|
||||
@@ -326,14 +343,19 @@ public class TableColumnMonitor<S>
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasLegacyIndexes = mDefaultOrder.stream()
|
||||
.anyMatch(column -> column.getProperties().containsKey(LEGACY_INDEX));
|
||||
|
||||
for(int x = 0; x < mDefaultOrder.size(); x++)
|
||||
{
|
||||
TableColumn<S,?> column = mDefaultOrder.get(x);
|
||||
double width = mPreferences.getDouble(getWidthKey(column), Double.NaN);
|
||||
Integer legacyIndex = column.getProperties().get(LEGACY_INDEX) instanceof Integer index ? index :
|
||||
(hasLegacyIndexes ? null : x);
|
||||
|
||||
if(Double.isNaN(width) && swingPreference != null && swingKey != null)
|
||||
if(Double.isNaN(width) && swingPreference != null && swingKey != null && legacyIndex != null)
|
||||
{
|
||||
int swingWidth = swingPreference.getInt(swingKey + ".column." + x, Integer.MAX_VALUE);
|
||||
int swingWidth = swingPreference.getInt(swingKey + ".column." + legacyIndex, Integer.MAX_VALUE);
|
||||
|
||||
if(swingWidth != Integer.MAX_VALUE && swingWidth > 0)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
|
||||
package io.github.dsheirer.identifier.alias;
|
||||
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
@@ -116,6 +118,16 @@ public class TalkerAliasManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current FROM radio in the identifier collection
|
||||
* @param identifierCollection to inspect
|
||||
* @return FROM radio identifier, or null
|
||||
*/
|
||||
public static Identifier getFromRadio(IdentifierCollection identifierCollection)
|
||||
{
|
||||
return identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a summary listing of talker aliases
|
||||
* @return summary.
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.identifier.alias;
|
||||
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Keeps the talker alias in a decoder's identifier collection matched to the radio that is talking, and assigns each
|
||||
* received talker alias to the right radio in the talker alias manager. Use one instance for each decoder state (or
|
||||
* timeslot) on the decoder state's thread.
|
||||
*
|
||||
* Talker aliases can arrive:
|
||||
* <ul>
|
||||
* <li>During the call, after the talking radio is identified (most protocols).</li>
|
||||
* <li>During the call, before the talking radio is identified. The alias is shown for the call and stored for
|
||||
* the radio once the radio is identified.</li>
|
||||
* <li>Between calls. Depending on the protocol, the alias belongs to the talker of the call that just ended (e.g.
|
||||
* P25 Phase 1 L3Harris aliases in the TDULC messages) or to the talker of the next call (e.g. P25 Phase 2
|
||||
* L3Harris aliases that arrive just before the Push-To-Talk). See {@link BetweenCalls}.</li>
|
||||
* <li>With their own radio identifier (e.g. Motorola), which is stored for that radio.</li>
|
||||
* </ul>
|
||||
*
|
||||
* The decoder state calls {@link #callActivity(long)} after it updates the identifier collection for call activity,
|
||||
* {@link #callEnded()} when a call ends, before it removes the FROM identifiers, and {@link #received} for each
|
||||
* talker alias.
|
||||
*/
|
||||
public class TalkerAliasTracker
|
||||
{
|
||||
/**
|
||||
* Talker that an alias received between calls belongs to
|
||||
*/
|
||||
public enum BetweenCalls
|
||||
{
|
||||
/**
|
||||
* The talker of the call that just ended
|
||||
*/
|
||||
PREVIOUS_TALKER,
|
||||
/**
|
||||
* The talker of the next call, when it starts within {@link #NEXT_CALL_WINDOW_MS}
|
||||
*/
|
||||
NEXT_TALKER
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum time from a talker alias received between calls to the start of the next call, for the alias to belong
|
||||
* to the next call's talker.
|
||||
*/
|
||||
public static final long NEXT_CALL_WINDOW_MS = 2000;
|
||||
|
||||
/**
|
||||
* Call that a received talker alias belongs to
|
||||
*/
|
||||
public enum Assignment
|
||||
{
|
||||
/**
|
||||
* The call in progress. The alias is shown in the identifier collection.
|
||||
*/
|
||||
CURRENT_CALL,
|
||||
/**
|
||||
* The call that just ended. The alias isn't shown, since no call is in progress.
|
||||
*/
|
||||
ENDED_CALL,
|
||||
/**
|
||||
* The next call. The alias is held and shown when the next call starts.
|
||||
*/
|
||||
NEXT_CALL,
|
||||
/**
|
||||
* Neither, e.g. an alias for a radio that isn't talking. The alias is stored for its radio, if known.
|
||||
*/
|
||||
UNASSIGNED
|
||||
}
|
||||
|
||||
private final MutableIdentifierCollection mIdentifierCollection;
|
||||
private final Supplier<TalkerAliasManager> mTalkerAliasManager;
|
||||
private final BetweenCalls mBetweenCalls;
|
||||
private boolean mCallActive;
|
||||
private TalkerAliasIdentifier mNextCallAlias;
|
||||
private long mNextCallAliasTimestamp;
|
||||
private Identifier mTalker;
|
||||
private RadioIdentifier mEndedCallRadio;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param identifierCollection of the decoder state
|
||||
* @param talkerAliasManager for storing the talker alias of each radio. The supplier can return null when the
|
||||
* decoder doesn't have a talker alias manager.
|
||||
* @param betweenCalls talker that an alias received between calls belongs to, for the protocol
|
||||
*/
|
||||
public TalkerAliasTracker(MutableIdentifierCollection identifierCollection,
|
||||
Supplier<TalkerAliasManager> talkerAliasManager, BetweenCalls betweenCalls)
|
||||
{
|
||||
mIdentifierCollection = identifierCollection;
|
||||
mTalkerAliasManager = talkerAliasManager;
|
||||
mBetweenCalls = betweenCalls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals call activity, after the decoder updates the identifier collection from a call message. Keeps the
|
||||
* talker alias matched to the talking radio: removes the alias when a different radio talks, stores an alias that
|
||||
* arrived before the talking radio was identified, and adds the stored alias for the talking radio.
|
||||
* @param timestamp of the call message
|
||||
*/
|
||||
public void callActivity(long timestamp)
|
||||
{
|
||||
if(!mCallActive && mNextCallAlias != null && Math.abs(timestamp - mNextCallAliasTimestamp) <= NEXT_CALL_WINDOW_MS)
|
||||
{
|
||||
//The alias received just before this call belongs to this call's talker. It's stored for the talker once
|
||||
//the talker is identified.
|
||||
mIdentifierCollection.update(mNextCallAlias);
|
||||
}
|
||||
|
||||
mNextCallAlias = null;
|
||||
mCallActive = true;
|
||||
mEndedCallRadio = null;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the talker alias matched to the talking radio after an identifier collection update, without signaling
|
||||
* call activity.
|
||||
*/
|
||||
public void update()
|
||||
{
|
||||
Identifier current = getFromRadio();
|
||||
|
||||
if(current != null)
|
||||
{
|
||||
Identifier alias = getTalkerAlias();
|
||||
TalkerAliasManager manager = mTalkerAliasManager.get();
|
||||
|
||||
if(mTalker != null && !isSameRadio(mTalker, current))
|
||||
{
|
||||
//A different radio is talking
|
||||
mIdentifierCollection.remove(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
}
|
||||
else if(mTalker == null && alias instanceof TalkerAliasIdentifier talkerAlias &&
|
||||
current instanceof RadioIdentifier radio && manager != null)
|
||||
{
|
||||
//The alias arrived during this call before the radio was identified
|
||||
manager.update(radio, talkerAlias);
|
||||
}
|
||||
|
||||
if(manager != null)
|
||||
{
|
||||
manager.enrichMutable(mIdentifierCollection);
|
||||
}
|
||||
}
|
||||
|
||||
mTalker = current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the end of the call. Call before removing the FROM identifiers from the identifier collection.
|
||||
* Remembers the talker for an alias that arrives after the call, and removes the talker alias. Can be called
|
||||
* more than once for the same call.
|
||||
*/
|
||||
public void callEnded()
|
||||
{
|
||||
Identifier current = getFromRadio();
|
||||
|
||||
if(current instanceof RadioIdentifier radio)
|
||||
{
|
||||
mEndedCallRadio = radio;
|
||||
}
|
||||
else if(mTalker instanceof RadioIdentifier radio)
|
||||
{
|
||||
mEndedCallRadio = radio;
|
||||
}
|
||||
|
||||
mCallActive = false;
|
||||
mTalker = null;
|
||||
mIdentifierCollection.remove(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tracker, e.g. when the decoder state resets.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mCallActive = false;
|
||||
mTalker = null;
|
||||
mEndedCallRadio = null;
|
||||
mNextCallAlias = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a received talker alias: stores it for its radio and shows it when the radio is talking.
|
||||
*
|
||||
* @param talkerAlias that was received
|
||||
* @param aliasRadio that the alias identifies itself (e.g. Motorola), or null when the alias belongs to the
|
||||
* talker of the current call, or of the previous or next call, depending on the protocol.
|
||||
* @param timestamp of the alias message
|
||||
* @return the call that the alias belongs to.
|
||||
*/
|
||||
public Assignment received(TalkerAliasIdentifier talkerAlias, RadioIdentifier aliasRadio, long timestamp)
|
||||
{
|
||||
if(talkerAlias == null)
|
||||
{
|
||||
return Assignment.UNASSIGNED;
|
||||
}
|
||||
|
||||
Identifier current = getFromRadio();
|
||||
RadioIdentifier radio = aliasRadio;
|
||||
|
||||
if(radio == null)
|
||||
{
|
||||
if(current instanceof RadioIdentifier currentRadio)
|
||||
{
|
||||
radio = currentRadio;
|
||||
}
|
||||
else if(!mCallActive)
|
||||
{
|
||||
if(mBetweenCalls == BetweenCalls.NEXT_TALKER)
|
||||
{
|
||||
//Hold the alias for the next call's talker
|
||||
mNextCallAlias = talkerAlias;
|
||||
mNextCallAliasTimestamp = timestamp;
|
||||
return Assignment.NEXT_CALL;
|
||||
}
|
||||
|
||||
radio = mEndedCallRadio;
|
||||
}
|
||||
}
|
||||
|
||||
TalkerAliasManager manager = mTalkerAliasManager.get();
|
||||
|
||||
if(radio != null && manager != null)
|
||||
{
|
||||
manager.update(radio, talkerAlias);
|
||||
}
|
||||
|
||||
if(current != null && radio != null && isSameRadio(current, radio))
|
||||
{
|
||||
mIdentifierCollection.update(talkerAlias);
|
||||
return Assignment.CURRENT_CALL;
|
||||
}
|
||||
|
||||
if(radio == null && mCallActive)
|
||||
{
|
||||
//The talking radio isn't identified yet. The alias is stored for the radio once it's identified.
|
||||
mIdentifierCollection.update(talkerAlias);
|
||||
return Assignment.CURRENT_CALL;
|
||||
}
|
||||
|
||||
if(radio != null && !mCallActive && mEndedCallRadio != null && isSameRadio(radio, mEndedCallRadio))
|
||||
{
|
||||
return Assignment.ENDED_CALL;
|
||||
}
|
||||
|
||||
return Assignment.UNASSIGNED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talker of the call that just ended, or null.
|
||||
*/
|
||||
public RadioIdentifier getEndedCallRadio()
|
||||
{
|
||||
return mEndedCallRadio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Talking radio for the current call, or null.
|
||||
*/
|
||||
public RadioIdentifier getCurrentRadio()
|
||||
{
|
||||
return getFromRadio() instanceof RadioIdentifier radio ? radio : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if a call is in progress
|
||||
*/
|
||||
public boolean isCallActive()
|
||||
{
|
||||
return mCallActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the identifiers are the same radio, comparing radio values so that a radio and its fully qualified
|
||||
* variant match.
|
||||
*/
|
||||
public static boolean isSameRadio(Identifier a, Identifier b)
|
||||
{
|
||||
if(a instanceof RadioIdentifier ra && b instanceof RadioIdentifier rb)
|
||||
{
|
||||
return Objects.equals(ra.getValue(), rb.getValue());
|
||||
}
|
||||
|
||||
return Objects.equals(a, b);
|
||||
}
|
||||
|
||||
private Identifier getFromRadio()
|
||||
{
|
||||
return TalkerAliasManager.getFromRadio(mIdentifierCollection);
|
||||
}
|
||||
|
||||
private Identifier getTalkerAlias()
|
||||
{
|
||||
return mIdentifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,12 @@ package io.github.dsheirer.module.decode.dmr;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.channel.state.TimeslotDecoderState;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
@@ -33,9 +33,10 @@ import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasManager;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasTracker;
|
||||
import io.github.dsheirer.identifier.integer.IntegerIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.message.EmptyTimeslotPlaceholderMessage;
|
||||
@@ -126,6 +127,11 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor;
|
||||
private DMRTrafficChannelManager mTrafficChannelManager;
|
||||
private DecodeEvent mCurrentCallEvent;
|
||||
//Call event of the call that just ended, for a talker alias that completes after the call ends
|
||||
private DecodeEvent mEndedCallEvent;
|
||||
//Keeps the talker alias matched to the talking radio and assigns received talker aliases to the right radio
|
||||
private final TalkerAliasTracker mTalkerAliasTracker = new TalkerAliasTracker(getIdentifierCollection(),
|
||||
this::getTalkerAliasManager, TalkerAliasTracker.BetweenCalls.PREVIOUS_TALKER);
|
||||
private boolean mIgnoreCRCChecksums;
|
||||
private DMRDecoderState mSisterDecoderState;
|
||||
|
||||
@@ -1315,24 +1321,54 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
|
||||
if(message instanceof TalkerAliasComplete tac && tac.hasTalkerAliasIdentifier())
|
||||
{
|
||||
getIdentifierCollection().update(tac.getTalkerAliasIdentifier());
|
||||
|
||||
Identifier fromRadio = getIdentifierCollection().getFromIdentifier();
|
||||
|
||||
if(hasTrafficChannelManager() && fromRadio instanceof RadioIdentifier radio)
|
||||
{
|
||||
mTrafficChannelManager.getTalkerAliasManager().update(radio, tac.getTalkerAliasIdentifier());
|
||||
}
|
||||
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
processTalkerAlias(tac.getTalkerAliasIdentifier(), tac.getTimestamp());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Talker alias manager shared with the traffic channel manager, or null when there's no traffic channel manager.
|
||||
*/
|
||||
private TalkerAliasManager getTalkerAliasManager()
|
||||
{
|
||||
return hasTrafficChannelManager() ? mTrafficChannelManager.getTalkerAliasManager() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a talker alias: stores it for the talking radio and adds it to the call that it belongs to.
|
||||
* @param talkerAlias that was received
|
||||
* @param timestamp of the message
|
||||
*/
|
||||
private void processTalkerAlias(TalkerAliasIdentifier talkerAlias, long timestamp)
|
||||
{
|
||||
switch(mTalkerAliasTracker.received(talkerAlias, null, timestamp))
|
||||
{
|
||||
case CURRENT_CALL ->
|
||||
{
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
mCurrentCallEvent.setIdentifierCollection(getIdentifierCollection().copyOf());
|
||||
broadcast(mCurrentCallEvent);
|
||||
}
|
||||
}
|
||||
case ENDED_CALL ->
|
||||
{
|
||||
//Alias completed after the call ended: add it to that call's event when it's the same talker
|
||||
if(mEndedCallEvent != null && TalkerAliasTracker.isSameRadio(mTalkerAliasTracker.getEndedCallRadio(),
|
||||
mEndedCallEvent.getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM)))
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection(
|
||||
mEndedCallEvent.getIdentifierCollection().getIdentifiers());
|
||||
mic.update(talkerAlias);
|
||||
mEndedCallEvent.setIdentifierCollection(mic);
|
||||
broadcast(mEndedCallEvent);
|
||||
}
|
||||
}
|
||||
default -> {} //Stored for the radio, for the radio's next call
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current call with encryption information.
|
||||
* @param embeddedEncryptionParameters decoded from the Voice Frame F
|
||||
@@ -1340,6 +1376,8 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
*/
|
||||
private void updateEncryptedCall(EmbeddedEncryptionParameters embeddedEncryptionParameters, boolean isGroup, long timestamp)
|
||||
{
|
||||
mTalkerAliasTracker.callActivity(timestamp);
|
||||
|
||||
if(mCurrentCallEvent != null)
|
||||
{
|
||||
String details = mCurrentCallEvent.getDetails();;
|
||||
@@ -1377,6 +1415,9 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
*/
|
||||
private void updateCurrentCall(DecodeEventType type, String details, long timestamp)
|
||||
{
|
||||
//Keep the talker alias matched to the talking radio before the call event copies the identifiers
|
||||
mTalkerAliasTracker.callActivity(timestamp);
|
||||
|
||||
Event event = (mCurrentCallEvent == null ? Event.START : Event.CONTINUATION);
|
||||
|
||||
//Create a repeater channel descriptor if we don't have one
|
||||
@@ -1428,10 +1469,12 @@ public class DMRDecoderState extends TimeslotDecoderState
|
||||
{
|
||||
mCurrentCallEvent.end(timestamp);
|
||||
broadcast(mCurrentCallEvent);
|
||||
mEndedCallEvent = mCurrentCallEvent;
|
||||
mCurrentCallEvent = null;
|
||||
}
|
||||
|
||||
getIdentifierCollection().remove(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
//Remembers the talker for a talker alias that completes after the call, and removes the talker alias
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(IdentifierClass.USER, Form.TONE, Role.FROM);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,17 +24,19 @@ import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasManager;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasTracker;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventType;
|
||||
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.nxdn.identifier.NXDNTalkerAliasIdentifier;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.NXDNLayer3Message;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation;
|
||||
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation;
|
||||
@@ -86,6 +88,9 @@ public class NXDNDecoderState extends DecoderState
|
||||
private final Channel mChannel;
|
||||
private final NXDNNetworkConfigurationMonitor mNetworkConfigurationMonitor = new NXDNNetworkConfigurationMonitor();
|
||||
private final NXDNTrafficChannelManager mTrafficChannelManager;
|
||||
//Keeps the talker alias matched to the talking radio and assigns received talker aliases to the right radio
|
||||
private final TalkerAliasTracker mTalkerAliasTracker = new TalkerAliasTracker(getIdentifierCollection(),
|
||||
this::getTalkerAliasManager, TalkerAliasTracker.BetweenCalls.PREVIOUS_TALKER);
|
||||
private boolean mEncryptedCallStateDetermined = false;
|
||||
private boolean mEncryptedCall = false;
|
||||
private int mDisconnectResponseCount = 0;
|
||||
@@ -139,7 +144,9 @@ public class NXDNDecoderState extends DecoderState
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case REQUEST_RESET:
|
||||
mTalkerAliasTracker.callEnded();
|
||||
resetState();
|
||||
mTalkerAliasTracker.reset();
|
||||
break;
|
||||
case NOTIFICATION_SOURCE_FREQUENCY:
|
||||
setCurrentFrequency(event.getFrequency());
|
||||
@@ -190,6 +197,32 @@ public class NXDNDecoderState extends DecoderState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Talker alias manager shared with the traffic channel manager, or null when there's no traffic channel manager.
|
||||
*/
|
||||
private TalkerAliasManager getTalkerAliasManager()
|
||||
{
|
||||
return hasTrafficChannelManager() ? mTrafficChannelManager.getTalkerAliasManager() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a talker alias: stores it for the talking radio and adds it to the call that it belongs to.
|
||||
* @param talkerAlias that was received
|
||||
* @param timestamp of the message
|
||||
*/
|
||||
private void processTalkerAlias(NXDNTalkerAliasIdentifier talkerAlias, long timestamp)
|
||||
{
|
||||
TalkerAliasTracker.Assignment assignment = mTalkerAliasTracker.received(talkerAlias, null, timestamp);
|
||||
|
||||
if(hasTrafficChannelManager() && (assignment == TalkerAliasTracker.Assignment.CURRENT_CALL ||
|
||||
assignment == TalkerAliasTracker.Assignment.ENDED_CALL))
|
||||
{
|
||||
RadioIdentifier radio = assignment == TalkerAliasTracker.Assignment.ENDED_CALL ?
|
||||
mTalkerAliasTracker.getEndedCallRadio() : mTalkerAliasTracker.getCurrentRadio();
|
||||
mTrafficChannelManager.processTalkerAlias(getCurrentChannel(), talkerAlias, radio, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the arguments as a new decode event
|
||||
* @param identifiers involved in the event
|
||||
@@ -265,6 +298,9 @@ public class NXDNDecoderState extends DecoderState
|
||||
{
|
||||
mIdleDuringCallCount = 0;
|
||||
getIdentifierCollection().update(vc.getIdentifiers());
|
||||
//Keep the talker alias matched to the radio that is talking
|
||||
mTalkerAliasTracker.callActivity(vc.getTimestamp());
|
||||
|
||||
mEncryptedCallStateDetermined = true;
|
||||
mEncryptedCall = vc.getEncryptionKeyIdentifier().isEncrypted();
|
||||
state = mEncryptedCall ? State.ENCRYPTED : State.CALL;
|
||||
@@ -326,6 +362,7 @@ public class NXDNDecoderState extends DecoderState
|
||||
mEncryptedCall = false;
|
||||
mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT;
|
||||
//Only remove the FROM identifier, anticipating a continuation call for the same talkgroup
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
break;
|
||||
case CONTROL_OUT_09_CC_DATA_CALL_RESPONSE:
|
||||
@@ -404,6 +441,7 @@ public class NXDNDecoderState extends DecoderState
|
||||
mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp());
|
||||
mEncryptedCallStateDetermined = false;
|
||||
mEncryptedCall = false;
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(IdentifierClass.USER);
|
||||
mDisconnectResponseCount++;
|
||||
|
||||
@@ -424,6 +462,7 @@ public class NXDNDecoderState extends DecoderState
|
||||
case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION:
|
||||
case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION:
|
||||
mNetworkConfigurationMonitor.process(layer3);
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(IdentifierClass.USER);
|
||||
break;
|
||||
case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
|
||||
@@ -645,12 +684,7 @@ public class NXDNDecoderState extends DecoderState
|
||||
{
|
||||
state = State.CALL;
|
||||
event = DecoderStateEvent.Event.CONTINUATION;
|
||||
var radio = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
|
||||
if(radio instanceof RadioIdentifier ri)
|
||||
{
|
||||
mTrafficChannelManager.processTalkerAlias(getCurrentChannel(), tac.getTalkerAlias(), ri, tac.getTimestamp());
|
||||
}
|
||||
processTalkerAlias(tac.getTalkerAlias(), tac.getTimestamp());
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -26,10 +26,14 @@ import io.github.dsheirer.controller.channel.ChannelEvent;
|
||||
import io.github.dsheirer.controller.channel.IChannelEventListener;
|
||||
import io.github.dsheirer.controller.channel.IChannelEventProvider;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasManager;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasTracker;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEvent;
|
||||
@@ -440,9 +444,11 @@ public class NXDNTrafficChannelManager extends TrafficChannelManager implements
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates the talker alias for the tracked call event.
|
||||
* Adds or updates the talker alias for the tracked call event, when the event is for the talker, or when the
|
||||
* event's talker isn't identified yet.
|
||||
* @param channel for the call
|
||||
* @param talkerAlias to add or update
|
||||
* @param radio that the alias belongs to, or null when the talker isn't identified yet
|
||||
* @param timestamp for the talker alias message
|
||||
*/
|
||||
public void processTalkerAlias(IChannelDescriptor channel, NXDNTalkerAliasIdentifier talkerAlias,
|
||||
@@ -457,11 +463,20 @@ public class NXDNTrafficChannelManager extends TrafficChannelManager implements
|
||||
|
||||
if(tracker != null)
|
||||
{
|
||||
tracker.addIdentifierIfMissing(talkerAlias);
|
||||
broadcast(tracker);
|
||||
Identifier eventRadio = tracker.getEvent().getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
|
||||
if(radio == null || eventRadio == null || TalkerAliasTracker.isSameRadio(radio, eventRadio))
|
||||
{
|
||||
tracker.addIdentifierIfMissing(talkerAlias);
|
||||
broadcast(tracker);
|
||||
}
|
||||
}
|
||||
|
||||
getTalkerAliasManager().update(radio, talkerAlias);
|
||||
if(radio != null)
|
||||
{
|
||||
getTalkerAliasManager().update(radio, talkerAlias);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -19,20 +19,24 @@
|
||||
package io.github.dsheirer.module.decode.p25;
|
||||
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.ChannelEvent;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
|
||||
import io.github.dsheirer.controller.channel.ChannelEvent;
|
||||
import io.github.dsheirer.controller.channel.IChannelEventListener;
|
||||
import io.github.dsheirer.controller.channel.IChannelEventProvider;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasManager;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupPreLoadDataContent;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.scramble.ScrambleParameterIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
@@ -942,6 +946,43 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a talker alias that completed after the call ended (e.g. a P25 Phase 1 L3Harris talker alias in the TDULC
|
||||
* messages) to the call event that just ended on the frequency and timeslot, when that event's FROM radio is the
|
||||
* talker. The event duration isn't changed.
|
||||
*
|
||||
* @param frequency of the traffic channel
|
||||
* @param timeslot of the traffic channel (TIMESLOT_1 for Phase 1)
|
||||
* @param radio that the talker alias belongs to
|
||||
* @param talkerAlias to add
|
||||
*/
|
||||
public void processTrafficTalkerAliasAfterCall(long frequency, int timeslot, RadioIdentifier radio,
|
||||
Identifier talkerAlias)
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
P25ChannelEventTracker tracker = getTracker(frequency, timeslot);
|
||||
|
||||
if(tracker != null && tracker.isComplete() && radio != null && talkerAlias != null)
|
||||
{
|
||||
Identifier from = tracker.getEvent().getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
|
||||
if(from instanceof RadioIdentifier eventRadio && eventRadio.getValue().equals(radio.getValue()))
|
||||
{
|
||||
tracker.addIdentifierIfMissing(talkerAlias);
|
||||
broadcast(tracker);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all current identifiers for an ongoing call event on the frequency and updates the event duration timestamp.
|
||||
*
|
||||
|
||||
@@ -21,11 +21,11 @@ package io.github.dsheirer.module.decode.p25.phase1;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelEvent;
|
||||
import io.github.dsheirer.controller.channel.IChannelEventListener;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
@@ -33,6 +33,9 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasManager;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasTracker;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
@@ -199,6 +202,9 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
* algorithm ID takes precedence over the link control service options when it is known.
|
||||
*/
|
||||
private Boolean mCallEncryptedByAlgorithm = null;
|
||||
//Keeps the talker alias matched to the talking radio and assigns received talker aliases to the right radio
|
||||
private final TalkerAliasTracker mTalkerAliasTracker = new TalkerAliasTracker(getIdentifierCollection(),
|
||||
this::getTalkerAliasManager, TalkerAliasTracker.BetweenCalls.PREVIOUS_TALKER);
|
||||
|
||||
/**
|
||||
* Constructs an APCO-25 decoder state with an optional traffic channel manager.
|
||||
@@ -327,12 +333,12 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
}
|
||||
else if(iMessage instanceof MotorolaTalkerAliasComplete tac && tac.isValid())
|
||||
{
|
||||
mTrafficChannelManager.getTalkerAliasManager().update(tac.getRadio(), tac.getAlias());
|
||||
mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), tac.getAlias(), tac.getTimestamp(), tac.toString());
|
||||
//Motorola talker aliases identify their radio
|
||||
processTalkerAlias(tac.getAlias(), tac.getRadio(), tac.getTimestamp(), tac.toString());
|
||||
}
|
||||
else if(iMessage instanceof LCHarrisTalkerAliasComplete talkerAlias)
|
||||
{
|
||||
processTalkerAlias(talkerAlias);
|
||||
processTalkerAlias(talkerAlias.getTalkerAlias(), null, talkerAlias.getTimestamp(), talkerAlias.toString());
|
||||
}
|
||||
else if(iMessage instanceof LinkControlWord lcw)
|
||||
{
|
||||
@@ -341,19 +347,34 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a fully reassembled L3Harris talker alias on the current traffic channel.
|
||||
* @param talkerAlias reassembled.
|
||||
* Talker alias manager shared with the traffic channel manager
|
||||
*/
|
||||
private void processTalkerAlias(LCHarrisTalkerAliasComplete talkerAlias)
|
||||
private TalkerAliasManager getTalkerAliasManager()
|
||||
{
|
||||
Identifier identifier = getIdentifierCollection().getFromIdentifier();
|
||||
return mTrafficChannelManager.getTalkerAliasManager();
|
||||
}
|
||||
|
||||
if(identifier instanceof RadioIdentifier radioIdentifier)
|
||||
/**
|
||||
* Processes a talker alias: stores it for its radio and adds it to the call that it belongs to. L3Harris talker
|
||||
* aliases can complete during the call, or after the call ends in the TDULC messages, and belong to the talker of
|
||||
* that call.
|
||||
* @param talkerAlias that was received
|
||||
* @param radio identified by the alias message, or null when the alias belongs to the talker of the current or
|
||||
* just ended call
|
||||
* @param timestamp of the message
|
||||
* @param context of the message
|
||||
*/
|
||||
private void processTalkerAlias(TalkerAliasIdentifier talkerAlias, RadioIdentifier radio, long timestamp,
|
||||
String context)
|
||||
{
|
||||
switch(mTalkerAliasTracker.received(talkerAlias, radio, timestamp))
|
||||
{
|
||||
mTrafficChannelManager.getTalkerAliasManager().update(radioIdentifier, talkerAlias.getTalkerAlias());
|
||||
case CURRENT_CALL -> mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), talkerAlias,
|
||||
timestamp, context);
|
||||
case ENDED_CALL -> mTrafficChannelManager.processTrafficTalkerAliasAfterCall(getCurrentFrequency(),
|
||||
P25P1Message.TIMESLOT_1, mTalkerAliasTracker.getEndedCallRadio(), talkerAlias);
|
||||
default -> {} //Stored for the radio, for the radio's next call
|
||||
}
|
||||
|
||||
mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), talkerAlias.getTalkerAlias(), talkerAlias.getTimestamp(), talkerAlias.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,12 +435,31 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the identifier collection with the channel user identifiers from the link control word and keeps the
|
||||
* talker alias matched to the radio that is talking.
|
||||
* @param isCallActivity true for a call in progress, or false for a terminator link control after the call
|
||||
*/
|
||||
private void updateChannelUserIdentifiers(LinkControlWord lcw, long timestamp, boolean isCallActivity)
|
||||
{
|
||||
getIdentifierCollection().update(mPatchGroupManager.update(lcw.getIdentifiers(), timestamp));
|
||||
|
||||
if(isCallActivity)
|
||||
{
|
||||
mTalkerAliasTracker.callActivity(timestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
mTalkerAliasTracker.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Link Control (LC) Channel user (ie current user on this channel).
|
||||
*/
|
||||
private void processLCChannelUser(LinkControlWord lcw, long timestamp)
|
||||
{
|
||||
getIdentifierCollection().update(mPatchGroupManager.update(lcw.getIdentifiers(), timestamp));
|
||||
updateChannelUserIdentifiers(lcw, timestamp, true);
|
||||
DecodeEventType decodeEventType = getLCDecodeEventType(lcw);
|
||||
|
||||
ServiceOptions serviceOptions = null;
|
||||
@@ -443,7 +483,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
serviceOptions = VoiceServiceOptions.createUnencrypted();
|
||||
}
|
||||
|
||||
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection());
|
||||
MutableIdentifierCollection mic = getMutableIdentifierCollection(getIdentifierCollection().getIdentifiers(), timestamp);
|
||||
mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), getCurrentChannel(), decodeEventType,
|
||||
serviceOptions, mic, timestamp, null, lcw.toString());
|
||||
@@ -853,6 +892,8 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
Identifier<?> radio = getIdentifierCollection().getFromIdentifier();
|
||||
|
||||
mCallEncryptedByAlgorithm = headerData.isEncryptedAudio();
|
||||
//A call starts. The HDU doesn't identify the talker.
|
||||
mTalkerAliasTracker.callActivity(message.getTimestamp());
|
||||
mTrafficChannelManager.processP1TrafficCallStart(getCurrentFrequency(), talkgroup, radio,
|
||||
headerData.getEncryptionKey(), mCurrentServiceOptions, getCurrentChannel(), message.getTimestamp());
|
||||
|
||||
@@ -921,9 +962,22 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
{
|
||||
mCallEncryptedByAlgorithm = null;
|
||||
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDU:" + message);
|
||||
clearCallUser();
|
||||
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the FROM radio and talker alias at the end of a call, so that the next call doesn't start (e.g. with an
|
||||
* HDU, which doesn't identify the talker) with the previous talker. The next call's link control adds the radio
|
||||
* that is talking and its stored talker alias, the same as the other protocols.
|
||||
*/
|
||||
private void clearCallUser()
|
||||
{
|
||||
//Remembers the talker for a talker alias that completes after the call ends
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(IdentifierClass.USER, Role.FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Terminator Data Unit with Link Control (TDULC) message and forwards valid Link Control Word message for
|
||||
* additional processing.
|
||||
@@ -942,6 +996,8 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDULC:" + message);
|
||||
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
|
||||
processLC(lcw, message.getTimestamp(), true);
|
||||
//After the terminator link control, which would otherwise add the radio and alias back
|
||||
clearCallUser();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1878,7 +1934,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
case UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED:
|
||||
if(isTerminator)
|
||||
{
|
||||
getIdentifierCollection().update(mPatchGroupManager.update(lcw.getIdentifiers(), timestamp));
|
||||
updateChannelUserIdentifiers(lcw, timestamp, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2159,6 +2215,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
|
||||
{
|
||||
case REQUEST_RESET:
|
||||
resetState();
|
||||
mTalkerAliasTracker.reset();
|
||||
mNetworkConfigurationMonitor.reset();
|
||||
break;
|
||||
case NOTIFICATION_SOURCE_FREQUENCY:
|
||||
|
||||
@@ -29,6 +29,7 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.IExtendedSourceMes
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.HarrisTalkerAliasAssembler;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBase;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasComplete;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock1;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock2;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSComplete;
|
||||
@@ -183,6 +184,8 @@ public class P25P1MessageProcessor implements Listener<IMessage>
|
||||
else if(message instanceof TDULCMessage tdulc)
|
||||
{
|
||||
boolean motorolaTalkerAliasComplete = false;
|
||||
LCHarrisTalkerAliasComplete harrisTalkerAliasComplete = null;
|
||||
boolean harrisTalkerAliasBlock = false;
|
||||
LinkControlWord lcw = tdulc.getLinkControlWord();
|
||||
|
||||
if(lcw instanceof IExtendedSourceMessage esm)
|
||||
@@ -199,22 +202,39 @@ public class P25P1MessageProcessor implements Listener<IMessage>
|
||||
}
|
||||
}
|
||||
|
||||
//L3Harris can carry the talker alias in the TDULC messages after the call ends
|
||||
else if(lcw != null && lcw.isValid() && lcw instanceof LCHarrisTalkerAliasBase harrisTalkerAlias)
|
||||
{
|
||||
harrisTalkerAliasBlock = true;
|
||||
harrisTalkerAliasComplete = mHarrisTalkerAliasAssembler.process(harrisTalkerAlias, message.getTimestamp());
|
||||
}
|
||||
|
||||
//Motorola carries the talker alias in the TDULC
|
||||
else if(mMotorolaTalkerAliasAssembler.add(lcw, message.getTimestamp()))
|
||||
{
|
||||
motorolaTalkerAliasComplete = true;
|
||||
}
|
||||
|
||||
//Harris carries the talker alias in the LDU1 link control messages, so reset the assembler on TDULC.
|
||||
mHarrisTalkerAliasAssembler.reset();
|
||||
//Reset the L3Harris assembler on other TDULC messages so that alias blocks from different calls
|
||||
//aren't combined. Keep the blocks while the TDULC messages carry the alias.
|
||||
if(!harrisTalkerAliasBlock)
|
||||
{
|
||||
mHarrisTalkerAliasAssembler.reset();
|
||||
}
|
||||
|
||||
dispatch(tdulc);
|
||||
|
||||
// Dispatch the complete talker alias after the TDULC to ensure correct log sequencing.
|
||||
// Dispatch the complete talker alias after the TDULC to ensure correct log sequencing. The decoder
|
||||
// state assigns it to the talker of the call that just ended.
|
||||
if(motorolaTalkerAliasComplete)
|
||||
{
|
||||
dispatch(mMotorolaTalkerAliasAssembler.assemble());
|
||||
}
|
||||
|
||||
if(harrisTalkerAliasComplete != null)
|
||||
{
|
||||
dispatch(harrisTalkerAliasComplete);
|
||||
}
|
||||
}
|
||||
else if(message instanceof HDUMessage hdu && mHeldTDULCMessage != null)
|
||||
{
|
||||
|
||||
@@ -103,9 +103,10 @@ public class HarrisTalkerAliasAssembler
|
||||
{
|
||||
if(mBlock1 != null && mBlock2 != null)
|
||||
{
|
||||
String fragment2 = mBlock2.getPayloadFragmentString();
|
||||
String fragment3 = mBlock3 != null ? mBlock3.getPayloadFragmentString() : null;
|
||||
String fragment4 = mBlock4 != null ? mBlock4.getPayloadFragmentString() : null;
|
||||
//Untrimmed, so that a space at the start or end of a fragment is kept within the alias
|
||||
String fragment2 = mBlock2.getPayloadFragmentText();
|
||||
String fragment3 = mBlock3 != null ? mBlock3.getPayloadFragmentText() : null;
|
||||
String fragment4 = mBlock4 != null ? mBlock4.getPayloadFragmentText() : null;
|
||||
return new LCHarrisTalkerAliasComplete(mBlock1.getMessage(), fragment2, fragment3, fragment4, mTimestamp);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,16 @@ public abstract class LCHarrisTalkerAliasBase extends LinkControlWord
|
||||
return new String(getPayloadFragment().toByteArray()).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload fragment as a string, including leading and trailing spaces, for joining the fragments of an alias.
|
||||
* A space at the start or end of a fragment can be a space within the alias.
|
||||
* @return fragment
|
||||
*/
|
||||
public String getPayloadFragmentText()
|
||||
{
|
||||
return new String(getPayloadFragment().toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* List of identifiers contained in this message
|
||||
*/
|
||||
|
||||
@@ -93,7 +93,8 @@ public class LCHarrisTalkerAliasComplete extends LinkControlWord implements IMes
|
||||
{
|
||||
if(mTalkerAlias == null)
|
||||
{
|
||||
String alias = getPayloadFragmentString();
|
||||
//Join the untrimmed fragments and trim the result, so that a space at a fragment boundary is kept
|
||||
String alias = getPayloadFragmentText();
|
||||
|
||||
if(mFragment2 != null && !alias.contains(mFragment2))
|
||||
{
|
||||
@@ -110,7 +111,7 @@ public class LCHarrisTalkerAliasComplete extends LinkControlWord implements IMes
|
||||
}
|
||||
}
|
||||
|
||||
mTalkerAlias = P25TalkerAliasIdentifier.create(alias);
|
||||
mTalkerAlias = P25TalkerAliasIdentifier.create(alias.trim());
|
||||
}
|
||||
|
||||
return mTalkerAlias;
|
||||
@@ -134,6 +135,16 @@ public class LCHarrisTalkerAliasComplete extends LinkControlWord implements IMes
|
||||
return new String(getPayloadFragment().toByteArray()).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Payload fragment as a string, including leading and trailing spaces, for joining the fragments of an alias.
|
||||
* A space at the start or end of a fragment can be a space within the alias.
|
||||
* @return fragment
|
||||
*/
|
||||
public String getPayloadFragmentText()
|
||||
{
|
||||
return new String(getPayloadFragment().toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
|
||||
@@ -22,19 +22,21 @@ import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.channel.IBandChannelDescriptor;
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.channel.state.TimeslotDecoderState;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.alias.P25TalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasManager;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasTracker;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKey;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupManager;
|
||||
@@ -147,6 +149,9 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
private PatchGroupManager mPatchGroupManager;
|
||||
private P25P2NetworkConfigurationMonitor mNetworkConfigurationMonitor = new P25P2NetworkConfigurationMonitor();
|
||||
private P25TrafficChannelManager mTrafficChannelManager;
|
||||
//Keeps the talker alias matched to the talking radio and assigns received talker aliases to the right radio
|
||||
private final TalkerAliasTracker mTalkerAliasTracker = new TalkerAliasTracker(getIdentifierCollection(),
|
||||
this::getTalkerAliasManager, TalkerAliasTracker.BetweenCalls.NEXT_TALKER);
|
||||
private int mEndPttOnFacchCounter = 0;
|
||||
|
||||
/**
|
||||
@@ -190,6 +195,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
*/
|
||||
protected void resetState()
|
||||
{
|
||||
mTalkerAliasTracker.callEnded();
|
||||
super.resetState();
|
||||
mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), System.currentTimeMillis(), "RESET STATE INVOKED");
|
||||
mEndPttOnFacchCounter = 0;
|
||||
@@ -239,6 +245,8 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot()));
|
||||
}
|
||||
|
||||
mTalkerAliasTracker.callActivity(message.getTimestamp());
|
||||
|
||||
//If we're tracking the call event, update the duration on it
|
||||
mTrafficChannelManager.processP2TrafficVoice(getCurrentFrequency(), getTimeslot(), message.getTimestamp());
|
||||
}
|
||||
@@ -265,7 +273,8 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
}
|
||||
else if(message instanceof MotorolaTalkerAliasComplete tac && tac.isValid())
|
||||
{
|
||||
mTrafficChannelManager.getTalkerAliasManager().update(tac.getRadio(), tac.getAlias());
|
||||
//Motorola talker aliases identify their radio
|
||||
processTalkerAlias(tac.getAlias(), tac.getRadio(), tac.getTimestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,6 +294,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
case MAC_6_HANGTIME:
|
||||
//During hangtime, the from talker is no longer involved in the call ... remove it because sometimes
|
||||
//we don't get the end PTT to signal the end.
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
break;
|
||||
}
|
||||
@@ -763,7 +773,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp()));
|
||||
}
|
||||
|
||||
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection());
|
||||
mTalkerAliasTracker.update();
|
||||
|
||||
continueState(State.ACTIVE);
|
||||
}
|
||||
@@ -1103,14 +1113,15 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp()));
|
||||
}
|
||||
|
||||
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection());
|
||||
|
||||
if(message.getMacPduType() == MacPduType.MAC_3_IDLE || message.getMacPduType() == MacPduType.MAC_6_HANGTIME)
|
||||
{
|
||||
mTalkerAliasTracker.update();
|
||||
continueState(State.ACTIVE);
|
||||
}
|
||||
else
|
||||
{
|
||||
mTalkerAliasTracker.callActivity(message.getTimestamp());
|
||||
|
||||
if(mac instanceof IServiceOptionsProvider sop)
|
||||
{
|
||||
IChannelDescriptor currentChannel = mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(),
|
||||
@@ -1153,7 +1164,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp()));
|
||||
}
|
||||
|
||||
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection());
|
||||
mTalkerAliasTracker.callActivity(message.getTimestamp());
|
||||
|
||||
if(mac instanceof PushToTalk ptt)
|
||||
{
|
||||
@@ -1225,6 +1236,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
if(mac instanceof EndPushToTalk)
|
||||
{
|
||||
//No matter what, remove the FROM identifier on end PTT.
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(Role.FROM);
|
||||
|
||||
//Only reset the identifiers if the call event is closed out, otherwise we might have a timing issue
|
||||
@@ -1519,6 +1531,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
if(mac instanceof MacRelease mr)
|
||||
{
|
||||
mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), message.getTimestamp(), "MAC RELEASE: " + mac.toString());
|
||||
mTalkerAliasTracker.callEnded();
|
||||
getIdentifierCollection().remove(IdentifierClass.USER);
|
||||
broadcast(message, mac, DecodeEventType.COMMAND,
|
||||
(mr.isForcedPreemption() ? "FORCED " : "") + "CALL PREEMPTION" +
|
||||
@@ -1720,17 +1733,34 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
{
|
||||
if(mac instanceof L3HarrisTalkerAlias talkerAlias)
|
||||
{
|
||||
P25TalkerAliasIdentifier alias = talkerAlias.getAlias();
|
||||
getIdentifierCollection().update(alias);
|
||||
mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), getTimeslot(), alias, message.getTimestamp());
|
||||
processTalkerAlias(talkerAlias.getAlias(), null, message.getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
//Add the alias to the talker alias manager if we know the associated radio
|
||||
Identifier from = getIdentifierCollection().getFromIdentifier();
|
||||
/**
|
||||
* Talker alias manager shared with the traffic channel manager
|
||||
*/
|
||||
private TalkerAliasManager getTalkerAliasManager()
|
||||
{
|
||||
return mTrafficChannelManager.getTalkerAliasManager();
|
||||
}
|
||||
|
||||
if(from instanceof RadioIdentifier ri)
|
||||
{
|
||||
mTrafficChannelManager.getTalkerAliasManager().update(ri, alias);
|
||||
}
|
||||
/**
|
||||
* Processes a talker alias: stores it for its radio and adds it to the call that it belongs to.
|
||||
* @param talkerAlias that was received
|
||||
* @param radio identified by the alias message, or null when the alias belongs to the talker of the current or
|
||||
* just ended call
|
||||
* @param timestamp of the message
|
||||
*/
|
||||
private void processTalkerAlias(TalkerAliasIdentifier talkerAlias, RadioIdentifier radio, long timestamp)
|
||||
{
|
||||
switch(mTalkerAliasTracker.received(talkerAlias, radio, timestamp))
|
||||
{
|
||||
case CURRENT_CALL -> mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), getTimeslot(),
|
||||
talkerAlias, timestamp);
|
||||
case ENDED_CALL -> mTrafficChannelManager.processTrafficTalkerAliasAfterCall(getCurrentFrequency(),
|
||||
getTimeslot(), mTalkerAliasTracker.getEndedCallRadio(), talkerAlias);
|
||||
default -> {} //Stored for the radio, for the radio's next call
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1855,6 +1885,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
|
||||
{
|
||||
case REQUEST_RESET:
|
||||
resetState();
|
||||
mTalkerAliasTracker.reset();
|
||||
mNetworkConfigurationMonitor.reset();
|
||||
break;
|
||||
case NOTIFICATION_SOURCE_FREQUENCY:
|
||||
|
||||
@@ -75,19 +75,22 @@ public class L3HarrisTalkerAlias extends MacStructureVendor
|
||||
{
|
||||
if(mAliasIdentifier == null)
|
||||
{
|
||||
int length = getLength() - 2;
|
||||
//The alias fills the rest of the structure after the opcode, vendor and length octets
|
||||
int start = ALIAS_START + getOffset();
|
||||
int end = Math.min(getOffset() + getLength() * 8, getMessage().size());
|
||||
|
||||
if(length > 0)
|
||||
if(end > start)
|
||||
{
|
||||
length *= 8;
|
||||
length += getOffset();
|
||||
byte[] bytes = getMessage().getSubMessage(start, end).getBytes();
|
||||
int length = 0;
|
||||
|
||||
if(length > getMessage().size())
|
||||
//The alias is padded with zero bytes
|
||||
while(length < bytes.length && bytes[length] != 0)
|
||||
{
|
||||
length = getMessage().size();
|
||||
length++;
|
||||
}
|
||||
|
||||
String alias = new String(getMessage().getSubMessage(ALIAS_START + getOffset(), length).getBytes()).trim();
|
||||
String alias = new String(bytes, 0, length).trim();
|
||||
mAliasIdentifier = P25TalkerAliasIdentifier.create(alias);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,23 @@ public class TableColumnMonitorTest
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void columnAddedBetweenStoredColumnsTakesItsDefaultPosition() throws Exception
|
||||
{
|
||||
onFx(() -> {
|
||||
//Stored by a version without the 'talker' column, with the user's order and a hidden column
|
||||
mPreferences.put(KEY + ".column.order", "status,from,from.alias,to");
|
||||
mPreferences.put(KEY + ".column.hidden", "to");
|
||||
TableView<String> table = createTable("status", "from", "talker", "from.alias", "to");
|
||||
TableColumnMonitor<String> monitor = monitor(table);
|
||||
assertEquals(List.of("status", "from", "talker", "from.alias", "to"), ids(table));
|
||||
assertEquals(List.of("status", "from", "talker", "from.alias"), visibleIds(table));
|
||||
|
||||
monitor.reset();
|
||||
assertEquals(List.of("status", "from", "talker", "from.alias", "to"), visibleIds(table));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lastVisibleColumnCantBeHidden() throws Exception
|
||||
{
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.identifier.alias;
|
||||
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25FullyQualifiedRadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* Tests assigning talker aliases to the right radio and keeping the shown talker alias matched to the talking radio.
|
||||
*/
|
||||
public class TalkerAliasTrackerTest
|
||||
{
|
||||
private static final RadioIdentifier RADIO_A = (RadioIdentifier)APCO25RadioIdentifier.createFrom(100);
|
||||
private static final RadioIdentifier RADIO_B = (RadioIdentifier)APCO25RadioIdentifier.createFrom(200);
|
||||
private static final TalkerAliasIdentifier ALIAS_A = P25TalkerAliasIdentifier.create("ENGINE 7");
|
||||
private static final TalkerAliasIdentifier ALIAS_B = P25TalkerAliasIdentifier.create("MEDIC 12");
|
||||
private MutableIdentifierCollection mIdentifiers;
|
||||
private TalkerAliasManager mManager;
|
||||
private TalkerAliasTracker mTracker;
|
||||
private long mTime = 1_000_000;
|
||||
|
||||
@BeforeEach
|
||||
public void setup()
|
||||
{
|
||||
mIdentifiers = new MutableIdentifierCollection();
|
||||
mManager = new TalkerAliasManager();
|
||||
mTracker = new TalkerAliasTracker(mIdentifiers, () -> mManager, TalkerAliasTracker.BetweenCalls.PREVIOUS_TALKER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call message from the radio: the decoder updates the identifiers and signals call activity
|
||||
*/
|
||||
private void talk(RadioIdentifier radio)
|
||||
{
|
||||
mIdentifiers.update(radio);
|
||||
mTracker.callActivity(mTime);
|
||||
mTime += 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call ends: the decoder signals the end and removes the FROM identifiers
|
||||
*/
|
||||
private void endCall()
|
||||
{
|
||||
mTracker.callEnded();
|
||||
mIdentifiers.remove(Role.FROM);
|
||||
}
|
||||
|
||||
private Identifier getShownAlias()
|
||||
{
|
||||
return mIdentifiers.getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
}
|
||||
|
||||
private Identifier getStoredAlias(RadioIdentifier radio)
|
||||
{
|
||||
MutableIdentifierCollection mic = new MutableIdentifierCollection();
|
||||
mic.update(radio);
|
||||
mManager.enrichMutable(mic);
|
||||
return mic.getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasDuringCallIsShownAndStored()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
assertEquals(TalkerAliasTracker.Assignment.CURRENT_CALL, mTracker.received(ALIAS_A, null, mTime));
|
||||
assertEquals(ALIAS_A, getShownAlias());
|
||||
assertEquals(ALIAS_A, getStoredAlias(RADIO_A));
|
||||
endCall();
|
||||
assertNull(getShownAlias());
|
||||
|
||||
//The talker's next call shows the stored alias right away
|
||||
talk(RADIO_A);
|
||||
assertEquals(ALIAS_A, getShownAlias());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasBeforeTheRadioIsIdentifiedIsStoredForTheRadio()
|
||||
{
|
||||
mTracker.callActivity(mTime); //Call starts without identifying the talker (e.g. a P25 Phase 1 HDU)
|
||||
assertEquals(TalkerAliasTracker.Assignment.CURRENT_CALL, mTracker.received(ALIAS_A, null, mTime));
|
||||
assertEquals(ALIAS_A, getShownAlias());
|
||||
|
||||
talk(RADIO_A);
|
||||
assertEquals(ALIAS_A, getShownAlias(), "Alias is kept when the talker is identified");
|
||||
assertEquals(ALIAS_A, getStoredAlias(RADIO_A), "and stored for the talker");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasBeforeTheRadioInANewCallIsntAssignedToThePreviousTalker()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
endCall();
|
||||
mTracker.callActivity(mTime); //Next call starts without identifying the talker
|
||||
mTracker.received(ALIAS_B, null, mTime);
|
||||
assertNull(getStoredAlias(RADIO_A), "Not stored for the previous call's talker");
|
||||
|
||||
talk(RADIO_B);
|
||||
assertEquals(ALIAS_B, getStoredAlias(RADIO_B));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasAfterTheCallEndsBelongsToThatCallsTalker()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
endCall();
|
||||
assertEquals(TalkerAliasTracker.Assignment.ENDED_CALL, mTracker.received(ALIAS_A, null, mTime));
|
||||
assertEquals(RADIO_A, mTracker.getEndedCallRadio());
|
||||
assertNull(getShownAlias(), "Not shown with no call in progress");
|
||||
assertEquals(ALIAS_A, getStoredAlias(RADIO_A));
|
||||
|
||||
talk(RADIO_B);
|
||||
assertNull(getShownAlias(), "Not shown for a different talker");
|
||||
endCall();
|
||||
|
||||
talk(RADIO_A);
|
||||
assertEquals(ALIAS_A, getShownAlias());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void callEndedMoreThanOnceKeepsTheTalker()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
endCall();
|
||||
endCall(); //e.g. repeated TDULC or End PTT messages
|
||||
assertEquals(TalkerAliasTracker.Assignment.ENDED_CALL, mTracker.received(ALIAS_A, null, mTime));
|
||||
assertEquals(ALIAS_A, getStoredAlias(RADIO_A));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasIsRemovedWhenADifferentRadioTalks()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
mTracker.received(ALIAS_A, null, mTime);
|
||||
talk(RADIO_B); //Talker changes without a call end
|
||||
assertNull(getShownAlias());
|
||||
|
||||
mManager.update(RADIO_B, ALIAS_B);
|
||||
talk(RADIO_B);
|
||||
assertEquals(ALIAS_B, getShownAlias());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasIsKeptWhenTheRadioBecomesFullyQualified()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
mTracker.received(ALIAS_A, null, mTime);
|
||||
mIdentifiers.update(APCO25FullyQualifiedRadioIdentifier.createFrom(100, 1, 2, 100));
|
||||
mTracker.callActivity(mTime);
|
||||
assertEquals(ALIAS_A, getShownAlias());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasWithItsOwnRadio()
|
||||
{
|
||||
//Motorola talker aliases identify their radio
|
||||
talk(RADIO_A);
|
||||
assertEquals(TalkerAliasTracker.Assignment.UNASSIGNED, mTracker.received(ALIAS_B, RADIO_B, mTime),
|
||||
"Alias for a radio that isn't talking");
|
||||
assertNull(getShownAlias());
|
||||
assertEquals(ALIAS_B, getStoredAlias(RADIO_B));
|
||||
|
||||
assertEquals(TalkerAliasTracker.Assignment.CURRENT_CALL, mTracker.received(ALIAS_A, RADIO_A, mTime));
|
||||
assertEquals(ALIAS_A, getShownAlias());
|
||||
endCall();
|
||||
|
||||
assertEquals(TalkerAliasTracker.Assignment.ENDED_CALL, mTracker.received(ALIAS_A, RADIO_A, mTime));
|
||||
assertEquals(TalkerAliasTracker.Assignment.UNASSIGNED, mTracker.received(ALIAS_B, RADIO_B, mTime));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasWithNoCallOrTalkerIsIgnored()
|
||||
{
|
||||
assertEquals(TalkerAliasTracker.Assignment.UNASSIGNED, mTracker.received(ALIAS_A, null, mTime));
|
||||
assertNull(getShownAlias());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void worksWithoutATalkerAliasManager()
|
||||
{
|
||||
TalkerAliasTracker tracker = new TalkerAliasTracker(mIdentifiers, () -> null,
|
||||
TalkerAliasTracker.BetweenCalls.PREVIOUS_TALKER);
|
||||
mIdentifiers.update(RADIO_A);
|
||||
tracker.callActivity(mTime);
|
||||
assertEquals(TalkerAliasTracker.Assignment.CURRENT_CALL, tracker.received(ALIAS_A, null, mTime));
|
||||
assertEquals(ALIAS_A, getShownAlias());
|
||||
mIdentifiers.update(RADIO_B);
|
||||
tracker.callActivity(mTime);
|
||||
assertNull(getShownAlias());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nextTalkerAliasBelongsToTheNextCall()
|
||||
{
|
||||
//P25 Phase 2 L3Harris: the alias arrives just before the next call's Push-To-Talk
|
||||
mTracker = new TalkerAliasTracker(mIdentifiers, () -> mManager, TalkerAliasTracker.BetweenCalls.NEXT_TALKER);
|
||||
talk(RADIO_A);
|
||||
endCall();
|
||||
|
||||
assertEquals(TalkerAliasTracker.Assignment.NEXT_CALL, mTracker.received(ALIAS_B, null, mTime));
|
||||
assertNull(getStoredAlias(RADIO_A), "Not stored for the previous call's talker");
|
||||
assertNull(getShownAlias(), "Not shown until the next call starts");
|
||||
|
||||
talk(RADIO_B);
|
||||
assertEquals(ALIAS_B, getShownAlias());
|
||||
assertEquals(ALIAS_B, getStoredAlias(RADIO_B));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nextTalkerAliasBeforeACallWithoutARadio()
|
||||
{
|
||||
mTracker = new TalkerAliasTracker(mIdentifiers, () -> mManager, TalkerAliasTracker.BetweenCalls.NEXT_TALKER);
|
||||
mTracker.received(ALIAS_B, null, mTime);
|
||||
mTracker.callActivity(mTime + 50); //Voice before the talker is identified
|
||||
assertEquals(ALIAS_B, getShownAlias());
|
||||
|
||||
talk(RADIO_B);
|
||||
assertEquals(ALIAS_B, getShownAlias());
|
||||
assertEquals(ALIAS_B, getStoredAlias(RADIO_B));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nextTalkerAliasExpires()
|
||||
{
|
||||
mTracker = new TalkerAliasTracker(mIdentifiers, () -> mManager, TalkerAliasTracker.BetweenCalls.NEXT_TALKER);
|
||||
mTracker.received(ALIAS_B, null, mTime);
|
||||
mTime += TalkerAliasTracker.NEXT_CALL_WINDOW_MS + 1;
|
||||
talk(RADIO_A);
|
||||
assertNull(getShownAlias(), "Too long before the call to belong to it");
|
||||
assertNull(getStoredAlias(RADIO_A));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25.phase1;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.module.decode.p25.P25TrafficChannelManager;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCGroupVoiceChannelUser;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.HarrisTalkerAliasAssembler;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBase;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBlock1;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBlock2;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBlock3;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasBlock4;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasComplete;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDUMessage;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
/**
|
||||
* Tests that P25 Phase 1 talker aliases are assigned to the right radio, including L3Harris talker aliases that
|
||||
* complete after the call ends, and that the FROM radio and talker alias don't carry over to the next call.
|
||||
*/
|
||||
public class P25P1DecoderStateTalkerAliasTest
|
||||
{
|
||||
private static final int NAC = 0x293;
|
||||
private static final int RADIO_A = 1001;
|
||||
private static final int RADIO_B = 2002;
|
||||
private static final int TALKGROUP = 100;
|
||||
private P25P1DecoderState mDecoderState;
|
||||
private P25TrafficChannelManager mTrafficChannelManager;
|
||||
private final MutableIdentifierCollection mIdentifiers = new MutableIdentifierCollection();
|
||||
private final List<IDecodeEvent> mEvents = new ArrayList<>();
|
||||
private long mTimestamp = 1_700_000_000_000L;
|
||||
|
||||
@BeforeEach
|
||||
public void setup()
|
||||
{
|
||||
Channel channel = new Channel("Test");
|
||||
channel.setDecodeConfiguration(new DecodeConfigP25Phase1());
|
||||
mTrafficChannelManager = new P25TrafficChannelManager(channel);
|
||||
mTrafficChannelManager.addDecodeEventListener(event -> {
|
||||
if(!mEvents.contains(event))
|
||||
{
|
||||
mEvents.add(event);
|
||||
}
|
||||
});
|
||||
mDecoderState = new P25P1DecoderState(channel, mTrafficChannelManager);
|
||||
mDecoderState.setDecoderStateListener(event -> {});
|
||||
//Mirrors the decoder state identifier collection, the same as the channel metadata and audio modules
|
||||
mDecoderState.setIdentifierUpdateListener(mIdentifiers);
|
||||
mDecoderState.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call from the radio: group voice channel user link control messages, as carried in the LDU1s.
|
||||
*/
|
||||
private void talk(int radio)
|
||||
{
|
||||
//A call carries the channel user link control in each LDU1
|
||||
for(int x = 0; x < 3; x++)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(72);
|
||||
message.load(32, 16, TALKGROUP); //Group address
|
||||
message.load(48, 24, radio); //Source address
|
||||
ChannelUser lcw = new ChannelUser(message, mTimestamp);
|
||||
lcw.setValid(true);
|
||||
mDecoderState.receive(lcw);
|
||||
mTimestamp += 360;
|
||||
}
|
||||
}
|
||||
|
||||
private void endCall()
|
||||
{
|
||||
mDecoderState.receive(new TDUMessage(new CorrectedBinaryMessage(28), NAC, mTimestamp));
|
||||
mTimestamp += 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Completed L3Harris talker alias, reassembled from link control blocks that carry 7 characters each.
|
||||
*/
|
||||
private void harrisTalkerAlias(String alias)
|
||||
{
|
||||
mDecoderState.receive(assemble(alias, mTimestamp));
|
||||
mTimestamp += 180;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the L3Harris talker alias blocks for the alias and reassembles them with the assembler.
|
||||
* @param alias of up to 28 characters
|
||||
* @param timestamp for the blocks
|
||||
* @return reassembled talker alias
|
||||
*/
|
||||
static LCHarrisTalkerAliasComplete assemble(String alias, long timestamp)
|
||||
{
|
||||
HarrisTalkerAliasAssembler assembler = new HarrisTalkerAliasAssembler();
|
||||
LCHarrisTalkerAliasComplete complete = null;
|
||||
int blocks = Math.max(2, (alias.length() + 6) / 7);
|
||||
|
||||
for(int block = 0; block < blocks; block++)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(72);
|
||||
message.load(2, 6, 50 + block); //L3Harris talker alias block opcode
|
||||
message.load(8, 8, 164); //L3Harris vendor
|
||||
|
||||
for(int x = 0; x < 7 && block * 7 + x < alias.length(); x++)
|
||||
{
|
||||
message.load(16 + x * 8, 8, alias.charAt(block * 7 + x));
|
||||
}
|
||||
|
||||
LCHarrisTalkerAliasBase lcw = switch(block)
|
||||
{
|
||||
case 0 -> new LCHarrisTalkerAliasBlock1(message);
|
||||
case 1 -> new LCHarrisTalkerAliasBlock2(message);
|
||||
case 2 -> new LCHarrisTalkerAliasBlock3(message);
|
||||
default -> new LCHarrisTalkerAliasBlock4(message);
|
||||
};
|
||||
lcw.setValid(true);
|
||||
LCHarrisTalkerAliasComplete assembled = assembler.process(lcw, timestamp);
|
||||
|
||||
if(assembled != null)
|
||||
{
|
||||
complete = assembled;
|
||||
}
|
||||
}
|
||||
|
||||
return complete;
|
||||
}
|
||||
|
||||
private Identifier getFromRadio()
|
||||
{
|
||||
return mIdentifiers.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
}
|
||||
|
||||
private String getTalkerAlias()
|
||||
{
|
||||
Identifier alias = mIdentifiers.getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM);
|
||||
return alias != null ? alias.getValue().toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group voice channel user link control as a message, the same way that the decoder state receives the
|
||||
* reassembled link control messages.
|
||||
*/
|
||||
private static class ChannelUser extends LCGroupVoiceChannelUser implements IMessage
|
||||
{
|
||||
private final long mMessageTimestamp;
|
||||
|
||||
ChannelUser(CorrectedBinaryMessage message, long timestamp)
|
||||
{
|
||||
super(message, timestamp, false);
|
||||
mMessageTimestamp = timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mMessageTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.APCO25;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimeslot()
|
||||
{
|
||||
return P25P1Message.TIMESLOT_1;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void harrisAliasAfterCallEndIsStoredForThatCallsTalker()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
assertEquals(RADIO_A, getFromRadio().getValue());
|
||||
endCall();
|
||||
assertNull(getFromRadio(), "FROM radio is cleared at the end of the call");
|
||||
|
||||
//Alias completes after the call ended
|
||||
harrisTalkerAlias("ENGINE 7");
|
||||
assertNull(getTalkerAlias(), "No call in progress, so the alias isn't shown");
|
||||
|
||||
//A different talker doesn't get the alias
|
||||
talk(RADIO_B);
|
||||
assertEquals(RADIO_B, getFromRadio().getValue());
|
||||
assertNull(getTalkerAlias());
|
||||
endCall();
|
||||
|
||||
//The talker's next call shows the alias
|
||||
talk(RADIO_A);
|
||||
assertEquals("ENGINE 7", getTalkerAlias());
|
||||
endCall();
|
||||
assertNull(getTalkerAlias(), "Talker alias is cleared at the end of the call");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void harrisAliasDuringCallIsShownAndStored()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
harrisTalkerAlias("ENGINE 7");
|
||||
assertEquals("ENGINE 7", getTalkerAlias());
|
||||
endCall();
|
||||
|
||||
talk(RADIO_A);
|
||||
assertEquals("ENGINE 7", getTalkerAlias());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void aliasIsReplacedWhenTheTalkerChangesWithoutACallEnd()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
harrisTalkerAlias("ENGINE 7");
|
||||
talk(RADIO_B);
|
||||
assertEquals(RADIO_B, getFromRadio().getValue());
|
||||
assertNull(getTalkerAlias());
|
||||
}
|
||||
|
||||
private static String getEventTalkerAlias(IDecodeEvent event)
|
||||
{
|
||||
Identifier alias = event.getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS,
|
||||
Role.FROM);
|
||||
return alias != null ? alias.getValue().toString() : null;
|
||||
}
|
||||
|
||||
private static Identifier getEventRadio(IDecodeEvent event)
|
||||
{
|
||||
return event.getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void harrisAliasAfterCallEndIsAddedToTheEndedCallEvent()
|
||||
{
|
||||
talk(RADIO_A);
|
||||
endCall();
|
||||
talk(RADIO_B);
|
||||
endCall();
|
||||
assertEquals(2, mEvents.size());
|
||||
long duration = mEvents.get(1).getDuration();
|
||||
|
||||
//Alias completes after radio B's call ended
|
||||
harrisTalkerAlias("ENGINE 7");
|
||||
assertEquals(2, mEvents.size(), "No new event");
|
||||
assertNull(getEventTalkerAlias(mEvents.get(0)), "Radio A's call event isn't changed");
|
||||
assertEquals(RADIO_B, getEventRadio(mEvents.get(1)).getValue());
|
||||
assertEquals("ENGINE 7", getEventTalkerAlias(mEvents.get(1)));
|
||||
assertEquals(duration, mEvents.get(1).getDuration(), "Event duration isn't changed");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spaceAtAFragmentBoundaryIsKept()
|
||||
{
|
||||
//Characters 7 and 8 are spaces, at the end of block 1 and the start of block 2
|
||||
assertEquals("ENGINE 7", assemble("ENGINE 7", 0).getTalkerAlias().getValue());
|
||||
assertEquals("ENGINE 7", assemble("ENGINE 7", 0).getTalkerAlias().getValue());
|
||||
assertEquals("BATTALION CHIEF 1", assemble("BATTALION CHIEF 1", 0).getTalkerAlias().getValue());
|
||||
//Trailing padding is removed
|
||||
assertEquals("A", assemble("A", 0).getTalkerAlias().getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.l3harris;
|
||||
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/**
|
||||
* Tests decoding P25 Phase 2 L3Harris talker alias MAC structures, using structures captured from a Duke Energy
|
||||
* Florida recording: opcode A8, vendor A4 (L3Harris), length 0x11 (17 octets), then 14 alias characters padded with
|
||||
* zero bytes.
|
||||
*/
|
||||
public class L3HarrisTalkerAliasTest
|
||||
{
|
||||
/**
|
||||
* Creates the alias structure at bit offset 8 (after the MAC header octet) in a FACCH or SACCH sized message.
|
||||
*/
|
||||
private static L3HarrisTalkerAlias create(int messageBits, int... structure)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(messageBits);
|
||||
|
||||
for(int x = 0; x < structure.length && (x + 2) * 8 <= messageBits; x++)
|
||||
{
|
||||
message.load((x + 1) * 8, 8, structure[x]);
|
||||
}
|
||||
|
||||
return new L3HarrisTalkerAlias(message, 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allFourteenCharactersAreDecoded()
|
||||
{
|
||||
//FACCH: FL-DCC-02_StPe
|
||||
L3HarrisTalkerAlias alias = create(156, 0xA8, 0xA4, 0x11, 0x46, 0x4C, 0x2D, 0x44, 0x43, 0x43, 0x2D, 0x30, 0x32,
|
||||
0x5F, 0x53, 0x74, 0x50, 0x65, 0xC5);
|
||||
assertEquals("FL-DCC-02_StPe", alias.getAlias().getValue());
|
||||
|
||||
//SACCH: LBVA-17168-SBK
|
||||
alias = create(180, 0xA8, 0xA4, 0x11, 0x4C, 0x42, 0x56, 0x41, 0x2D, 0x31, 0x37, 0x31, 0x36, 0x38, 0x2D, 0x53,
|
||||
0x42, 0x4B, 0x00, 0x00, 0x00, 0xA7);
|
||||
assertEquals("LBVA-17168-SBK", alias.getAlias().getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zeroPaddingEndsTheAlias()
|
||||
{
|
||||
//A shorter alias padded with zero bytes, followed by unrelated bytes after the structure
|
||||
L3HarrisTalkerAlias alias = create(156, 0xA8, 0xA4, 0x11, 0x46, 0x4C, 0x2D, 0x44, 0x43, 0x43, 0x2D, 0x30, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x44);
|
||||
assertEquals("FL-DCC-0", alias.getAlias().getValue());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user