From 9359f523c1a5677ea4815ae8524eb89894270a9f Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sun, 1 Nov 2026 06:20:47 -0500 Subject: [PATCH] #2421 Motorola talker alias with Cyrillic now decodes correctly (#2551) Co-authored-by: denny --- .../module/decode/dmr/DMRDecoderState.java | 81 ++----------- .../dmr/DMRNetworkConfigurationMonitor.java | 6 +- .../dmr/message/data/lc/LCMessageFactory.java | 12 +- .../decode/dmr/message/data/lc/LCOpcode.java | 10 +- .../data/lc/full/TalkerAliasAssembler.java | 65 +++++----- .../data/lc/full/TalkerAliasComplete.java | 32 ++++- .../full/motorola/CapacityMaxTalkerAlias.java | 92 --------------- .../CapacityMaxTalkerAliasContinuation.java | 99 ---------------- .../lc/full/TalkerAliasAssemblerTest.java | 111 ++++++++++++++++++ 9 files changed, 204 insertions(+), 304 deletions(-) delete mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAlias.java delete mode 100644 src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java create mode 100644 src/test/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssemblerTest.java 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 22680bf8..9bee8e44 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 @@ -76,8 +76,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasComp import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser; -import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAlias; -import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAliasContinuation; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; @@ -1193,73 +1191,6 @@ public class DMRDecoderState extends TimeslotDecoderState } } break; - case FULL_CAPACITY_MAX_TALKER_ALIAS: - if(message instanceof CapacityMaxTalkerAlias alias) - { - //If we have a talker alias identifier, append this value. - Identifier existing = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM); - - if(existing instanceof DmrTalkerAliasIdentifier talkerAlias && - !talkerAlias.equals(alias.getTalkerAliasIdentifier()) && - !talkerAlias.getValue().contains(alias.getTalkerAliasIdentifier().getValue())) - { - //Concatenate the existing talker alias fragment with the base alias value. - DmrTalkerAliasIdentifier updated = DmrTalkerAliasIdentifier - .create(alias.getTalkerAliasIdentifier().getValue() + talkerAlias.getValue()); - getIdentifierCollection().update(updated); - - Identifier fromRadio = getIdentifierCollection().getFromIdentifier(); - - if(hasTrafficChannelManager() && fromRadio instanceof RadioIdentifier radio) - { - mTrafficChannelManager.getTalkerAliasManager().update(radio, updated); - } - } - else - { - getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); - } - - if(mCurrentCallEvent != null) - { - broadcast(mCurrentCallEvent); - } - } - break; - case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION: - if(message instanceof CapacityMaxTalkerAliasContinuation alias) - { - //If we have a talker alias identifier, append this value. - Identifier existing = getIdentifierCollection().getIdentifier(IdentifierClass.USER, Form.TALKER_ALIAS, Role.FROM); - - if(existing instanceof DmrTalkerAliasIdentifier talkerAlias && - !talkerAlias.equals(alias.getTalkerAliasIdentifier()) && - !talkerAlias.getValue().contains(alias.getTalkerAliasIdentifier().getValue())) - { - //Concatenate the existing talker alias value with the updated continuation fragment. - DmrTalkerAliasIdentifier updated = DmrTalkerAliasIdentifier.create(talkerAlias.getValue() + - alias.getTalkerAliasIdentifier().getValue()); - getIdentifierCollection().update(updated); - - Identifier fromRadio = getIdentifierCollection().getFromIdentifier(); - - if(hasTrafficChannelManager() && fromRadio instanceof RadioIdentifier radio) - { - mTrafficChannelManager.getTalkerAliasManager().update(radio, updated); - } - } - else - { - //Temporarily place the continuation fragment alias into the identifier collection. - getIdentifierCollection().update(alias.getTalkerAliasIdentifier()); - } - - if(mCurrentCallEvent != null) - { - broadcast(mCurrentCallEvent); - } - } - break; case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER: if(message instanceof CapacityPlusWideAreaVoiceChannelUser cpwavcu) { @@ -1382,6 +1313,18 @@ public class DMRDecoderState extends TimeslotDecoderState if(message instanceof TalkerAliasComplete tac && tac.hasTalkerAliasIdentifier()) { getIdentifierCollection().update(tac.getTalkerAliasIdentifier()); + + Identifier fromRadio = getIdentifierCollection().getFromIdentifier(); + + if(hasTrafficChannelManager() && fromRadio instanceof RadioIdentifier radio) + { + mTrafficChannelManager.getTalkerAliasManager().update(radio, tac.getTalkerAliasIdentifier()); + } + + if(mCurrentCallEvent != null) + { + broadcast(mCurrentCallEvent); + } } break; } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java index 3481bd87..c4d16597 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/DMRNetworkConfigurationMonitor.java @@ -144,8 +144,10 @@ public class DMRNetworkConfigurationMonitor switch(linkControl.getOpcode()) { case FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER: - case FULL_CAPACITY_MAX_TALKER_ALIAS: - case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION: + case FULL_CAPACITY_MAX_TALKER_ALIAS_HEADER: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_1: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_2: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_3: mBrand = BRAND_MOTOROLA_CAPACITY_MAX_TIER_3_TRUNKING; break; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java index b5cc3f4d..1b026c76 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCMessageFactory.java @@ -39,8 +39,6 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGr import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraXptChannelGrant; -import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAlias; -import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxTalkerAliasContinuation; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityMaxVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser; import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser; @@ -170,12 +168,6 @@ public class LCMessageFactory case FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER: flc = new CapacityMaxVoiceChannelUser(message, timestamp, timeslot); break; - case FULL_CAPACITY_MAX_TALKER_ALIAS: - flc = new CapacityMaxTalkerAlias(message, timestamp, timeslot); - break; - case FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION: - flc = new CapacityMaxTalkerAliasContinuation(message, timestamp, timeslot); - break; case FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER: flc = new CapacityPlusEncryptedVoiceChannelUser(message, timestamp, timeslot); break; @@ -197,18 +189,22 @@ public class LCMessageFactory break; case FULL_STANDARD_TALKER_ALIAS_HEADER: case FULL_HYTERA_TALKER_ALIAS_HEADER: + case FULL_CAPACITY_MAX_TALKER_ALIAS_HEADER: flc = new TalkerAliasHeader(message, timestamp, timeslot); break; case FULL_STANDARD_TALKER_ALIAS_BLOCK_1: case FULL_HYTERA_TALKER_ALIAS_BLOCK_1: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_1: flc = new TalkerAliasBlock1(message, timestamp, timeslot); break; case FULL_STANDARD_TALKER_ALIAS_BLOCK_2: case FULL_HYTERA_TALKER_ALIAS_BLOCK_2: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_2: flc = new TalkerAliasBlock2(message, timestamp, timeslot); break; case FULL_STANDARD_TALKER_ALIAS_BLOCK_3: case FULL_HYTERA_TALKER_ALIAS_BLOCK_3: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_3: flc = new TalkerAliasBlock3(message, timestamp, timeslot); break; case FULL_HYTERA_XPT_CHANNEL_GRANT: diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java index f621d18b..afcd0827 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/LCOpcode.java @@ -47,8 +47,10 @@ public enum LCOpcode FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 4, "WAN GROUP VOICE CHANNEL USER"), FULL_CAPACITY_MAX_GROUP_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 16, "CAPMAX GROUP VOICE CHANNEL USER"), - FULL_CAPACITY_MAX_TALKER_ALIAS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 20, "CAPMAX TALKER ALIAS"), - FULL_CAPACITY_MAX_TALKER_ALIAS_CONTINUATION(Vendor.MOTOROLA_CAPACITY_PLUS, true, 21, "CAPMAX TALKER ALIAS CONTINUATION"), + FULL_CAPACITY_MAX_TALKER_ALIAS_HEADER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 20, "CAPMAX TALKER ALIAS HEADER"), + FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_1(Vendor.MOTOROLA_CAPACITY_PLUS, true, 21, "CAPMAX TALKER ALIAS BLOCK 1"), + FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_2(Vendor.MOTOROLA_CAPACITY_PLUS, true, 22, "CAPMAX TALKER ALIAS BLOCK 2"), + FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_3(Vendor.MOTOROLA_CAPACITY_PLUS, true, 23, "CAPMAX TALKER ALIAS BLOCK 3"), //Observed on Cap+ Multi-Site System during an encrypted voice call FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"), //Observed on Cap+ Multi-Site System during an encrypted voice call @@ -90,7 +92,9 @@ public enum LCOpcode private static final EnumSet TALKER_ALIAS_OPCODES = EnumSet.of(FULL_STANDARD_TALKER_ALIAS_HEADER, FULL_STANDARD_TALKER_ALIAS_BLOCK_1, FULL_STANDARD_TALKER_ALIAS_BLOCK_2, FULL_STANDARD_TALKER_ALIAS_BLOCK_3, FULL_HYTERA_TALKER_ALIAS_HEADER, FULL_HYTERA_TALKER_ALIAS_BLOCK_1, FULL_HYTERA_TALKER_ALIAS_BLOCK_2, - FULL_HYTERA_TALKER_ALIAS_BLOCK_3); + FULL_HYTERA_TALKER_ALIAS_BLOCK_3, FULL_CAPACITY_MAX_TALKER_ALIAS_HEADER, + FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_1, FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_2, + FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_3); private final Vendor mVendor; private final boolean mFull; diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssembler.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssembler.java index 91c7d8df..2c523896 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssembler.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssembler.java @@ -26,6 +26,9 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage; */ public class TalkerAliasAssembler { + //Header (49) plus three blocks (56 each) + private static final int MAX_BIT_LENGTH = 217; + //Timeslot 1 fragments private TalkerAliasHeader mHeaderTs1; private TalkerAliasBlock1 mBlock1Ts1; @@ -84,6 +87,7 @@ public class TalkerAliasAssembler { case FULL_STANDARD_TALKER_ALIAS_HEADER: case FULL_HYTERA_TALKER_ALIAS_HEADER: + case FULL_CAPACITY_MAX_TALKER_ALIAS_HEADER: if(message instanceof TalkerAliasHeader tah) { if(message.getTimeslot() == 1) @@ -101,6 +105,7 @@ public class TalkerAliasAssembler } case FULL_STANDARD_TALKER_ALIAS_BLOCK_1: case FULL_HYTERA_TALKER_ALIAS_BLOCK_1: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_1: if(message instanceof TalkerAliasBlock1 tab1) { if(message.getTimeslot() == 1) @@ -118,6 +123,7 @@ public class TalkerAliasAssembler } case FULL_STANDARD_TALKER_ALIAS_BLOCK_2: case FULL_HYTERA_TALKER_ALIAS_BLOCK_2: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_2: if(message instanceof TalkerAliasBlock2 tab2) { if(message.getTimeslot() == 1) @@ -135,6 +141,7 @@ public class TalkerAliasAssembler } case FULL_STANDARD_TALKER_ALIAS_BLOCK_3: case FULL_HYTERA_TALKER_ALIAS_BLOCK_3: + case FULL_CAPACITY_MAX_TALKER_ALIAS_BLOCK_3: if(message instanceof TalkerAliasBlock3 tab3) { if(message.getTimeslot() == 1) @@ -187,39 +194,37 @@ public class TalkerAliasAssembler { if(header != null) { + CorrectedBinaryMessage message = new CorrectedBinaryMessage(MAX_BIT_LENGTH); + message.load(0, header.getPayloadFragment()); + int availableBits = 49; + + //Load the contiguous fragments that are available. UTF-8 aliases need all of them because the header's + //character length under-counts the bytes when characters are multi-byte, so a longer alias replaces a + //shorter one as the remaining fragments arrive. + if(block1 != null) + { + message.load(49, block1.getPayloadFragment()); + availableBits = 105; + + if(block2 != null) + { + message.load(105, block2.getPayloadFragment()); + availableBits = 161; + + if(block3 != null) + { + message.load(161, block3.getPayloadFragment()); + availableBits = MAX_BIT_LENGTH; + } + } + } + int bitLength = header.getTotalBitLength(); - if(1 <= bitLength && bitLength <= 49) + if(1 <= bitLength && bitLength <= availableBits) { - return new TalkerAliasComplete(header.getPayloadFragment(), header.getFormat(), header.getCharacterLength(), - header.getTimestamp(), header.getTimeslot()); - } - else if(50 <= bitLength && bitLength <= 105 && block1 != null) - { - CorrectedBinaryMessage message = new CorrectedBinaryMessage(105); - message.load(0, header.getPayloadFragment()); - message.load(49, block1.getPayloadFragment()); - return new TalkerAliasComplete(message, header.getFormat(), header.getCharacterLength(), - header.getTimestamp(), header.getTimeslot()); - } - else if(106 <= bitLength && bitLength <= 161 && block1 != null && block2 != null) - { - CorrectedBinaryMessage message = new CorrectedBinaryMessage(161); - message.load(0, header.getPayloadFragment()); - message.load(49, block1.getPayloadFragment()); - message.load(105, block2.getPayloadFragment()); - return new TalkerAliasComplete(message, header.getFormat(), header.getCharacterLength(), - header.getTimestamp(), header.getTimeslot()); - } - else if(162 <= bitLength && bitLength <= 217 && block1 != null && block2 != null && block3 != null) - { - CorrectedBinaryMessage message = new CorrectedBinaryMessage(217); - message.load(0, header.getPayloadFragment()); - message.load(49, block1.getPayloadFragment()); - message.load(105, block2.getPayloadFragment()); - message.load(161, block3.getPayloadFragment()); - return new TalkerAliasComplete(message, header.getFormat(), header.getCharacterLength(), - header.getTimestamp(), header.getTimeslot()); + return new TalkerAliasComplete(message.getSubMessage(0, availableBits), header.getFormat(), + header.getCharacterLength(), header.getTimestamp(), header.getTimeslot()); } } diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasComplete.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasComplete.java index f37c6868..b0a8d6dd 100644 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasComplete.java +++ b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasComplete.java @@ -24,6 +24,7 @@ import io.github.dsheirer.identifier.Identifier; import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier; import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode; import io.github.dsheirer.module.decode.dmr.message.type.TalkerAliasDataFormat; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -98,7 +99,7 @@ public class TalkerAliasComplete extends FullLCMessage { try { - mAlias = getMessage().parseUTF8(1, mCharacterCount); + mAlias = parseUTF8(); } catch(Exception e) { @@ -124,6 +125,35 @@ public class TalkerAliasComplete extends FullLCMessage return mAlias; } + /** + * Parses all of the assembled bytes as UTF-8 and limits the result to the character count, instead of limiting the + * bytes to the character count, since characters outside of the ASCII range are encoded in two or more bytes. + * @return alias, excluding null padding and any trailing character that is missing its remaining bytes. + */ + private String parseUTF8() throws UnsupportedEncodingException + { + String alias = getMessage().parseUTF8(1, (getMessage().size() - 1) / 8); + + int nullIndex = alias.indexOf('\0'); + + if(nullIndex >= 0) + { + alias = alias.substring(0, nullIndex); + } + + if(alias.endsWith("\uFFFD")) + { + alias = alias.substring(0, alias.length() - 1); + } + + if(alias.codePointCount(0, alias.length()) > mCharacterCount) + { + alias = alias.substring(0, alias.offsetByCodePoints(0, mCharacterCount)); + } + + return alias; + } + /** * Talker Alias identifier. * @return identifier or null if the alias string is null or empty, meaning it could not be decoded. diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAlias.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAlias.java deleted file mode 100644 index ef8b0ad8..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAlias.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola; - -import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier; - -import java.util.ArrayList; -import java.util.List; - -/** - * Capacity Max Talker Alias (FLCO 20 / FID 0x10) - *

