From b188ca6ee98cf44aab39a0f3ce95e1ca2592aa3c Mon Sep 17 00:00:00 2001 From: Leon Morten Richter Date: Sun, 11 Oct 2026 12:58:15 +0200 Subject: [PATCH 1/4] fix: out of order message assembly --- pyais/stream.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pyais/stream.py b/pyais/stream.py index d29ffad..5f4b144 100644 --- a/pyais/stream.py +++ b/pyais/stream.py @@ -183,16 +183,23 @@ def _assemble_messages(self) -> Generator[NMEAMessage, None, None]: # A unique stream is identified by the following tuple slot = (seq_id, msg.channel, msg.talker_id, msg.type, msg.frag_cnt) - if slot not in buffer: - # Create a new array in the buffer that has enough space for all fragments - buffer[slot] = [None, ] * max(msg.fragment_count, 0xff) - - buffer[slot][msg.frag_num - 1] = msg - msg_parts = buffer[slot][0:msg.fragment_count] + if msg.frag_num == 1: + # start (overwrites any pending) + buffer[slot] = [msg] + [None] * (msg.frag_cnt - 1) + elif slot in buffer: + if msg.frag_num == sum(p is not None for p in buffer[slot]) + 1: + # strictly contiguous + buffer[slot][msg.frag_num - 1] = msg + else: + # drop out-of-order fragment + continue + else: + # orphan fragment (no start seen) + continue # Check if all fragments are found - not_none_parts = [m for m in msg_parts if m is not None] - if not_none_parts and len(not_none_parts) == msg.fragment_count: + not_none_parts = [m for m in buffer[slot] if m is not None] + if len(not_none_parts) == msg.frag_cnt: msg = NMEAMessage.assemble_from_iterable(not_none_parts) yield self.__insert_wrapper_msg(msg) del buffer[slot] From 96d893b9e41e440cfdc5b8adae9c20d8add14176 Mon Sep 17 00:00:00 2001 From: Leon Morten Richter Date: Sun, 11 Oct 2026 13:19:16 +0200 Subject: [PATCH 2/4] fix: out of order message assembly --- pyais/stream.py | 2 +- tests/multi.txt | 170 ++++++++------------------------------ tests/test_file_stream.py | 59 ++++++------- 3 files changed, 60 insertions(+), 171 deletions(-) diff --git a/pyais/stream.py b/pyais/stream.py index 5f4b144..fa44ef5 100644 --- a/pyais/stream.py +++ b/pyais/stream.py @@ -187,7 +187,7 @@ def _assemble_messages(self) -> Generator[NMEAMessage, None, None]: # start (overwrites any pending) buffer[slot] = [msg] + [None] * (msg.frag_cnt - 1) elif slot in buffer: - if msg.frag_num == sum(p is not None for p in buffer[slot]) + 1: + if msg.frag_num == sum(p is not None for p in buffer[slot]) + 1: # strictly contiguous buffer[slot][msg.frag_num - 1] = msg else: diff --git a/tests/multi.txt b/tests/multi.txt index a2e70ba..f782522 100644 --- a/tests/multi.txt +++ b/tests/multi.txt @@ -1,134 +1,36 @@ -#fragments = [] -#seen = set() -#for sentence_type in ("VDM", "VDO"): -# for channel in ("A", "B"): -# for i in range(10): -# key = str(i) + channel + sentence_type + "X" -# mmsi = 123456789 + i * 10 + ord(channel) + ord(sentence_type[0]) + ord(sentence_type[1]) + ord(sentence_type[2]) -# assert mmsi not in seen -# seen.add(mmsi) -# msg = MessageType8.create(mmsi=mmsi, data=b"".join(key.encode("ascii") for _ in range(20))) -# fragments.extend(encode_msg(msg, seq_id=i, radio_channel=channel, sentence_type=sentence_type)) -# -#random.shuffle(fragments) -#print("\n".join(fragments)) -!AIVDO,3,3,1,A,F351ETA?F351ETA?F351ETA?F351ETA?F351ETA?F351ETA?,0*15 -!AIVDM,3,3,7,B,F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=,0*12 -!AIVDM,3,3,2,B,F392ETA=F392ETA=F392ETA=F392ETA=F392ETA=F392ETA=,0*17 -!AIVDO,3,3,8,B,F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?,0*1F -!AIVDO,3,3,2,B,F392ETA?F392ETA?F392ETA?F392ETA?F392ETA?F392ETA?,0*15 -!AIVDM,3,3,8,B,F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=,0*1D -!AIVDM,3,2,6,A,ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=,0*1C -!AIVDM,3,2,9,A,ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=,0*0F -!AIVDO,3,2,4,B,ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?,0*14 -!AIVDO,3,2,7,B,ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?,0*1B -!AIVDM,3,1,1,A,81mg>Ah00351ETA=F351ETA=F351ETA=F351ETA=F351ETA=F351ETA=F351,0*36 -!AIVDM,3,2,7,B,ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=,0*19 -!AIVDM,3,3,6,A,F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=,0*10 -!AIVDM,3,2,3,B,ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=,0*6D -!AIVDO,3,2,2,B,ETA?F392ETA?F392ETA?F392ETA?F392ETA?F392ETA?F392ETA?F392ETA?,0*6A -!AIVDO,3,1,0,A,81mg>?h00311ETA?F311ETA?F311ETA?F311ETA?F311ETA?F311ETA?F311,0*4F -!AIVDM,3,3,6,B,F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=,0*13 -!AIVDM,3,1,0,A,81mg>?@00311ETA=F311ETA=F311ETA=F311ETA=F311ETA=F311ETA=F311,0*65 -!AIVDO,3,1,8,B,81mg>T0003Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2,0*14 -!AIVDO,3,2,9,B,ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?,0*0D -!AIVDO,3,3,4,B,F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?,0*13 -!AIVDM,3,1,2,A,81mg>D@00391ETA=F391ETA=F391ETA=F391ETA=F391ETA=F391ETA=F391,0*14 -!AIVDM,3,1,6,A,81mg>N@003I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1ETA=F3I1,0*6A -!AIVDO,3,2,7,A,ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?,0*1B -!AIVDM,3,2,5,B,ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=,0*13 -!AIVDO,3,1,6,A,81mg>Nh003I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1,0*40 -!AIVDO,3,2,5,B,ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?,0*11 -!AIVDO,3,3,0,B,F312ETA?F312ETA?F312ETA?F312ETA?F312ETA?F312ETA?,0*17 -!AIVDO,3,3,7,A,F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?,0*13 -!AIVDO,3,2,6,A,ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?,0*1E -!AIVDM,3,3,2,A,F391ETA=F391ETA=F391ETA=F391ETA=F391ETA=F391ETA=,0*14 -!AIVDO,3,3,2,A,F391ETA?F391ETA?F391ETA?F391ETA?F391ETA?F391ETA?,0*16 -!AIVDO,3,1,2,A,81mg>Dh00391ETA?F391ETA?F391ETA?F391ETA?F391ETA?F391ETA?F391,0*3E -!AIVDM,3,2,0,B,ETA=F312ETA=F312ETA=F312ETA=F312ETA=F312ETA=F312ETA=F312ETA=,0*62 -!AIVDM,3,3,3,A,F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=,0*15 -!AIVDO,3,1,5,A,81mg>L@003E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1,0*65 -!AIVDM,3,3,5,A,F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=,0*13 -!AIVDO,3,1,4,A,81mg>Ih003A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1,0*4D -!AIVDM,3,3,4,A,F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=,0*12 -!AIVDO,3,2,0,B,ETA?F312ETA?F312ETA?F312ETA?F312ETA?F312ETA?F312ETA?F312ETA?,0*60 -!AIVDM,3,2,8,A,ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=,0*0A -!AIVDO,3,1,0,B,81mg>@000312ETA?F312ETA?F312ETA?F312ETA?F312ETA?F312ETA?F312,0*68 -!AIVDM,3,2,5,A,ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=,0*13 -!AIVDO,3,2,2,A,ETA?F391ETA?F391ETA?F391ETA?F391ETA?F391ETA?F391ETA?F391ETA?,0*6A -!AIVDO,3,2,8,A,ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?,0*08 -!AIVDM,3,1,9,A,81mg>Uh003U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1,0*4A -!AIVDM,3,3,7,A,F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=,0*11 -!AIVDM,3,3,0,A,F311ETA=F311ETA=F311ETA=F311ETA=F311ETA=F311ETA=,0*16 -!AIVDO,3,3,1,B,F352ETA?F352ETA?F352ETA?F352ETA?F352ETA?F352ETA?,0*16 -!AIVDM,3,1,9,B,81mg>V0003U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2,0*11 -!AIVDM,3,1,1,B,81mg>B000352ETA=F352ETA=F352ETA=F352ETA=F352ETA=F352ETA=F352,0*6D -!AIVDM,3,1,2,B,81mg>DP00392ETA=F392ETA=F392ETA=F392ETA=F392ETA=F392ETA=F392,0*04 -!AIVDM,3,2,6,B,ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=,0*1C -!AIVDM,3,2,4,A,ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=,0*16 -!AIVDO,3,2,4,A,ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?,0*14 -!AIVDO,3,2,6,B,ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?,0*1E -!AIVDM,3,3,8,A,F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=,0*1E -!AIVDM,3,1,6,B,81mg>NP003I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2ETA=F3I2,0*7A -!AIVDM,3,3,9,A,F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=F3U1ETA=,0*1F -!AIVDO,3,3,5,B,F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?,0*12 -!AIVDM,3,2,1,A,ETA=F351ETA=F351ETA=F351ETA=F351ETA=F351ETA=F351ETA=F351ETA=,0*67 -!AIVDO,3,2,1,B,ETA?F352ETA?F352ETA?F352ETA?F352ETA?F352ETA?F352ETA?F352ETA?,0*65 -!AIVDM,3,2,7,A,ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=,0*19 -!AIVDM,3,1,3,B,81mg>G0003=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2,0*62 -!AIVDM,3,1,4,B,81mg>IP003A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2,0*77 -!AIVDO,3,1,2,B,81mg>E000392ETA?F392ETA?F392ETA?F392ETA?F392ETA?F392ETA?F392,0*67 -!AIVDO,3,3,4,A,F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?F3A1ETA?,0*10 -!AIVDO,3,2,0,A,ETA?F311ETA?F311ETA?F311ETA?F311ETA?F311ETA?F311ETA?F311ETA?,0*60 -!AIVDO,3,3,6,A,F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?F3I1ETA?,0*12 -!AIVDO,3,1,8,A,81mg>Sh003Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1,0*4B -!AIVDO,3,3,8,A,F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?F3Q1ETA?,0*1C -!AIVDO,3,2,3,B,ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?,0*6F -!AIVDO,3,1,9,B,81mg>VP003U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2,0*73 -!AIVDM,3,1,5,A,81mg>Kh003E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1ETA=F3E1,0*48 -!AIVDO,3,3,7,B,F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?,0*10 -!AIVDO,3,3,9,B,F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?F3U2ETA?,0*1E -!AIVDO,3,1,3,B,81mg>GP003=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2,0*00 -!AIVDO,3,3,0,A,F311ETA?F311ETA?F311ETA?F311ETA?F311ETA?F311ETA?,0*14 -!AIVDO,3,3,6,B,F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?,0*11 -!AIVDO,3,1,6,B,81mg>O0003I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2ETA?F3I2,0*19 -!AIVDM,3,2,2,A,ETA=F391ETA=F391ETA=F391ETA=F391ETA=F391ETA=F391ETA=F391ETA=,0*68 -!AIVDM,3,1,7,B,81mg>Q0003M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2ETA=F3M2,0*00 -!AIVDO,3,1,3,A,81mg>G@003=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1,0*10 -!AIVDO,3,3,9,A,F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?,0*1D -!AIVDO,3,1,7,A,81mg>Q@003M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1ETA?F3M1,0*72 -!AIVDM,3,3,4,B,F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=,0*11 -!AIVDM,3,3,1,A,F351ETA=F351ETA=F351ETA=F351ETA=F351ETA=F351ETA=,0*17 -!AIVDO,3,1,9,A,81mg>V@003U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1,0*63 -!AIVDM,3,1,8,A,81mg>S@003Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1ETA=F3Q1,0*61 -!AIVDM,3,3,1,B,F352ETA=F352ETA=F352ETA=F352ETA=F352ETA=F352ETA=,0*14 -!AIVDO,3,1,1,B,81mg>BP00352ETA?F352ETA?F352ETA?F352ETA?F352ETA?F352ETA?F352,0*0F -!AIVDO,3,1,1,A,81mg>B@00351ETA?F351ETA?F351ETA?F351ETA?F351ETA?F351ETA?F351,0*1F -!AIVDM,3,2,3,A,ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=,0*6D -!AIVDO,3,2,1,A,ETA?F351ETA?F351ETA?F351ETA?F351ETA?F351ETA?F351ETA?F351ETA?,0*65 -!AIVDM,3,2,8,B,ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=,0*0A -!AIVDM,3,3,5,B,F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=,0*10 -!AIVDM,3,2,2,B,ETA=F392ETA=F392ETA=F392ETA=F392ETA=F392ETA=F392ETA=F392ETA=,0*68 -!AIVDM,3,3,9,B,F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=,0*1C -!AIVDO,3,3,5,A,F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?,0*11 -!AIVDM,3,1,4,A,81mg>I@003A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1ETA=F3A1,0*67 -!AIVDO,3,1,4,B,81mg>J0003A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2ETA?F3A2,0*16 -!AIVDO,3,1,7,B,81mg>QP003M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2ETA?F3M2,0*62 -!AIVDM,3,1,3,A,81mg>Fh003=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1ETA=F3=1,0*3B -!AIVDM,3,1,8,B,81mg>SP003Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2ETA=F3Q2,0*71 -!AIVDM,3,2,4,B,ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=F3A2ETA=,0*16 -!AIVDM,3,2,9,B,ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=F3U2ETA=,0*0F -!AIVDO,3,1,5,B,81mg>LP003E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2ETA?F3E2,0*75 -!AIVDO,3,3,3,A,F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?,0*17 -!AIVDM,3,2,0,A,ETA=F311ETA=F311ETA=F311ETA=F311ETA=F311ETA=F311ETA=F311ETA=,0*62 -!AIVDM,3,2,1,B,ETA=F352ETA=F352ETA=F352ETA=F352ETA=F352ETA=F352ETA=F352ETA=,0*67 -!AIVDM,3,1,5,B,81mg>L0003E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2ETA=F3E2,0*17 -!AIVDM,3,1,0,B,81mg>?P00312ETA=F312ETA=F312ETA=F312ETA=F312ETA=F312ETA=F312,0*75 -!AIVDO,3,2,5,A,ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?F3E1ETA?,0*11 -!AIVDO,3,2,3,A,ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?F3=1ETA?,0*6F -!AIVDO,3,2,9,A,ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?F3U1ETA?,0*0D -!AIVDM,3,3,3,B,F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=F3=2ETA=,0*16 -!AIVDM,3,1,7,A,81mg>Ph003M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1ETA=F3M1,0*59 -!AIVDO,3,2,8,B,ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?F3Q2ETA?,0*08 -!AIVDO,3,3,3,B,F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?F3=2ETA?,0*14 -!AIVDM,3,3,0,B,F312ETA=F312ETA=F312ETA=F312ETA=F312ETA=F312ETA=,0*15 +!AIVDM,2,2,1,A,rbb,0*50 +!AIVDM,2,1,1,A,8h30otA?0@580p1kfH00,2*2C + +# This is is in order +\g:2-10-4242*6E\!AIVDM,9,1,7,A,55?MbV02,0*5D +\g:3-10-4242*6F\!AIVDM,9,2,7,A,;H;shRMDH@jNOvN20S8,0*7F", - b"!AIVDM,1,1,,B,100h00PP0@PHFV`Mg5gTH?vNPUIp,0*3B", - b"!AIVDM,2,1,1,A,55?MbV02;H;sAp:;R2APP08:c,0*0E", - b"!BSVDM,1,1,,A,15Mj23`PB`o=Of>KjvnJg8PT0L2R,0*7E", + b'!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23', + b'!AIVDM,1,1,,A,133sVfPP00PD>hRMDH@jNOvN20S8,0*7F', + b'!AIVDM,1,1,,B,100h00PP0@PHFV`Mg5gTH?vNPUIp,0*3B', + b'!AIVDM,1,1,,B,23?up2001gGRju>Ap:;R2APP08:c,0*0E', + b'!BSVDM,1,1,,A,15Mj23`PB`o=Of>KjvnJg8PT0L2R,0*7E', b'!SAVDM,2,1,4,A,55Mub7P00001L@;SO7TI8DDltqB222222222220O0000067<0620@jhQDTVG,0*43', b'!SAVDM,2,2,4,A,30H88888880,2*49', b'!SAVDM,1,1,,B,35Mj2p001qo@5tVKLBWmIDJT01:@,0*33', @@ -69,35 +68,27 @@ def test_nmea_sorter_unsorted(self): def test_sort_with_different_msg_ids(self): msgs = [ - b"!AIVDM,2,2,9,B,888888888888880,2*2E", + b"!AIVDM,2,2,9,B,888888888888880,2*2E", # missing first frag b"!AIVDM,2,1,9,B,53nFBv01SJB6U30A2hCDhD`888,0*4D", + b"!AIVDM,2,2,8,A,88888888880,2*2C", # missing first frag + b"!AIVDM,2,1,1,A,55?MbV02;H;sB6U30A2hCDhD`888,0*4D", # never completes b'!SAVDM,2,2,4,A,30H88888880,2*49', - b"!AIVDM,2,2,1,A,88888888880,2*25", - b"!AIVDM,4,4,1,A,88888888880,2*25", - b"!AIVDM,4,3,1,A,88888888880,2*25", - b"!AIVDM,4,2,1,A,88888888880,2*25", - b"!AIVDM,4,1,1,A,88888888880,2*25", + b"!AIVDM,2,2,1,A,88888888880,2*25", # missing first frag + b"!AIVDM,4,4,1,A,88888888880,2*25", # missing first frag + b"!AIVDM,4,3,1,A,88888888880,2*25", # missing first frag + b"!AIVDM,4,2,1,A,88888888880,2*25", # missing first frag + b"!AIVDM,4,1,1,A,88888888880,2*25", # missing first frag ] expected = [ - b"!AIVDM,2,1,9,B,53nFBv01SJB6U30A2hCDhD`888,0*4D", - b"!AIVDM,2,2,8,A,88888888880,2*2C", b'!SAVDM,2,1,4,A,55Mub7P00001L@;SO7TI8DDltqB222222222220O0000067<0620@jhQDTVG,0*43', b'!SAVDM,2,2,4,A,30H88888880,2*49', - b"!AIVDM,2,1,1,A,55?MbV02;H;s Date: Sun, 11 Oct 2026 13:25:50 +0200 Subject: [PATCH 3/4] chore: changelog --- CHANGELOG.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 30eea66..aeccedc 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -20,6 +20,11 @@ counts and numbers * tag blocks with a malformed checksum (missing, repeated `*` or non-hex) are treated as invalid +* stricter multi-fragment message assembly + * messages are only assembled from fragments received in order, + starting with fragment 1 (matches libais and gpsd behavior) + * orphaned fragments (no fragment 1 seen) and out-of-order fragments + are dropped instead of being buffered or merged ------------------------------------------------------------------------------- Version 3.3.0 04 Oct 2026 ------------------------------------------------------------------------------- From aed78415c55ff3277b8cdd71428bffd0aed70bb2 Mon Sep 17 00:00:00 2001 From: Leon Morten Richter Date: Sun, 11 Oct 2026 13:54:18 +0200 Subject: [PATCH 4/4] chore: add a more complex reassembly test case --- tests/multi.txt | 20 ++++++++++++++++++++ tests/test_file_stream.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/multi.txt b/tests/multi.txt index f782522..2f85237 100644 --- a/tests/multi.txt +++ b/tests/multi.txt @@ -2,6 +2,26 @@ !AIVDM,2,1,1,A,8h30otA?0@580p1kfH00,2*2C +# same sentence received twice +!AIVDM,2,1,1,A,8h30otA?0@580p1kfH00,2*2C + +# the first fragment of A was lost +!AIVDM,2,1,1,B,8h30otA?0@580p1kfH00,2*2C +!AIVDM,2,2,1,B,Ge31BoGt>580p1kfH00,2*2C + +# interleaving keys +!AIVDM,2,1,1,A,8h30otA?0@580p1kfH00,2*2C +!AIVDM,2,2,1,A,Ge31BoGt>580p1kfH00,2*2C +!AIVDM,2,2,3,A,Ge31BoGt>580p1kfH00,2*2C +!AIVDM,2,2,4,B,Ge31BoGt>580p1kfH00,2*2C + # This is is in order \g:2-10-4242*6E\!AIVDM,9,1,7,A,55?MbV02,0*5D \g:3-10-4242*6F\!AIVDM,9,2,7,A,;H;s