Code Monkey home page Code Monkey logo

pyais's Introduction

Logo

pyais

AIS message encoding and decoding. 100% pure Python.

PyPI license codecov downloads CI Documentation Status


Supports AIVDM/AIVDO messages. Supports single messages, files and TCP/UDP sockets. This library has been used and tested extensively in representative real-world scenarios. This includes tests with live feeds from Spire, the Norwegian Coastal Administration and others. I test each major release against a selection of public and non-public data sources to ensure the broadest possible compatibility.

You can find the full documentation on readthedocs.

Binary releases (Debian packages) are provided by the pyais-debian starting with version v2.5.6. They are downloadable under the Releases page.

Acknowledgements

Jetbrains Logo

This project is a grateful recipient of the free Jetbrains Open Source sponsorship. Thank you. 🙇

General

AIS (Automatic Identification System) is a communication system that allows ships to automatically exchange information such as vessel identification, position, course, and speed. This information is transmitted via VHF radio and can be received by other ships and coastal stations, allowing them to accurately determine the location and movement of nearby vessels. AIS is often used for collision avoidance, traffic management, and search and rescue operations. AIS messages are often transmitted via NMEA 0183.

NMEA (National Marine Electronics Association) is an organization that develops and maintains standards for the interface of marine electronic equipment. NMEA 0183 is a standard for communicating marine instrument data between equipment on a boat. It defines the electrical interface and data protocol for sending data between marine instruments such as GPS, sonar, and autopilot.

Here is an example of an AIS sentence:

!AIVDM,1,1,,B,15MwkT1P37G?fl0EJbR0OwT0@MS,0*4E

This AIS sentence is known as a "Position Report" message and is used to transmit information about a vessel's position, course, and speed. AIS messages are transmitted in digital format and consist of a series of comma-separated fields that contain different types of data. Here is a breakdown of each field in this particular sentence:

  • !AIVDM: This field indicates that the sentence is an AIS message in the "VDM" (VDO Message) format.
  • 1,1: These fields indicate the total number of sentences in the message and the current sentence number, respectively. In this case, the message consists of a single sentence.
  • : This field is left blank. This field can contain the sequence number.
  • B: This field indicates the communication channel being used to transmit the message. In this case, the channel is "B".
  • 15MwkT1P37G?fl0EJbR0OwT0@MS: This field contains the payload of the message, which is encoded using a variant of ASCII known as "Six-bit ASCII". The payload contains information such as the vessel's identification, position, course, and speed. 0*4E: This field is a checksum that is used to verify the integrity of the sentence.

pyais is a Python modul to encode and decode AIS messages.

Installation

The project is available at Pypi:

$ pip install pyais

Usage

There are many examples in the examples directory.

Decode a single part AIS message using decode()::

from pyais import decode

decoded = decode(b"!AIVDM,1,1,,B,15NG6V0P01G?cFhE`R2IU?wn28R>,0*05")
print(decoded)

The decode() functions accepts a list of arguments: One argument for every part of a multipart message::

from pyais import decode

parts = [
    b"!AIVDM,2,1,4,A,55O0W7`00001L@gCWGA2uItLth@DqtL5@F22220j1h742t0Ht0000000,0*08",
    b"!AIVDM,2,2,4,A,000000000000000,2*20",
]

# Decode a multipart message using decode
decoded = decode(*parts)
print(decoded)

Also the decode() function accepts either strings or bytes::

from pyais import decode

decoded_b = decode(b"!AIVDM,1,1,,B,15NG6V0P01G?cFhE`R2IU?wn28R>,0*05")
decoded_s = decode("!AIVDM,1,1,,B,15NG6V0P01G?cFhE`R2IU?wn28R>,0*05")
assert decoded_b == decoded_s

Decode the message into a dictionary::

from pyais import decode

decoded = decode(b"!AIVDM,1,1,,B,15NG6V0P01G?cFhE`R2IU?wn28R>,0*05")
as_dict = decoded.asdict()
print(as_dict)

Read a file::

from pyais.stream import FileReaderStream

filename = "sample.ais"

for msg in FileReaderStream(filename):
    decoded = msg.decode()
    print(decoded)

Decode a stream of messages (e.g. a list or generator)::

from pyais import IterMessages

fake_stream = [
    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,13eaJF0P00Qd388Eew6aagvH85Ip,0*45",
    b"!AIVDM,1,1,,A,14eGrSPP00ncMJTO5C6aBwvP2D0?,0*7A",
    b"!AIVDM,1,1,,A,15MrVH0000KH<:V:NtBLoqFP2H9:,0*2F",
]
for msg in IterMessages(fake_stream):
    print(msg.decode())

Live feed

The Norwegian Coastal Administration offers real-time AIS data. This live feed can be accessed via TCP/IP without prior registration. The AIS data is freely available under the norwegian license for public data:

Data can be read from a TCP/IP socket and is encoded according to IEC 62320-1:

  • IP: 153.44.253.27
  • Port: 5631

Refer to the examples/live_stream.py for a practical example on how to read & decode AIS data from a TCP/IP socket. This is useful for debugging or for getting used to pyais.

Encode

It is also possible to encode messages.

❗ Every message needs at least a single keyword argument: mmsi. All other fields have most likely default values.

Encode data using a dictionary

You can pass a dict that has a set of key-value pairs:

  • use from pyais.encode import encode_dict to import encode_dict method
  • it takes a dictionary of data and some NMEA specific kwargs and returns the NMEA 0183 encoded AIS sentence.
  • only keys known to each message are considered
    • other keys are simply omitted
    • you can get list of available keys by looking at pyais/encode.py
    • you can also call MessageType1.fields() to get a list of fields programmatically for each message
  • every message needs at least two keyword arguments:
    • mmsi the MMSI number to encode
    • type or msg_type the type of the message to encode (1-27)

NOTE: This method takes care of splitting large payloads (larger than 60 characters) into multiple sentences. With a total of 80 maximum chars excluding end of line per sentence, and 20 chars head + tail in the nmea 0183 carrier protocol, 60 chars remain for the actual payload. Therefore, it returns a list of messages.

from pyais.encode import encode_dict

data = {
    'course': 219.3,
    'lat': 37.802,
    'lon': -122.341,
    'mmsi': '366053209',
    'type': 1,
}
# This will create a type 1 message for the MMSI 366053209 with lat, lon and course values specified above
encoded = encode_dict(data, radio_channel="B", talker_id="AIVDM")[0]

Create a message directly

It is also possible to create messages directly and pass them to encode_payload.

from pyais.messages import MessageType5
from pyais.encode import encode_msg

payload = MessageType5.create(mmsi="123", shipname="Titanic", callsign="TITANIC", destination="New York")
encoded = encode_msg(payload)
print(encoded)

Under the hood

graph LR
    raw -->|"!AIVDM,1,1,,B,6B?n;be,2*4A"| nmea
    nmea[NMEASentence] -->|parse NMEA sentence layer| ais[AISSentence]
    ais -->|decode| final[AISMessage]

Decoding each AIS message is a three step process.

At first, the NMEA 0183 physical protocol layer is parsed. The NMEA layer is the outer protocol layer that is used by many different sentences/protocols for data transmission. Just like Ethernet can be used as a data link protocol to transfer data between nodes, the NMEA protocol can be used to transmit data between maritime equipment.

After the raw message was parsed into a NMEASentence, the inner protocol layer is parsed. While there are tons of different inner protocols that build upon NMEA, pyais currently only supports AIS sentences. Every AISSentence holds basic information about the AIS message like:

  • the AIS message ID
  • the number of fill bits required for ASCII6 encoding
  • the fragment count and fragment number
  • the actual AIS payload
  • the sequence number

