Skip to main content

Sans-I/O NMEA 0183 + AIS (AIVDM/AIVDO) parser, Rust-backed

Project description

marlin-py

Python bindings for the Marlin Rust suite — NMEA 0183 envelope parsing, typed sentence decoding, AIS (AIVDM/AIVDO) message decoding with multi-sentence reassembly, and MISB ST 0601 (KLV) encode/decode.

Status

Wraps four Rust crates: marlin-nmea-envelope (framing + checksum), marlin-nmea-0183 (GGA, GLL, HDT, RMC, VTG, PSXN, PRDID typed decoders), marlin-ais (Types 1/2/3/5/18/19/24A/24B + reassembly), and marlin-klv (MISB ST 0601 UAS Datalink Local Set encode/decode). py.typed marker and .pyi stubs ship with the package; mypy --strict is clean across the entire python/marlin/, tests/, and examples/ tree.

Install

The PyPI distribution name is marlin-py; the Python import name stays plain marlin.

# From PyPI — wheels for Linux x86_64 / aarch64, macOS universal2,
# Windows x86_64. abi3 wheel covers Python 3.9 through 3.13+.
pip install marlin-py

# Local development — build the extension in-place
cd bindings/python
maturin develop

Quickstart: envelope

from marlin.envelope import StreamingParser

