diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 776b90a..a14be48 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.7'] + python: ['3.7', '3.8', '3.9'] os: ['ubuntu-latest'] steps: - uses: actions/checkout@master diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 01906bf..38e9c26 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,12 @@ ==================== pyais CHANGELOG ==================== +------------------------------------------------------------------------------- + Version 1.6.2 2 May 2021 +------------------------------------------------------------------------------- + +* Improves `decode_msg` by adding meaningful error messages + ------------------------------------------------------------------------------- Version 1.6.0 2 May 2021 ------------------------------------------------------------------------------- diff --git a/pyais/__init__.py b/pyais/__init__.py index e500749..abe1c88 100644 --- a/pyais/__init__.py +++ b/pyais/__init__.py @@ -4,7 +4,7 @@ __license__ = 'MIT' -__version__ = '1.6.1' +__version__ = '1.6.2' __all__ = ( 'decode_msg', diff --git a/pyais/decode.py b/pyais/decode.py index 634c66b..c067c2b 100644 --- a/pyais/decode.py +++ b/pyais/decode.py @@ -1,7 +1,7 @@ from functools import partial -from typing import Any, Dict, Union +from typing import Any, Dict, Union, List -import bitarray # type: ignore +import bitarray from pyais import messages from pyais.constants import ( @@ -14,7 +14,7 @@ StationIntervals, NavAid ) -from pyais.exceptions import UnknownMessageException +from pyais.exceptions import UnknownMessageException, MissingMultipartMessageException, TooManyMessagesException from pyais.util import get_int, encode_bin_as_ascii6, get_mmsi @@ -715,8 +715,24 @@ def decode_msg(*args: Union[str, bytes]) -> Dict[str, Any]: # Make everything bytes message_as_bytes = tuple(msg.encode('utf-8') if isinstance(msg, str) else msg for msg in args) - # Create temporary messages - temp = [messages.NMEAMessage(m) for m in message_as_bytes] + # Convert bytes into NMEAMessage and remember fragment_count and fragment_numbers + temp: List[messages.NMEAMessage] = [] + frags: List[int] = [] + frag_cnt: int = 1 + for msg in message_as_bytes: + nmea = messages.NMEAMessage(msg) + temp.append(nmea) + frags.append(nmea.fragment_number) + frag_cnt = nmea.fragment_count + + # Make sure provided parts assemble a single (multiline message) + if len(message_as_bytes) > frag_cnt: + raise TooManyMessagesException(f"Got {len(message_as_bytes)} messages, but fragment count is {frag_cnt}") + + # Make sure all parts of a multipart message are provided + diff = [x for x in range(1, frag_cnt + 1) if x not in frags] + if len(diff): + raise MissingMultipartMessageException(f"Missing fragment numbers: {diff}") # Assemble temporary messages final = messages.NMEAMessage.assemble_from_iterable(temp) diff --git a/pyais/exceptions.py b/pyais/exceptions.py index bfbdf42..0df158f 100644 --- a/pyais/exceptions.py +++ b/pyais/exceptions.py @@ -6,3 +6,11 @@ class InvalidNMEAMessageException(Exception): class UnknownMessageException(Exception): """Message not supported yet""" pass + + +class MissingMultipartMessageException(Exception): + """Multipart message with missing parts provided""" + + +class TooManyMessagesException(Exception): + """Too many messages""" diff --git a/pyais/messages.py b/pyais/messages.py index 1749f74..24ebb5d 100644 --- a/pyais/messages.py +++ b/pyais/messages.py @@ -1,7 +1,7 @@ import json from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union -from bitarray import bitarray # type: ignore +from bitarray import bitarray from pyais.ais_types import AISType from pyais.constants import TalkerID @@ -179,7 +179,7 @@ def __str__(self) -> str: def __getitem__(self, item: str) -> Union[int, str, bytes, bitarray]: if isinstance(item, str): try: - return getattr(self, item) + return getattr(self, item) # type: ignore except AttributeError: raise KeyError(item) else: diff --git a/pyais/util.py b/pyais/util.py index 8129621..5ef6529 100644 --- a/pyais/util.py +++ b/pyais/util.py @@ -4,7 +4,7 @@ from operator import xor from typing import Any, Generator, Hashable, TYPE_CHECKING, Callable -from bitarray import bitarray # type: ignore +from bitarray import bitarray if TYPE_CHECKING: BaseDict = OrderedDict[Hashable, Any] @@ -43,7 +43,7 @@ def decode_into_bit_array(data: bytes) -> bitarray: # Convert 8 bit binary to 6 bit binary c -= 0x30 if (c < 0x60) else 0x38 c &= 0x3F - bit_arr += f'{c:06b}' + bit_arr += bitarray(f'{c:06b}') return bit_arr diff --git a/tests/test_decode_raw.py b/tests/test_decode_raw.py index 54770b7..781e2c5 100644 --- a/tests/test_decode_raw.py +++ b/tests/test_decode_raw.py @@ -1,7 +1,7 @@ import unittest from pyais import decode_msg -from pyais.exceptions import InvalidNMEAMessageException +from pyais.exceptions import InvalidNMEAMessageException, MissingMultipartMessageException, TooManyMessagesException class TestDecode(unittest.TestCase): @@ -64,9 +64,36 @@ def test_decode_multiline_message(self): self.assertEqual(decoded["shipname"], "NORDIC HAMBURG") self.assertEqual(decoded["destination"], "CTT-LAYBY") - decoded = decode_msg( - b'!AIVDM,2,1,1,A,538CQ>02A;h?D9QC800pu8@T>0P4l9E8L0000017Ah:;;5r50Ahm5;C0,0*07', - b'!AIVDM,2,2,1,A,F@V@00000000000,2*35', - b'!AIVDM,2,1,9,A,538CQ>02A;h?D9QC800pu8@T>0P4l9E8L0000017Ah:;;5r50Ahm5;C0,0*0F', - b'!AIVDM,2,2,9,A,F@V@00000000000,2*3D', + def test_too_many_messages(self): + with self.assertRaises(TooManyMessagesException) as err: + decode_msg( + b'!AIVDM,2,1,1,A,538CQ>02A;h?D9QC800pu8@T>0P4l9E8L0000017Ah:;;5r50Ahm5;C0,0*07', + b'!AIVDM,2,2,1,A,F@V@00000000000,2*35', + b'!AIVDM,2,1,9,A,538CQ>02A;h?D9QC800pu8@T>0P4l9E8L0000017Ah:;;5r50Ahm5;C0,0*0F', + b'!AIVDM,2,2,9,A,F@V@00000000000,2*3D', + ) + self.assertEqual(str(err.exception), "Got 4 messages, but fragment count is 2") + + def test_multipart_error_message(self): + """Refer to issue #37""" + msg_1 = "!AIVDM,2,1,0,A,539p4OT00000@7W3K@08ThiLE8@E:0000000001S0h9135Pl?0R0C@UDQp00,0*68" + msg_2 = "!AIVDM,2,2,0,A,00000000000,2*24" + + with self.assertRaises(MissingMultipartMessageException) as err: + decode_msg(msg_1) + self.assertEqual(str(err.exception), "Missing fragment numbers: [2]") + + with self.assertRaises(MissingMultipartMessageException) as err: + decode_msg(msg_2) + self.assertEqual(str(err.exception), "Missing fragment numbers: [1]") + + with self.assertRaises(MissingMultipartMessageException) as err: + decode_msg( + "!AIVDM,3,2,0,A,539p4OT00000@7W3K@08ThiLE8@E:0000000001S0h9135Pl?0R0C@UDQp00,0*68", + ) + self.assertEqual(str(err.exception), "Missing fragment numbers: [1, 3]") + + decode_msg( + msg_1, + msg_2 )