Finally, the AIS payload is decoded based on the AIS ID. There are 27 different types of top level messages that are identified by their AIS ID.

Tag block

Some messages may look strange at first. Typical AIS messages look roughly like this:

!AIVDM,1,1,,A,16:=?;0P00`SstvFnFbeGH6L088h,0*44
!AIVDM,1,1,,A,16`l:v8P0W8Vw>fDVB0t8OvJ0H;9,0*0A
!AIVDM,1,1,,A,169a:nP01g`hm4pB7:E0;@0L088i,0*5E

But sometimes such messages look something like this:

\s:2573135,c:1671620143*0B\!AIVDM,1,1,,A,16:=?;0P00`SstvFnFbeGH6L088h,0*44
\s:2573238,c:1671620143*0B\!AIVDM,1,1,,A,16`l:v8P0W8Vw>fDVB0t8OvJ0H;9,0*0A
\s:2573243,c:1671620143*0B\!AIVDM,1,1,,A,169a:nP01g`hm4pB7:E0;@0L088i,0*5E

These three messages are the same messages as above - only with a prefix, the so called tag block. Tag blocks are essential key-value pairs that are wrapped between \s. Every valid NMEA sentence may have one of the these tag blocks. Tag blocks are used to hold extra information and somewhat similar to Gatehouse messages.

A tag block consists of any number of comma-separated key-value pairs, followed by a checksum:

  • s:2573135,c:1671620143*0B -> s:2573135 & c:1671620143 & 0*B

The checksum is the same as for all NMEA messages. Regarding the key value pairs:

  • each key is a single letter
  • each letter represents a field:
    • c: Receiver timestamp in Unix epoch (e.g. 1671620143)
    • d: Destination station (e.g. FooBar)
    • n: Line count (e.g. 123)
    • r: Relative time
    • s: Source station (e.g. APIDSSRC1)
    • t: Text (e.g.g Hello World!)

Some things to keep in mind when working with tag blocks and pyais:

  • tag blocks are optional (a message may or may not have a tag block)
  • tag blocks are lazily decoded by pyais to save resources (need to call tb.init())
  • only some fields are supported by pyais (c,d,n,r,s,t)
    • unknown fields are simply omitted

How to work with tag blocks

from pyais.stream import IterMessages


text = """
\s:2573135,c:1671620143*0B\!AIVDM,1,1,,A,16:=?;0P00`SstvFnFbeGH6L088h,0*44
\s:2573238,c:1671620143*0B\!AIVDM,1,1,,A,16`l:v8P0W8Vw>fDVB0t8OvJ0H;9,0*0A
\s:2573243,c:1671620143*0B\!AIVDM,1,1,,A,169a:nP01g`hm4pB7:E0;@0L088i,0*5E
"""

messages = [line.encode() for line in text.split() if line]

with IterMessages(messages) as s:
    for msg in s:
        if msg.tag_block is not None:
            # Not every message has a tag block
            # Therefore, check if the tag block is not None
            # Also, it is required to call `.init()`, because tag blocks are lazily parsed
            msg.tag_block.init()
            # Print the tag block data as a dictionary
            print(msg.tag_block.asdict())
        print(msg.decode())

Gatehouse wrappers

Some AIS messages have so-called Gatehouse wrappers. These encapsulating messages contain extra information, such as time and checksums. Some readers also process these. See some more documentation here.

As an example, see the following, which is followed by a regular !AIVDM message

$PGHP,1,2020,12,31,23,59,58,239,0,0,0,1,2C*5B

Such messages are parsed by pyais only when using any of the classes from pyais.stream. e.g. FileReaderStream or TCPStream.

Such additional information can then be accessed by the .wrapper_msg of every NMEASentence. This attribute is None by default.

Communication State

The ITU documentation provides details regarding the Time-division multiple access (TDMA) synchronization.

Such details include information used by the slot allocation algorithm (either SOTDMA or ITDMA) including their synchronization state.

Refer to readthedocs for more information.

AIS Filters

The filtering system is built around a series of filter classes, each designed to filter messages based on specific criteria. Filters are chained together using the FilterChain class, which allows combining multiple filters into a single, sequential filtering process. The system is flexible, allowing for the easy addition or removal of filters from the chain.

How It Works

  1. AIS Stream: Messages are provided as a stream to the filters.
  2. Filter Application: Each filter in the chain applies its criteria to the stream, passing the messages that meet the criteria to the next filter.
  3. Filter Chain: The FilterChain class orchestrates the passing of messages through each filter, from the first to the last.

Filters

1. AttributeFilter

  • Description: Filters messages based on a user-defined function.
  • Usage: Initialize with a function that takes an AIS message and returns True if the message should be kept.

2. NoneFilter

  • Description: Filters out messages where specified attributes are None.
  • Usage: Initialize with the names of attributes that should not be None in the messages.

3. MessageTypeFilter

  • Description: Filters messages based on their type.
  • Usage: Initialize with message types to include.

4. DistanceFilter

  • Description: Filters messages based on distance from a reference point.
  • Usage: Initialize with a reference point (latitude and longitude) and a distance threshold in kilometers.

5. GridFilter

  • Description: Filters messages based on whether they fall within a specified geographical grid.
  • Usage: Initialize with the boundaries of the grid (minimum and maximum latitude and longitude).

Utility Functions

1. Haversine

  • Description: Calculates the great circle distance between two points on the Earth.
  • Parameters: Takes two tuples representing the latitude and longitude of each point.
  • Returns: Distance between the points in kilometers.

2. Is In Grid

  • Description: Checks if a point is within a defined geographical grid.
  • Parameters: Latitude and longitude of the point and the boundaries of the grid.
  • Returns: True if the point is within the grid, False otherwise.

FilterChain

  • Description: Chains multiple filters together into a single filtering process.
  • Usage: Initialize with a list of filters to be applied in order. The chain can be used to filter a stream of AIS messages.

Example Usage

from pyais import decode
# ... (importing necessary classes)

# Define and initialize filters
attribute_filter = AttributeFilter(lambda x: not hasattr(x, 'turn') or x.turn == -128.0)
none_filter = NoneFilter('lon', 'lat', 'mmsi2')
message_type_filter = MessageTypeFilter(1, 2, 3)
distance_filter = DistanceFilter((51.900, 5.320), distance_km=1000)
grid_filter = GridFilter(lat_min=50, lon_min=0, lat_max=52, lon_max=5)

# Create a filter chain
chain = FilterChain([
    attribute_filter,
    none_filter,
    message_type_filter,
    distance_filter,
    grid_filter,
])

# Decode AIS data and filter
data = [decode(b"!AIVDM..."), ...]
filtered_data = list(chain.filter(data))

for msg in filtered_data:
    print(msg.lat, msg.lon)

AIS tracker

pyais comes with the the ability to collect and maintain the state of individual vessels over time. This is necessary because several messages can give different information about a ship. In addition, the data changes constantly (e.g. position, speed and course).

Thus the information split across multiple different AIS messages needs to be collected, consolidated and aggregated as a single track. This functionality is handled by the AISTracker class.

NOTE: Each track (or vessel) is solely identified by its MMSI.

import pathlib

from pyais import AISTracker
from pyais.stream import FileReaderStream

filename = pathlib.Path(__file__).parent.joinpath('sample.ais')

