Skip to content

Commit

Permalink
Make NMEA messages suscribtable (#26)
Browse files Browse the repository at this point in the history
* Make NMEA messages suscribtable

* Add tests for deprecated attributes
  • Loading branch information
M0r13n authored May 2, 2021
1 parent beecc0e commit adc9757
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 61 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
====================
pyais CHANGELOG
====================
-------------------------------------------------------------------------------
Version 1.6.0 2 May 2021
-------------------------------------------------------------------------------


* Makes `NMEAMessage` subscribable
* Adds documentation on readthedocs.org
* Renames instance attributes of `NMEAMessage`:
- msg_type to type
- count to message_fragments
- index to fragment_number
- seq_id to message_id
- data to payload#
* Adds fill_bits field to NMEAMessage

-------------------------------------------------------------------------------
Version 1.4.0 6 Mar 2021
-------------------------------------------------------------------------------
Expand Down
43 changes: 42 additions & 1 deletion docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,45 @@ The current development version is available on `github
$ git clone https://github.com/M0r13n/pyais.git
$ cd pyais
$ sudo python setup.py install
$ sudo python setup.py install
## Known problems

During installation, you may encounter problems due to missing header files. The error looks like this:

````sh
...
bitarray/_bitarray.c:13:10: fatal error: Python.h: No such file or directory
13 | #include "Python.h"
| ^~~~~~~~~~
compilation terminated.
error: command 'x86_64-linux-gnu-gcc' failed with exit status 1
...
````

In order to solve this issue, you need to install header files and static libraries for python dev:

````sh
$ sudo apt install python3-dev
````

#### Installation in Visualstudio

You may encounter the error:

````sh
...
error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools
...
````

To solve this issue, you need to:

1. get the up to date buildstools from: https://visualstudio.microsoft.com/visual-cpp-build-tools/
2. install them via the included installation manager app
3. use default options during installation
4. install Pyais via pip: `pip install pyais`
77 changes: 77 additions & 0 deletions docs/messages.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,83 @@
Message interface
##################

NMEA messages
----------------

The `NMEAMessage` is the first level of abstraction during parsing/decoding.
Every message that is decoded, is transformed into a `NMEAMessage`.


Every instance of `NMEAMessage` has a fixed set of attributes::

msg = NMEAMessage(b"!AIVDM,1,1,,A,15Mj23P000G?q7fK>g:o7@1:0L3S,0*1B")

msg.ais_id # => AIS message type as :int:
msg.raw # => Raw, decoded message as :byte:
msg.talker # => Talker ID as :str:
msg.type # => Message type (VDM, VDO, etc.) :str:
msg.message_fragments # => Number of fragments (some messages need more than one, maximum generally is 9) as :int:
msg.fragment_number # => Sentence number (1 unless it is a multi-sentence message) as :int:
msg.message_id # => Optional (can be None) sequential message ID (for multi-sentence messages) as :int:
msg.channel # => The AIS channel (A or B) as :str:
msg.payload # => he encoded AIS data, using AIS-ASCII6 as :bytes:
msg.fill_bits # => unused bits at end of data (0-5) as :int:
msg.checksum # => NMEA CRC1 checksum :int:
msg.bit_array # => Payload as :bitarray:


Fields are also subscribable::

msg = NMEAMessage(b"!AIVDM,1,1,,A,15Mj23P000G?q7fK>g:o7@1:0L3S,0*1B")

msg['ais_id'] == msg.ais_id
msg['raw'] == msg.raw
# etc. ..

Every message can be transformed into a dictionary::

msg = NMEAMessage(b"!AIVDM,1,1,,A,15Mj23P000G?q7fK>g:o7@1:0L3S,0*1B")

msg.asdict() == {'ais_id': 1,
'bit_array': '000001000101011101110010000010000011100000000000000000000000010111001111111001000111101110011011001110101111001010110111000111010000000001001010000000011100000011100011',
'channel': 'A',
'checksum': 27,
'fill_bits': 0,
'fragment_number': 1,
'message_fragments': 1,
'message_id': None,
'payload': '15Mj23P000G?q7fK>g:o7@1:0L3S',
'raw': '!AIVDM,1,1,,A,15Mj23P000G?q7fK>g:o7@1:0L3S,0*1B',
'talker': 'AI',
'type': 'VDM'}

Multiline messages can be created as follows::

msg_1_part_0 = b'!AIVDM,2,1,1,A,538CQ>02A;h?D9QC800pu8@T>0P4l9E8L0000017Ah:;;5r50Ahm5;C0,0*07'
msg_1_part_1 = b'!AIVDM,2,2,1,A,F@V@00000000000,2*35'

assert NMEAMessage.assemble_from_iterable(
messages=[
NMEAMessage(msg_1_part_0),
NMEAMessage(msg_1_part_1)
]
).decode()

In order to decode a NMEA message, it is first transformed into a `AISMessage`. See the documentation below for details::

msg = NMEAMessage(b"!AIVDM,1,1,,A,15Mj23P000G?q7fK>g:o7@1:0L3S,0*1B")
ais = msg.decode()

Sometimes, you might want quick access to a serialized JSON representation of a `NMEAMessage`::

NMEAMessage(b"!AIVDM,1,1,,A,15Mj23P000G?q7fK>g:o7@1:0L3S,0*1B").decode().to_json()



AISMessage
----------------


Every `AISMessage` message has the following interface:


Expand Down
2 changes: 1 addition & 1 deletion pyais/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


__license__ = 'MIT'
__version__ = '1.5.0'
__version__ = '1.6.0'

__all__ = (
'decode_msg',
Expand Down
133 changes: 95 additions & 38 deletions pyais/messages.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import json
from typing import Any, Dict, Optional, Sequence, Tuple, Type
from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union

from bitarray import bitarray # type: ignore

from pyais.ais_types import AISType
from pyais.constants import TalkerID
from pyais.decode import decode
from pyais.exceptions import InvalidNMEAMessageException
from pyais.util import decode_into_bit_array, get_int, compute_checksum
from pyais.util import decode_into_bit_array, get_int, compute_checksum, deprecated


def validate_message(msg: bytes) -> None:
Expand Down Expand Up @@ -115,12 +115,13 @@ class NMEAMessage(object):
'ais_id',
'raw',
'talker',
'msg_type',
'count',
'index',
'seq_id',
'type',
'message_fragments',
'fragment_number',
'message_id',
'channel',
'data',
'payload',
'fill_bits',
'checksum',
'bit_array'
)
Expand All @@ -143,11 +144,11 @@ def __init__(self, raw: bytes) -> None:
# Unpack NMEA message parts
(
head,
count,
index,
seq_id,
message_fragments,
fragment_number,
message_id,
channel,
data,
payload,
checksum
) = values

Expand All @@ -156,38 +157,58 @@ def __init__(self, raw: bytes) -> None:
self.talker: TalkerID = TalkerID(talker)

# The type of message is then identified by the next 3 characters
self.msg_type: str = head[3:].decode('ascii')

# Store other important parts
self.count: int = int(count)
self.index: int = int(index)
self.seq_id: bytes = seq_id
self.channel: bytes = channel
self.data: bytes = data
self.type: str = head[3:].decode('ascii')

# Total number of fragments
self.message_fragments: int = int(message_fragments)
# Current fragment index
self.fragment_number: int = int(fragment_number)
# Optional message index for multiline messages
self.message_id: Optional[int] = int(message_id) if message_id else None
# Channel (A or B)
self.channel: str = channel.decode('ascii')
# Decoded message payload as byte string
self.payload: bytes = payload
# Fill bits (0 to 5)
self.fill_bits: int = int(chr(checksum[0]))
# Message Checksum (hex value)
self.checksum = int(checksum[2:], 16)

# Finally decode bytes into bits
self.bit_array: bitarray = decode_into_bit_array(self.data)
self.bit_array: bitarray = decode_into_bit_array(self.payload)
self.ais_id: int = get_int(self.bit_array, 0, 6)

def __str__(self) -> str:
return str(self.raw)

def __getitem__(self, item: str) -> Union[int, str, bytes, bitarray]:
if isinstance(item, str):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(item)
else:
raise TypeError(f"Index must be str, not {type(item).__name__}")

def asdict(self) -> Dict[str, Any]:
def serializable(o: object) -> Any:
if isinstance(o, bytes):
return o.decode('utf-8')
elif isinstance(o, bitarray):
return o.to01()

return o

return dict(
[
(slot, serializable(getattr(self, slot)))
for slot in self.__slots__
]
)
"""
Convert the class to dict.
@return: A dictionary that holds all fields, defined in __slots__
"""
return {
'ais_id': self.ais_id, # int
'raw': self.raw.decode('ascii'), # str
'talker': self.talker.value, # str
'type': self.type, # str
'message_fragments': self.message_fragments, # int
'fragment_number': self.fragment_number, # int
'message_id': self.message_id, # None or int
'channel': self.channel, # str
'payload': self.payload.decode('ascii'), # str
'fill_bits': self.fill_bits, # int
'checksum': self.checksum, # int
'bit_array': self.bit_array.to01(), # str
}

def __eq__(self, other: object) -> bool:
return all([getattr(self, attr) == getattr(other, attr) for attr in self.__slots__])
Expand All @@ -213,11 +234,11 @@ def assemble_from_iterable(cls, messages: Sequence["NMEAMessage"]) -> "NMEAMessa

for msg in messages:
raw += msg.raw
data += msg.data
data += msg.payload
bit_array += msg.bit_array

messages[0].raw = raw
messages[0].data = data
messages[0].payload = data
messages[0].bit_array = bit_array
return messages[0]

Expand All @@ -227,15 +248,51 @@ def is_valid(self) -> bool:

@property
def is_single(self) -> bool:
return not self.seq_id and self.index == self.count == 1
return not self.message_id and self.fragment_number == self.message_fragments == 1

@property
def is_multi(self) -> bool:
return not self.is_single

@property
def fragment_count(self) -> int:
return self.count
return self.message_fragments

@deprecated
def count(self) -> int:
"""
Only there fore legacy compatibility.
Was renamed to `message_fragments`
@return: message_fragments as int
"""
return self.message_fragments

@deprecated
def index(self) -> int:
"""
Only there fore legacy compatibility.
Was renamed to `fragment_number`
@return: fragment_number as int
"""
return self.fragment_number

@deprecated
def seq_id(self) -> Optional[int]:
"""
Only there fore legacy compatibility.
Was renamed to `message_id`
@return: message_id as int
"""
return self.message_id

@deprecated
def data(self) -> bytes:
"""
Only there fore legacy compatibility.
Was renamed to `payload`
@return: payload as sequence of bytes
"""
return self.payload

def decode(self, silent: bool = True) -> Optional["AISMessage"]:
"""
Expand Down
2 changes: 1 addition & 1 deletion pyais/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def _assemble_messages(self) -> Generator[NMEAMessage, None, None]:
elif msg.is_multi:
queue.append(msg)

if msg.index == msg.count:
if msg.fragment_number == msg.message_fragments:
yield msg.assemble_from_iterable(queue)
queue.clear()
else:
Expand Down
Loading

0 comments on commit adc9757

Please sign in to comment.