#2660 Talker alias cleanup across the decoders

This commit is contained in:
denny
2026-11-15 07:31:36 -05:00
parent a4a3a67901
commit e9207e4067
22 changed files with 1437 additions and 99 deletions

View File

@@ -56,7 +56,7 @@ public class ChannelDetailView extends VBox implements Listener<ProcessingChain>
Button refreshButton = new Button("Refresh"); Button refreshButton = new Button("Refresh");
refreshButton.setTooltip(new Tooltip("Update the channel details")); 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), HBox labels = new HBox(5, new Label("System:"), grow(mSystemLabel), new Label("Site:"), grow(mSiteLabel),
new Label("Channel Name:"), grow(mNameLabel), refreshButton); new Label("Channel Name:"), grow(mNameLabel), refreshButton);
@@ -68,6 +68,14 @@ public class ChannelDetailView extends VBox implements Listener<ProcessingChain>
getChildren().addAll(labels, mDetailTextArea); 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) private static Label grow(Label label)
{ {
label.setMaxWidth(Double.MAX_VALUE); label.setMaxWidth(Double.MAX_VALUE);

View File

@@ -42,6 +42,7 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener; import io.github.dsheirer.sample.Listener;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map; 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, columns.add(NowPlayingCells.column("from", "From", null, Pos.CENTER,
row -> { row -> {
ChannelMetadata metadata = row.getChannelMetadata(); String text = mTalkgroupFormatPreference.format(row.getChannelMetadata().getFromIdentifier());
String text = mTalkgroupFormatPreference.format(metadata.getFromIdentifier()); return text == null || text.isEmpty() ? EMPTY_IDENTIFIER : text;
}, Row::revisionProperty));
if(text == null || text.isEmpty()) //Talker alias sent over the air by the radio (e.g. DMR or P25)
{ columns.add(NowPlayingCells.column("talker", "Talker", null, Pos.CENTER,
return EMPTY_IDENTIFIER; row -> {
} Identifier talkerAlias = row.getChannelMetadata().getTalkerAliasIdentifier();
else if(metadata.hasTalkerAliasIdentifier()) return talkerAlias != null && talkerAlias.getValue() != null ? talkerAlias.getValue().toString() : null;
{
return text + " " + metadata.getTalkerAliasIdentifier();
}
return text;
}, Row::revisionProperty)); }, Row::revisionProperty));
TableColumn<Row,AliasDisplay> fromAlias = NowPlayingCells.column("from.alias", "Alias", "From Alias", 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.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; return columns;
} }

View File