parser = StreamingParser()
parser.feed(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n")
for sentence in parser:
    talker = sentence.talker.decode() if sentence.talker else ""
    print(talker, sentence.sentence_type, sentence.checksum_ok)
    # GP GGA True

OneShotParser handles UDP datagrams (no \r\n required). parse() is a one-call convenience for a single complete sentence.

Quickstart: NMEA typed decode

from marlin.envelope import parse
from marlin.nmea import Nmea0183Parser, decode_gga, DecodeOptions, PrdidDialect

# Single-sentence convenience.
raw = parse(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47")  # raises EnvelopeError on framing/checksum failure
gga = decode_gga(raw)                # raises DecodeError if not GGA or fields are malformed
print(gga.latitude_deg, gga.longitude_deg, gga.fix_quality)

# Streaming parser — same feed/iterate API as the envelope layer.
parser = Nmea0183Parser.streaming()
parser.feed(b"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n")
for msg in parser:
    print(type(msg).__name__, msg)

DecodeOptions configures dialect-sensitive sentences:

opts = DecodeOptions().with_prdid_dialect(PrdidDialect.PITCH_ROLL_HEADING)
parser = Nmea0183Parser.streaming(options=opts)

Per-sentence extension functions (decode_gga, decode_vtg, decode_hdt, decode_psxn, decode_prdid) are public so downstream code can build its own message enum and delegate to Marlin for the standard types.

Quickstart: AIS

from marlin.ais import AisParser, PositionReportA

parser = AisParser.streaming()
parser.feed(b"!AIVDM,1,1,,A,13aGmP0P00PD;88MD5MTDww@2<0L,0*23\r\n")
for msg in parser:
    if isinstance(msg.body, PositionReportA):
        print(msg.body.mmsi, msg.body.latitude_deg, msg.body.longitude_deg)

Multi-sentence reassembly is automatic — feed fragments in order and the parser yields a complete AisMessage only when the last fragment arrives.

Quickstart: KLV

from marlin.klv import St0601, decode, encode

s = St0601(timestamp_us=1_700_000_000_000_000)
s.sensor_latitude_degrees = 60.1768      # engineering-unit property
s.platform_heading_degrees = 159.97
wire = encode(s)                          # bytes
got = decode(wire)
print(got.sensor_latitude_degrees, got.timestamp_us)

St0601 fields are properties, not setter methods: assign s.sensor_latitude_degrees = ..., don't call s.set_sensor_latitude_degrees(...). decode raises KlvError on a malformed local set (bad key, truncated bytes, checksum mismatch). precision_timestamp(data) reads Tag 2 alone, without verifying the checksum, for callers that only need a cheap timestamp peek.

AIS clock modes

Fragment reassembly has three timeout behaviours, selected at construction.

No timeout — fragments are buffered until the 16-slot eviction cap is reached. The oldest incomplete group is discarded when a 17th arrives. No clock reads occur:

parser = AisParser.streaming()          # timeout_ms omitted → no timeout

Auto clock — the binding calls time.monotonic_ns() internally to drive the timeout. Use this when system time is reliable and you do not need deterministic replay:

parser = AisParser.streaming(timeout_ms=60_000)          # clock="auto" implied
# or explicitly:
parser = AisParser.streaming(timeout_ms=60_000, clock="auto")

Manual clock — the caller drives the clock via tick(now_ms=...). No clock reads occur inside the binding. This is the correct mode for unit tests, simulators, and any environment where time is patched:

parser = AisParser.streaming(timeout_ms=60_000, clock="manual")

parser.feed(fragment_bytes)
parser.tick(now_ms=current_time_ms)     # expire stale groups at this instant
for msg in parser:
    ...

Calling tick() on a parser that wasn't built with clock="manual" raises ValueError — including parsers built with no timeout_ms (which default to clock="auto").

Errors

Every exception is a MarlinError subclass (importable from marlin):

  • EnvelopeError — framing or checksum failure
  • DecodeError — field-level decode failure in a typed NMEA sentence
  • AisError — AIS armor or bit-level decode failure
  • ReassemblyError — fragment reassembly violation (out-of-order, channel mismatch, or timeout eviction)
  • KlvError — malformed KLV input (bad local-set key, truncated bytes, checksum mismatch)

Property tests (via Hypothesis) verify panic-freedom on arbitrary byte inputs — no input can cause the binding to crash the interpreter.

Examples

Six runnable scripts live in bindings/python/examples/:

Script What it shows
one_shot_no_crlf.py Single-datagram framing without \r\n (OneShotParser)
parse_log_file.py Disk-backed .nmea log replay (StreamingParser)
streaming_tcp_style.py TCP-style chunked feed; sentences straddle feed() boundaries
decode_aivdm_log.py Multi-fragment AIS reassembly: Type 1 + Type 5
parse_stdin.py Stdin pipe reader for live NMEA/AIS capture
live_ais_dashboard.py Live ship tracker: envelope + AIS in one script, periodic refresh

Type stubs

The py.typed marker is included in the package. Every public class, function, and constant has a .pyi stub. Downstream projects that run mypy --strict get full type-check coverage without extra configuration.

Rust crates

The Python bindings wrap four Rust crates: marlin-nmea-envelope, marlin-nmea-0183, marlin-ais, marlin-klv.

MSRV / Python version

Rust 1.82. Python 3.9+.

License

Dual-licensed under MIT OR Apache-2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

marlin_py-0.1.3.tar.gz (195.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

marlin_py-0.1.3-cp39-abi3-win_amd64.whl (385.2 kB view details)

Uploaded CPython 3.9+Windows x86-64

marlin_py-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (461.2 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

marlin_py-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (448.5 kB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

marlin_py-0.1.3-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl (847.0 kB view details)

Uploaded CPython 3.9+macOS 10.12+ universal2 (ARM64, x86-64)macOS 10.12+ x86-64macOS 11.0+ ARM64

File details

Details for the file marlin_py-0.1.3.tar.gz.

File metadata

  • Download URL: marlin_py-0.1.3.tar.gz
  • Upload date:
  • Size: 195.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for marlin_py-0.1.3.tar.gz
Algorithm Hash digest
SHA256 b5dc6a5b7d2a4de5e4633454c0f2bd885c99e504832b53e0a9007cf38cd94537
MD5 5cc5fbe5d1e70cb8f034daa399c8f782
BLAKE2b-256 d0c0e5d08c65d2ca770bccefab0a5770e14d6aa8ed376dd7f54b8bbc120f633d

See more details on using hashes here.

Provenance

The following attestation bundles were made for marlin_py-0.1.3.tar.gz:

Publisher: python-bindings.yml on jkr78/marlin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file marlin_py-0.1.3-cp39-abi3-win_amd64.whl.

File metadata

  • Download URL: marlin_py-0.1.3-cp39-abi3-win_amd64.whl
  • Upload date:
  • Size: 385.2 kB
  • Tags: CPython 3.9+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for marlin_py-0.1.3-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 fbc1973e85364def67e69c6e22f8cafaf6df4ce4e1c60029c1bc46c7dde09405
MD5 5f14362407155f2d8ba1b5db05962549
BLAKE2b-256 5e495867c9aea733c0ba0feeabb7ec54a554219ae11eda4db46ac8047dea460f

See more details on using hashes here.

Provenance

The following attestation bundles were made for marlin_py-0.1.3-cp39-abi3-win_amd64.whl:

Publisher: python-bindings.yml on jkr78/marlin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file marlin_py-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for marlin_py-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 0afa79ae776ece5526f8952a06aa55f2cc84366d59347ca77271803bc21cd7c3
MD5 af659d5c07cc9bbacbde9ea61ee5d8ca
BLAKE2b-256 330c1248792cc7ce8918c9f65bdae334be8a405dae1fb3259f35cb1cce9187a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for marlin_py-0.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: python-bindings.yml on jkr78/marlin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file marlin_py-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for marlin_py-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 d83e767eb496b421ccf6a05db08a0013057a40c8e32d062122ba92f4158da4b4
MD5 cd9a186e665b00a6cd84b57be1ec34b2
BLAKE2b-256 1210200506e46253dee8f42c643c0c111809a83693ae21d82120885179540f24

See more details on using hashes here.

Provenance

The following attestation bundles were made for marlin_py-0.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: python-bindings.yml on jkr78/marlin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file marlin_py-0.1.3-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl.

File metadata

File hashes

Hashes for marlin_py-0.1.3-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
Algorithm Hash digest
SHA256 91f308fcd2d9ac5be6b8fade7cea45c7548e6456affe6325481b9c1ae9220da0
MD5 b28cf3bc8347a981ed5c0fd0ceed1406
BLAKE2b-256 ecbe0d2f6918fc4b2319c49f4c4c1bb0a9e19133ca0cf08511d5b3c9c6bf698e

See more details on using hashes here.

Provenance

The following attestation bundles were made for marlin_py-0.1.3-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl:

Publisher: python-bindings.yml on jkr78/marlin

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page