with AISTracker() as tracker:
    for msg in FileReaderStream(str(filename)):
        tracker.update(msg)
        latest_tracks = tracker.n_latest_tracks(10)

# Get the latest 10 tracks
print('latest 10 tracks', ','.join(str(t.mmsi) for t in latest_tracks))

# Get a specific track
print(tracker.get_track(249191000))

Unlike most other trackers, AISTracker handles out of order reception of messages. This means that it is possible to pass messages to update() whose timestamp is older that of the message before. The latter is useful when working with multiple stations and/or different kinds of metadata.

But this comes with a performance penalty. In order to cleanup expired tracks and/or to get the latest N tracks the tracks need to be sorted after their timestamp. Thus, cleanup() and n_latest_tracks() have a complexity of O(N * log(N)). Depending on the number of messages in your stream this may or may not be good enough.

If you know that your messages in your stream are ordered after their timestamp and/or you never pass a custom timestamp to update(), you should set the stream_is_ordered=True flag when creating a new AISTracker instance. If this flag is set AISTracker internally stores the tracks in order. Thus, cleanup() and n_latest_tracks() have a complexity of O(k).

Callbacks

It is possible to register event listeners as callbacks, so that you are is instantly notified whenever a track is created, updated, or deleted.

import pyais
from pyais.tracker import AISTrackEvent

host = '153.44.253.27'
port = 5631


def handle_create(track):
    # called every time an AISTrack is created
    print('create', track.mmsi)


def handle_update(track):
    # called every time an AISTrack is updated
    print('update', track.mmsi)


def handle_delete(track):
    # called every time an AISTrack is deleted (pruned)
    print('delete', track.mmsi)


with pyais.AISTracker() as tracker:
    tracker.register_callback(AISTrackEvent.CREATED, handle_create)
    tracker.register_callback(AISTrackEvent.UPDATED, handle_update)
    tracker.register_callback(AISTrackEvent.DELETED, handle_delete)

    for msg in pyais.TCPConnection(host, port=port):
        tracker.update(msg)
        latest_tracks = tracker.n_latest_tracks(10)

Performance Considerations

You may refer to the Code Review Stack Exchange question . After a some research I decided to use the bitarray module as foundation. This module uses a C extension under the hood and has a nice user interface in Python. Performance is also great. Decoding this sample with roughly 85k messages takes less than 6 seconds on my machine. For comparison, the C++ based libais module parses the same file in ~ 2 seconds.

Disclaimer

This module is a private project of mine and does not claim to be complete. I try to improve and extend it, but there may be bugs. If you find such a bug feel free to submit an issue or even better create a pull-request. :-)

Coverage

Currently, this module is able to decode most message types. There are only a few exceptions. These are messages that only occur in very rare cases and that you will probably never observe. The module was able to completely decode a 4 hour stream with real-time data from San Francisco Bay Area without any errors or problems. If you find a bug or missing feature, please create an issue.

Known Issues

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

...

    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:

$ sudo apt install python3-dev

For developers

After you cloned the repo head into the pyais base directory.

Then install all dependencies:

$ pip install .[test]

Make sure that all tests pass and that there aren't any issues:

$ make test

Now you are ready to start developing on the project! Don't forget to add tests for every new change or feature!

Docker

Use Docker to run your application inside a container. At first you need to build the image locally:

docker build . -t pyais

Afterwards, run the container (bash):

docker run -it --rm pyais /bin/bash

You can then run the examples inside the container:

python ./examples/live_stream.py

Funfacts

Python3.11 is faster

With Python3.11 significant improvements to the CPython Runtime were made:

Some results from the internal performance test:

3.10: Decoding 82758 messages took: 3.233757972717285

3.11: Decoding 82758 messages took: 2.5866270065307617

pyais's People

Contributors

2maz avatar alfonnews avatar avitase avatar cglewis avatar davidalo avatar giangblackk avatar inrixia avatar krakoer avatar luthir avatar m0r13n avatar mjakob avatar prefixfelix avatar someusername13 avatar zheka8 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pyais's Issues

Speed and other fields returning unexpected values

For some fields I'm getting speed and other field values as a integer rather than a float.

Looking at this segment

pyais/pyais/messages.py

Lines 794 to 818 in cc47fb1

class MessageType18(Payload):
"""
Standard Class B CS Position Report
Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_18_standard_class_b_cs_position_report
"""
msg_type = bit_field(6, int, default=18)
repeat = bit_field(2, int, default=0)
mmsi = bit_field(30, int, from_converter=from_mmsi, to_converter=to_mmsi)
reserved = bit_field(8, int, default=0)
speed = bit_field(10, int, default=0)
accuracy = bit_field(1, int, default=0)
lon = bit_field(28, int, from_converter=from_lat_lon, to_converter=to_lat_lon, signed=True, default=0)
lat = bit_field(27, int, from_converter=from_lat_lon, to_converter=to_lat_lon, signed=True, default=0)
course = bit_field(12, int, from_converter=from_course, to_converter=to_course, default=0)
heading = bit_field(9, int, default=0)
second = bit_field(6, int, default=0)
reserved_2 = bit_field(2, int, default=0)
cs = bit_field(1, bool, default=0)
display = bit_field(1, bool, default=0)
dsc = bit_field(1, bool, default=0)
band = bit_field(1, bool, default=0)
msg22 = bit_field(1, bool, default=0)
assigned = bit_field(1, bool, default=0)
raim = bit_field(1, bool, default=0)
radio = bit_field(20, int, default=0)

I'm somewhat confused as the type given to bit_field is int for everything while properties like speed/lat/lon etc should all be floats.
Also shouldn't the defaults be None for missing values, or am I misinterpreting what the default property represents?
If it is meant to be 0 and not None the very least it should be 0.0 for floats and not 0

Please can you explain the use of default in the bit_field function.

[docs] messages.rst content duplicated.

Hello 😊

Some of the contents of the docs/messages.rst seem to overlap. 🤔
as MessageType1 ('line 105 ~ 174 and 175 ~ 244') is duplicated.

at this part

MessageType1
    AIS Vessel position report using SOTDMA (Self-Organizing Time Division Multiple Access)
    Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_types_1_2_and_3_position_report_class_a


    Attributes:
        * `msg_type`

...

There is no way to gracefully break out of a stream

Hello There
In UDPStream, TCPStream when you enter the loop there is no way to break out of the loop when there is no message incoming from UDPStream,TCPStream so using a UDPStream, TCPStream in a GUI application with the capability of starting and stopping the stream is impossible.

Variable-width text fields processed as constant-width ones

What am I trying to do:
print(*pyais.encode_msg(pyais.messages.MessageType12.create(mmsi=111111111, dest_mmsi=999999999, text='TEST')), sep='\n')
What I expect to get:
!AIVDM,1,1,,A,<1aucikfJjOtD5CD,0*55
What I actually get

!AIVDM,3,1,0,A,<1aucikfJjOtD5CD000000000000000000000000000000000000000000000,0*57
!AIVDM,3,2,0,A,0000000000000000000000000000000000000000000000000000000000000,0*27
!AIVDM,3,3,0,A,0000000000000000000000000000000000000000000000,0*16

As stated in https://gpsd.gitlab.io/gpsd/AIVDM.htm width of "text" field in MessageType12 and MessageType14 may be shorter than given maximum width, but pyais treats these fields as a constat-width ones, and trails given short text with a bunch of @ signs up to maximum length.

Seems it would be better to somehow calculate actual required width of the field while encoding

Proposal for update