- * See also: FLCO 21 - Talker Alias Continuation - */ -public class CapacityMaxTalkerAlias extends CapacityPlusVoiceChannelUser -{ - private static final int[] ENCODING_TYPE = new int[]{16, 17, 18}; - private static final int[] LENGTH = new int[]{19, 20, 21, 22}; - private static final int ALIAS_START = 24; - private static final int ALIAS_END = ALIAS_START + (6 * 8); //6 characters? - private DmrTalkerAliasIdentifier mTalkerAliasIdentifier; - private List mIdentifiers; - - /** - * Constructs an instance. - * - * @param message for the link control payload - * @param timestamp - * @param timeslot - */ - public CapacityMaxTalkerAlias(CorrectedBinaryMessage message, long timestamp, int timeslot) - { - super(message, timestamp, timeslot); - } - - public int getLength() - { - return getMessage().getInt(LENGTH); - } - - @Override - public String toString() - { - StringBuilder sb = new StringBuilder(); - - if(!isValid()) - { - sb.append("[CRC-ERROR] "); - } - if(isEncrypted()) - { - sb.append(" *ENCRYPTED*"); - } - if(isReservedBitSet()) - { - sb.append(" *RESERVED-BIT*"); - } - - sb.append("FLC MOTOROLA CAPMAX TALKER ALIAS:").append(getTalkerAliasIdentifier()); - sb.append(" LENGTH:").append(getLength()); - sb.append(" MSG:").append(getMessage().toHexString()); - return sb.toString(); - } - - /** - * Talker alias identifier - * - * @return identifier - */ - public DmrTalkerAliasIdentifier getTalkerAliasIdentifier() - { - if(mTalkerAliasIdentifier == null) - { - String alias = new String(getMessage().get(ALIAS_START, ALIAS_END).getBytes()).trim(); - mTalkerAliasIdentifier = DmrTalkerAliasIdentifier.create(alias); - } - - return mTalkerAliasIdentifier; - } - - @Override - public List getIdentifiers() - { - if(mIdentifiers == null) - { - mIdentifiers = new ArrayList<>(); - mIdentifiers.add(getTalkerAliasIdentifier()); - } - - return mIdentifiers; - } -} diff --git a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java b/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java deleted file mode 100644 index badd68b0..00000000 --- a/src/main/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/motorola/CapacityMaxTalkerAliasContinuation.java +++ /dev/null @@ -1,99 +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.dmr.message.data.lc.full.motorola; - -import io.github.dsheirer.bits.CorrectedBinaryMessage; -import io.github.dsheirer.identifier.Identifier; -import io.github.dsheirer.identifier.alias.DmrTalkerAliasIdentifier; -import java.util.ArrayList; -import java.util.List; - -/** - * Capacity Max Talker Alias Continuation (FLCO 21 / FID 0x10) - */ -public class CapacityMaxTalkerAliasContinuation extends CapacityPlusVoiceChannelUser -{ - private static final int ALIAS_START = 16; - private static final int ALIAS_END = ALIAS_START + (7 * 8); - private DmrTalkerAliasIdentifier mTalkerAliasIdentifier; - private List mIdentifiers; - - /** - * Constructs an instance. - * - * @param message for the link control payload - * @param timestamp - * @param timeslot - */ - public CapacityMaxTalkerAliasContinuation(CorrectedBinaryMessage message, long timestamp, int timeslot) - { - super(message, timestamp, timeslot); - } - - @Override - public String toString() - { - StringBuilder sb = new StringBuilder(); - - if(!isValid()) - { - sb.append("[CRC-ERROR] "); - } - if(isEncrypted()) - { - sb.append(" *ENCRYPTED*"); - } - if(isReservedBitSet()) - { - sb.append(" *RESERVED-BIT*"); - } - - sb.append("FLC MOTOROLA CAPMAX TALKER ALIAS CONTINUED:").append(getTalkerAliasIdentifier()); - sb.append(" MSG:").append(getMessage().toHexString()); - return sb.toString(); - } - - /** - * Talker alias identifier - * @return identifier - */ - public DmrTalkerAliasIdentifier getTalkerAliasIdentifier() - { - if(mTalkerAliasIdentifier == null) - { - String alias = new String(getMessage().get(ALIAS_START, ALIAS_END).getBytes()).trim(); - mTalkerAliasIdentifier = DmrTalkerAliasIdentifier.create(alias); - } - - return mTalkerAliasIdentifier; - } - - @Override - public List getIdentifiers() - { - if(mIdentifiers == null) - { - mIdentifiers = new ArrayList<>(); - mIdentifiers.add(getTalkerAliasIdentifier()); - } - - return mIdentifiers; - } -} diff --git a/src/test/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssemblerTest.java b/src/test/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssemblerTest.java new file mode 100644 index 00000000..f6fd6873 --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/dmr/message/data/lc/full/TalkerAliasAssemblerTest.java @@ -0,0 +1,111 @@ +package io.github.dsheirer.module.decode.dmr.message.data.lc.full; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.github.dsheirer.bits.CorrectedBinaryMessage; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +/** + * Tests assembly of talker alias fragments, using Motorola Capacity Max (FID 0x10, FLCO 20-23) link control. + */ +public class TalkerAliasAssemblerTest +{ + private static final int FLCO_HEADER = 20; + private static final int FID_MOTOROLA = 0x10; + private static final int FORMAT_UTF8 = 2; + + /** + * Creates a 72-bit link control message (minus CRC) carrying the 7 octets that follow the FLCO and FID. + */ + private static FullLCMessage message(int flco, byte[] payload) + { + CorrectedBinaryMessage message = new CorrectedBinaryMessage(96); + message.load(2, 6, flco); + message.load(8, 8, FID_MOTOROLA); + + for(int x = 0; x < 7; x++) + { + message.load(16 + (x * 8), 8, payload[x] & 0xFF); + } + + return switch(flco - FLCO_HEADER) + { + case 0 -> new TalkerAliasHeader(message, 0, 1); + case 1 -> new TalkerAliasBlock1(message, 0, 1); + case 2 -> new TalkerAliasBlock2(message, 0, 1); + default -> new TalkerAliasBlock3(message, 0, 1); + }; + } + + /** + * Fragments the alias into the header and three blocks, padded with nulls. + */ + private static FullLCMessage[] fragment(String alias, int length) + { + ByteBuffer buffer = ByteBuffer.allocate(28); + buffer.put((byte)((FORMAT_UTF8 << 6) | (length << 1))); + buffer.put(alias.getBytes(StandardCharsets.UTF_8)); + byte[] bytes = buffer.array(); + + FullLCMessage[] messages = new FullLCMessage[4]; + + for(int x = 0; x < 4; x++) + { + byte[] payload = new byte[7]; + System.arraycopy(bytes, x * 7, payload, 0, 7); + messages[x] = message(FLCO_HEADER + x, payload); + } + + return messages; + } + + private static TalkerAliasComplete assemble(String alias, int length) + { + TalkerAliasAssembler assembler = new TalkerAliasAssembler(); + TalkerAliasComplete complete = null; + + for(FullLCMessage message : fragment(alias, length)) + { + assertTrue(message.getOpcode().isTalkerAliasOpcode(), "Talker alias opcode: " + message.getOpcode()); + TalkerAliasComplete latest = assembler.process(message); + complete = latest != null ? latest : complete; + } + + return complete; + } + + @Test + public void utf8LengthInCharacters() + { + assertEquals("ТЕРНАВА", assemble("ТЕРНАВА", 7).getAliasValue()); + assertEquals("САЛАМАНДРА", assemble("САЛАМАНДРА", 10).getAliasValue()); + } + + @Test + public void utf8LengthInBytes() + { + assertEquals("ТЕРНАВА", assemble("ТЕРНАВА", 14).getAliasValue()); + assertEquals("САЛАМАНДРА", assemble("САЛАМАНДРА", 20).getAliasValue()); + } + + @Test + public void asciiInUtf8Format() + { + assertEquals("DISPATCH 1", assemble("DISPATCH 1", 10).getAliasValue()); + } + + @Test + public void partialCharacterIsNotDisplayed() + { + TalkerAliasAssembler assembler = new TalkerAliasAssembler(); + FullLCMessage[] messages = fragment("САЛАМАНДРА", 10); + assembler.process(messages[0]); + TalkerAliasComplete partial = assembler.process(messages[1]); + assertEquals("САЛАМА", partial.getAliasValue()); + assertFalse(partial.getAliasValue().contains("\uFFFD")); + } +}