From 59e64a73ff9417dc7bf4d32ccb7098ce6e4eb40e Mon Sep 17 00:00:00 2001 From: Denny Sheirer Date: Sun, 1 Nov 2026 07:09:43 -0500 Subject: [PATCH] #2338 Resolves P25P1MessageAssembler full - can't add additional dibit (#2554) Co-authored-by: denny --- .../decode/p25/phase1/P25P1MessageFramer.java | 16 ++ .../p25/phase1/P25P1DecoderLSMTest.java | 6 +- .../p25/phase1/P25P1MessageFramerTest.java | 238 ++++++++++++++++++ 3 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramerTest.java diff --git a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java index 35358081..e20d1737 100644 --- a/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java +++ b/src/main/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramer.java @@ -796,6 +796,10 @@ public class P25P1MessageFramer extends C4FMMessageFramer { dispatchMessage(); } + else + { + mMessageAssembler = null; + } } else { @@ -803,6 +807,18 @@ public class P25P1MessageFramer extends C4FMMessageFramer dispatchDroppedSamples(droppedBitCount); dispatchMessage(); } + + //A multi-block TSBK or PDU reconfigures the assembler for its next block. A place holder that resolved to + //a PDU may already contain those blocks, so dispatch each block that's fully assembled. + while(mMessageAssembler != null && mMessageAssembler.isComplete()) + { + dispatchMessage(); + } + + //The new NID ended the multi-block sequence, so the assembler can't continue into the next frame, which + //would prevent starting the assembler for that frame. The elapsed dibits are reported as sync loss. + mMessageAssembler = null; + mPDUSequence = null; } mDibitCounter -= 57; diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java index b6958c27..66413ffc 100644 --- a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1DecoderLSMTest.java @@ -278,7 +278,7 @@ public class P25P1DecoderLSMTest /** * Adds the CRC-CCITT checksum, 1/2 rate trellis encodes and interleaves the TSBK into 98 dibits. */ - private static List encodeTSBK(BitSet tsbk) + static List encodeTSBK(BitSet tsbk) { BitSet message = (BitSet)tsbk.clone(); int checksum = 0xFFFF; @@ -387,7 +387,7 @@ public class P25P1DecoderLSMTest /** * Adds the most significant bits first, as dibits. */ - private static void addBits(List dibits, long value, int bitCount) + static void addBits(List dibits, long value, int bitCount) { for(int bit = bitCount - 2; bit >= 0; bit -= 2) { @@ -398,7 +398,7 @@ public class P25P1DecoderLSMTest /** * Sets the value at the bit offset, most significant bit first. */ - private static void setValue(BitSet bits, int offset, int bitCount, int value) + static void setValue(BitSet bits, int offset, int bitCount, int value) { for(int x = 0; x < bitCount; x++) { diff --git a/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramerTest.java b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramerTest.java new file mode 100644 index 00000000..0fbbc3c7 --- /dev/null +++ b/src/test/java/io/github/dsheirer/module/decode/p25/phase1/P25P1MessageFramerTest.java @@ -0,0 +1,238 @@ +/* + * ***************************************************************************** + * Copyright (C) 2014-2026 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * **************************************************************************** + */ + +package io.github.dsheirer.module.decode.p25.phase1; + +import io.github.dsheirer.dsp.symbol.Dibit; +import io.github.dsheirer.edac.bch.BCH_63_16_23_P25; +import io.github.dsheirer.message.IMessage; +import io.github.dsheirer.module.decode.p25.phase1.message.tsbk.TSBKMessage; +import io.github.dsheirer.module.decode.p25.phase1.sync.P25P1SyncDetector; +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * JUnit tests for the P25 Phase 1 message framer recovery from a frame with a failed NID. The failed frame assembles + * as a place holder and the valid NID of the next frame forces completion of that place holder, which must not impact + * assembly of the next frame. + */ +public class P25P1MessageFramerTest +{ + private static final int NAC = 0x293; + private static final int TSBK_DUID = 7; + private static final int OPCODE_RFSS_STATUS_BROADCAST = 58; + private static final int PDU_FORMAT_AMBTC = 23; + private static final int FRAME_DIBITS = 180; //Sync, NID, header/single TSBK, status dibits and null padding + private static final int DATA_BLOCK_DIBITS = 108; //98 dibits, 3 status dibits and null padding, to next frame + private static final int SYNC_DIBITS = 24; + private static final int SYNC_FRAMES = 8; //Valid frames preceding the failed frame so that the NAC is tracked + private static final int NEXT_FRAMES = 3; //Valid frames following the failed frame + + private final Random mRandom = new Random(1); + + /** + * Test: control case with all frames detected. + */ + @Test + void testNoFailedFrame() + { + assertEquals(SYNC_FRAMES + NEXT_FRAMES, process(Collections.emptyList())); + } + + /** + * Test: a missed AMBTC PDU, fully assembled as a place holder, dispatches all of its data blocks without the + * 'message under assembly is full' error and without losing the next frame, for each supported block count. + */ + @Test + void testPlaceHolderResolvedAsPDU() + { + for(int blocks = 1; blocks <= 5; blocks++) + { + assertEquals(SYNC_FRAMES + NEXT_FRAMES, process(getPDU(blocks, blocks)), blocks + " data blocks"); + } + } + + /** + * Test: a missed AMBTC PDU that's truncated by the next frame, where the header indicates more data blocks than + * were assembled, doesn't capture the next frame as data blocks. + */ + @Test + void testPlaceHolderResolvedAsTruncatedPDU() + { + assertEquals(SYNC_FRAMES + NEXT_FRAMES, process(getPDU(3, 1))); + } + + /** + * Test: a missed corrupt TSBK, where the failed CRC can't indicate the last block, doesn't capture the next frame + * as the second TSBK of the sequence. + */ + @Test + void testPlaceHolderResolvedAsCorruptTSBK() + { + List noise = new ArrayList<>(); + + for(int x = 0; x < FRAME_DIBITS; x++) + { + noise.add(Dibit.values()[mRandom.nextInt(4)]); + } + + assertEquals(SYNC_FRAMES + NEXT_FRAMES, process(noise)); + } + + /** + * Processes valid TSBK frames, the frame that fails sync detection, and then more valid TSBK frames. + * @param failedFrame dibits + * @return count of valid TSBK messages dispatched by the framer. + */ + private int process(List failedFrame) + { + int[] validCount = new int[1]; + P25P1MessageFramer framer = new P25P1MessageFramer(); + framer.setListener((IMessage message) -> { + if(message instanceof TSBKMessage tsbk && tsbk.isValid()) + { + validCount[0]++; + } + }); + framer.start(); + + for(int x = 0; x < SYNC_FRAMES; x++) + { + process(framer, getFrame(TSBK_DUID, getTSBK(), FRAME_DIBITS), true); + } + + process(framer, failedFrame, false); + + for(int x = 0; x < NEXT_FRAMES; x++) + { + process(framer, getFrame(TSBK_DUID, getTSBK(), FRAME_DIBITS), true); + } + + //A message is dispatched on the dibit following the message, so flush the final frame + process(framer, Collections.nCopies(FRAME_DIBITS, Dibit.D00_PLUS_1), false); + + return validCount[0]; + } + + /** + * Processes the frame dibits, optionally indicating a sync detection after the final sync pattern dibit. + */ + private static void process(P25P1MessageFramer framer, List dibits, boolean detectSync) + { + for(int x = 0; x < dibits.size(); x++) + { + framer.process(dibits.get(x)); + + if(detectSync && x == SYNC_DIBITS - 1) + { + framer.syncDetected(); + } + } + } + + /** + * Creates a single block TSBK with the last block indicator set. + */ + private BitSet getTSBK() + { + BitSet tsbk = new BitSet(); + tsbk.set(0); + P25P1DecoderLSMTest.setValue(tsbk, 2, 6, OPCODE_RFSS_STATUS_BROADCAST); + + for(int x = 16; x < 80; x++) + { + tsbk.set(x, mRandom.nextBoolean()); + } + + return tsbk; + } + + /** + * Creates an AMBTC PDU frame. + * @param indicatedBlocks count of blocks to follow in the header + * @param actualBlocks count of data blocks in the frame. + */ + private List getPDU(int indicatedBlocks, int actualBlocks) + { + BitSet header = new BitSet(); + header.set(2); //Outbound + P25P1DecoderLSMTest.setValue(header, 3, 5, PDU_FORMAT_AMBTC); + P25P1DecoderLSMTest.setValue(header, 49, 7, indicatedBlocks); + + List data = getData(header); + + for(int x = 0; x < actualBlocks; x++) + { + data.addAll(P25P1DecoderLSMTest.encodeTSBK(getTSBK())); + } + + return addStatusDibits(data, FRAME_DIBITS + actualBlocks * DATA_BLOCK_DIBITS); + } + + /** + * Creates a frame with the sync, NID, and encoded message. + */ + private static List getFrame(int duid, BitSet message, int length) + { + return addStatusDibits(getData(duid, message), length); + } + + private static List getData(BitSet pduHeader) + { + return getData(12, pduHeader); + } + + private static List getData(int duid, BitSet message) + { + List data = new ArrayList<>(); + P25P1DecoderLSMTest.addBits(data, P25P1SyncDetector.SYNC_PATTERN, 48); + P25P1DecoderLSMTest.addBits(data, BCH_63_16_23_P25.encode(NAC, duid) << 1, 64); //64th (parity) bit is zero + data.addAll(P25P1DecoderLSMTest.encodeTSBK(message)); + return data; + } + + /** + * Inserts a status dibit at every 36th dibit and null pads the frame to the length. + */ + private static List addStatusDibits(List data, int length) + { + List frame = new ArrayList<>(); + int pointer = 0; + + for(int x = 1; x <= length; x++) + { + if(x % 36 == 0) + { + frame.add(Dibit.D01_PLUS_3); //Outbound (repeater) status + } + else + { + frame.add(pointer < data.size() ? data.get(pointer++) : Dibit.D00_PLUS_1); + } + } + + return frame; + } +}