I use this code to decode nmea0183 data (!AIVD...) collected from the AIS-reciever in my boat. Sometimes the message get errors. When the pyais try to decode the corrupted AIS-message, the code stop with a message as follows.
Traceback (most recent call last):

File "C:\Users\Asus\OneDrive\Documents__Python\decode_AIS\decode_AIS.py", line 93, in
for msg in FileReaderStream(filename):

File "C:\Users\Asus\anaconda3\lib\site-packages\pyais\stream.py", line 54, in _assemble_messages
msg: NMEAMessage = NMEAMessage(line)

File "C:\Users\Asus\anaconda3\lib\site-packages\pyais\messages.py", line 218, in init
self.bit_array: bitarray = decode_into_bit_array(self.payload, self.fill_bits)

File "C:\Users\Asus\anaconda3\lib\site-packages\pyais\util.py", line 34, in decode_into_bit_array
raise ValueError(f"Invalid character: {chr(c)}")

ValueError: Invalid character: !

The above error come with the message: !AIVDM,1,1,,B,3815;`100!PhmnPPwL=3OmUd0Dg:,0*45

I would prefer if errors like this one doesn't stop the execution, and instead just give a notice that there was a message with an error, and the continue the process of the next AIS message.

Is the checksum check not commonly used?

Hi.
First of all, thank you for making a good library.

While using decode() in this library, I found that checksum check was not performed. (I proceeded with decoding by changing only the 'checksum' field.)
The output was printed by adding print(nmea.is_vailed) to _assamble_messages() belonging to decode.py. It seems that the result of False should be raise, but all decoding proceeds normally.

# Mosaic messages for security reasons
is_vaild result:  True   ais_msg:  b'!AIVDM,1,1,,A,xxxxxxxxxxa3OsLEKwfmKB:L0@8g,0*55\n'
is_vaild result:  False   ais_msg:  b'!AIVDM,1,1,,A,xxxxxxxxxxa3OsLEKwfmKB:L0@8g,0*15\n'
is_vaild result:  False   ais_msg:  b'!AIVDM,1,1,,A,xxxxxxxxxxa3OsLEKwfmKB:L0@8g,0*10\n'
is_vaild result:  False   ais_msg:  b'!AIVDM,1,1,,A,xxxxxxxxxxa3OsLEKwfmKB:L0@8g,0*1A\n'
is_vaild result:  False   ais_msg:  b'!AIVDM,1,1,,A,xxxxxxxxxx3OsLEKwfmKB:L0@8g,0*B5'

Is the checksum check function not commonly used? Or is there another reason?
Thank you. :)

AttributeError: 'NoneType' object has no attribute 'to_json'

Hi,

I have this:

msg = b'!AIVDM,1,1,,B,0S9edj0P03PecbBN`ja@0?w42cFC,0*7C'
msg = NMEAMessage(msg)

which returns

None

so if I pass this directly to convert it to json

message = msg.decode().to_json()
I get:

Traceback (most recent call last):
File "./get3.py", line 33, in
message = msg.decode().to_json()
AttributeError: 'NoneType' object has no attribute 'to_json'

to_json can't handle these types, might be a good feature for new minor release :-)

Best,
Frank

Wrong Value in turn field (AIS1-3) ?

Hi,

Here an output from an ais msg:

{'type': 1, 'repeat': 0, 'mmsi': 'XXXXXX', 'status': <NavigationStatus.Undefined: 15>, 'turn': -128, 'speed': 0.1, 'accuracy': 1, 'lon': 4.303693333333333, 'lat': 51.270986666666666, 'course': 360.0, 'heading': 511, 'second': 55, 'maneuver': <ManeuverIndicator.NotAvailable: 0>, 'raim': 1, 'radio': 82243}

The "turn"-Field with -128 is not correct i think. A value of 128 (positiv) is the value for "no information" and it is not possible to save -128 with 8 bits (singed int -127 - 128).

Ty & Gz!

Gatehouse wrapper messages feature

Some AIS messages have so-called Gatehouse wrappers. These encapsulating messages contain extra information, such as time and checksums. Some readers also process these. See some more documentation here.

We have created a small mixin that allows for storing this extra information. We also added an extra message type to parse the metadata lines. Could you have a look to see if this is an approach you would be willing to adopt?

As an example, see the following, which is followed by a regular !AIVDM message

$PGHP,1,2020,12,31,23,59,58,239,0,0,0,1,2C*5B

I will submit the code as a PR referring to this issue.

ImportError: cannot import name 'OrderedDict'

I am dealing with the issue described in the title. I am using Google Colab for dealing with the data (Python 3.6.9). This is the notebook with the code. It appears some problem related with the typing library. Also, you can download the ais used in this link.

Thank you so much for your work!

from pyais import FileReaderStream

filename = '20201015T122532.txt'
for msg in FileReaderStream(filename):
    decoded_message = msg.decode()
    ais_content = decoded_message.content
/usr/local/lib/python3.6/dist-packages/pyais/util.py in <module>()
      1 from functools import partial, reduce
      2 from operator import xor
----> 3 from typing import OrderedDict, Sequence, Iterable
      4 
      5 from bitarray import bitarray

ImportError: cannot import name 'OrderedDict'

ValueError: invalid literal for int() with base 16: b''

Maybe this is not as intended?

I am parsing a file of all sorts of bad data and decoding it; I handle the exceptions; however this seems like a bad way to handle a missing checksum?

!AIVDM,1,1,,A,100u3FP04r28t0<WcshcQI<H0H79,0

Traceback (most recent call last): File "pyais_test.py", line 14, in <module> decoded = pyais.decode(i) File "c:\Python38\lib\site-packages\pyais\decode.py", line 34, in decode nmea = _assemble_messages(*parts) File "c:\Python38\lib\site-packages\pyais\decode.py", line 13, in _assemble_messages nmea = NMEAMessage(msg) File "c:\Python38\lib\site-packages\pyais\messages.py", line 201, in __init__ self.checksum = int(checksum[2:], 16) ValueError: invalid literal for int() with base 16: b''

All the best

Different payload after decode/encode roundtrip

First of all thanks for maintaining this fantastic project!

I am currently trying to generate some mock NMEA data by modifying fields in an existing data stream. That requires a decode/encode roundtrip (followed by another decode later on).

I noticed that the NMEA sentences change, even when not modifying the AIS data. After decoding another time, the AIS data is also different.

Example

example.py

import sys

from pyais.decode import _assemble_messages
import pyais.encode
from pyais.stream import BinaryIOStream

# Read NMEA sentences from stdin
for nmea in BinaryIOStream(sys.stdin.buffer):
    print(nmea)
    assert nmea.is_valid
    # decode AIS message
    m = nmea.decode()
    print(m)
    # encode and decode AIS -> NMEA -> AIS three more times
    for _ in range(3):
        sentences = pyais.encode.encode_msg(
            m, talker_id='AI'+nmea.type, radio_channel=nmea.channel)
        sentences = [s.encode() for s in sentences]
        nmea = _assemble_messages(*sentences)
        print(nmea)
        assert nmea.is_valid
        m = nmea.decode()
        print(m)
    print('---')

After feeding this script with the file from this repo's test folder, many sentences and messages don't match up e.g. the first set of output:

python example.py < ais_test_messages