@@ -26,6 +26,7 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.channel.IChannelDescriptor; import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.eventbus.MyEventBus; import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.filter.FilterSet; import io.github.dsheirer.filter.FilterSet;
import io.github.dsheirer.gui.table.TableColumnMonitor;
import io.github.dsheirer.icon.IconModel; import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier; 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 -> formatIdentifiers(row.getItem().getIdentifierCollection(), Role.FROM, userPreferences),
Row::revisionProperty)); 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", TableColumn<Row<IDecodeEvent>,AliasDisplay> fromAlias = NowPlayingCells.column("from.alias", "Alias",
"From Alias", Pos.CENTER, row -> getAliases(row.getItem().getIdentifierCollection(), Role.FROM, aliasModel), "From Alias", Pos.CENTER, row -> getAliases(row.getItem().getIdentifierCollection(), Role.FROM, aliasModel),
Row::revisionProperty); Row::revisionProperty);
@@ -220,9 +225,54 @@ public class DecodeEventView extends HistoryTableView<IDecodeEvent,IDecodeEvent>
columns.add(NowPlayingCells.column("details", "Details", null, Pos.CENTER_LEFT, columns.add(NowPlayingCells.column("details", "Details", null, Pos.CENTER_LEFT,
row -> row.getItem().getDetails(), Row::revisionProperty)); 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; 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. * Event timestamp formatter that uses the timestamp format preference.
*/ */
@@ -278,7 +328,7 @@ public class DecodeEventView extends HistoryTableView<IDecodeEvent,IDecodeEvent>
return null; return null;
} }
List<Identifier> identifiers = identifierCollection.getIdentifiers(role); List<Identifier> identifiers = getIdentifiers(identifierCollection, role);
if(identifiers == null || identifiers.isEmpty()) if(identifiers == null || identifiers.isEmpty())
{ {
@@ -319,7 +369,7 @@ public class DecodeEventView extends HistoryTableView<IDecodeEvent,IDecodeEvent>
return null; return null;
} }
List<Identifier> identifiers = identifierCollection.getIdentifiers(role); List<Identifier> identifiers = getIdentifiers(identifierCollection, role);
if(identifiers == null || identifiers.isEmpty()) if(identifiers == null || identifiers.isEmpty())
{ {

View File

@@ -62,14 +62,22 @@ public class NowPlayingView extends SplitPane
mMessageActivityView = new MessageActivityView(userPreferences); mMessageActivityView = new MessageActivityView(userPreferences);
mChannelSpectrumView = new ChannelSpectrumView(playlistManager); mChannelSpectrumView = new ChannelSpectrumView(playlistManager);
Tab detailsTab = new Tab("Details", mChannelDetailView);
Tab channelTab = new Tab("Channel", mChannelSpectrumView); 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); new Tab("Messages", mMessageActivityView), channelTab);
mTabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE); mTabPane.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
//Turn the channel spectrum processing on only while the channel tab is selected //Turn the channel spectrum processing on only while the channel tab is selected
mTabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldTab, newTab) -> mTabPane.getSelectionModel().selectedItemProperty().addListener((observable, oldTab, newTab) -> {
mChannelSpectrumView.setPanelVisible(newTab == channelTab)); 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(mChannelDetailView);
mChannelMetadataView.addProcessingChainSelectionListener(mDecodeEventView); mChannelMetadataView.addProcessingChainSelectionListener(mDecodeEventView);

View File

@@ -50,6 +50,11 @@ public class TableColumnMonitor<S>
* column header text doesn't identify the column by itself (e.g. two columns titled 'Alias'). * 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"; 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 ORDER = ".column.order";
private static final String HIDDEN = ".column.hidden"; private static final String HIDDEN = ".column.hidden";
private static final String WIDTH = ".column.width."; private static final String WIDTH = ".column.width.";
@@ -186,6 +191,18 @@ public class TableColumnMonitor<S>
column.getProperties().put(CONFIGURATION_NAME, name); 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 * Restores the default column order and visibility and removes the stored order and visibility settings. The
* column widths aren't changed. * 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++) for(int x = 0; x < mDefaultOrder.size(); x++)
{ {
TableColumn<S,?> column = mDefaultOrder.get(x); TableColumn<S,?> column = mDefaultOrder.get(x);
double width = mPreferences.getDouble(getWidthKey(column), Double.NaN); 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) if(swingWidth != Integer.MAX_VALUE && swingWidth > 0)
{ {

View File

@@ -19,7 +19,9 @@
package io.github.dsheirer.identifier.alias; package io.github.dsheirer.identifier.alias;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role; 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 * Creates a summary listing of talker aliases
* @return summary. * @return summary.

View File

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

View File

@@ -20,12 +20,12 @@ package io.github.dsheirer.module.decode.dmr;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent; 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.Event;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State; import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.channel.state.TimeslotDecoderState; 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.ChannelType;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification; import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification;
import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier; 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.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role; 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.integer.IntegerIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.log.LoggingSuppressor; import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.message.EmptyTimeslotPlaceholderMessage; import io.github.dsheirer.message.EmptyTimeslotPlaceholderMessage;
@@ -126,6 +127,11 @@ public class DMRDecoderState extends TimeslotDecoderState
private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor; private DMRNetworkConfigurationMonitor mNetworkConfigurationMonitor;
private DMRTrafficChannelManager mTrafficChannelManager; private DMRTrafficChannelManager mTrafficChannelManager;
private DecodeEvent mCurrentCallEvent; 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 boolean mIgnoreCRCChecksums;
private DMRDecoderState mSisterDecoderState; private DMRDecoderState mSisterDecoderState;
@@ -1315,24 +1321,54 @@ public class DMRDecoderState extends TimeslotDecoderState
case FULL_STANDARD_TALKER_ALIAS_COMPLETE: case FULL_STANDARD_TALKER_ALIAS_COMPLETE:
if(message instanceof TalkerAliasComplete tac && tac.hasTalkerAliasIdentifier()) if(message instanceof TalkerAliasComplete tac && tac.hasTalkerAliasIdentifier())
{ {
getIdentifierCollection().update(tac.getTalkerAliasIdentifier()); processTalkerAlias(tac.getTalkerAliasIdentifier(), tac.getTimestamp());
Identifier fromRadio = getIdentifierCollection().getFromIdentifier();
if(hasTrafficChannelManager() && fromRadio instanceof RadioIdentifier radio)
{
mTrafficChannelManager.getTalkerAliasManager().update(radio, tac.getTalkerAliasIdentifier());
}
if(mCurrentCallEvent != null)
{
broadcast(mCurrentCallEvent);
}
} }
break; 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. * Updates the current call with encryption information.
* @param embeddedEncryptionParameters decoded from the Voice Frame F * @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) private void updateEncryptedCall(EmbeddedEncryptionParameters embeddedEncryptionParameters, boolean isGroup, long timestamp)
{ {
mTalkerAliasTracker.callActivity(timestamp);
if(mCurrentCallEvent != null) if(mCurrentCallEvent != null)
{ {
String details = mCurrentCallEvent.getDetails();; String details = mCurrentCallEvent.getDetails();;
@@ -1377,6 +1415,9 @@ public class DMRDecoderState extends TimeslotDecoderState
*/ */
private void updateCurrentCall(DecodeEventType type, String details, long timestamp) 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); Event event = (mCurrentCallEvent == null ? Event.START : Event.CONTINUATION);
//Create a repeater channel descriptor if we don't have one //Create a repeater channel descriptor if we don't have one
@@ -1428,10 +1469,12 @@ public class DMRDecoderState extends TimeslotDecoderState
{ {
mCurrentCallEvent.end(timestamp); mCurrentCallEvent.end(timestamp);
broadcast(mCurrentCallEvent); broadcast(mCurrentCallEvent);
mEndedCallEvent = mCurrentCallEvent;
mCurrentCallEvent = null; 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); getIdentifierCollection().remove(IdentifierClass.USER, Form.TONE, Role.FROM);
} }

View File

@@ -24,17 +24,19 @@ import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent; import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State; import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.controller.channel.Channel; import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role; 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.identifier.radio.RadioIdentifier;
import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType; import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.event.DecodeEventType; import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent; 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.NXDNLayer3Message;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation; import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.ControlChannelInformation;
import io.github.dsheirer.module.decode.nxdn.layer3.broadcast.SiteInformation; 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 Channel mChannel;
private final NXDNNetworkConfigurationMonitor mNetworkConfigurationMonitor = new NXDNNetworkConfigurationMonitor(); private final NXDNNetworkConfigurationMonitor mNetworkConfigurationMonitor = new NXDNNetworkConfigurationMonitor();
private final NXDNTrafficChannelManager mTrafficChannelManager; 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 mEncryptedCallStateDetermined = false;
private boolean mEncryptedCall = false; private boolean mEncryptedCall = false;
private int mDisconnectResponseCount = 0; private int mDisconnectResponseCount = 0;
@@ -139,7 +144,9 @@ public class NXDNDecoderState extends DecoderState
switch(event.getEvent()) switch(event.getEvent())
{ {
case REQUEST_RESET: case REQUEST_RESET:
mTalkerAliasTracker.callEnded();
resetState(); resetState();
mTalkerAliasTracker.reset();
break; break;
case NOTIFICATION_SOURCE_FREQUENCY: case NOTIFICATION_SOURCE_FREQUENCY:
setCurrentFrequency(event.getFrequency()); 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 * Broadcasts the arguments as a new decode event
* @param identifiers involved in the event * @param identifiers involved in the event
@@ -265,6 +298,9 @@ public class NXDNDecoderState extends DecoderState
{ {
mIdleDuringCallCount = 0; mIdleDuringCallCount = 0;
getIdentifierCollection().update(vc.getIdentifiers()); getIdentifierCollection().update(vc.getIdentifiers());
//Keep the talker alias matched to the radio that is talking
mTalkerAliasTracker.callActivity(vc.getTimestamp());
mEncryptedCallStateDetermined = true; mEncryptedCallStateDetermined = true;
mEncryptedCall = vc.getEncryptionKeyIdentifier().isEncrypted(); mEncryptedCall = vc.getEncryptionKeyIdentifier().isEncrypted();
state = mEncryptedCall ? State.ENCRYPTED : State.CALL; state = mEncryptedCall ? State.ENCRYPTED : State.CALL;
@@ -326,6 +362,7 @@ public class NXDNDecoderState extends DecoderState
mEncryptedCall = false; mEncryptedCall = false;
mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT; mIdleDuringCallCount = IDLE_DURING_CALL_MAX_COUNT;
//Only remove the FROM identifier, anticipating a continuation call for the same talkgroup //Only remove the FROM identifier, anticipating a continuation call for the same talkgroup
mTalkerAliasTracker.callEnded();
getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().remove(Role.FROM);
break; break;
case CONTROL_OUT_09_CC_DATA_CALL_RESPONSE: case CONTROL_OUT_09_CC_DATA_CALL_RESPONSE:
@@ -404,6 +441,7 @@ public class NXDNDecoderState extends DecoderState
mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp()); mTrafficChannelManager.processEndCall(getCurrentChannel(), layer3.getTimestamp());
mEncryptedCallStateDetermined = false; mEncryptedCallStateDetermined = false;
mEncryptedCall = false; mEncryptedCall = false;
mTalkerAliasTracker.callEnded();
getIdentifierCollection().remove(IdentifierClass.USER); getIdentifierCollection().remove(IdentifierClass.USER);
mDisconnectResponseCount++; mDisconnectResponseCount++;
@@ -424,6 +462,7 @@ public class NXDNDecoderState extends DecoderState
case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION: case CONTROL_OUT_27_BC_ADJACENT_SITE_INFORMATION:
case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION: case CONTROL_OUT_28_BC_FAILURE_STATUS_INFORMATION:
mNetworkConfigurationMonitor.process(layer3); mNetworkConfigurationMonitor.process(layer3);
mTalkerAliasTracker.callEnded();
getIdentifierCollection().remove(IdentifierClass.USER); getIdentifierCollection().remove(IdentifierClass.USER);
break; break;
case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION: case CONTROL_OUT_26_BC_CONTROL_CHANNEL_INFORMATION:
@@ -645,12 +684,7 @@ public class NXDNDecoderState extends DecoderState
{ {
state = State.CALL; state = State.CALL;
event = DecoderStateEvent.Event.CONTINUATION; event = DecoderStateEvent.Event.CONTINUATION;
var radio = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM); processTalkerAlias(tac.getTalkerAlias(), tac.getTimestamp());
if(radio instanceof RadioIdentifier ri)
{
mTrafficChannelManager.processTalkerAlias(getCurrentChannel(), tac.getTalkerAlias(), ri, tac.getTimestamp());
}
} }
break; break;

View File

@@ -26,10 +26,14 @@ import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.IChannelEventListener; import io.github.dsheirer.controller.channel.IChannelEventListener;
import io.github.dsheirer.controller.channel.IChannelEventProvider; import io.github.dsheirer.controller.channel.IChannelEventProvider;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; 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.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection; 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.TalkerAliasManager;
import io.github.dsheirer.identifier.alias.TalkerAliasTracker;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier; import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.event.DecodeEvent; 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 channel for the call
* @param talkerAlias to add or update * @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 * @param timestamp for the talker alias message
*/ */
public void processTalkerAlias(IChannelDescriptor channel, NXDNTalkerAliasIdentifier talkerAlias, public void processTalkerAlias(IChannelDescriptor channel, NXDNTalkerAliasIdentifier talkerAlias,
@@ -457,11 +463,20 @@ public class NXDNTrafficChannelManager extends TrafficChannelManager implements
if(tracker != null) if(tracker != null)
{ {
tracker.addIdentifierIfMissing(talkerAlias); Identifier eventRadio = tracker.getEvent().getIdentifierCollection()
broadcast(tracker); .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 finally
{ {

View File

@@ -19,20 +19,24 @@
package io.github.dsheirer.module.decode.p25; package io.github.dsheirer.module.decode.p25;
import io.github.dsheirer.channel.IChannelDescriptor; 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.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.Event;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.IChannelEventListener; import io.github.dsheirer.controller.channel.IChannelEventListener;
import io.github.dsheirer.controller.channel.IChannelEventProvider; import io.github.dsheirer.controller.channel.IChannelEventProvider;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest; 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.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection; 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.TalkerAliasManager;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier; import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroupPreLoadDataContent; 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.scramble.ScrambleParameterIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.log.LoggingSuppressor; 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. * Updates all current identifiers for an ongoing call event on the frequency and updates the event duration timestamp.
* *

View File

@@ -21,11 +21,11 @@ package io.github.dsheirer.module.decode.p25.phase1;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent; import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent;
import io.github.dsheirer.channel.state.DecoderState; 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.Event;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State; 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.ChannelType;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelEvent; import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.IChannelEventListener; import io.github.dsheirer.controller.channel.IChannelEventListener;
import io.github.dsheirer.identifier.Form; 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.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role; 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.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier; import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; 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. * algorithm ID takes precedence over the link control service options when it is known.
*/ */
private Boolean mCallEncryptedByAlgorithm = null; 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. * 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()) else if(iMessage instanceof MotorolaTalkerAliasComplete tac && tac.isValid())
{ {
mTrafficChannelManager.getTalkerAliasManager().update(tac.getRadio(), tac.getAlias()); //Motorola talker aliases identify their radio
mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), tac.getAlias(), tac.getTimestamp(), tac.toString()); processTalkerAlias(tac.getAlias(), tac.getRadio(), tac.getTimestamp(), tac.toString());
} }
else if(iMessage instanceof LCHarrisTalkerAliasComplete talkerAlias) else if(iMessage instanceof LCHarrisTalkerAliasComplete talkerAlias)
{ {
processTalkerAlias(talkerAlias); processTalkerAlias(talkerAlias.getTalkerAlias(), null, talkerAlias.getTimestamp(), talkerAlias.toString());
} }
else if(iMessage instanceof LinkControlWord lcw) 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. * Talker alias manager shared with the traffic channel manager
* @param talkerAlias reassembled.
*/ */
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). * Link Control (LC) Channel user (ie current user on this channel).
*/ */
private void processLCChannelUser(LinkControlWord lcw, long timestamp) private void processLCChannelUser(LinkControlWord lcw, long timestamp)
{ {
getIdentifierCollection().update(mPatchGroupManager.update(lcw.getIdentifiers(), timestamp)); updateChannelUserIdentifiers(lcw, timestamp, true);
DecodeEventType decodeEventType = getLCDecodeEventType(lcw); DecodeEventType decodeEventType = getLCDecodeEventType(lcw);
ServiceOptions serviceOptions = null; ServiceOptions serviceOptions = null;
@@ -443,7 +483,6 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
serviceOptions = VoiceServiceOptions.createUnencrypted(); serviceOptions = VoiceServiceOptions.createUnencrypted();
} }
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection());
MutableIdentifierCollection mic = getMutableIdentifierCollection(getIdentifierCollection().getIdentifiers(), timestamp); MutableIdentifierCollection mic = getMutableIdentifierCollection(getIdentifierCollection().getIdentifiers(), timestamp);
mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), getCurrentChannel(), decodeEventType, mTrafficChannelManager.processP1TrafficCurrentUser(getCurrentFrequency(), getCurrentChannel(), decodeEventType,
serviceOptions, mic, timestamp, null, lcw.toString()); serviceOptions, mic, timestamp, null, lcw.toString());
@@ -853,6 +892,8 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
Identifier<?> radio = getIdentifierCollection().getFromIdentifier(); Identifier<?> radio = getIdentifierCollection().getFromIdentifier();
mCallEncryptedByAlgorithm = headerData.isEncryptedAudio(); mCallEncryptedByAlgorithm = headerData.isEncryptedAudio();
//A call starts. The HDU doesn't identify the talker.
mTalkerAliasTracker.callActivity(message.getTimestamp());
mTrafficChannelManager.processP1TrafficCallStart(getCurrentFrequency(), talkgroup, radio, mTrafficChannelManager.processP1TrafficCallStart(getCurrentFrequency(), talkgroup, radio,
headerData.getEncryptionKey(), mCurrentServiceOptions, getCurrentChannel(), message.getTimestamp()); headerData.getEncryptionKey(), mCurrentServiceOptions, getCurrentChannel(), message.getTimestamp());
@@ -921,9 +962,22 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
{ {
mCallEncryptedByAlgorithm = null; mCallEncryptedByAlgorithm = null;
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDU:" + message); mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDU:" + message);
clearCallUser();
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); 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 * Process Terminator Data Unit with Link Control (TDULC) message and forwards valid Link Control Word message for
* additional processing. * additional processing.
@@ -942,6 +996,8 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDULC:" + message); mTrafficChannelManager.processP1TrafficCallEnd(getCurrentFrequency(), message.getTimestamp(), "TDULC:" + message);
broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE)); broadcast(new DecoderStateEvent(this, Event.DECODE, State.ACTIVE));
processLC(lcw, message.getTimestamp(), true); 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: case UNIT_TO_UNIT_VOICE_CHANNEL_USER_EXTENDED:
if(isTerminator) if(isTerminator)
{ {
getIdentifierCollection().update(mPatchGroupManager.update(lcw.getIdentifiers(), timestamp)); updateChannelUserIdentifiers(lcw, timestamp, false);
} }
else else
{ {
@@ -2159,6 +2215,7 @@ public class P25P1DecoderState extends DecoderState implements IChannelEventList
{ {
case REQUEST_RESET: case REQUEST_RESET:
resetState(); resetState();
mTalkerAliasTracker.reset();
mNetworkConfigurationMonitor.reset(); mNetworkConfigurationMonitor.reset();
break; break;
case NOTIFICATION_SOURCE_FREQUENCY: case NOTIFICATION_SOURCE_FREQUENCY:

View File

@@ -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.LinkControlWord;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.HarrisTalkerAliasAssembler; 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.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.LCHarrisTalkerGPSBlock1;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock2; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSBlock2;
import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerGPSComplete; 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) else if(message instanceof TDULCMessage tdulc)
{ {
boolean motorolaTalkerAliasComplete = false; boolean motorolaTalkerAliasComplete = false;
LCHarrisTalkerAliasComplete harrisTalkerAliasComplete = null;
boolean harrisTalkerAliasBlock = false;
LinkControlWord lcw = tdulc.getLinkControlWord(); LinkControlWord lcw = tdulc.getLinkControlWord();
if(lcw instanceof IExtendedSourceMessage esm) 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 //Motorola carries the talker alias in the TDULC
else if(mMotorolaTalkerAliasAssembler.add(lcw, message.getTimestamp())) else if(mMotorolaTalkerAliasAssembler.add(lcw, message.getTimestamp()))
{ {
motorolaTalkerAliasComplete = true; motorolaTalkerAliasComplete = true;
} }
//Harris carries the talker alias in the LDU1 link control messages, so reset the assembler on TDULC. //Reset the L3Harris assembler on other TDULC messages so that alias blocks from different calls
mHarrisTalkerAliasAssembler.reset(); //aren't combined. Keep the blocks while the TDULC messages carry the alias.
if(!harrisTalkerAliasBlock)
{
mHarrisTalkerAliasAssembler.reset();
}
dispatch(tdulc); 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) if(motorolaTalkerAliasComplete)
{ {
dispatch(mMotorolaTalkerAliasAssembler.assemble()); dispatch(mMotorolaTalkerAliasAssembler.assemble());
} }
if(harrisTalkerAliasComplete != null)
{
dispatch(harrisTalkerAliasComplete);
}
} }
else if(message instanceof HDUMessage hdu && mHeldTDULCMessage != null) else if(message instanceof HDUMessage hdu && mHeldTDULCMessage != null)
{ {

View File

@@ -103,9 +103,10 @@ public class HarrisTalkerAliasAssembler
{ {
if(mBlock1 != null && mBlock2 != null) if(mBlock1 != null && mBlock2 != null)
{ {
String fragment2 = mBlock2.getPayloadFragmentString(); //Untrimmed, so that a space at the start or end of a fragment is kept within the alias
String fragment3 = mBlock3 != null ? mBlock3.getPayloadFragmentString() : null; String fragment2 = mBlock2.getPayloadFragmentText();
String fragment4 = mBlock4 != null ? mBlock4.getPayloadFragmentString() : null; String fragment3 = mBlock3 != null ? mBlock3.getPayloadFragmentText() : null;
String fragment4 = mBlock4 != null ? mBlock4.getPayloadFragmentText() : null;
return new LCHarrisTalkerAliasComplete(mBlock1.getMessage(), fragment2, fragment3, fragment4, mTimestamp); return new LCHarrisTalkerAliasComplete(mBlock1.getMessage(), fragment2, fragment3, fragment4, mTimestamp);
} }

View File

@@ -61,6 +61,16 @@ public abstract class LCHarrisTalkerAliasBase extends LinkControlWord
return new String(getPayloadFragment().toByteArray()).trim(); 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 * List of identifiers contained in this message
*/ */

View File

@@ -93,7 +93,8 @@ public class LCHarrisTalkerAliasComplete extends LinkControlWord implements IMes
{ {
if(mTalkerAlias == null) 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)) 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; return mTalkerAlias;
@@ -134,6 +135,16 @@ public class LCHarrisTalkerAliasComplete extends LinkControlWord implements IMes
return new String(getPayloadFragment().toByteArray()).trim(); 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 @Override
public long getTimestamp() public long getTimestamp()
{ {

View File

@@ -22,19 +22,21 @@ import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.channel.IBandChannelDescriptor; import io.github.dsheirer.channel.IBandChannelDescriptor;
import io.github.dsheirer.channel.IChannelDescriptor; import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.state.ChangeChannelTimeoutEvent; 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.Event;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State; import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.channel.state.TimeslotDecoderState; 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.ChannelType;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass; import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierUpdateListener; import io.github.dsheirer.identifier.IdentifierUpdateListener;
import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role; 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.encryption.EncryptionKey;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroupManager; import io.github.dsheirer.identifier.patch.PatchGroupManager;
@@ -147,6 +149,9 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
private PatchGroupManager mPatchGroupManager; private PatchGroupManager mPatchGroupManager;
private P25P2NetworkConfigurationMonitor mNetworkConfigurationMonitor = new P25P2NetworkConfigurationMonitor(); private P25P2NetworkConfigurationMonitor mNetworkConfigurationMonitor = new P25P2NetworkConfigurationMonitor();
private P25TrafficChannelManager mTrafficChannelManager; 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; private int mEndPttOnFacchCounter = 0;
/** /**
@@ -190,6 +195,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
*/ */
protected void resetState() protected void resetState()
{ {
mTalkerAliasTracker.callEnded();
super.resetState(); super.resetState();
mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), System.currentTimeMillis(), "RESET STATE INVOKED"); mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), System.currentTimeMillis(), "RESET STATE INVOKED");
mEndPttOnFacchCounter = 0; mEndPttOnFacchCounter = 0;
@@ -239,6 +245,8 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
broadcast(new DecoderStateEvent(this, Event.CONTINUATION, State.CALL, getTimeslot())); 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 //If we're tracking the call event, update the duration on it
mTrafficChannelManager.processP2TrafficVoice(getCurrentFrequency(), getTimeslot(), message.getTimestamp()); 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()) 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: case MAC_6_HANGTIME:
//During hangtime, the from talker is no longer involved in the call ... remove it because sometimes //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. //we don't get the end PTT to signal the end.
mTalkerAliasTracker.callEnded();
getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().remove(Role.FROM);
break; break;
} }
@@ -763,7 +773,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp()));
} }
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection()); mTalkerAliasTracker.update();
continueState(State.ACTIVE); continueState(State.ACTIVE);
} }
@@ -1103,14 +1113,15 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp()));
} }
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection());
if(message.getMacPduType() == MacPduType.MAC_3_IDLE || message.getMacPduType() == MacPduType.MAC_6_HANGTIME) if(message.getMacPduType() == MacPduType.MAC_3_IDLE || message.getMacPduType() == MacPduType.MAC_6_HANGTIME)
{ {
mTalkerAliasTracker.update();
continueState(State.ACTIVE); continueState(State.ACTIVE);
} }
else else
{ {
mTalkerAliasTracker.callActivity(message.getTimestamp());
if(mac instanceof IServiceOptionsProvider sop) if(mac instanceof IServiceOptionsProvider sop)
{ {
IChannelDescriptor currentChannel = mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), IChannelDescriptor currentChannel = mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(),
@@ -1153,7 +1164,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp()));
} }
mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection()); mTalkerAliasTracker.callActivity(message.getTimestamp());
if(mac instanceof PushToTalk ptt) if(mac instanceof PushToTalk ptt)
{ {
@@ -1225,6 +1236,7 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
if(mac instanceof EndPushToTalk) if(mac instanceof EndPushToTalk)
{ {
//No matter what, remove the FROM identifier on end PTT. //No matter what, remove the FROM identifier on end PTT.
mTalkerAliasTracker.callEnded();
getIdentifierCollection().remove(Role.FROM); getIdentifierCollection().remove(Role.FROM);
//Only reset the identifiers if the call event is closed out, otherwise we might have a timing issue //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) if(mac instanceof MacRelease mr)
{ {
mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), message.getTimestamp(), "MAC RELEASE: " + mac.toString()); mTrafficChannelManager.processP2TrafficCallEnd(getCurrentFrequency(), getTimeslot(), message.getTimestamp(), "MAC RELEASE: " + mac.toString());
mTalkerAliasTracker.callEnded();
getIdentifierCollection().remove(IdentifierClass.USER); getIdentifierCollection().remove(IdentifierClass.USER);
broadcast(message, mac, DecodeEventType.COMMAND, broadcast(message, mac, DecodeEventType.COMMAND,
(mr.isForcedPreemption() ? "FORCED " : "") + "CALL PREEMPTION" + (mr.isForcedPreemption() ? "FORCED " : "") + "CALL PREEMPTION" +
@@ -1720,17 +1733,34 @@ public class P25P2DecoderState extends TimeslotDecoderState implements Identifie
{ {
if(mac instanceof L3HarrisTalkerAlias talkerAlias) if(mac instanceof L3HarrisTalkerAlias talkerAlias)
{ {
P25TalkerAliasIdentifier alias = talkerAlias.getAlias(); processTalkerAlias(talkerAlias.getAlias(), null, message.getTimestamp());
getIdentifierCollection().update(alias); }
mTrafficChannelManager.processP2TrafficCurrentUser(getCurrentFrequency(), getTimeslot(), alias, 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) /**
{ * Processes a talker alias: stores it for its radio and adds it to the call that it belongs to.
mTrafficChannelManager.getTalkerAliasManager().update(ri, alias); * @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: case REQUEST_RESET:
resetState(); resetState();
mTalkerAliasTracker.reset();
mNetworkConfigurationMonitor.reset(); mNetworkConfigurationMonitor.reset();
break; break;
case NOTIFICATION_SOURCE_FREQUENCY: case NOTIFICATION_SOURCE_FREQUENCY:

View File

@@ -75,19 +75,22 @@ public class L3HarrisTalkerAlias extends MacStructureVendor
{ {
if(mAliasIdentifier == null) 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; byte[] bytes = getMessage().getSubMessage(start, end).getBytes();
length += getOffset(); 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); mAliasIdentifier = P25TalkerAliasIdentifier.create(alias);
} }
} }

View File

@@ -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 @Test
public void lastVisibleColumnCantBeHidden() throws Exception public void lastVisibleColumnCantBeHidden() throws Exception
{ {

View File

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

View File

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

View File

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