#2421 Motorola talker alias with Cyrillic now decodes correctly (#2551)

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-11-01 06:20:47 -05:00
committed by GitHub
parent e385d2fdec
commit 9359f523c1
9 changed files with 204 additions and 304 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
* <p>
* 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<Identifier> 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<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkerAliasIdentifier());
}
return mIdentifiers;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
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<Identifier> 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<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkerAliasIdentifier());
}
return mIdentifiers;
}
}

View File

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