b'!AIVDM,1,1,,A,13HOI:0P0000VOHLCnHQKwvL05Ip,0*23\n'
MessageType1(msg_type=1, repeat=0, mmsi=227006760, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=None, speed=0.0, accuracy=False, lon=0.13138, lat=49.475577, course=36.7, heading=511, second=14, maneuver=0, spare_1=b'\x00', raim=False, radio=22136)
b'!AIVDM,1,1,,A,13HOI:00002IuQi?IR5gwqh0EWP,2*0E'
MessageType1(msg_type=1, repeat=0, mmsi=227006760, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=0.0, speed=0.0, accuracy=False, lon=33.633373, lat=-84.936497, course=409.5, heading=312, second=0, maneuver=0, spare_1=b'\xa0', raim=False, radio=1656)
b'!AIVDM,1,1,,A,13HOI:00002IuQg?IR5gwqh0D0Ip,0*0D'
MessageType1(msg_type=1, repeat=0, mmsi=227006760, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=0.0, speed=0.0, accuracy=False, lon=33.633372, lat=-84.936497, course=409.5, heading=312, second=0, maneuver=0, spare_1=b'\xa0', raim=False, radio=1656)
b'!AIVDM,1,1,,A,13HOI:00002IuQg?IR5gwqh0D0Ip,0*0D'
MessageType1(msg_type=1, repeat=0, mmsi=227006760, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=0.0, speed=0.0, accuracy=False, lon=33.633372, lat=-84.936497, course=409.5, heading=312, second=0, maneuver=0, spare_1=b'\xa0', raim=False, radio=1656)

A second roundtrip changes the payload again, although fewer bits seem to change (sometimes all AIS fields stay the same). After the second roundtrip, the data stays the same and further encodes and decodes result in the same NMEA sentence and AIS information.

Urgent need

Nice project...
Though am not a python dev but i strongly believe if you have reached the level of decoding ais that's shows a better understanding.

Well am a Java dev building an app related to ais... i have some questions i will like to ask can you privately.

edit:
Thanks for idea and time

Add missing encoders

Currently, the following messages are supported:

  • Typ 1
  • Typ 2
  • Typ 3
  • Typ 4
  • Typ 5
  • Typ 6
  • Typ 7
  • Typ 8
  • Typ 9
  • Typ 10
  • Typ 11
  • Typ 12
  • Typ 13
  • Typ 14
  • Typ 15
  • Typ 16
  • Typ 17
  • Typ 18
  • Typ 19
  • Typ 20
  • Typ 21
  • Typ 22
  • Typ 23
  • Typ 24
  • Typ 25
  • Typ 26
  • Typ 27

The scope of this issue is to add support for encoding all messages.

Messagetype27 lat/long should be signed

pyais/pyais/messages.py

Lines 1362 to 1363 in 21da419

lon = bit_field(18, float, from_converter=from_lat_lon_600, to_converter=to_lat_lon_600, default=0)
lat = bit_field(17, float, from_converter=from_lat_lon_600, to_converter=to_lat_lon_600, default=0)

Discovered this while testing, seemed my numbers were off by half and never negative. Doing more testing today. I'm no ais expert but it seems like a bug.

decode.py: decode_msg_23 should have 'shiptype' not 'ship_type'

There are two different keys for ship type. Please only use one.

`
decode_msg_5 ...
'shiptype': ShipType(get_int_from_data(232, 240)),
decode_msg_19 ..
'shiptype': ShipType(get_int_from_data(232, 240)),
...
decode_msg_23 . . .
'ship_type': ShipType(get_int_from_data(114, 122))

`

Incorrect decoding field types

After the changes made in #54 I decided to check the field types for every message type in the decoder against the docs.

I have found a number of fields which have incorrect or inconsistent types, I have detailed them all below to be fixed.

All Types

The naming is inconsistent for spare and regional, MessageType18 has reserved and reserved_2 rather than reserved_1 and reserved_2 and both fields change this up on a per message basis.
_1, _2 etc should be used for all fields which have multiple instances such as reserved and spare.

Single bit fields accuracy, raim, dte, retransmit, assigned, cs, display, dsc, band, msg22, virtual_aid, power, addressed, band_a, band_b, structured and gnss are boolean according to the docs for some message types and int for others. As these are single bit fields they should all be boolean for all message types.

Currently accuracy and gnss are not boolean for all message types in the library.

Types 1, 2 & 3

turn according to the docs is a Signed integer with scale - renders as float, suffix is decimal places but its type is int in the library.
As the docs say renders as float, I believe this is actually meant to be a float.

Type 6 & Type 8 & Type 17 & Type 25

data should be a string representation of the binary and not a int as you cannot have leading 0's in python integers.

Type 19

reserved/regional naming is completely inconsistent here. Naming for the Regional reserved fields previously has followed reserved, reserved_2... but here it is reserved, regional. The naming of the reserved & spare fields should be consistent across all message types and use _1 as previously stated.

Type 24 & Type 25 & Type 26

The dict of types you provided in #54 appears to be missing vendorid, model and serial from from Type 24 and addressed, structured, app_id from Type 25 & Type 26

I don't know how you generated them but I presume these missing fields are due to the increased complexity with these types.

TypeError: 'dict' object is not callable

Hello,
I'm trying to use the decode function by following through the example code but end up getting the error saying "TypeError: 'dict' object is not callable". This is code:
decoded_s = decode("!AIVDM,1,1,,B,15NG6V0P01G?cFhE`R2IU?wn28R>,0*05")

May be it's something that I did not set up correctly. Can you please give me an advice on how to fix this issue? Thank you very much.

Add a console_scripts entrypoint

It would be nice, if one would be able to install the package and have a program inside your terminal, that is able to decode AIS messages.

in v2-alpha, MessageType21 payload includes shipname, which I think should just be name

In messages.py:

lass MessageType21(Payload):
    """
    Aid-to-Navigation Report
    Src: https://gpsd.gitlab.io/gpsd/AIVDM.html#_type_21_aid_to_navigation_report
    """
....
    shipname = bit_field(120, str, default='')

I believe shipname should simply be name, which is what's listed in that gitlab URL.

I found this when testing a script I use which displays ship names my receiver is picking up. With ~v1.7 I would get only the shipnames, but with v2 in addition to ships I'd get A, B, C, 1, 2, etc., which are aids to navigation markers from type 21 messages.

How to decode invalid messages

Hi,

I am dealing with a stream of AIS messages. Approximately 20 % are invalid. The library raises an exception and the message is not processed. For example, the following two:

!ABVDM,1,1,7,,B3seA8000?mPBpURDA?Q3wmQnDOJ,0*3C,1616655884
!AIVDM,1,1,,B,53ktrJ82>ia4=50<0020<5=@Dhv0t8T@u<0000001PV854Si0;mR@CPH13p0hDm1C3h0000,2*35

The first one has the channel empty and the second has more than 82 characters. I inserted both of them in maritec and the messages were decoded. The "only problem" they had is an erroneous checksum. Is there a way to decode them with pyais? Using NMEAMessage function an error is raised and the message is not decoded. It could be possible to print warnings instead of raising exceptions?

Thank you so much for your work.

EDIT: other option could be to bypass the validate_message routine with an argument. Its default would be False.

Support for multiline AIS messages?

Can multiline AIS messages be processed ? For example:

!AIVDM,2,1,5,A,53KQ<L01pv7ptTtN22059@DlU>2222222222220l1@D555V00:R0CRkp8888,0*41
!AIVDM,2,2,5,A,88888888880,2*21

Best regards,
Andrés

int_to_bin assumes signed integer when converting to bytes

Problem was found when encoding a dictionary back into a NMEA message.

In the int_to_bin function in the util.py file, the second to last statement converts the value into bytes and then into a bit array. However, when the value is converted to_bytes it always assumes the value is signed.

