Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------------------------------------------------------------------------
Expand Down
23 changes: 15 additions & 8 deletions pyais/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
190 changes: 56 additions & 134 deletions tests/multi.txt
Original file line number Diff line number Diff line change
@@ -1,134 +1,56 @@
#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,<?mr4SdWwnh2>rbb,0*50
!AIVDM,2,1,1,A,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,2,1,A,Ge31BoGt>580p1kfH00,2*2C

# same sentence received twice
!AIVDM,2,1,1,A,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,1,1,A,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,2,1,A,Ge31BoGt>580p1kfH00,2*2C

# the first fragment of A was lost
!AIVDM,2,1,1,B,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,2,1,A,Ge31BoGt>580p1kfH00,2*2C
!AIVDM,2,2,1,B,Ge31BoGt>580p1kfH00,2*2C

# interleaving keys
!AIVDM,2,1,1,A,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,1,1,B,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,1,3,A,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,1,4,B,8h30otA?0@<o;NPPGeCC3oT3m5<UI0QmD00o;NPP,0*0F
!AIVDM,2,2,1,B,Ge31BoGt>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<HtK,0*6A
\g:4-10-4242*68\!AIVDM,9,3,7,A,R20EHE:0,0*09
\g:5-10-4242*69\!AIVDM,9,4,7,A,@T4@Dn22,0*56
\g:6-10-4242*6A\!AIVDM,9,5,7,A,22222216,0*1A
\g:7-10-4242*6B\!AIVDM,9,6,7,A,L961O5Gf,0*37
\g:8-10-4242*64\!AIVDM,9,7,7,A,0NSQEp6C,0*23
\g:9-10-4242*65\!AIVDM,9,8,7,A,lRp88888,0*66
\g:10-10-4242*5D\!AIVDM,9,9,7,A,8888880,2*23

# This is in reverse order - should never work
\g:10-10-4242*5D\!AIVDM,9,9,7,A,8888880,2*23
\g:9-10-4242*65\!AIVDM,9,8,7,A,lRp88888,0*66
\g:8-10-4242*64\!AIVDM,9,7,7,A,0NSQEp6C,0*23
\g:7-10-4242*6B\!AIVDM,9,6,7,A,L961O5Gf,0*37
\g:6-10-4242*6A\!AIVDM,9,5,7,A,22222216,0*1A
\g:5-10-4242*69\!AIVDM,9,4,7,A,@T4@Dn22,0*56
\g:4-10-4242*68\!AIVDM,9,3,7,A,R20EHE:0,0*09
\g:3-10-4242*6F\!AIVDM,9,2,7,A,;H;s<HtK,0*6A
\g:2-10-4242*6E\!AIVDM,9,1,7,A,55?MbV02,0*5D

# First frags swapped
\g:3-10-4242*6F\!AIVDM,9,2,7,A,;H;s<HtK,0*6A
\g:2-10-4242*6E\!AIVDM,9,1,7,A,55?MbV02,0*5D
\g:4-10-4242*68\!AIVDM,9,3,7,A,R20EHE:0,0*09
\g:5-10-4242*69\!AIVDM,9,4,7,A,@T4@Dn22,0*56
\g:6-10-4242*6A\!AIVDM,9,5,7,A,22222216,0*1A
\g:7-10-4242*6B\!AIVDM,9,6,7,A,L961O5Gf,0*37
\g:8-10-4242*64\!AIVDM,9,7,7,A,0NSQEp6C,0*23
\g:9-10-4242*65\!AIVDM,9,8,7,A,lRp88888,0*66
\g:10-10-4242*5D\!AIVDM,9,9,7,A,8888880,2*23
59 changes: 23 additions & 36 deletions tests/test_file_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,13 @@ def test_nmea_sorter_unsorted(self):
output = []
for msg in sorter:
output += msg.raw.splitlines()

self.assertEqual(output, [
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,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
b"!AIVDM,2,2,1,A,88888888880,2*25",
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'!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',
Expand All @@ -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,53nFBv01SJ<thHp6220H4heHTf2222222222221?50:454o<`9QSlUDp,0*09",
b'!SAVDM,2,1,4,A,55Mub7P00001L@;SO7TI8DDltqB222222222220O0000067<0620@jhQDTVG,0*43',
b'!SAVDM,2,1,4,A,55Mub7P00001L@;SO7TI8DDltqB222222222220O0000067<0620@jhQDTVG,0*43', # second frag comes later
b'!SAVDM,1,1,,B,35Mj2p001qo@5tVKLBWmIDJT01:@,0*33',
b"!AIVDM,2,2,8,A,88888888880,2*2C",
b"!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
b"!AIVDM,2,1,8,A,56;OaD02B8EL990b221`P4v1T4pN0HDpN2222216HHN>B6U30A2hCDhD`888,0*4D",
b"!AIVDM,2,2,8,A,88888888880,2*2C", # missing first frag
b"!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C", # second frag comes later
b"!AIVDM,2,1,8,A,56;OaD02B8EL990b221`P4v1T4pN0HDpN2222216HHN>B6U30A2hCDhD`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,53nFBv01SJ<thHp6220H4heHTf2222222222221?50:454o<`9QSlUDp,0*09",
b"!AIVDM,2,2,9,B,888888888888880,2*2E",
b'!SAVDM,1,1,,B,35Mj2p001qo@5tVKLBWmIDJT01:@,0*33',
b"!AIVDM,2,1,8,A,56;OaD02B8EL990b221`P4v1T4pN0HDpN2222216HHN>B6U30A2hCDhD`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<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C",
b"!AIVDM,2,2,1,A,88888888880,2*25",
b"!AIVDM,4,1,1,A,88888888880,2*25",
b"!AIVDM,4,2,1,A,88888888880,2*25",
b"!AIVDM,4,3,1,A,88888888880,2*25",
b"!AIVDM,4,4,1,A,88888888880,2*25",
b'!AIVDM,2,1,1,A,55?MbV02;H;s<HtKR20EHE:0@T4@Dn2222222216L961O5Gf0NSQEp6ClRp8,0*1C',
b'!AIVDM,2,2,1,A,88888888880,2*25'
]

sorter = IterMessages(msgs)
Expand Down Expand Up @@ -225,16 +216,12 @@ def test_timestamp_messages(self):
def test_complex_message_reassembly(self):
"""Ensure that multi fragment messages are reassembled correctly"""
file = pathlib.Path(__file__).parent.joinpath('multi.txt')
for i, msg in enumerate(FileReaderStream(file), 1):
decoded = msg.decode()

key = decoded.data[0:6].decode("ascii")
mmsi = 123456789 + int(key[0]) * 10 + ord(key[1]) + ord(key[2]) + ord(key[3]) + ord(key[4])

assert decoded.mmsi == mmsi
assert decoded.data == b"".join(decoded.data[0:6] for _ in range(20))[:-1]
tot = 0
for msg in FileReaderStream(file):
msg.decode()
tot += 1

assert i == 40
assert tot == 8

def test_frag_num_below_250(self):
result = list(ByteStream([b"!AIVDM,2,-300,4,A,55O0W7`00001L@gCWGA2uItLth@DqtL5@F22220j1h742t0Ht0000000,0*08\n"]))
Expand Down
Loading