diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.java index c85579303..40f1e2218 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadata.java @@ -22,6 +22,7 @@ import io.github.dsheirer.alias.Alias; import io.github.dsheirer.alias.AliasList; import io.github.dsheirer.alias.AliasModel; +import io.github.dsheirer.identifier.Form; import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierUpdateListener; import io.github.dsheirer.identifier.IdentifierUpdateNotification; @@ -57,6 +58,7 @@ public class ChannelMetadata implements Listener, private DecoderTypeConfigurationIdentifier mDecoderTypeConfigurationIdentifier; private Identifier mFromIdentifier; private List mFromIdentifierAliases; + private Identifier mTalkerAliasIdentifier; private Identifier mToIdentifier; private List mToIdentifierAliases; private Integer mTimeslot; @@ -238,6 +240,22 @@ public List getFromIdentifierAliases() return mFromIdentifierAliases; } + /** + * Optional talker alias identifier + */ + public Identifier getTalkerAliasIdentifier() + { + return mTalkerAliasIdentifier; + } + + /** + * Indicates if we have a non-null talker alias identifier + */ + public boolean hasTalkerAliasIdentifier() + { + return mTalkerAliasIdentifier != null; + } + /** * Current call event TO identifier */ @@ -409,15 +427,22 @@ public void receive(IdentifierUpdateNotification update) case USER: if(identifier.getRole() == Role.FROM) { - mFromIdentifier = update.isAdd() ? identifier : null; - - if(mAliasList != null && mFromIdentifier != null) + if(identifier.getForm() == Form.TALKER_ALIAS) { - mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier); + mTalkerAliasIdentifier = identifier; } else { - mFromIdentifierAliases = Collections.EMPTY_LIST; + mFromIdentifier = update.isAdd() ? identifier : null; + + if(mAliasList != null && mFromIdentifier != null) + { + mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier); + } + else + { + mFromIdentifierAliases = Collections.EMPTY_LIST; + } } broadcastUpdate(ChannelMetadataField.USER_FROM); diff --git a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java index e89b4042a..325d0e7d3 100644 --- a/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java +++ b/src/main/java/io/github/dsheirer/channel/metadata/ChannelMetadataPanel.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2020 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +38,14 @@ import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor; import io.github.dsheirer.sample.Broadcaster; import io.github.dsheirer.sample.Listener; +import java.awt.Color; +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.text.DecimalFormat; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; import net.miginfocom.swing.MigLayout; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,14 +60,6 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; -import java.awt.Color; -import java.awt.Component; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.text.DecimalFormat; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; public class ChannelMetadataPanel extends JPanel implements ListSelectionListener { @@ -294,6 +294,10 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole { text = EMPTY_VALUE; } + else if(hasAdditionalIdentifier(channelMetadata)) + { + text = text + " " + getAdditionalIdentifier(channelMetadata); + } label.setText(text); } @@ -306,6 +310,8 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole } public abstract Identifier getIdentifier(ChannelMetadata channelMetadata); + public abstract boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata); + public abstract Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata); } /** @@ -323,6 +329,18 @@ public Identifier getIdentifier(ChannelMetadata channelMetadata) { return channelMetadata.getFromIdentifier(); } + + @Override + public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata) + { + return channelMetadata.getTalkerAliasIdentifier(); + } + + @Override + public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata) + { + return channelMetadata.hasTalkerAliasIdentifier(); + } } /** @@ -340,6 +358,11 @@ public Identifier getIdentifier(ChannelMetadata channelMetadata) { return channelMetadata.getToIdentifier(); } + + @Override + public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata) {return null;} + @Override + public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata) {return false;} } public class ColoredStateCellRenderer extends DefaultTableCellRenderer diff --git a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java index 8c246ec69..bfc49965a 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/DmrViewer.java @@ -1,6 +1,6 @@ /* * ***************************************************************************** - * Copyright (C) 2014-2023 Dennis Sheirer + * Copyright (C) 2014-2024 Dennis Sheirer * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -46,6 +46,7 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.SelectionMode; import javafx.scene.control.TableColumn; import javafx.scene.control.TablePosition; import javafx.scene.control.TableView; @@ -284,6 +285,7 @@ private TableView getMessageTableView() if(mMessageTableView == null) { mMessageTableView = new TableView<>(); + mMessageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); mMessageTableView.setPlaceholder(getLoadingIndicator()); SortedList sortedList = new SortedList<>(mFilteredMessages); sortedList.comparatorProperty().bind(mMessageTableView.comparatorProperty()); diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java index 7937e54e9..df0821141 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P1Viewer.java @@ -62,6 +62,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.SelectionMode; import javafx.scene.control.TableColumn; import javafx.scene.control.TablePosition; import javafx.scene.control.TableView; @@ -380,6 +381,7 @@ private TableView getMessagePackageTableView() if(mMessagePackageTableView == null) { mMessagePackageTableView = new TableView<>(); + mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); mMessagePackageTableView.setPlaceholder(getLoadingIndicator()); SortedList sortedList = new SortedList<>(mFilteredMessagePackages); sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty()); diff --git a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java index 94723b6b8..b5a1d3405 100644 --- a/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java +++ b/src/main/java/io/github/dsheirer/gui/viewer/P25P2Viewer.java @@ -63,6 +63,7 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.SelectionMode; import javafx.scene.control.TableColumn; import javafx.scene.control.TablePosition; import javafx.scene.control.TableView; @@ -459,6 +460,7 @@ private TableView getMessagePackageTableView() if(mMessagePackageTableView == null) { mMessagePackageTableView = new TableView<>(); + mMessagePackageTableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); mMessagePackageTableView.setPlaceholder(getLoadingIndicator()); SortedList sortedList = new SortedList<>(mFilteredMessagePackages); sortedList.comparatorProperty().bind(mMessagePackageTableView.comparatorProperty()); diff --git a/src/main/java/io/github/dsheirer/identifier/alias/TalkerAliasManager.java b/src/main/java/io/github/dsheirer/identifier/alias/TalkerAliasManager.java new file mode 100644 index 000000000..5c7b12860 --- /dev/null +++ b/src/main/java/io/github/dsheirer/identifier/alias/TalkerAliasManager.java @@ -0,0 +1,145 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.identifier.alias; + +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.IdentifierCollection; +import io.github.dsheirer.identifier.MutableIdentifierCollection; +import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.radio.RadioIdentifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Talker alias cache manager. Collects observed talker aliases and inserts them into an identifier collection when + * the corresponding radio is active in a FROM role. + * + * This implementation is thread safe and is intended to be used across control and traffic channels. + */ +public class TalkerAliasManager +{ + private Map mAliasMap = new ConcurrentHashMap<>(); + + /** + * Constructs an instance + */ + public TalkerAliasManager() + { + } + + /** + * Updates the alias for the + * @param identifier + * @param alias + */ + public void update(RadioIdentifier identifier, TalkerAliasIdentifier alias) + { + if(identifier.getRole() == Role.FROM) + { + mAliasMap.put(identifier.getValue(), alias); + } + } + + /** + * Indicates if an alias exists for the identifier + * @param radioIdentifier to test + * @return true if an alias exists. + */ + public boolean hasAlias(RadioIdentifier radioIdentifier) + { + return mAliasMap.containsKey(radioIdentifier.getValue()); + } + + /** + * Enriches the immutable identifier collection by detecting a radio identifier with the FROM role, lookup a + * matching alias, and insert the alias into a new mutable identifier collection. + * @param originalIC to enrich + * @return enriched identifier collection or the original identifier collection if we don't have an alias. + */ + public synchronized IdentifierCollection enrich(IdentifierCollection originalIC) + { + Identifier fromRadio = originalIC.getFromIdentifier(); + + if(fromRadio instanceof RadioIdentifier rid) + { + TalkerAliasIdentifier alias = mAliasMap.get(rid.getValue()); + + if(alias != null) + { + MutableIdentifierCollection enrichedIC = new MutableIdentifierCollection(originalIC.getIdentifiers()); + enrichedIC.update(alias); + return enrichedIC; + } + } + + return originalIC; + } + + /** + * Enriches the mutable identifier collection by detecting a radio identifier with the FROM role, lookup a + * matching alias, and insert the alias into the mutable identifier collection argument. + * @param mic to enrich + */ + public synchronized void enrichMutable(MutableIdentifierCollection mic) + { + Identifier fromRadio = mic.getFromIdentifier(); + + if(fromRadio instanceof RadioIdentifier rid) + { + TalkerAliasIdentifier alias = mAliasMap.get(rid.getValue()); + + if(alias != null) + { + mic.update(alias); + } + } + } + + /** + * Creates a summary listing of talker aliases + * @return summary. + */ + public synchronized String getAliasSummary() + { + StringBuilder sb = new StringBuilder(); + sb.append("Active System Radio Aliases\n"); + List radios = new ArrayList<>(mAliasMap.keySet()); + + if(radios.size() > 0) + { + Collections.sort(radios); + for(Integer radio : radios) + { + sb.append(" ").append(radio); + sb.append("\t").append(mAliasMap.get(radio).getValue()); + sb.append("\n"); + } + } + else + { + sb.append(" None\n"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/identifier/patch/PatchGroupManager.java b/src/main/java/io/github/dsheirer/identifier/patch/PatchGroupManager.java index 959f30c88..6dcd8bf12 100644 --- a/src/main/java/io/github/dsheirer/identifier/patch/PatchGroupManager.java +++ b/src/main/java/io/github/dsheirer/identifier/patch/PatchGroupManager.java @@ -63,13 +63,13 @@ public String getPatchGroupSummary() if(trackers.isEmpty()) { - sb.append("None"); + sb.append(" None\n"); } else { for(PatchGroupTracker tracker : trackers) { - sb.append(" ").append(tracker.mPatchGroupIdentifier).append("\n"); + sb.append(" ").append(tracker.mPatchGroupIdentifier).append("\n"); } } diff --git a/src/main/java/io/github/dsheirer/message/AbstractMessage.java b/src/main/java/io/github/dsheirer/message/AbstractMessage.java index 5689b4b57..1d6ab58c3 100644 --- a/src/main/java/io/github/dsheirer/message/AbstractMessage.java +++ b/src/main/java/io/github/dsheirer/message/AbstractMessage.java @@ -29,6 +29,20 @@ */ public abstract class AbstractMessage { + public static final int OCTET_0_BIT_0 = 0; + public static final int OCTET_1_BIT_8 = 8; + public static final int OCTET_2_BIT_16 = 16; + public static final int OCTET_3_BIT_24 = 24; + public static final int OCTET_4_BIT_32 = 32; + public static final int OCTET_5_BIT_40 = 40; + public static final int OCTET_6_BIT_48 = 48; + public static final int OCTET_7_BIT_56 = 56; + public static final int OCTET_8_BIT_64 = 64; + public static final int OCTET_9_BIT_72 = 72; + public static final int OCTET_10_BIT_80 = 80; + public static final int OCTET_11_BIT_88 = 88; + public static final int OCTET_12_BIT_96 = 96; + private CorrectedBinaryMessage mMessage; private int mOffset = 0; @@ -130,6 +144,17 @@ public int getInt(IntField field) } } + /** + * Access the integer value for the specified field at the specified offset. + * @param field description + * @param offset to the start of the field. + * @return integer value. + */ + public int getInt(IntField field, int offset) + { + return getMessage().getInt(field, offset); + } + /** * Indicates if the field has a non-zero value, meaning that any of the bits for the field are set to a one. * @param field to inspect. diff --git a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java index cb766ba1f..861094c50 100644 --- a/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java @@ -188,7 +188,7 @@ public static List getPrimaryModules(ChannelMapModel channelMapModel, Ch processPassport(channel, modules, aliasList, decodeConfig); break; case P25_PHASE1: - processP25Phase1(channel, userPreferences, modules, aliasList, channelType, (DecodeConfigP25Phase1) decodeConfig); + processP25Phase1(channel, userPreferences, modules, aliasList, trafficChannelManager); break; case P25_PHASE2: processP25Phase2(channel, userPreferences, modules, aliasList, trafficChannelManager); @@ -262,32 +262,37 @@ else if(trafficChannelManager instanceof P25TrafficChannelManager p25) * @param modules collection to add to * @param aliasList for the channel */ - private static void processP25Phase1(Channel channel, UserPreferences userPreferences, List modules, AliasList aliasList, ChannelType channelType, DecodeConfigP25Phase1 decodeConfig) + private static void processP25Phase1(Channel channel, UserPreferences userPreferences, List modules, + AliasList aliasList, TrafficChannelManager trafficChannelManager) { - DecodeConfigP25Phase1 p25Config = decodeConfig; - - switch(p25Config.getModulation()) + if(channel.getDecodeConfiguration() instanceof DecodeConfigP25Phase1 p1) { - case C4FM: - modules.add(new P25P1DecoderC4FM()); - break; - case CQPSK: - modules.add(new P25P1DecoderLSM()); - break; - default: - throw new IllegalArgumentException("Unrecognized P25 Phase 1 Modulation [" + - p25Config.getModulation() + "]"); + switch(p1.getModulation()) + { + case C4FM: + modules.add(new P25P1DecoderC4FM()); + break; + case CQPSK: + modules.add(new P25P1DecoderLSM()); + break; + default: + throw new IllegalArgumentException("Unrecognized P25 Phase 1 Modulation [" + p1.getModulation() + "]"); + } } - if(channelType == ChannelType.STANDARD) + if(channel.getChannelType() == ChannelType.STANDARD) { - P25TrafficChannelManager trafficChannelManager = new P25TrafficChannelManager(channel); - modules.add(trafficChannelManager); - modules.add(new P25P1DecoderState(channel, trafficChannelManager)); + P25TrafficChannelManager primaryTCM = new P25TrafficChannelManager(channel); + modules.add(primaryTCM); + modules.add(new P25P1DecoderState(channel, primaryTCM)); + } + else if(trafficChannelManager instanceof P25TrafficChannelManager parentTCM) + { + modules.add(new P25P1DecoderState(channel, parentTCM)); } else { - modules.add(new P25P1DecoderState(channel)); + mLog.warn("Expected non-null traffic channel manager for channel " + channel.getName()); } modules.add(new P25P1AudioModule(userPreferences, aliasList)); diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java index 1d019c8d2..48e6372ca 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRDecoderState.java @@ -1457,12 +1457,20 @@ private void closeCurrentCallEvent(long timestamp) @Override public String getActivitySummary() { + StringBuilder sb = new StringBuilder(); + if(mNetworkConfigurationMonitor != null) { - return mNetworkConfigurationMonitor.getActivitySummary(); + sb.append(mNetworkConfigurationMonitor.getActivitySummary()); + sb.append("\n\n"); + sb.append(mTrafficChannelManager.getTalkerAliasManager().getAliasSummary()); + } + else + { + sb.append(mTrafficChannelManager.getTalkerAliasManager().getAliasSummary()); } - return ""; + return sb.toString(); } @Override diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java index 7da486918..f463cf028 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRTrafficChannelManager.java @@ -31,6 +31,7 @@ import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.alias.TalkerAliasManager; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.message.MessageHistoryPreloadData; import io.github.dsheirer.message.MessageHistoryRequest; @@ -103,6 +104,7 @@ public class DMRTrafficChannelManager extends TrafficChannelManager implements I private Listener mChannelEventListener; private Listener mDecodeEventListener; private TrafficChannelTeardownMonitor mTrafficChannelTeardownMonitor = new TrafficChannelTeardownMonitor(); + private TalkerAliasManager mTalkerAliasManager = new TalkerAliasManager(); private Channel mParentChannel; private boolean mIgnoreDataCalls; @@ -129,6 +131,14 @@ public DMRTrafficChannelManager(Channel parentChannel) createTrafficChannels(); } + /** + * Talker alias manager + */ + public TalkerAliasManager getTalkerAliasManager() + { + return mTalkerAliasManager; + } + /** * Sets the current parent control channel frequency so that channel grants for the current frequency do not * produce an additional traffic channel allocation. diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasHeader.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasHeader.java index 39952c817..32eda8183 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasHeader.java @@ -70,9 +70,6 @@ public int getCharacterLength() */ public int getTotalBitLength() { - TalkerAliasDataFormat format = getFormat(); - int bitsPerCharacter = format.getBitsPerCharacter(); - int characterLength = getCharacterLength(); int total = getFormat().getBitsPerCharacter() * getCharacterLength(); //Max payload bit length is 217 (49 + 56 + 56 + 56) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java index 6fd999baa..24d3f0038 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/P25TrafficChannelManager.java @@ -30,6 +30,7 @@ import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.IdentifierCollection; import io.github.dsheirer.identifier.MutableIdentifierCollection; +import io.github.dsheirer.identifier.alias.TalkerAliasManager; import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier; import io.github.dsheirer.identifier.patch.PatchGroupPreLoadDataContent; import io.github.dsheirer.identifier.scramble.ScrambleParameterIdentifier; @@ -118,6 +119,7 @@ public class P25TrafficChannelManager extends TrafficChannelManager implements I //Used only for data calls private DecodeEventDuplicateDetector mDuplicateDetector = new DecodeEventDuplicateDetector(); private ReentrantLock mLock = new ReentrantLock(); + private TalkerAliasManager mTalkerAliasManager = new TalkerAliasManager(); /** * Constructs an instance. @@ -141,6 +143,15 @@ else if(parentChannel.getDecodeConfiguration() instanceof DecodeConfigP25Phase2 } } + /** + * Talker alias manager + * @return manager + */ + public TalkerAliasManager getTalkerAliasManager() + { + return mTalkerAliasManager; + } + /** * Stores the frequency band (aka Identifier Update) to use for preload data in starting a new traffic channel. * @param frequencyBand to store diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25AnnouncementTalkgroup.java b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25AnnouncementTalkgroup.java index f18864985..573467b77 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25AnnouncementTalkgroup.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25AnnouncementTalkgroup.java @@ -1,28 +1,24 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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 + * **************************************************************************** */ package io.github.dsheirer.module.decode.p25.identifier.talkgroup; import io.github.dsheirer.identifier.Role; -import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; /** * APCO25 Announcement Talkgroup - grouping of talkgroups within a system. @@ -42,7 +38,7 @@ public APCO25AnnouncementTalkgroup(Integer value) /** * Creates an APCO-25 announcement group talkgroup identifier */ - public static TalkgroupIdentifier create(int talkgroup) + public static APCO25Talkgroup create(int talkgroup) { return new APCO25AnnouncementTalkgroup(talkgroup); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25Talkgroup.java b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25Talkgroup.java index 058bba5f7..fe43f5a13 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25Talkgroup.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/identifier/talkgroup/APCO25Talkgroup.java @@ -1,23 +1,20 @@ /* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer * - * * ****************************************************************************** - * * Copyright (C) 2014-2019 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 - * * ***************************************************************************** + * 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 + * **************************************************************************** */ package io.github.dsheirer.module.decode.p25.identifier.talkgroup; @@ -47,7 +44,7 @@ public Protocol getProtocol() /** * Creates an APCO-25 TO talkgroup identifier */ - public static TalkgroupIdentifier create(int talkgroup) + public static APCO25Talkgroup create(int talkgroup) { return new APCO25Talkgroup(talkgroup); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java index c20c61202..9a792a582 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderState.java @@ -38,6 +38,7 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; import io.github.dsheirer.identifier.patch.PatchGroupManager; import io.github.dsheirer.identifier.patch.PatchGroupPreLoadDataContent; +import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.log.LoggingSuppressor; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.DecoderType; @@ -62,10 +63,12 @@ import io.github.dsheirer.module.decode.p25.phase1.message.hdu.HeaderData; import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisReturnToControlChannel; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisTalkerAliasComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaEmergencyAlarmActivation; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.MotorolaTalkerAliasComplete; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCCallTermination; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommand; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCExtendedFunctionCommandExtended; @@ -181,8 +184,8 @@ */ public class P25P1DecoderState extends DecoderState implements IChannelEventListener { - private static final Logger mLog = LoggerFactory.getLogger(P25P1DecoderState.class); - private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog); + private static final Logger LOGGER = LoggerFactory.getLogger(P25P1DecoderState.class); + private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOGGER); private final Channel mChannel; private final P25P1Decoder.Modulation mModulation; private final PatchGroupManager mPatchGroupManager = new PatchGroupManager(); @@ -315,6 +318,30 @@ public void receive(IMessage iMessage) break; } } + else if(iMessage instanceof MotorolaTalkerAliasComplete tac) + { + mTrafficChannelManager.getTalkerAliasManager().update(tac.getRadio(), tac.getAlias()); + } + else if(iMessage instanceof LCHarrisTalkerAliasComplete talkerAlias) + { + processTalkerAlias(talkerAlias); + } + } + + /** + * Process a fully reassembled L3Harris talker alias on the current traffic channel. + * @param talkerAlias reassembled. + */ + private void processTalkerAlias(LCHarrisTalkerAliasComplete talkerAlias) + { + Identifier identifier = getIdentifierCollection().getFromIdentifier(); + + if(identifier instanceof RadioIdentifier radioIdentifier) + { + mTrafficChannelManager.getTalkerAliasManager().update(radioIdentifier, talkerAlias.getTalkerAlias()); + } + + mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), talkerAlias.getTalkerAlias(), talkerAlias.getTimestamp()); } /** @@ -332,6 +359,7 @@ private void processChannelGrant(APCO25Channel apco25Channel, ServiceOptions ser if(apco25Channel.getValue().getDownlinkFrequency() > 0) { MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers, timestamp); + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); mTrafficChannelManager.processP1ChannelGrant(apco25Channel, serviceOptions, mic, opcode, timestamp); } } @@ -348,6 +376,7 @@ private void processChannelUpdate(APCO25Channel channel, ServiceOptions serviceO Opcode opcode, long timestamp) { MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers, timestamp); + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); mTrafficChannelManager.processP1ChannelUpdate(channel, serviceOptions, mic, opcode, timestamp); } @@ -399,6 +428,7 @@ else if(lcw.isEncrypted()) serviceOptions = VoiceServiceOptions.createUnencrypted(); } + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection()); mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), getCurrentChannel(), decodeEventType, serviceOptions, getIdentifierCollection(), timestamp, null ); @@ -445,6 +475,8 @@ private void broadcastEvent(List identifiers, long timestamp, Decode { MutableIdentifierCollection mic = getMutableIdentifierCollection(identifiers, timestamp); + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); + broadcast(P25DecodeEvent.builder(decodeEventType, timestamp) .channel(getCurrentChannel()) .details(details) @@ -675,7 +707,7 @@ private void processAMBTC(P25P1Message message) //don't allow that to corrupt the real frequency bands for this system. break; default: -// mLog.debug("Unrecognized AMBTC Opcode: " + ambtc.getHeader().getOpcode().name()); +// LOGGER.debug("Unrecognized AMBTC Opcode: " + ambtc.getHeader().getOpcode().name()); break; } } @@ -802,6 +834,7 @@ private void processHDU(IMessage message) MutableIdentifierCollection mic = getMutableIdentifierCollection(hdu.getIdentifiers(), message.getTimestamp()); String details = headerData.isEncryptedAudio() ? headerData.getEncryptionKey().toString() : null; DecodeEventType type = headerData.isEncryptedAudio() ? DecodeEventType.CALL_ENCRYPTED : DecodeEventType.CALL; + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); mTrafficChannelManager.processP1CurrentUser(getCurrentFrequency(), getCurrentChannel(), type, serviceOptions, mic, message.getTimestamp(), details); @@ -1960,6 +1993,8 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator case ADJACENT_SITE_STATUS_BROADCAST: case ADJACENT_SITE_STATUS_BROADCAST_EXPLICIT: + case CHANNEL_IDENTIFIER_UPDATE: + case CHANNEL_IDENTIFIER_UPDATE_VU: case PROTECTION_PARAMETER_BROADCAST: case SECONDARY_CONTROL_CHANNEL_BROADCAST: case SECONDARY_CONTROL_CHANNEL_BROADCAST_EXPLICIT: @@ -1991,8 +2026,8 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator null, timestamp); } break; - case MOTOROLA_RADIO_REPROGRAM_HEADER: - case MOTOROLA_RADIO_REPROGRAM_RECORD: + case MOTOROLA_TALKER_ALIAS_HEADER: + case MOTOROLA_TALKER_ALIAS_DATA_BLOCK: if(isTerminator) { closeCurrentCallEvent(timestamp); @@ -2161,10 +2196,14 @@ private void processLC(LinkControlWord lcw, long timestamp, boolean isTerminator { closeCurrentCallEvent(timestamp); } -// LOGGING_SUPPRESSOR.info(lcw.getVendor().toString() + lcw.getOpcodeNumber() + lcw.getMessage().toHexString(), -// 1, "Unrecognized LCW Opcode: " + lcw.getOpcode().name() + " VENDOR:" + lcw.getVendor() + -// " OPCODE:" + lcw.getOpcodeNumber() + " MSG:" + lcw.getMessage().toHexString() + -// " CHAN:" + getCurrentChannel() + " FREQ:" + getCurrentFrequency()); + +// if(lcw.getVendor().isLoggable()) +// { +// LOGGING_SUPPRESSOR.info(lcw.getVendor().toString() + lcw.getOpcodeNumber() + lcw.getMessage().toHexString(), +// 1, "Unrecognized LCW Opcode: " + lcw.getOpcode().name() + " VENDOR:" + lcw.getVendor() + +// " OPCODE:" + lcw.getOpcodeNumber() + " MSG:" + lcw.getMessage().toHexString() + +// " CHAN:" + getCurrentChannel() + " FREQ:" + getCurrentFrequency()); +// } break; } } @@ -2186,6 +2225,8 @@ public String getActivitySummary() sb.append(mNetworkConfigurationMonitor.getActivitySummary()); sb.append("\n"); sb.append(mPatchGroupManager.getPatchGroupSummary()); + sb.append("\n"); + sb.append(mTrafficChannelManager.getTalkerAliasManager().getAliasSummary()); return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java index 52061f57b..814bf2910 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageProcessor.java @@ -25,6 +25,8 @@ import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBandReceiver; import io.github.dsheirer.module.decode.p25.phase1.message.lc.ExtendedSourceLinkControlWord; 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.motorola.LCMotorolaTalkerAliasAssembler; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCSourceIDExtension; import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDU1Message; import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage; @@ -38,7 +40,7 @@ /** * APCO25 Phase 1 Message Processor. * - * Performs post-message creation processing before the message is sent downstream. + * Performs post-message creation processing and enrichment before the message is sent downstream. */ public class P25P1MessageProcessor implements Listener { @@ -60,6 +62,19 @@ public class P25P1MessageProcessor implements Listener */ private ExtendedSourceLinkControlWord mExtendedSourceLinkControlWord; + /** + * Motorola talker alias assembler for link control header and data blocks. + */ + private LCMotorolaTalkerAliasAssembler mMotorolaTalkerAliasAssembler = new LCMotorolaTalkerAliasAssembler(); + + /** + * Harris talker alias assembler for link control talker alias blocks + */ + private HarrisTalkerAliasAssembler mHarrisTalkerAliasAssembler = new HarrisTalkerAliasAssembler(); + + /** + * Constructs an instance + */ public P25P1MessageProcessor() { } @@ -79,19 +94,40 @@ public void preload(P25FrequencyBandPreloadDataContent content) } } + /** + * Processes the message for enrichment or reassembly of fragments and sends the enriched message and any additional + * messages that were created during the enrichment to the registered message listener. + * @param message to process + */ @Override public void receive(IMessage message) { + //Optional message created during processing that should be sent after the current argument is sent. + IMessage additionalMessageToSend = null; + if(message.isValid()) { //Reassemble extended source link control messages. if(message instanceof LDU1Message ldu) { reassembleLC(ldu.getLinkControlWord()); + + //Send the LCW to the harris talker alias assembler + additionalMessageToSend = mHarrisTalkerAliasAssembler.process(ldu.getLinkControlWord(), ldu.getTimestamp()); } else if(message instanceof TDULinkControlMessage tdu) { - reassembleLC(tdu.getLinkControlWord()); + LinkControlWord lcw = tdu.getLinkControlWord(); + reassembleLC(lcw); + + //Motorola carries the talker alias in the TDULC + if(mMotorolaTalkerAliasAssembler.add(lcw, message.getTimestamp())) + { + additionalMessageToSend = mMotorolaTalkerAliasAssembler.assemble(); + } + + //Harris carries the talker alias in the LDU voice messages, so reset the assembler on TDULC. + mHarrisTalkerAliasAssembler.reset(); } //Insert frequency band identifier update messages into channel-type messages */ @@ -119,13 +155,25 @@ else if(message instanceof TDULinkControlMessage tdu) if(message instanceof IFrequencyBand) { IFrequencyBand bandIdentifier = (IFrequencyBand)message; - mFrequencyBandMap.put(bandIdentifier.getIdentifier(), bandIdentifier); + + //Only store the frequency band if it's new so we don't hold on to more than one instance of the + //frequency band message. Otherwise, we'll hold on to several instances of each message as they get + //injected into other messages with channel information. + if(!mFrequencyBandMap.containsKey(bandIdentifier.getIdentifier())) + { + mFrequencyBandMap.put(bandIdentifier.getIdentifier(), bandIdentifier); + } } } if(mMessageListener != null) { mMessageListener.receive(message); + + if(additionalMessageToSend != null) + { + mMessageListener.receive(additionalMessageToSend); + } } } @@ -153,17 +201,27 @@ else if(linkControlWord instanceof LCSourceIDExtension sie && mExtendedSourceLin } } + /** + * Prepares for disposal of this instance. + */ public void dispose() { mFrequencyBandMap.clear(); mMessageListener = null; } + /** + * Sets the message listener to receive the output from this processor. + * @param listener to receive output messages + */ public void setMessageListener(Listener listener) { mMessageListener = listener; } + /** + * Clears a registered message listener. + */ public void removeMessageListener() { mMessageListener = null; diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25P1Message.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25P1Message.java index 8a2e91320..c4c137659 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25P1Message.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/P25P1Message.java @@ -31,21 +31,6 @@ */ public abstract class P25P1Message extends TimeslotMessage implements IMessage { - //P25 Phase 1 ICD defines octets starting at zero. - public static final int OCTET_0_BIT_0 = 0; - public static final int OCTET_1_BIT_8 = 8; - public static final int OCTET_2_BIT_16 = 16; - public static final int OCTET_3_BIT_24 = 24; - public static final int OCTET_4_BIT_32 = 32; - public static final int OCTET_5_BIT_40 = 40; - public static final int OCTET_6_BIT_48 = 48; - public static final int OCTET_7_BIT_56 = 56; - public static final int OCTET_8_BIT_64 = 64; - public static final int OCTET_9_BIT_72 = 72; - public static final int OCTET_10_BIT_80 = 80; - public static final int OCTET_11_BIT_88 = 88; - public static final int OCTET_12_BIT_96 = 96; - private Identifier mNAC; /** diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.java index ec629aacb..fbe2ca28a 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlOpcode.java @@ -21,6 +21,7 @@ import io.github.dsheirer.module.decode.p25.reference.Vendor; import java.util.EnumSet; +import java.util.logging.Logger; /** * Opcodes used for Link Control Words. @@ -99,19 +100,27 @@ public enum LinkControlOpcode MOTOROLA_UNIT_GPS("MOTOROLA UNIT GPS", 6), MOTOROLA_EMERGENCY_ALARM_ACTIVATION("MOTOROLA EMERGENCY ALARM ACTIVATION", 10), MOTOROLA_TALK_COMPLETE("MOTOROLA TALK_COMPLETE", 15), - MOTOROLA_RADIO_REPROGRAM_HEADER("MOTOROLA RADIO REPROGRAM HEADER", 21), - MOTOROLA_RADIO_REPROGRAM_RECORD("MOTOROLA RADIO REPROGRAM CONTINUATION", 23), + MOTOROLA_TALKER_ALIAS_HEADER("MOTOROLA TALKER ALIAS HEADER", 21), + MOTOROLA_TALKER_ALIAS_DATA_BLOCK("MOTOROLA TALKER ALIAS DATA BLOCK", 23), MOTOROLA_UNKNOWN("MOTOROLA UNKNOWN", -1), L3HARRIS_RETURN_TO_CONTROL_CHANNEL("UNKNOWN OPCODE 10", 10), L3HARRIS_UNKNOWN_2A("UNKNOWN OPCODE 42", 42), L3HARRIS_UNKNOWN_2B("UNKNOWN OPCODE 43", 43), + L3HARRIS_TALKER_ALIAS_BLOCK_1("TALKER ALIAS BLOCK 1", 50), + L3HARRIS_TALKER_ALIAS_BLOCK_2("TALKER ALIAS BLOCK 2", 51), + L3HARRIS_TALKER_ALIAS_BLOCK_3("TALKER ALIAS BLOCK 3", 52), + L3HARRIS_TALKER_ALIAS_BLOCK_4("TALKER ALIAS BLOCK 4", 53), L3HARRIS_UNKNOWN("L3HARRIS UNKNOWN", -1), + //This is used for reassembled talker aliases and is not an actual opcode + TALKER_ALIAS_COMPLETE("TALKER ALIAS COMPLETE", -1), + UNKNOWN("UNKNOWN", -1); private String mLabel; private int mCode; + private static Logger LOGGER = Logger.getLogger(LinkControlOpcode.class.getName()); /** * Constructor @@ -142,7 +151,8 @@ public enum LinkControlOpcode * L3Harris Opcodes */ public static final EnumSet L3HARRIS_OPCODES = EnumSet.of(L3HARRIS_RETURN_TO_CONTROL_CHANNEL, - L3HARRIS_UNKNOWN_2A, L3HARRIS_UNKNOWN_2B, L3HARRIS_UNKNOWN); + L3HARRIS_UNKNOWN_2A, L3HARRIS_UNKNOWN_2B, L3HARRIS_TALKER_ALIAS_BLOCK_1, L3HARRIS_TALKER_ALIAS_BLOCK_2, + L3HARRIS_TALKER_ALIAS_BLOCK_3, L3HARRIS_TALKER_ALIAS_BLOCK_4, L3HARRIS_UNKNOWN); /** * Network/channel related opcodes @@ -213,6 +223,14 @@ public static LinkControlOpcode fromValue(int value, Vendor vendor) return L3HARRIS_UNKNOWN_2A; case 43: return L3HARRIS_UNKNOWN_2B; + case 50: + return L3HARRIS_TALKER_ALIAS_BLOCK_1; + case 51: + return L3HARRIS_TALKER_ALIAS_BLOCK_2; + case 52: + return L3HARRIS_TALKER_ALIAS_BLOCK_3; + case 53: + return L3HARRIS_TALKER_ALIAS_BLOCK_4; default: return L3HARRIS_UNKNOWN; } @@ -234,9 +252,9 @@ public static LinkControlOpcode fromValue(int value, Vendor vendor) case 15: return MOTOROLA_TALK_COMPLETE; case 21: - return MOTOROLA_RADIO_REPROGRAM_HEADER; + return MOTOROLA_TALKER_ALIAS_HEADER; case 23: - return MOTOROLA_RADIO_REPROGRAM_RECORD; + return MOTOROLA_TALKER_ALIAS_DATA_BLOCK; default: return MOTOROLA_UNKNOWN; } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.java index 1ac522a39..357a9af68 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/LinkControlWordFactory.java @@ -21,6 +21,10 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisReturnToControlChannel; +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.LCHarrisUnknownOpcode42; import io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris.LCHarrisUnknownOpcode43; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaEmergencyAlarmActivation; @@ -28,9 +32,9 @@ import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupAdd; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUpdate; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaGroupRegroupVoiceChannelUser; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaRadioReprogramHeader; -import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaRadioReprogramRecord; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkComplete; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkerAliasDataBlock; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaTalkerAliasHeader; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnitGPS; import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.LCMotorolaUnknownOpcode; import io.github.dsheirer.module.decode.p25.phase1.message.lc.standard.LCAdjacentSiteStatusBroadcast; @@ -158,6 +162,14 @@ public static LinkControlWord create(CorrectedBinaryMessage message) return new LCHarrisUnknownOpcode42(message); case L3HARRIS_UNKNOWN_2B: return new LCHarrisUnknownOpcode43(message); + case L3HARRIS_TALKER_ALIAS_BLOCK_1: + return new LCHarrisTalkerAliasBlock1(message); + case L3HARRIS_TALKER_ALIAS_BLOCK_2: + return new LCHarrisTalkerAliasBlock2(message); + case L3HARRIS_TALKER_ALIAS_BLOCK_3: + return new LCHarrisTalkerAliasBlock3(message); + case L3HARRIS_TALKER_ALIAS_BLOCK_4: + return new LCHarrisTalkerAliasBlock4(message); case L3HARRIS_UNKNOWN: return new UnknownLinkControlWord(message); @@ -173,10 +185,10 @@ public static LinkControlWord create(CorrectedBinaryMessage message) return new LCMotorolaGroupRegroupVoiceChannelUpdate(message); case MOTOROLA_UNIT_GPS: return new LCMotorolaUnitGPS(message); - case MOTOROLA_RADIO_REPROGRAM_HEADER: - return new LCMotorolaRadioReprogramHeader(message); - case MOTOROLA_RADIO_REPROGRAM_RECORD: - return new LCMotorolaRadioReprogramRecord(message); + case MOTOROLA_TALKER_ALIAS_HEADER: + return new LCMotorolaTalkerAliasHeader(message); + case MOTOROLA_TALKER_ALIAS_DATA_BLOCK: + return new LCMotorolaTalkerAliasDataBlock(message); case MOTOROLA_EMERGENCY_ALARM_ACTIVATION: return new LCMotorolaEmergencyAlarmActivation(message); case MOTOROLA_UNKNOWN: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/HarrisTalkerAliasAssembler.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/HarrisTalkerAliasAssembler.java new file mode 100644 index 000000000..b5c4675c5 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/HarrisTalkerAliasAssembler.java @@ -0,0 +1,116 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; + +/** + * Assembles a talker alias from talker alias blocks 1-4 + */ +public class HarrisTalkerAliasAssembler +{ + private LCHarrisTalkerAliasBlock1 mBlock1; + private LCHarrisTalkerAliasBlock2 mBlock2; + private LCHarrisTalkerAliasBlock3 mBlock3; + private LCHarrisTalkerAliasBlock4 mBlock4; + private long mTimestamp; + + /** + * Constructs an instance. + */ + public HarrisTalkerAliasAssembler() + { + } + + /** + * Resets the contents when a terminator or idle message is received. + */ + public void reset() + { + mBlock1 = null; + mBlock2 = null; + mBlock3 = null; + mBlock4 = null; + } + + /** + * Processes the FLC talker alias message and returns a fully assembled alias when at least 2 fragments are received. + * @param lcw containing Harris LC talker alias blocks 1-4 + * @return fully assembled talker alias, if available, or null. + */ + public LCHarrisTalkerAliasComplete process(LinkControlWord lcw, long timestamp) + { + mTimestamp = timestamp; + + if(lcw.isValid()) + { + switch(lcw.getOpcode()) + { + case L3HARRIS_TALKER_ALIAS_BLOCK_1: + if(lcw instanceof LCHarrisTalkerAliasBlock1 block1) + { + mBlock1 = block1; + return assemble(); + } + break; + case L3HARRIS_TALKER_ALIAS_BLOCK_2: + if(lcw instanceof LCHarrisTalkerAliasBlock2 block2) + { + mBlock2 = block2; + return assemble(); + } + break; + case L3HARRIS_TALKER_ALIAS_BLOCK_3: + if(lcw instanceof LCHarrisTalkerAliasBlock3 block3) + { + mBlock3 = block3; + return assemble(); + } + break; + case L3HARRIS_TALKER_ALIAS_BLOCK_4: + if(lcw instanceof LCHarrisTalkerAliasBlock4 block4) + { + mBlock4 = block4; + return assemble(); + } + break; + } + } + + return null; + } + + /** + * Assembles a complete alias from the 4 fragment blocks + * @return assembled alias or null. + */ + private LCHarrisTalkerAliasComplete assemble() + { + if(mBlock1 != null && mBlock2 != null) + { + String fragment2 = mBlock2.getPayloadFragmentString(); + String fragment3 = mBlock3 != null ? mBlock3.getPayloadFragmentString() : null; + String fragment4 = mBlock4 != null ? mBlock4.getPayloadFragmentString() : null; + return new LCHarrisTalkerAliasComplete(mBlock1.getMessage(), fragment2, fragment3, fragment4, mTimestamp); + } + + return null; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBase.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBase.java new file mode 100644 index 000000000..e70eb1550 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBase.java @@ -0,0 +1,72 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; +import java.util.Collections; +import java.util.List; + +/** + * Base class for L3Harris talker alias messages + */ +public abstract class LCHarrisTalkerAliasBase extends LinkControlWord +{ + private static final int PAYLOAD_START = OCTET_2_BIT_16; + private static final int PAYLOAD_END = OCTET_9_BIT_72; + + /** + * Constructs a Link Control Word from the binary message sequence. + * + * @param message + */ + public LCHarrisTalkerAliasBase(CorrectedBinaryMessage message) + { + super(message); + } + + /** + * Payload fragment carried by the header. + * @return payload fragment. + */ + public CorrectedBinaryMessage getPayloadFragment() + { + return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END); + } + + /** + * Payload fragment as a string with empty space removed. + * @return fragment + */ + public String getPayloadFragmentString() + { + return new String(getPayloadFragment().toByteArray()).trim(); + } + + /** + * List of identifiers contained in this message + */ + @Override + public List getIdentifiers() + { + return Collections.emptyList(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock1.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock1.java new file mode 100644 index 000000000..374d2ab0c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock1.java @@ -0,0 +1,59 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * L3Harris Opcode 50 (0x32) Talker Alias Block 1 (of 4). + */ +public class LCHarrisTalkerAliasBlock1 extends LCHarrisTalkerAliasBase +{ + /** + * Constructs a Link Control Word from the binary message sequence. + * + * @param message + */ + public LCHarrisTalkerAliasBlock1(CorrectedBinaryMessage message) + { + super(message); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("**CRC-FAILED** "); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + else + { + sb.append("L3HARRIS TALKER ALIAS FRAGMENT 1/4:").append(getPayloadFragmentString()); + } + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock2.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock2.java new file mode 100644 index 000000000..332745c9c --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock2.java @@ -0,0 +1,62 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * L3Harris Opcode 51 (0x33) Talker Alias Block 2. + */ +public class LCHarrisTalkerAliasBlock2 extends LCHarrisTalkerAliasBase +{ + private static final int PAYLOAD_START = OCTET_2_BIT_16; + private static final int PAYLOAD_END = OCTET_9_BIT_72; + + /** + * Constructs a Link Control Word from the binary message sequence. + * + * @param message + */ + public LCHarrisTalkerAliasBlock2(CorrectedBinaryMessage message) + { + super(message); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("**CRC-FAILED** "); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + else + { + sb.append("L3HARRIS TALKER ALIAS FRAGMENT 2/4:").append(getPayloadFragmentString()); + } + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock3.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock3.java new file mode 100644 index 000000000..7be5a25a3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock3.java @@ -0,0 +1,62 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * L3Harris Opcode 52 (0x34) Talker Alias Block 3. + */ +public class LCHarrisTalkerAliasBlock3 extends LCHarrisTalkerAliasBase +{ + private static final int PAYLOAD_START = OCTET_2_BIT_16; + private static final int PAYLOAD_END = OCTET_9_BIT_72; + + /** + * Constructs a Link Control Word from the binary message sequence. + * + * @param message + */ + public LCHarrisTalkerAliasBlock3(CorrectedBinaryMessage message) + { + super(message); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("**CRC-FAILED** "); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + else + { + sb.append("L3HARRIS TALKER ALIAS FRAGMENT 3/4:").append(getPayloadFragmentString()); + } + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock4.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock4.java new file mode 100644 index 000000000..6c4c2f4b3 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasBlock4.java @@ -0,0 +1,62 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; + +/** + * L3Harris Opcode 53 (0x35) Talker Alias Block 4. + */ +public class LCHarrisTalkerAliasBlock4 extends LCHarrisTalkerAliasBase +{ + private static final int PAYLOAD_START = OCTET_2_BIT_16; + private static final int PAYLOAD_END = OCTET_9_BIT_72; + + /** + * Constructs a Link Control Word from the binary message sequence. + * + * @param message + */ + public LCHarrisTalkerAliasBlock4(CorrectedBinaryMessage message) + { + super(message); + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + + if(!isValid()) + { + sb.append("**CRC-FAILED** "); + } + + if(isEncrypted()) + { + sb.append(" ENCRYPTED"); + } + else + { + sb.append("L3HARRIS TALKER ALIAS FRAGMENT 4/4:").append(getPayloadFragmentString()); + } + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasComplete.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasComplete.java new file mode 100644 index 000000000..89fa21ae9 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/l3harris/LCHarrisTalkerAliasComplete.java @@ -0,0 +1,162 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.l3harris; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.alias.P25TalkerAliasIdentifier; +import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier; +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.LinkControlOpcode; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; +import io.github.dsheirer.protocol.Protocol; +import java.util.ArrayList; +import java.util.List; + +/** + * L3Harris talker alias complete. This is a talker alias fully reassembled from data blocks 1-4 + */ +public class LCHarrisTalkerAliasComplete extends LinkControlWord implements IMessage +{ + private static final int PAYLOAD_START = OCTET_2_BIT_16; + private static final int PAYLOAD_END = OCTET_9_BIT_72; + + private String mFragment2; + private String mFragment3; + private String mFragment4; + private TalkerAliasIdentifier mTalkerAlias; + private long mTimestamp; + + /** + * Constructs an instance + * @param correctedBinaryMessage that is LCW for block 1 + * @param fragment2 alias fragment from block 2 + * @param fragment3 alias fragment from block 3 + * @param fragment4 alias fragment from block 4 + * @param timestamp of the most recent LDU and LCW message + */ + public LCHarrisTalkerAliasComplete(CorrectedBinaryMessage correctedBinaryMessage, String fragment2, String fragment3, String fragment4, long timestamp) + { + super(correctedBinaryMessage); + mFragment2 = fragment2; + mFragment3 = fragment3; + mFragment4 = fragment4; + mTimestamp = timestamp; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("L3HARRIS TALKER ALIAS REASSEMBLED:").append(getTalkerAlias()); + return sb.toString(); + } + + @Override + public LinkControlOpcode getOpcode() + { + return LinkControlOpcode.TALKER_ALIAS_COMPLETE; + } + + @Override + public int getOpcodeNumber() + { + return LinkControlOpcode.TALKER_ALIAS_COMPLETE.getCode(); + } + + /** + * Fully (or partially) reassembled talker alias. + * + * Note: in real world examples, data blocks 1-2 have the alias and data blocks 3-4 repeat the same content as + * data blocks 1-2, so we inspect each data block fragment to see if that fragment is already contained in the + * talker alias as we reconstruct it from the fragments and don't add any content that's already in the assembly. + * @return alias. + */ + public TalkerAliasIdentifier getTalkerAlias() + { + if(mTalkerAlias == null) + { + String alias = getPayloadFragmentString(); + + if(mFragment2 != null && !alias.contains(mFragment2)) + { + alias += mFragment2; + + if(mFragment3 != null && !alias.contains(mFragment3)) + { + alias += mFragment3; + + if(mFragment4 != null && !alias.contains(mFragment4)) + { + alias += mFragment4; + } + } + } + + mTalkerAlias = P25TalkerAliasIdentifier.create(alias); + } + + return mTalkerAlias; + } + + /** + * Payload fragment carried by the header. + * @return payload fragment. + */ + public CorrectedBinaryMessage getPayloadFragment() + { + return getMessage().getSubMessage(PAYLOAD_START, PAYLOAD_END); + } + + /** + * Payload fragment as a string with empty space removed. + * @return fragment + */ + public String getPayloadFragmentString() + { + return new String(getPayloadFragment().toByteArray()).trim(); + } + + @Override + public long getTimestamp() + { + return mTimestamp; + } + + @Override + public Protocol getProtocol() + { + return Protocol.APCO25; + } + + @Override + public int getTimeslot() + { + return P25P1Message.TIMESLOT_0; + } + + @Override + public List getIdentifiers() + { + List identifiers = new ArrayList<>(); + identifiers.add(getTalkerAlias()); + return identifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasAssembler.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasAssembler.java new file mode 100644 index 000000000..a343f173b --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasAssembler.java @@ -0,0 +1,147 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.message.TimeslotMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.LinkControlWord; +import io.github.dsheirer.protocol.Protocol; +import java.util.HashMap; +import java.util.Map; + +/** + * Reassembles a P25 Motorola talker alias from a Header and Data Blocks. + * + * This is used for traffic channels and monitors: + * - Phase 1: link control messaging stream. + * - Phase 2: mac messaging + */ +public class LCMotorolaTalkerAliasAssembler +{ + private static final int DATA_BLOCK_FRAGMENT_LENGTH = 44; + private LCMotorolaTalkerAliasHeader mHeader; + private Map mDataBlocks = new HashMap<>(); + private int mSequence = -1; + private long mMostRecentTimestamp; + + /** + * Constructs an instance + */ + public LCMotorolaTalkerAliasAssembler() + { + } + + /** + * Link control word to process. + * @param lcw to add + * @return true if we can (now) assemble a complete talker alias from the header and data blocks. + */ + public boolean add(LinkControlWord lcw, long timestamp) + { + if(lcw instanceof LCMotorolaTalkerAliasHeader header) + { + mMostRecentTimestamp = timestamp; + + if(mSequence != header.getSequence()) + { + mDataBlocks.clear(); + mSequence = header.getSequence(); + } + + mHeader = header; + } + else if(lcw instanceof LCMotorolaTalkerAliasDataBlock block) + { + mMostRecentTimestamp = timestamp; + + if(block.getSequence() != mSequence) + { + mHeader = null; + mDataBlocks.clear(); + mSequence = block.getSequence(); + } + + mDataBlocks.put(block.getBlockNumber(), block); + } + else + { + return false; //For all lcw's that are not headers or data blocks + } + + return isComplete(); + } + + /** + * Indicates if the assembler has a header and the correct number of data blocks to reassemble an alias. + */ + private boolean isComplete() + { + if(mHeader != null) + { + int dataBlockCount = mHeader.getBlockCount(); + + for(int x = 1; x <= dataBlockCount; x++) + { + if(!mDataBlocks.containsKey(x)) + { + return false; + } + } + + return true; + } + + return false; + } + + /** + * Assembles the talker alias once the header and data blocks have been collected. + * + * Note: do not invoke unless the add() methods indicate that the assembler is complete. + * @return assembled talker alias + * @throws IllegalStateException if the assembler can't assemble the alias. + */ + public MotorolaTalkerAliasComplete assemble() throws IllegalStateException + { + if(!isComplete()) + { + throw new IllegalStateException("Can't assemble talker alias - missing header or data block(s)"); + } + + int dataBlockCount = mHeader.getBlockCount(); + CorrectedBinaryMessage reassembled = new CorrectedBinaryMessage(dataBlockCount * DATA_BLOCK_FRAGMENT_LENGTH); + + int offset = 0; + + for(int x = 1; x <= dataBlockCount; x++) + { + LCMotorolaTalkerAliasDataBlock block = mDataBlocks.get(x); + reassembled.load(offset, block.getFragment()); + offset += DATA_BLOCK_FRAGMENT_LENGTH; + } + + MotorolaTalkerAliasComplete complete = new MotorolaTalkerAliasComplete(reassembled, mHeader.getTalkgroup(), + mHeader.getSequence(), TimeslotMessage.TIMESLOT_0, mMostRecentTimestamp, Protocol.APCO25); + + mHeader = null; + mDataBlocks.clear(); + return complete; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaRadioReprogramRecord.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasDataBlock.java similarity index 64% rename from src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaRadioReprogramRecord.java rename to src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasDataBlock.java index f22710bc9..92735a3ca 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaRadioReprogramRecord.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasDataBlock.java @@ -19,6 +19,7 @@ package io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola; +import io.github.dsheirer.bits.BinaryMessage; import io.github.dsheirer.bits.CorrectedBinaryMessage; import io.github.dsheirer.bits.IntField; import io.github.dsheirer.identifier.Identifier; @@ -30,17 +31,19 @@ * Motorola Link Control opcode 0x17 (23). This is possibly a radio reprogramming record segment that is used * in combination with LCO 0x15. See notes in header of LCMotorolaRadioReprogramHeader class. */ -public class LCMotorolaRadioReprogramRecord extends LinkControlWord +public class LCMotorolaTalkerAliasDataBlock extends LinkControlWord { - private static final IntField RECORD_NUMBER = IntField.length8(OCTET_2_BIT_16); - private static final IntField SEQUENCE_NUMBER = IntField.length4(OCTET_3_BIT_24); + private static final IntField BLOCK_NUMBER = IntField.length8(OCTET_2_BIT_16); + private static final IntField SEQUENCE = IntField.length4(OCTET_3_BIT_24); + private static final int FRAGMENT_START = OCTET_3_BIT_24 + 4; //inclusive + private static final int FRAGMENT_END = OCTET_9_BIT_72; //exclusive /** * Constructs a Link Control Word from the binary message sequence. * * @param message */ - public LCMotorolaRadioReprogramRecord(CorrectedBinaryMessage message) + public LCMotorolaTalkerAliasDataBlock(CorrectedBinaryMessage message) { super(message); } @@ -59,28 +62,38 @@ public String toString() sb.append(" ENCRYPTED"); } - sb.append("MOTOROLA RADIO REPROGRAM RECORD:").append(getRecordNumber()); - sb.append(" OF SEQUENCE:").append(getSequenceNumber()); + sb.append("MOTOROLA TALKER ALIAS DATA BLOCK:").append(getBlockNumber()); + sb.append(" OF SEQUENCE:").append(getSequence()); + sb.append(" FRAGMENT:").append(getFragment().toHexString()); sb.append(" MSG:").append(getMessage().toHexString()); return sb.toString(); } /** - * Record number + * Fragment of encoded alias. */ - public int getRecordNumber() + public BinaryMessage getFragment() { - return getInt(RECORD_NUMBER); + return getMessage().getSubMessage(getOffset() + FRAGMENT_START, getOffset() + FRAGMENT_END); } /** - * Sequence number + * Block number */ - public int getSequenceNumber() + public int getBlockNumber() { - return getInt(SEQUENCE_NUMBER); + return getInt(BLOCK_NUMBER); } + /** + * Sequence number that identifies the header and data blocks as all part of the same sequence. + */ + public int getSequence() + { + return getInt(SEQUENCE); + } + + @Override public List getIdentifiers() { diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaRadioReprogramHeader.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasHeader.java similarity index 68% rename from src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaRadioReprogramHeader.java rename to src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasHeader.java index 48d4886fa..75d903ee2 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaRadioReprogramHeader.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/LCMotorolaTalkerAliasHeader.java @@ -28,23 +28,18 @@ import java.util.List; /** - * Motorola Link Control Opcode 0x15. - * - * Note: I suspect this is some form of radio reprogramming message, possibly used to update encryption. + * Motorola Talker Alias Header - Link Control Opcode 0x15. * * I've seen LCOpcode 0x15 and 0x17 twice and in both cases it was sent in a TDULC at the end of a call for the same - * radio on the CNYICC system. The header references the talkgroup that the radio is calling, so it may be some form - * of dynamic regrouping, but the total content is quite large. - * - * Note: this seems to only be targeted to certain radios since it wasn't sent at the end of every call. + * radio on the CNYICC system. * * Examples observed on traffic channels in TDULC at the end of a call following the Motorola Talk Complete message * for Radio: 15,104,082 (0xE67852) and TG:7101 (0x1BBD) * Example 1 * - * 1590 1BBD 07 0100 F 8BA / 1BBD=TG, 07=record count, 0100=?, F=sequence number, 8BA=crc checksum + * 1590 1BBD 07 0100 F 8BA / 1BBD=TG, 07=record count, 0100=Format/Unicode?, F=sequence number, 8BA=crc checksum * 1790 01 F BEE00 2AE E67 / BEE00=WACN, 2AE=SYS, E67...= RADIO ID - * 1790 02 F 852 83ED1081 / ...852=RADIO ID cont. + * 1790 02 F 852 83ED1081 / ...852=RADIO ID cont. Encoded alias ... * 1790 03 F E33C03E9B3E * 1790 04 F 35647DE0C00 * 1790 05 F C8A83E351E4 @@ -68,13 +63,15 @@ * * The reprogramming payload sequence starts by sending the full SUID for the radio (BEE00.2AE.E67852). */ -public class LCMotorolaRadioReprogramHeader extends LinkControlWord +public class LCMotorolaTalkerAliasHeader extends LinkControlWord { private static final IntField TALKGROUP = IntField.length16(OCTET_2_BIT_16); - private static final IntField RECORD_COUNT = IntField.length8(OCTET_4_BIT_32); - private static final IntField SEQUENCE_NUMBER = IntField.length4(OCTET_7_BIT_56); - private static final IntField SEQUENCE_CHECKSUM = IntField.length12(OCTET_7_BIT_56 + 4); - private Identifier mTalkgroup; + private static final IntField BLOCK_COUNT = IntField.length8(OCTET_4_BIT_32); + private static final IntField FORMAT = IntField.length8(OCTET_5_BIT_40); //Value 1 observed - unicode? + private static final IntField UNKNOWN = IntField.length8(OCTET_6_BIT_48); //Always 0x00 + private static final IntField SEQUENCE = IntField.length4(OCTET_7_BIT_56); + private static final IntField CHECKSUM = IntField.length12(OCTET_7_BIT_56 + 4); + private APCO25Talkgroup mTalkgroup; private List mIdentifiers; /** @@ -82,7 +79,7 @@ public class LCMotorolaRadioReprogramHeader extends LinkControlWord * * @param message */ - public LCMotorolaRadioReprogramHeader(CorrectedBinaryMessage message) + public LCMotorolaTalkerAliasHeader(CorrectedBinaryMessage message) { super(message); } @@ -100,10 +97,12 @@ public String toString() { sb.append(" ENCRYPTED"); } - sb.append("MOTOROLA RADIO REPROGRAM HEADER"); + sb.append("MOTOROLA TALKER ALIAS HEADER"); sb.append(" TG:").append(getTalkgroup()); - sb.append(" RECORD COUNT:").append(getRecordCount()); - sb.append(" SEQUENCE:").append(getSequenceNumber()); + sb.append(" SEQUENCE:").append(getSequence()); + sb.append(" BLOCKS TO FOLLOW:").append(getBlockCount()); + sb.append(" FORMAT:").append(getFormat()); + sb.append(" UNK:").append(Integer.toHexString(getInt(UNKNOWN)).toUpperCase()); sb.append(" MSG:").append(getMessage().toHexString()); return sb.toString(); } @@ -111,7 +110,7 @@ public String toString() /** * Talkgroup */ - public Identifier getTalkgroup() + public APCO25Talkgroup getTalkgroup() { if(mTalkgroup == null) { @@ -122,20 +121,36 @@ public Identifier getTalkgroup() } /** - * Count of continuation messages to follow - * @return + * Alias encoding format + */ + public String getFormat() + { + int format = getInt(FORMAT); + + if(format == 1) + { + return "1-UNICODE"; //best guess + } + else + { + return String.valueOf(format); + } + } + + /** + * Sequence number that ties the header to each of the data blocks. */ - public int getRecordCount() + public int getSequence() { - return getInt(RECORD_COUNT); + return getInt(SEQUENCE); } /** - * Sequence number + * Number of data blocks that follow this header. */ - public int getSequenceNumber() + public int getBlockCount() { - return getInt(SEQUENCE_NUMBER); + return getInt(BLOCK_COUNT); } /** diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/MotorolaTalkerAliasComplete.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/MotorolaTalkerAliasComplete.java new file mode 100644 index 000000000..548ab7124 --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/message/lc/motorola/MotorolaTalkerAliasComplete.java @@ -0,0 +1,201 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.bits.IntField; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.identifier.alias.P25TalkerAliasIdentifier; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.message.TimeslotMessage; +import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25FullyQualifiedRadioIdentifier; +import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; +import io.github.dsheirer.protocol.Protocol; +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola completely assembled talker alias. Note: this is not a true link control word. It is reassembled from a + * header and data blocks. + */ +public class MotorolaTalkerAliasComplete extends TimeslotMessage implements IMessage +{ + private static final IntField SUID_WACN = IntField.length20(OCTET_0_BIT_0); + private static final IntField SUID_SYSTEM = IntField.length12(OCTET_2_BIT_16 + 4); + private static final IntField SUID_ID = IntField.length24(OCTET_4_BIT_32); + private static final IntField CHUNK = IntField.length16(0); + private static final int ENCODED_ALIAS_START = OCTET_7_BIT_56; + private static final int CHUNK_SIZE = 16; + + private APCO25Talkgroup mTalkgroup; + private APCO25FullyQualifiedRadioIdentifier mRadio; + private P25TalkerAliasIdentifier mAlias; + private List mIdentifiers; + private int mSequence; + private Protocol mProtocol; + + /** + * Constructs a Link Control Word from the binary message sequence. + * + * @param message assembled from the data blocks + * @param talkgroup from the header + * @param dataBlockCount from the header + * @param sequence number for the alias + * @param timeslot for the message + * @param timestamp of the most recent header or data block + * @param protocol for the message + */ + public MotorolaTalkerAliasComplete(CorrectedBinaryMessage message, APCO25Talkgroup talkgroup, int sequence, + int timeslot, long timestamp, Protocol protocol) + { + super(message, timeslot, timestamp); + mTalkgroup = talkgroup; + mSequence = sequence; + mProtocol = protocol; + } + + public String toString() + { + StringBuilder sb = new StringBuilder(); + if(getTimeslot() > TIMESLOT_0) + { + sb.append("TS").append(getTimeslot()).append(" "); + } + sb.append("MOTOROLA TALKER ALIAS COMPLETE"); + sb.append(" RADIO:").append(getRadio()); + sb.append(" TG:").append(getTalkgroup()); + sb.append(" ENCODED:").append(getEncodedAlias().toHexString()); + sb.append(" ALIAS:").append(getAlias()); + sb.append(" SEQUENCE:").append(mSequence); + sb.append(" MSG:").append(getMessage().toHexString()); + return sb.toString(); + } + + /** + * Protocol - P25 Phase 1 or Phase 2 + */ + @Override + public Protocol getProtocol() + { + return mProtocol; + } + + /** + * Sequence number for the alias. + */ + public int getSequence() + { + return mSequence; + } + + /** + * Decoded alias string + */ + public P25TalkerAliasIdentifier getAlias() + { + if(mAlias == null) + { + //BinaryMessage encoded = getEncodedAlias(); + //TODO: implement decoding of the encoded alias. + String alias = "*ALIAS " + getRadio().getValue() + "*"; //Temporary value until we implement decoding + mAlias = P25TalkerAliasIdentifier.create(alias); + } + + return mAlias; + } + + /** + * Calculates the 16-bit value at the specified chunk number. + * @param chunk to get + * @return int value at the chunk + */ + private int getChunkValue(int chunk) + { + int lastIndex = ENCODED_ALIAS_START + (chunk * CHUNK_SIZE) - 1; + + if(getMessage().size() >= lastIndex) + { + return getInt(CHUNK, ENCODED_ALIAS_START + ((chunk - 1) * CHUNK_SIZE)); + } + + return 0; + } + + /** + * Extracts the encoded alias payload. + * @return encoded alias binary message + */ + private BinaryMessage getEncodedAlias() + { + int length = 1; + + for(int x = 16; x > 1; x--) + { + if(getChunkValue(x) > 0) + { + length = x; + break; + } + } + + return getMessage().getSubMessage(ENCODED_ALIAS_START, ENCODED_ALIAS_START + (length * 16)); + } + + /** + * Talkgroup + */ + public APCO25Talkgroup getTalkgroup() + { + return mTalkgroup; + } + + /** + * Radio that is being aliased. + */ + public APCO25FullyQualifiedRadioIdentifier getRadio() + { + if(mRadio == null) + { + int wacn = getInt(SUID_WACN); + int system = getInt(SUID_SYSTEM); + int id = getInt(SUID_ID); + + mRadio = APCO25FullyQualifiedRadioIdentifier.createFrom(id, wacn, system, id); + } + + return mRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getRadio()); + //TODO: uncomment once alias decoding is implemented. +// mIdentifiers.add(getAlias()); + } + + return mIdentifiers; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java index d876806e0..1baf0982e 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2DecoderState.java @@ -33,10 +33,12 @@ import io.github.dsheirer.identifier.IdentifierUpdateListener; import io.github.dsheirer.identifier.MutableIdentifierCollection; import io.github.dsheirer.identifier.Role; +import io.github.dsheirer.identifier.alias.P25TalkerAliasIdentifier; import io.github.dsheirer.identifier.encryption.EncryptionKey; import io.github.dsheirer.identifier.patch.PatchGroupIdentifier; import io.github.dsheirer.identifier.patch.PatchGroupManager; import io.github.dsheirer.identifier.patch.PatchGroupPreLoadDataContent; +import io.github.dsheirer.identifier.radio.RadioIdentifier; import io.github.dsheirer.log.LoggingSuppressor; import io.github.dsheirer.message.IMessage; import io.github.dsheirer.module.decode.DecoderType; @@ -49,6 +51,7 @@ import io.github.dsheirer.module.decode.p25.identifier.channel.APCO25Channel; import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand; import io.github.dsheirer.module.decode.p25.phase1.message.P25P1Message; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.MotorolaTalkerAliasComplete; import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence; import io.github.dsheirer.module.decode.p25.phase2.message.mac.IP25ChannelGrantDetailProvider; import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; @@ -252,41 +255,18 @@ else if(message instanceof EncryptionSynchronizationSequence ess) continueState(State.CALL); } } + else if(message instanceof MotorolaTalkerAliasComplete tac) + { + //Debug +// if(!mTrafficChannelManager.getTalkerAliasManager().hasAlias(tac.getRadio())) +// { +// System.out.println(tac); +// } + mTrafficChannelManager.getTalkerAliasManager().update(tac.getRadio(), tac.getAlias()); + } } } - /** - * Updates the channel state according to the PDU type - */ - private static State getStateFromPduType(MacPduType macPduType) - { - switch(macPduType) - { - case MAC_0_SIGNAL: - return State.CONTROL; - case MAC_1_PTT: - return State.CALL; - case MAC_2_END_PTT: - return State.TEARDOWN; - case MAC_4_ACTIVE: - case MAC_6_HANGTIME: - case MAC_3_IDLE: - default: - return State.ACTIVE; - } - } - - /** - * Adds the current channel to the local identifier collection which will cause it to be broadcast to all of the - * other listeners and will allow both timeslots on this channel to receive it and update accordingly. - * - * @param channel to broadcast - */ - private void broadcastCurrentChannel(APCO25Channel channel) - { - getIdentifierCollection().update(channel); - } - /** * Process MAC message structures */ @@ -607,10 +587,10 @@ private void processMacMessage(MacMessage message) case MOTOROLA_89_GROUP_REGROUP_DELETE: processDynamicRegrouping(message, mac); break; - case MOTOROLA_91_GROUP_REGROUP_UNKNOWN: + case MOTOROLA_91_TALKER_ALIAS_HEADER: //Unknown break; - case MOTOROLA_95_UNKNOWN_149: + case MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK: //Unknown break; case MOTOROLA_A0_GROUP_REGROUP_VOICE_CHANNEL_USER_EXTENDED: @@ -689,15 +669,16 @@ private MutableIdentifierCollection getIdentifierCollectionForUser(Identifier us */ private MutableIdentifierCollection getIdentifierCollectionForUsers(List identifiersToAdd, long timestamp) { - MutableIdentifierCollection ic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); - ic.remove(IdentifierClass.USER); - ic.remove(Form.CHANNEL); + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.remove(Form.CHANNEL); for(Identifier identifier : identifiersToAdd) { //Filter the identifiers through the patch group manager - ic.update(mPatchGroupManager.update(identifier, timestamp)); + mic.update(mPatchGroupManager.update(identifier, timestamp)); } - return ic; + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); + return mic; } /** @@ -828,6 +809,7 @@ private void processCallAlert(MacMessage message, MacStructure mac) */ private void processChannelGrant(MacMessage message, MacStructure mac) { + //TODO: will we ever see a channel grant on a non-control channel? if(message.getMacPduType() == MacPduType.MAC_3_IDLE || message.getMacPduType() == MacPduType.MAC_6_HANGTIME) { for(Identifier identifier : mac.getIdentifiers()) @@ -836,6 +818,8 @@ private void processChannelGrant(MacMessage message, MacStructure mac) getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); } + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection()); + continueState(State.ACTIVE); } @@ -1152,24 +1136,20 @@ else if(getTimeslot() == P25P1Message.TIMESLOT_1) */ private void processChannelUser(MacMessage message, MacStructure mac) { - if(message.getMacPduType() == MacPduType.MAC_3_IDLE || message.getMacPduType() == MacPduType.MAC_6_HANGTIME) + for(Identifier identifier : mac.getIdentifiers()) { - for(Identifier identifier : mac.getIdentifiers()) - { - //Add to the identifier collection after filtering through the patch group manager - getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); - } + //Add to the identifier collection after filtering through the patch group manager + getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); + } + + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection()); + if(message.getMacPduType() == MacPduType.MAC_3_IDLE || message.getMacPduType() == MacPduType.MAC_6_HANGTIME) + { continueState(State.ACTIVE); } else { - for(Identifier identifier : mac.getIdentifiers()) - { - //Add to the identifier collection after filtering through the patch group manager - getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); - } - if(mac instanceof IServiceOptionsProvider sop) { IChannelDescriptor currentChannel = mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), getCurrentChannel(), sop.getServiceOptions(), mac.getOpcode(), getIdentifierCollection().copyOf(), message.getTimestamp(), null); @@ -1210,12 +1190,14 @@ private void processPushToTalk(MacMessage message, MacStructure mac) getIdentifierCollection().update(mPatchGroupManager.update(identifier, message.getTimestamp())); } + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(getIdentifierCollection()); + if(mac instanceof PushToTalk ptt) { VoiceServiceOptions vso = ptt.isEncrypted() ? VoiceServiceOptions.createEncrypted() : VoiceServiceOptions.createUnencrypted(); - - mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), getCurrentChannel(), vso, mac.getOpcode(), getIdentifierCollection().copyOf(), message.getTimestamp(), ptt.isEncrypted() ? ptt.getEncryptionKey().toString() : null); - + mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), getCurrentChannel(), vso, + mac.getOpcode(), getIdentifierCollection().copyOf(), message.getTimestamp(), + ptt.isEncrypted() ? ptt.getEncryptionKey().toString() : null); broadcast(new DecoderStateEvent(this, Event.START, ptt.isEncrypted() ? State.ENCRYPTED : State.CALL, getTimeslot())); } } @@ -1762,9 +1744,17 @@ private void processTalkerAlias(MacMessage message, MacStructure mac) { if(mac instanceof L3HarrisTalkerAlias talkerAlias) { - Identifier alias = talkerAlias.getAlias(); + P25TalkerAliasIdentifier alias = talkerAlias.getAlias(); getIdentifierCollection().update(alias); mTrafficChannelManager.processP2CurrentUser(getCurrentFrequency(), getTimeslot(), alias, message.getTimestamp()); + + //Add the alias to the talker alias manager if we know the associated radio + Identifier from = getIdentifierCollection().getFromIdentifier(); + + if(from instanceof RadioIdentifier ri) + { + mTrafficChannelManager.getTalkerAliasManager().update(ri, alias); + } } } @@ -1788,9 +1778,8 @@ private void processUnitRegistration(MacMessage message, MacStructure mac) } } - /** - * Creates and broadcasts a decode event. + * Creates and broadcasts a decode event and broadcasts it for the specified channel * * @param message for the event * @param mac for the event @@ -1798,13 +1787,13 @@ private void processUnitRegistration(MacMessage message, MacStructure mac) * @param eventType of event * @param details to populate for the event */ - private void broadcast(MacMessage message, MacStructure mac, IChannelDescriptor channel, DecodeEventType eventType, String details) + private void broadcast(MacMessage message, MacStructure mac, IChannelDescriptor channel, DecodeEventType eventType, + String details) { - MutableIdentifierCollection collection = getUpdatedMutableIdentifierCollection(mac); - + MutableIdentifierCollection mic = getUpdatedMutableIdentifierCollection(mac); broadcast(P25DecodeEvent.builder(eventType, message.getTimestamp()).channel(channel) .details(details) - .identifiers(collection) + .identifiers(mic) .timeslot(getTimeslot()) .build()); } @@ -1819,14 +1808,7 @@ private void broadcast(MacMessage message, MacStructure mac, IChannelDescriptor */ private void broadcast(MacMessage message, MacStructure structure, DecodeEventType eventType, String details) { - MutableIdentifierCollection icQueuedResponse = getUpdatedMutableIdentifierCollection(structure); - - broadcast(P25DecodeEvent.builder(eventType, message.getTimestamp()) - .channel(getCurrentChannel()) - .details(details) - .identifiers(icQueuedResponse) - .timeslot(getTimeslot()) - .build()); + broadcast(message, structure, getCurrentChannel(), eventType, details); } /** @@ -1838,10 +1820,11 @@ private void broadcast(MacMessage message, MacStructure structure, DecodeEventTy */ private MutableIdentifierCollection getUpdatedMutableIdentifierCollection(MacStructure mac) { - MutableIdentifierCollection icQueuedResponse = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); - icQueuedResponse.remove(IdentifierClass.USER); - icQueuedResponse.update(mac.getIdentifiers()); - return icQueuedResponse; + MutableIdentifierCollection mic = new MutableIdentifierCollection(getIdentifierCollection().getIdentifiers()); + mic.remove(IdentifierClass.USER); + mic.update(mac.getIdentifiers()); + mTrafficChannelManager.getTalkerAliasManager().enrichMutable(mic); + return mic; } /** @@ -1926,6 +1909,8 @@ public String getActivitySummary() sb.append(mNetworkConfigurationMonitor.getActivitySummary()); sb.append("\n"); sb.append(mPatchGroupManager.getPatchGroupSummary()); + sb.append("\n"); + sb.append(mTrafficChannelManager.getTalkerAliasManager().getAliasSummary()); return sb.toString(); } diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java index 78b165761..9a684bc7f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/P25P2MessageProcessor.java @@ -31,17 +31,19 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.MacMessage; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureMultiFragment; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MultiFragmentContinuationMessage; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasAssembler; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasDataBlock; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasHeader; import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractSignalingTimeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.Timeslot; import io.github.dsheirer.module.decode.p25.phase2.timeslot.Voice2Timeslot; import io.github.dsheirer.sample.Listener; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.util.List; import java.util.Map; import java.util.TreeMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class P25P2MessageProcessor implements Listener { @@ -52,6 +54,8 @@ public class P25P2MessageProcessor implements Listener private MacMessage mMacMessageWithMultiFragment1; private MacStructureMultiFragment mMacStructureMultiFragment1; private MacStructureMultiFragment mMacStructureMultiFragment2; + private MotorolaTalkerAliasAssembler mMotorolaTalkerAliasAssembler1 = new MotorolaTalkerAliasAssembler(P25P2Message.TIMESLOT_1); + private MotorolaTalkerAliasAssembler mMotorolaTalkerAliasAssembler2 = new MotorolaTalkerAliasAssembler(P25P2Message.TIMESLOT_2); private Listener mMessageListener; //Map of up to 16 band identifiers per RFSS. These identifier update messages are inserted into any message that @@ -217,14 +221,33 @@ else if(mMacMessageWithMultiFragment1 != null && mMacMessageWithMultiFragment1 ! } } - /* Store band identifiers so that they can be injected into channel - * type messages */ + //Store band identifiers so that they can be injected into channel type messages if(macMessage.getMacStructure() instanceof IFrequencyBand bandIdentifier) { mFrequencyBandMap.put(bandIdentifier.getIdentifier(), bandIdentifier); } + //Send the message to the listener mMessageListener.receive(macMessage); + + /** + * We reassemble Motorola talker alias messages here so that we can send the assembled + * message to message listener, after the fragment has been sent to the listener. + */ + if(macMessage.getMacStructure() instanceof MotorolaTalkerAliasHeader || + macMessage.getMacStructure() instanceof MotorolaTalkerAliasDataBlock) + { + if(macMessage.getTimeslot() == P25P2Message.TIMESLOT_1 && + mMotorolaTalkerAliasAssembler1.add(macMessage.getMacStructure(), macMessage.getTimestamp())) + { + mMessageListener.receive(mMotorolaTalkerAliasAssembler1.assemble()); + } + else if(macMessage.getTimeslot() == P25P2Message.TIMESLOT_2 && + mMotorolaTalkerAliasAssembler2.add(macMessage.getMacStructure(), macMessage.getTimestamp())) + { + mMessageListener.receive(mMotorolaTalkerAliasAssembler2.assemble()); + } + } } } else if(timeslot instanceof AbstractVoiceTimeslot) diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java index 32002d545..c73a467af 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacMessageFactory.java @@ -136,12 +136,12 @@ import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupChannelGrantUpdate; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupDeleteCommand; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupExtendedFunctionCommand; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupTalkerAlias; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupTalkerAliasContinuation; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUpdate; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserAbbreviated; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaGroupRegroupVoiceChannelUserExtended; import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaQueuedResponse; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasDataBlock; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola.MotorolaTalkerAliasHeader; import io.github.dsheirer.module.decode.p25.reference.Vendor; import java.util.ArrayList; import java.util.List; @@ -584,10 +584,10 @@ public static MacStructure createMacStructure(CorrectedBinaryMessage message, in return new MotorolaGroupRegroupExtendedFunctionCommand(message, offset); case MOTOROLA_89_GROUP_REGROUP_DELETE: return new MotorolaGroupRegroupDeleteCommand(message, offset); - case MOTOROLA_91_GROUP_REGROUP_UNKNOWN: - return new MotorolaGroupRegroupTalkerAlias(message, offset); - case MOTOROLA_95_UNKNOWN_149: - return new MotorolaGroupRegroupTalkerAliasContinuation(message, offset); + case MOTOROLA_91_TALKER_ALIAS_HEADER: + return new MotorolaTalkerAliasHeader(message, offset); + case MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK: + return new MotorolaTalkerAliasDataBlock(message, offset); case MOTOROLA_A0_GROUP_REGROUP_VOICE_CHANNEL_USER_EXTENDED: return new MotorolaGroupRegroupVoiceChannelUserExtended(message, offset); case MOTOROLA_A3_GROUP_REGROUP_CHANNEL_GRANT_IMPLICIT: diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java index bb180b1e0..ae663935f 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/MacOpcode.java @@ -119,8 +119,8 @@ public enum MacOpcode MOTOROLA_84_GROUP_REGROUP_EXTENDED_FUNCTION_COMMAND(Vendor.MOTOROLA, 133, "MOTOROLA GROUP REGROUP", 11), MOTOROLA_89_GROUP_REGROUP_DELETE(Vendor.MOTOROLA, 137, "MOTOROLA GROUP REGROUP DELETE", 17), //Opcode 144 uses STANDARD vendor ID for some reason. - MOTOROLA_91_GROUP_REGROUP_UNKNOWN(Vendor.MOTOROLA, 145, "MOTOROLA GROUP REGROUP UNKNOWN", 17), - MOTOROLA_95_UNKNOWN_149(Vendor.MOTOROLA, 149, "MOTOROLA UNKNOWN 149", 17), + MOTOROLA_91_TALKER_ALIAS_HEADER(Vendor.MOTOROLA, 145, "MOTOROLA GROUP REGROUP UNKNOWN", 17), + MOTOROLA_95_TALKER_ALIAS_DATA_BLOCK(Vendor.MOTOROLA, 149, "MOTOROLA UNKNOWN 149", 17), MOTOROLA_A0_GROUP_REGROUP_VOICE_CHANNEL_USER_EXTENDED(Vendor.MOTOROLA, 160, "MOTOROLA GROUP REGROUP VOICE CHANNEL USER EXTENDED", 16), MOTOROLA_A3_GROUP_REGROUP_CHANNEL_GRANT_IMPLICIT(Vendor.MOTOROLA, 163, "MOTOROLA GROUP REGROUP CHANNEL GRANT IMPLICIT", 11), MOTOROLA_A4_GROUP_REGROUP_CHANNEL_GRANT_EXPLICIT(Vendor.MOTOROLA, 164, "MOTOROLA GROUP REGROUP CHANNEL GRANT EXPLICIT", 13), diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAlias.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAlias.java index 2e32a37cb..e69de29bb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAlias.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAlias.java @@ -1,108 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * **************************************************************************** - */ - -package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola; - -import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.bits.IntField; -import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier; -import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25FullyQualifiedRadioIdentifier; -import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor; -import java.util.ArrayList; -import java.util.List; - -/** - * Motorola Group Regroup Talker Alias - 17-bytes long - */ -public class MotorolaGroupRegroupTalkerAlias extends MacStructureVendor -{ - private static final IntField TALKGROUP = IntField.length16(24); - private static final IntField UNKNOWN = IntField.length32(40); - private static final IntField SOURCE_SUID_WACN = IntField.length20(72); - private static final IntField SOURCE_SUID_SYSTEM = IntField.length12(92); - private static final IntField SOURCE_SUID_UNIT = IntField.length24(104); - private List mIdentifiers; - private TalkgroupIdentifier mTalkgroup; - private APCO25FullyQualifiedRadioIdentifier mRadio; - - /** - * Constructs the message - * - * @param message containing the message bits - * @param offset into the message for this structure - */ - public MotorolaGroupRegroupTalkerAlias(CorrectedBinaryMessage message, int offset) - { - super(message, offset); - } - - /** - * Textual representation of this message - */ - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append("MOTOROLA GROUP REGROUP TALKER ALIAS TALKGROUP:").append(getTalkgroup()); - sb.append(" SOURCE SUID RADIO:").append(getRadio()); - sb.append(" UNK:").append(Integer.toHexString(getInt(UNKNOWN)).toUpperCase()); - sb.append(" MSG:").append(getMessage().get(getOffset(), getMessage().length()).toHexString()); - return sb.toString(); - } - - /** - * Talkgroup identifier - */ - public TalkgroupIdentifier getTalkgroup() - { - if(mTalkgroup == null) - { - mTalkgroup = APCO25Talkgroup.create(getMessage().getInt(TALKGROUP, getOffset())); - } - - return mTalkgroup; - } - - public APCO25FullyQualifiedRadioIdentifier getRadio() - { - if(mRadio == null) - { - int wacn = getMessage().getInt(SOURCE_SUID_WACN, getOffset()); - int system = getMessage().getInt(SOURCE_SUID_SYSTEM, getOffset()); - int unit = getMessage().getInt(SOURCE_SUID_UNIT, getOffset()); - mRadio = APCO25FullyQualifiedRadioIdentifier.createFrom(unit, wacn, system, unit); - } - - return mRadio; - } - - @Override - public List getIdentifiers() - { - if(mIdentifiers == null) - { - mIdentifiers = new ArrayList<>(); - mIdentifiers.add(getTalkgroup()); - mIdentifiers.add(getRadio()); - } - - return mIdentifiers; - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAliasContinuation.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAliasContinuation.java index 6ea069fdb..e69de29bb 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAliasContinuation.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaGroupRegroupTalkerAliasContinuation.java @@ -1,88 +0,0 @@ -/* - * ***************************************************************************** - * Copyright (C) 2014-2024 Dennis Sheirer - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see - * **************************************************************************** - */ - -package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola; - -import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor; -import java.util.ArrayList; -import java.util.List; - -/** - * Motorola Unknown Opcode 149 (0x95) Talker Alias continuation message. - * - * This was observed at the end of a Group Regroup call sequence during HANGTIME. Each radio doesn't know the alias - * until it receives it once and then queues the alias for subsequent use. - * - * This opcode is observed following an Opcode 145 message and seems to be a continuation message. - * - * Examples: - * 959011 018E58B82BAB4D3B70E9A8457F9D C67 - * 959011 0287000000000000000000000000 E26 - * - * Example 2: - * 9190110BCD02010026BEE004A403151724FD9 Opcode 145 - * 959011012436C4C022EC902A9943EDAA69E76 Opcode 149 - * 95901102298FAA77683FAE0000000000000AB Opcode 149 - * (immediately followed in the same call sequence by a slight variation) - * 9190110BCD02010036BEE004A403151724513 Opcode 145 - * 959011013436C4C022EC902A9943EDAA699F6 Opcode 149 - * 95901102398FAA77683FAE00000000000072B Opcode 149 - * - * The opcode, vendor and length octets are consistent. The first octet seems to be a sequence identifier, 01, 02, etc. - * Nothing else seems to match any of the other identifiers that were active at call time. - */ -public class MotorolaGroupRegroupTalkerAliasContinuation extends MacStructureVendor -{ - private List mIdentifiers; - - /** - * Constructs the message - * - * @param message containing the message bits - * @param offset into the message for this structure - */ - public MotorolaGroupRegroupTalkerAliasContinuation(CorrectedBinaryMessage message, int offset) - { - super(message, offset); - } - - /** - * Textual representation of this message - */ - public String toString() - { - StringBuilder sb = new StringBuilder(); - sb.append("MOTOROLA TALKER ALIAS CONTINUATION"); - sb.append(" MSG:").append(getMessage().get(getOffset(), getMessage().length()).toHexString()); - return sb.toString(); - } - - @Override - public List getIdentifiers() - { - if(mIdentifiers == null) - { - mIdentifiers = new ArrayList<>(); - } - - return mIdentifiers; - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasAssembler.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasAssembler.java new file mode 100644 index 000000000..c1d8f90fb --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasAssembler.java @@ -0,0 +1,152 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.lc.motorola.MotorolaTalkerAliasComplete; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructure; +import io.github.dsheirer.protocol.Protocol; +import java.util.HashMap; +import java.util.Map; + +/** + * Reassembles a P25 Motorola talker alias from a Header and Data Blocks. + * + * This is used for traffic channels and monitors: + * - Phase 2: mac messaging + */ +public class MotorolaTalkerAliasAssembler +{ + private static final int HEADER_FRAGMENT_LENGTH = 64; + private static final int DATA_BLOCK_FRAGMENT_LENGTH = 100; + private MotorolaTalkerAliasHeader mHeader; + private Map mDataBlocks = new HashMap<>(); + private int mSequence = -1; + private long mMostRecentTimestamp; + private int mTimeslot; + + /** + * Constructs an instance + */ + public MotorolaTalkerAliasAssembler(int timeslot) + { + mTimeslot = timeslot; + } + + /** + * Link control word to process. + * @param mac to add + * @return true if we can (now) assemble a complete talker alias from the header and data blocks. + */ + public boolean add(MacStructure mac, long timestamp) + { + if(mac instanceof MotorolaTalkerAliasHeader header) + { + mMostRecentTimestamp = timestamp; + + if(mSequence != header.getSequence()) + { + mDataBlocks.clear(); + mSequence = header.getSequence(); + } + + mHeader = header; + } + else if(mac instanceof MotorolaTalkerAliasDataBlock block) + { + mMostRecentTimestamp = timestamp; + + if(block.getSequence() != mSequence) + { + mHeader = null; + mDataBlocks.clear(); + mSequence = block.getSequence(); + } + + mDataBlocks.put(block.getBlockNumber(), block); + } + else + { + return false; //For all lcw's that are not headers or data blocks + } + + return isComplete(); + } + + /** + * Indicates if the assembler has a header and the correct number of data blocks to reassemble an alias. + */ + private boolean isComplete() + { + if(mHeader != null) + { + int dataBlockCount = mHeader.getBlockCount(); + + for(int x = 1; x <= dataBlockCount; x++) + { + if(!mDataBlocks.containsKey(x)) + { + return false; + } + } + + return true; + } + + return false; + } + + /** + * Assembles the talker alias once the header and data blocks have been collected. + * + * Note: do not invoke unless the add() methods indicate that the assembler is complete. + * @return assembled talker alias + * @throws IllegalStateException if the assembler can't assemble the alias. + */ + public MotorolaTalkerAliasComplete assemble() throws IllegalStateException + { + if(!isComplete()) + { + throw new IllegalStateException("Can't assemble talker alias - missing header or data block(s)"); + } + + int dataBlockCount = mHeader.getBlockCount(); + CorrectedBinaryMessage reassembled = new CorrectedBinaryMessage(HEADER_FRAGMENT_LENGTH + + (dataBlockCount * DATA_BLOCK_FRAGMENT_LENGTH)); + + int offset = 0; + reassembled.load(offset, mHeader.getFragment()); + offset += HEADER_FRAGMENT_LENGTH; + + for(int x = 1; x <= dataBlockCount; x++) + { + MotorolaTalkerAliasDataBlock block = mDataBlocks.get(x); + reassembled.load(offset, block.getFragment()); + offset += DATA_BLOCK_FRAGMENT_LENGTH; + } + + MotorolaTalkerAliasComplete complete = new MotorolaTalkerAliasComplete(reassembled, mHeader.getTalkgroup(), + mHeader.getSequence(), mTimeslot, mMostRecentTimestamp, Protocol.APCO25_PHASE2); + + mHeader = null; + mDataBlocks.clear(); + return complete; + } +} diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasDataBlock.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasDataBlock.java new file mode 100644 index 000000000..6051f65cd --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasDataBlock.java @@ -0,0 +1,107 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.bits.IntField; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor; +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Talker Alias continuation block Opcode 149 (0x95). This follows Motorola Talker Alias header message. + * + * Examples: + * 959011 018E58B82BAB4D3B70E9A8457F9D C67 + * 959011 0287000000000000000000000000 E26 + * + * The opcode, vendor and length octets are consistent. The first octet seems to be an identifier, 01, 02, etc. + * Nothing else seems to match any of the other identifiers that were active at call time. + */ +public class MotorolaTalkerAliasDataBlock extends MacStructureVendor +{ + private static final IntField BLOCK_NUMBER = IntField.length8(OCTET_4_BIT_24); + private static final IntField SEQUENCE = IntField.length4(OCTET_5_BIT_32); + private static final int FRAGMENT_START = OCTET_5_BIT_32 + 4; + private static final int FRAGMENT_END = OCTET_18_BIT_136; + + private List mIdentifiers; + + /** + * Constructs the message + * + * @param message containing the message bits + * @param offset into the message for this structure + */ + public MotorolaTalkerAliasDataBlock(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + /** + * Textual representation of this message + */ + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("MOTOROLA TALKER ALIAS DATA BLOCK:").append(getBlockNumber()); + sb.append(" OF SEQUENCE:").append(getSequence()); + sb.append(" FRAGMENT:").append(getFragment().toHexString()); + sb.append(" MSG:").append(getMessage().get(getOffset(), getMessage().length()).toHexString()); + return sb.toString(); + } + + /** + * Fragment of encoded alias. + */ + public BinaryMessage getFragment() + { + return getMessage().getSubMessage(getOffset() + FRAGMENT_START, getOffset() + FRAGMENT_END); + } + + /** + * Block number + */ + public int getBlockNumber() + { + return getInt(BLOCK_NUMBER); + } + + /** + * Sequence number that identifies the header and data blocks as all part of the same sequence. + */ + public int getSequence() + { + return getInt(SEQUENCE); + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + } + + return mIdentifiers; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasHeader.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasHeader.java new file mode 100644 index 000000000..2956e13aa --- /dev/null +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase2/message/mac/structure/motorola/MotorolaTalkerAliasHeader.java @@ -0,0 +1,158 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.motorola; + +import io.github.dsheirer.bits.BinaryMessage; +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import io.github.dsheirer.bits.IntField; +import io.github.dsheirer.identifier.Identifier; +import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25FullyQualifiedRadioIdentifier; +import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup; +import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.MacStructureVendor; +import java.util.ArrayList; +import java.util.List; + +/** + * Motorola Talker Alias Header - Opcode 145 + */ +public class MotorolaTalkerAliasHeader extends MacStructureVendor +{ + private static final IntField TALKGROUP = IntField.length16(OCTET_4_BIT_24); + private static final IntField BLOCK_COUNT = IntField.length8(OCTET_6_BIT_40); + private static final IntField FORMAT = IntField.length8(OCTET_7_BIT_48); //Value 1 observed - unicode? + private static final IntField UNKNOWN = IntField.length8(OCTET_8_BIT_56); //Always 0x00 + private static final IntField SEQUENCE = IntField.length4(OCTET_9_BIT_64); + private static final IntField SOURCE_SUID_WACN = IntField.length20(OCTET_10_BIT_72); + private static final IntField SOURCE_SUID_SYSTEM = IntField.length12(OCTET_12_BIT_88 + 4); + private static final IntField SOURCE_SUID_UNIT = IntField.length24(OCTET_14_BIT_104); + private static final int FRAGMENT_START = OCTET_10_BIT_72; + private static final int FRAGMENT_END = OCTET_18_BIT_136; + private List mIdentifiers; + private APCO25Talkgroup mTalkgroup; + private APCO25FullyQualifiedRadioIdentifier mRadio; + + /** + * Constructs the message + * + * @param message containing the message bits + * @param offset into the message for this structure + */ + public MotorolaTalkerAliasHeader(CorrectedBinaryMessage message, int offset) + { + super(message, offset); + } + + /** + * Textual representation of this message + */ + public String toString() + { + StringBuilder sb = new StringBuilder(); + sb.append("MOTOROLA TALKER ALIAS HEADER TG:").append(getTalkgroup()); + sb.append(" RADIO:").append(getRadio()); + sb.append(" SEQUENCE:").append(getSequence()); + sb.append(" BLOCKS TO FOLLOW:").append(getBlockCount()); + sb.append(" FORMAT:").append(getFormat()); + sb.append(" FRAGMENT:").append(getFragment().toHexString()); + sb.append(" UNK:").append(Integer.toHexString(getInt(UNKNOWN)).toUpperCase()); + sb.append(" MSG:").append(getMessage().get(getOffset(), getMessage().length()).toHexString()); + return sb.toString(); + } + + /** + * Fragment that is the start of the encoded alias. + */ + public BinaryMessage getFragment() + { + return getMessage().getSubMessage(getOffset() + FRAGMENT_START, getOffset() + FRAGMENT_END); + } + + /** + * Alias encoding format + */ + public String getFormat() + { + int format = getInt(FORMAT); + + if(format == 1) + { + return "1-UNICODE"; //possibly + } + else + { + return String.valueOf(format); + } + } + + /** + * Sequence number that ties the header to each of the data blocks. + */ + public int getSequence() + { + return getInt(SEQUENCE); + } + + /** + * Number of data blocks that follow this header. + */ + public int getBlockCount() + { + return getInt(BLOCK_COUNT); + } + + /** + * Talkgroup identifier + */ + public APCO25Talkgroup getTalkgroup() + { + if(mTalkgroup == null) + { + mTalkgroup = APCO25Talkgroup.create(getMessage().getInt(TALKGROUP, getOffset())); + } + + return mTalkgroup; + } + + public APCO25FullyQualifiedRadioIdentifier getRadio() + { + if(mRadio == null) + { + int wacn = getMessage().getInt(SOURCE_SUID_WACN, getOffset()); + int system = getMessage().getInt(SOURCE_SUID_SYSTEM, getOffset()); + int unit = getMessage().getInt(SOURCE_SUID_UNIT, getOffset()); + mRadio = APCO25FullyQualifiedRadioIdentifier.createFrom(unit, wacn, system, unit); + } + + return mRadio; + } + + @Override + public List getIdentifiers() + { + if(mIdentifiers == null) + { + mIdentifiers = new ArrayList<>(); + mIdentifiers.add(getTalkgroup()); + mIdentifiers.add(getRadio()); + } + + return mIdentifiers; + } +} \ No newline at end of file diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/reference/Vendor.java b/src/main/java/io/github/dsheirer/module/decode/p25/reference/Vendor.java index 5c424fcc7..c42a5ed61 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/reference/Vendor.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/reference/Vendor.java @@ -1,5 +1,26 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2024 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + package io.github.dsheirer.module.decode.p25.reference; +import java.util.EnumSet; + public enum Vendor { STANDARD( "STANDARD", "STANDARD", 0), @@ -263,13 +284,21 @@ public enum Vendor private String mLabel; private String mDescription; private int mValue; - - private Vendor( String label, String description, int value ) + + /** + * Constructs an instance + * @param label for the vendor + * @param description of the vendor + * @param value for the vendor + */ + Vendor( String label, String description, int value ) { mLabel = label; mDescription = description; mValue = value; } + + public static EnumSet LOGGABLE_VENDORS = EnumSet.of(STANDARD, MOTOROLA, HARRIS); public String getLabel() { @@ -286,6 +315,14 @@ public int getValue() return mValue; } + /** + * Vendors where we log when new opcodes are encountered. + */ + public boolean isLoggable() + { + return LOGGABLE_VENDORS.contains(this); + } + public static Vendor fromValue( int value ) { if( 0 <= value && value <= 255 ) diff --git a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java index 35da717a9..e7d92c051 100644 --- a/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java +++ b/src/main/java/io/github/dsheirer/source/tuner/frequency/FrequencyController.java @@ -280,18 +280,21 @@ public void setFrequencyCorrection(double correction) throws SourceException */ public void addSourceEventProcessor(ISourceEventProcessor processor) { - mTunable.getLock().lock(); - - try + if(mTunable != null) { - if(!mProcessors.contains(processor)) + mTunable.getLock().lock(); + + try { - mProcessors.add(processor); + if(!mProcessors.contains(processor)) + { + mProcessors.add(processor); + } + } + finally + { + mTunable.getLock().unlock(); } - } - finally - { - mTunable.getLock().unlock(); } }