I ran into an issue when encoding a message type 5 that had a draught value of 12.8. 12.8 gets multiplied up to 128 in the from_course function. 128 is then ultimately passed to int_to_bin and expected to fit into 1 byte. However, a signed value of 128 doesn't fit into 1 byte. So I think int_to_bin needs to be told the signed-ness of the value from the calling function, it can't always be set to signed=True.

IndexError: bitarray index out of range

Hi,

I have this message:

ais_print = message.decode()
and I get this:

{'type': 5, 'repeat': 0, 'mmsi': 248659000, 'ais_version': 1, 'imo': 9745500, 'callsign': '9HA4748', 'shipname': 'CMA CGM PREGOLIA', 'shiptype': <ShipType.Cargo_HazardousCategory_A: 71>, 'to_bow': 183, 'to_stern': 12, 'to_port': 30, 'to_starboard': 2, 'epfd': <EpfdType.GPS: 1>, 'month': 7, 'day': 20, 'hour': 22, 'minute': 30, 'draught': 10.5, 'destination': 'DEHAM', 'dte': False}

then

ais_content = message.decode().to_json()
and I get this:

{'nmea': {'ais_id': 5, 'raw': '!AIVDM,2,1,2,B,53e8t>42De5kTP7COCP<l60<Ln118DLthT400017Fp<N25rFNJA1B0CH,0*18', 'talker': 'AI', 'msg_type': 'VDM', 'count': 2, 'index': 1, 'seq_id': '2', 'channel': 'B', 'data': '53e8t>42De5kTP7COCP<l60<Ln118DLthT400017Fp<N25rFNJA1B0CH', 'checksum': 24, 'bit_array': '000101000011101101001000111100001110000100000010010100101101000101110011100100100000000111010011011111010011100000001100110100000110000000001100011100110110000001000001001000010100011100111100110000100100000100000000000000000000000001000111010110111000001100011110000010000101111010010110011110011010010001000001010010000000010011011000'}, 'decoded': {'type': 5, 'repeat': 0, 'mmsi': 248659000, 'ais_version': 1, 'imo': 9745500, 'callsign': '9HA4748', 'shipname': 'CMA CGM PREGOLIA', 'shiptype': 71, 'to_bow': 183, 'to_stern': 12, 'to_port': 30, 'to_starboard': 2, 'epfd': 1, 'month': 7, 'day': 20, 'hour': 22, 'minute': 30, 'draught': 10.5, 'destination': 'DEHAM', 'dte': False}, 'date': '2020-07-20 23:17:14.338438'}

The 'date' is added later on to the dict.

And with the next message pyais crushes, not sure if it is a coincident with the messages above:

Traceback (most recent call last):
File "./get2.py", line 31, in
ais_print = message.decode()
File "/usr/local/lib/python3.7/dist-packages/pyais/messages.py", line 139, in decode
return AISMessage(self)
File "/usr/local/lib/python3.7/dist-packages/pyais/messages.py", line 151, in init
self.content = decode(self.nmea)
File "/usr/local/lib/python3.7/dist-packages/pyais/decode.py", line 677, in decode
return DECODE_MSGmsg.ais_id
File "/usr/local/lib/python3.7/dist-packages/pyais/decode.py", line 38, in decode_msg_1
'raim': bit_arr[148],
IndexError: bitarray index out of range

Not sure how to debug this, hope it helps?!

Best
Frank

Incorect fill bits

There are errors in messages generated by pyais:

!AIVDM,2,1,,B,54Rofa@2;t484<o3340t8=V3400000000000000t2H0147?P06lm1D3R@h@0,2*45
!AIVDM,2,2,,B,00000000000,2*17

It should be:

!AIVDM,2,1,,B,54Rofa@2;t484<o3340t8=V3400000000000000t2H0147?P06jkk3hTVQAh,0*41
!AIVDM,2,2,,B,00000000000,2*17

Number of fill bits on the first line is ...,2*45. Proper value is ...,0*41.
It is probably allways 0 on the first line of multiline messages.

Decoding of Binary Payloads

Thanks for the great module! Am I correct in understanding that the current code does not decode the binary payloads contained in AIS messages 6, 8, 25, and 26?

Is ROT(Turn) decoding properly?

Hello,

I am seeing a lot of sentences where the value for "turn" is being set to None, which is causing them to equal <null> when I store them. Using some of the lines from your decode.py example:

import pyais as pa
from pyais import decode

print('version: ', end='')
print(pa.__version__)

# Decode a single part using decode
decoded = decode(b"!AIVDM,1,1,,B,15NG6V0P01G?cFhE`R2IU?wn28R>,0*05")
print(decoded)

# Added my sentence for example 2
decoded = decode("!AIVDM,1,1,,B,15Nfbk3P00rq:4`Gkga7MwvP2L0L,0*0D")
print(decoded)

For both sentence we gate a value of None for turn:

version: 2.1.2
MessageType1(msg_type=1, repeat=0, mmsi=367380120, status=<NavigationStatus.UnderWayUsingEngine: 0>, turn=None, speed=0.1, accuracy=False, lon=-122.404333, lat=37.806948, course=245.2, heading=511, second=59, maneuver=0, spare_1=b'\x00', raim=True, radio=34958)
MessageType1(msg_type=1, repeat=0, mmsi=367766220, status=<NavigationStatus.RestrictedManoeuverability: 3>, turn=None, speed=0.0, accuracy=True, lon=-71.39986, lat=41.60838, course=191.1, heading=511, second=16, maneuver=0, spare_1=b'\x00', raim=True, radio=114716)

From an online decoder (http://ais.tbsalling.dk/) using the same sentence(s) we get a value of no turn information available:

rateOfTurn | -128

When I decode and store these values as a float they are being set to <null>. I am confused as to how the from_converter and to_converter are working. Can you please explain? Is the bit field just not receiving the right data?

Thanks.
-bill

print(sys.version)
3.9.13 (tags/v3.9.13:6de2ca5, May 17 2022, 16:36:42) [MSC v.1929 64 bit (AMD64)]

Decoding AIS message type 5, class A static data

I receive AIS messages on Icom m605 VHF radio and forward them to OpenCPN. All the targets around show fine and with full metadata there.

I decode the same stream of messages with pyais, log the messages and count the message types for statistics. However, it seems my counter for message type 5 (class A static data) seems to not increase, and my counter for decoding errors does. All the other message counts seem sane.

I have to dig into this deeper and catch few such messages in the wild, but anyone else has success with decoding said message type? It might of course be a problem on how Icom generates the NMEA messages/checksums etc, but it seems odd as OpenCPN seems to not have a problem as otherwise it could not show the ship names etc.

I will update this issue when I have some actual example messages to digest.

edit. here is an example message captured live from Icom that causes decoding error:

!AIVDM,2,1,3,A,53KPT5P00000tP7?KOTh4AV104lDh6222222220U1P5337<B05iRCS0CQ0kh,0*39

edit2. I can see it now, it is a multipart messege with second part missing for some reason. I will have to dig deeper, it might be my reception and not pyais problem at all.

edit3. It seems I found the problem. My NMEA multiplexer is interleaving the messages from AIS and GPS in a way that pyais does not seem to like - there is (almost) always a gnss sentence or two interleaved between multipart type 5 messages. OpenCPN seems to not care, but pyais does.

I think ultimately this is not a pyais issue. So this issue can be closed.

Decoding messages with a unexpected bit_arr length silently fails returning None

When decoding messages, if the bit_arr length is less than what is needed for every property expected to be decoded None is returned with no error.

Test Code:

import pyais
print(pyais.NMEAMessage(bytes("!ARVDM,2,1,3,B,E>m1c1>9TV`9WW@97QUP0000000F@lEpmdceP00003b,0*5C", 'utf-8')).decode().content)

As you can see below I have commented out several properties except for assigned which attempts to access index 270 of the bit_arr.
When this is uncommented the returned value is None, however when commented (there are no properties that access outside of the bit_arr length) you can see the message decodes as expected.
image

Ideally properties that do not have data or fail to decode would result in None.
I appear to be getting this for most if not all of my type 21 messages.

Q&A

Single Messages
"Supports single messages": Can someone recommend a library that decodes multi-line messages. Or is there a work around that can leverage pyais?

I have code that can collate the message parts and put them all together in any manner a library might require. All I need is one that then can decode.

Message types
"Currently, this module is able to decode most message types. There are only a few exceptions. "

Which specific messages are not decoded? I have a close to production need to decode everything. Any plan to add the messages types that are currently not decoded?

Could NMEAMessage contain fields for each field in raw?

Having easy access to the results of NMEAMessage(foo).raw.decode().split(',') would make things neater for certain tasks, in case it's necessary to manually process those fields.

For example:
!AIVDM,1,1,,A,15Mj23P000G?q7fK>g:o7@1:0L3S,0*1B
would have fields:
{'message_fragments': 1, 'fragment_number': 1, 'message_id': None, 'channel': 'A', 'payload': '15Mj23P000G?q7fK>g:o7@1:0L3S', 'fill_bits':0, 'checksum': '*1B'}

Non-deterministic results from 'decode_msg'

I am seeing some non-determinism in decode_msg output, e.g.:

>>> raw = b'!AIVDM,1,1,,A,D02=ag0flffp,0*11'
>>> pyais.decode_msg(raw)['offset2']
2
>>> pyais.decode_msg(raw)['offset2']
0

Memory overflow? This AIS message itself seems short but I would expect an error if it was unexpected length?

python: 3.9
pyais: 1.6.2
bitarray: 2.3.4

Problem connecting to UDP server

Hi all, I am trying to use pyais to read a UDP output from an AIS receiver on the network. I can read it fine using netcat:

jpe@pmpc1770:~>nc -vu 192.171.162.68 12345
Connection to 192.171.162.68 12345 port [udp/*] succeeded!
\s:PML,c:1622632119,t:LIVE*10\!AIVDM,1,1,,A,13P:DSP000wdk1vLm2mRa@C:08GI,0*74
\s:PML,c:1622632119,t:LIVE*10\!AIVDM,1,1,,B,13M@LCP000wdm98LmIidL7u<0L0M,0*6C
\s:PML,c:1622632119,t:LIVE*10\!AIVDM,1,1,,A,13:dR<5Ohkwe;BDLlES`=Fo>0<0Q,0*53
\s:PML,c:1622632119,t:LIVE*10\!AIVDM,1,1,,B,139K?fP00ewe2jLLe7C01P1>00T1,0*59
\s:PML,c:1622632119,t:LIVE*10\!AIVDM,1,1,,B,339K?i001;wdoV:LjCuW9Ec@2000,0*01
\s:PML,c:1622632120,t:LIVE*1A\!AIVDM,1,1,,B,13MAPkPP00OdoipLl@E>4?wB2D08,0*16
\s:PML,c:1622632121,t:LIVE*1B\!AIVDM,1,1,,A,13M@ENh000wdn9RLm76<BUG>08Hb,0*3C
\s:PML,c:1622632121,t:LIVE*1B\!AIVDM,1,1,,B,13M@E50000OdfwlLmKBSC@sD08Hl,0*6F
\s:PML,c:1622632122,t:LIVE*18\$AIHBT,5.0,A,8*28
\s:PML,c:1622632122,t:LIVE*18\!AIVDM,1,1,,B,15QJmN002;OehoJLU5rprW;H0<0t,0*47
\s:PML,c:1622632122,t:LIVE*18\!AIVDM,1,1,,A,13OhOf0P?w<tSF0l4Q@>4?wv0PS7,0*16
\s:PML,c:1622632122,t:LIVE*18\!AIVDM,1,1,,B,13P<SaS000wdiKhLmEFPhlIF0@I`,0*63
\s:PML,c:1622632123,t:LIVE*19\!AIVDM,1,1,,B,13P;r00000wdmFlLmLCf42CF2@Ij,0*40
\s:PML,c:1622632123,t:LIVE*19\!AIVDM,1,1,,A,339K?fPP@ewe2jPLe7OP801F0000,0*4C
\s:PML,c:1622632123,t:LIVE*19\!AIVDM,1,1,,B,B3`p=Ph00GsAhuW<urAwWwn5CP06,0*1D
\s:PML,c:1622632123,t:LIVE*19\!AIVDM,1,1,,A,339K?fP00ewe2jPLe7OP801F0000,0*5C
\s:PML,c:1622632123,t:LIVE*19\!AIVDM,1,1,,A,339K?i001:wdoV0LjCf7:5cF20s0,0*4C
\s:PML,c:1622632124,t:LIVE*1E\!AIVDM,1,1,,B,B3MAat00?7s@LdW<pkQG7wn1h8JK,0*76
\s:PML,c:1622632124,t:LIVE*1E\!AIVDM,1,1,,A,13M@DFh000wdm9HLm5pTLEPn0@JO,0*0E
\s:PML,c:1622632124,t:LIVE*1E\!AIVDM,1,1,,A,B3P:skh00?sAK6W=6VgQ3wnQnDRb,0*60
\s:PML,c:1622632124,t:LIVE*1E\!AIVDM,1,1,,B,B3P=uwh00?sBPfW=43wQ3wnUoP06,0*66
\s:PML,c:1622632126,t:LIVE*1C\!AIVDM,1,1,,B,339K?fP00fwe2jVLe7a01h1L00w@,0*78
\s:PML,c:1622632126,t:LIVE*1C\!AIVDM,1,1,,B,339K?i001:wdoUlLjCIW8mcN20w1,0*08
\s:PML,c:1622632126,t:LIVE*1C\!AIVDM,1,1,,A,13M@BR001JOdmO>LlUlMm;7L00S6,0*69
\s:PML,c:1622632126,t:LIVE*1C\!AIVDM,1,1,,A,B3P7m>h0<Os02>7<cRR@gwoQl<0N,0*2F
\s:PML,c:1622632126,t:LIVE*1C\!AIVDM,1,1,,A,B3P9Nr00<wsDHh7;bovWcwoQl8Kv,0*3D

However if I try the following Python code:

#!/usr/bin/env python3
import socket
from pyais.stream import UDPStream

host = "192.171.162.68"
port = 12345

for msg in UDPStream(host, port):
    # msg.decode()
    print(msg)
    # do something with it

I get the error:

Traceback (most recent call last):
  File "/users/rsg/jpe/projects/pml-ais/log-roof-station.py", line 8, in <module>
    for msg in UDPStream(host, port):
  File "/users/rsg/jpe/.local/lib/python3.8/site-packages/pyais/stream.py", line 279, in __init__
    sock.bind((host, port))
OSError: [Errno 99] Cannot assign requested address

Also I tried the commandline utility and same issue:

jpe@pmpc1770:~>ais-decode socket -t udp 192.171.162.68 12345
Traceback (most recent call last):
  File "/usr/local/bin/ais-decode", line 8, in <module>
    sys.exit(main())
  File "/users/rsg/jpe/.local/lib/python3.8/site-packages/pyais/main.py", line 145, in main
    exit_code: int = namespace.func(namespace)
  File "/users/rsg/jpe/.local/lib/python3.8/site-packages/pyais/main.py", line 99, in decode_from_socket
    with stream_cls(args.destination, args.port) as s:
  File "/users/rsg/jpe/.local/lib/python3.8/site-packages/pyais/stream.py", line 279, in __init__
    sock.bind((host, port))
OSError: [Errno 99] Cannot assign requested address

My OS is Ubuntu 20.04 and Python 3.8.5. Does anyone know what the error is? Thank you

messages with missing sequential ID number or grater than 9

I used this library to decode coming NMAE data from an AIS receiver, I don't really have that deep understanding of the working principle on NMEA messages but from what I found that the fourth number after the comma which is the sequential ID should be from 1 to 9
However on my case I faced two uncommon types where sometimes the sequential ID is 10, or just empty which will leads to "not supported type of messages" error.
As quick fix, on my code I added a condition to set the sequential ID to any number less than 9 in these cases, just to avoid the rising errors...because I notice that changing the "sequential ID" doesn't have any effect on the other message values (mmsi, type ...)
It seems not the correct way of solving this but it's a temporary solution...
I hope you consider my report, and If go wrong on any part please don't hesitate to contact me.

Make fields accessible by __getitem__

I'm working with pyais and I'm finding the syntax NMEAMessage(blah).asdict()['mmsi']) clumsy. It should be possible to subscript these fields if NMEAMessage defined a getitem.

I will try to enter a pull request at some point, if I'm able to make time.

failed parse some MessageType5 data , it throw 'MissingMultipartMessageException'

face this problem, the decode.py code logic bellow:

# Make sure provided parts assemble a single (multiline message)
  if len(args) > frag_cnt:
      raise TooManyMessagesException(f"Got {len(args)} messages, but fragment count is {frag_cnt}")

while parse string :
!AIVDM,2,1,6,A,55MwgS`00001L@?;OK80b022220l1@:44vT4,0*6B

it throw exception:

 raise MissingMultipartMessageException(f"Missing fragment numbers: {diff}")
pyais.exceptions.MissingMultipartMessageException: Missing fragment numbers: [2]

what is the reason ?

if I comment the check logic , then all things works fine , anyone can give tip for this case ?

Only one attribute of decode

How I take just only the attribute of decode ?. For example, I use the code:

from pyais.stream import FileReaderStream

filename = "sample.ais" #archive with NMEA message

for msg in FileReaderStream(filename):
    decoded = msg.decode()
    print(decoded)

, but I want print only lat or long, and not all datas.

Exception when trying to decode multipart payloads

When trying to decode messages with the decode_msg function, I run into the exception that the message is not currently supported (in the file decode.py). I've checked out the file but don't understand what the problem is. One AIS payload that was encoded correctly looks as following:

AIS:
 !AIVDM,1,1,,B,34SNn:50000eUKHN`Ai0a41d0DU:,0*40
Decoded:
 {'type': 3, 'repeat': 0, 'mmsi': '305641000', 'status': <NavigationStatus.Moored: 5>, 'turn': 0, 'speed': 0.0, 'accuracy': 0, 'lon': 9.958153333333334, 'lat': 53.52864666666667, 'course': 16.400000000000002, 'heading': 128, 'second': 54, 'maneuver': <ManeuverIndicator.NotAvailable: 0>, 'raim': 0, 'radio': 84298}

and it works right until I run into a message with !AIVDM,2:

!AIVDM,2,1,0,A,539p4OT00000@7W3K@08ThiLE8@E:0000000001S0h9135Pl?0R0C@UDQp00,0*68
{'type': 5, 'repeat': 0, 'mmsi': '211682430', 'ais_version': 1, 'imo': 0, 'callsign': 'DA9064', 'shipname': 'BILLWERDER', 'shiptype': <ShipType.OtherType_NoAdditionalInformation: 99>, 'to_bow': 6, 'to_stern': 9, 'to_port': 1, 'to_starboard': 3, 'epfd': <EpfdType.GPS: 1>, 'month': 6, 'day': 1, 'hour': 20, 'minute': 15, 'draught': 0.2, 'destination': 'HAMBURG', 'dte': 0}
!AIVDM,2,2,0,A,00000000000,2*24
pyais.exceptions.UnknownMessageException: The message b'!AIVDM,2,2,0,A,00000000000,2*24' is not currently supported!

Is multipart payload decoding not supported yet or am I not understanding what exactly to do when I encounter a multipart payload?

Invalid NMEA message that halts the execution of a py script

While receiving data from AISHUB, I got the following error message when I ran a script to create a separate json file for every minute. It seems to point to an invalid NMEA message. This error kills the script.

Unfortunately this happens every few minutes. It would be great if it just continues and drops the invalid nmea message.

Error message:

Traceback (most recent call last):
  File "/home/rstudio/AISHUB_stream2json.py", line 23, in <module>
    for msg in TCPStream(host, port):
  File "/home/rstudio/.local/share/r-miniconda/envs/r-reticulate/lib/python3.6/site-packages/pyais/stream.py", line 37, in _assemble_messages
    msg: NMEAMessage = NMEAMessage(line)
  File "/home/rstudio/.local/share/r-miniconda/envs/r-reticulate/lib/python3.6/site-packages/pyais/messages.py", line 43, in __init__
    raise InvalidNMEAMessageException("A NMEA message needs to have exactly 7 comma separated entries.")
pyais.exceptions.InvalidNMEAMessageException: A NMEA message needs to have exactly 7 comma separated entries.

Code I used to run:

from pyais import TCPStream
from datetime import datetime
import json
import os
# import shutil

# AISHUB server details
host = "data.aishub.net"
port = 4214 

start = datetime.now()
# now = datetime.now()

# new_file.close()
dt_string = start.strftime("%Y%m%d_%H%M%S")
filename_parts = ["./1_stream/", "nmea_", dt_string, ".json"]
filename = "".join(filename_parts)
filename_parts_destination = ["./1_stream_completed/", "nmea_", dt_string, ".json"]
filename_destination = "".join(filename_parts_destination)

new_file=open(filename,mode="w",encoding="utf-8")

for msg in TCPStream(host, port):
  print(msg)
  # print("---")
  json_data = msg.decode().to_json()
  print("---")
  json_data = json_data.replace(' ', '') 
  json_data = json_data.replace('\n', '') 
  if (datetime.now() - start).total_seconds() > 60:
    new_file.close()
    os.rename(filename, filename_destination)
    start = datetime.now()
    dt_string = start.strftime("%Y%m%d_%H%M%S")
    filename_parts = ["./1_stream/", "nmea_", dt_string, ".json"]
    filename = "".join(filename_parts)
    filename_parts_destination = ["./1_stream_completed/", "nmea_", dt_string, ".json"]
    filename_destination = "".join(filename_parts_destination)
    new_file=open(filename,mode="w",encoding="utf-8")
  json2file = "".join([json_data,"\n"])
  print("---")
  new_file.write(json2file)
  print("---")

Decoding MMSI as int loses leading zeroes

All of the messages in pyais/decode.py are decoding the MMSI value as an integer. However, if there are leading zeroes (which is possible and valid -- see navcen documentation) they are going to be lost. In order to preserve the full original MMSI value, shouldn't these be parsed into strings?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.