nmea-kit 0.2.2

Bidirectional NMEA 0183 parser and encoder with AIS decoding
Documentation

nmea-kit

Bidirectional NMEA 0183 parser/encoder with AIS message decoding, written in Rust.

  • 23 NMEA sentence types — parse and encode with checksum
  • 7 AIS message types — decode Class A/B position reports and static data
  • Shared frame layer — handles $ (NMEA) and ! (AIS) framing, IEC 61162-450 tag blocks
  • Zero dependencies
  • No nom, no proc-macroFieldReader/FieldWriter helpers for clean sequential parsing

Quick start

Parse an NMEA sentence

use nmea_kit::{parse_frame, NmeaSentence};

let frame = parse_frame("$IIMWD,046.,T,046.,M,10.1,N,05.2,M*43").unwrap();
let sentence = NmeaSentence::parse(&frame);

match sentence {
    NmeaSentence::Mwd(mwd) => {
        println!("True wind dir: {:?}°", mwd.wind_dir_true);
        println!("Wind speed: {:?} kts", mwd.wind_speed_kts);
    }
    _ => {}
}

Encode and send an NMEA sentence

use nmea_kit::nmea::NmeaEncodable;
use nmea_kit::nmea::sentences::Dbt;

let dbt = Dbt {
    depth_feet: Some(7.7),
    depth_meters: Some(2.3),
    depth_fathoms: Some(1.3),
};

let sentence = dbt.to_sentence("SD");
// "$SDDBT,7.7,f,2.3,M,1.3,F*05\r\n"

Decode AIS messages

use nmea_kit::parse_frame;
use nmea_kit::ais::{AisParser, AisMessage};

let mut parser = AisParser::new();
let frame = parse_frame("!AIVDM,1,1,,A,13aEOK?P00PD2wVMdLDRhgvL289?,0*26").unwrap();

if let Some(AisMessage::Position(pos)) = parser.decode(&frame) {
    println!("MMSI: {}, lat: {:?}, lon: {:?}", pos.mmsi, pos.latitude, pos.longitude);
}

Architecture

raw line ──→ parse_frame() ──→ NmeaFrame { prefix, talker, sentence_type, fields }
                                    |
                     +--------------+--------------+
                     v              v               v
               $ + known      $ + unknown     ! (AIVDM/AIVDO)
                     |              |               |
                     v              v               v
              Typed struct    Raw fields      AisMessage enum
              (Mwd, Apb..)   (pass-through)  (Types 1-5,18,19,24)

Frame layer validates checksum, strips tag blocks, extracts talker ID and sentence type. Shared by both NMEA and AIS.

NMEA content uses FieldReader/FieldWriter for sequential field parsing and encoding. Each sentence type is a standalone struct with parse(), encode(), and to_sentence(). Parsing is lenient: parse() always returns Some for known types, mapping missing or malformed fields to None. This is intentional for marine instruments that often produce partial data.

AIS content decodes 6-bit ASCII armor into a bitstream, handles multi-fragment reassembly, and extracts typed fields. Read-only (transmitting AIS requires certified hardware).

Supported types

NMEA 0183 sentences (bidirectional) — full coverage list

Category Sentences
Position RMC, GGA, GLL, GNS
Satellites GBS, GST
Wind MWD, MWV
Heading HDT, HDG, HDM
Course & Speed VBW, VTG, VHW
Depth DPT, DBT, DBS, DBK
Steering ROT
Environment MTW, XDR¹
Waypoints & Routes RMB
Time ZDA

¹ Xdr has an additional to_sentences() -> Vec<String> method that automatically splits many measurements into multiple sentences to stay within the 82-character NMEA line limit.

AIS messages (read-only)

File Type Description
position_a.rs 1, 2, 3 Class A position report
voyage_a.rs 5 Static and voyage data
position_b.rs 18 Class B standard position
position_b_ext.rs 19 Class B+ extended position
static_b.rs 24 Class B static data report

Key improvements over existing crates

Issue nmea 0.7 / ais 0.12 nmea-kit
Sentence coverage ~10 types, rest manual 23 types, all typed
Encoding Read-only Bidirectional (parse + encode)
Error distinction Can't tell unsupported vs malformed Frame errors vs content errors
AIS lat/lon precision f32 (11m error) f64
AIS sentinels 91/181/511 leak to caller Filtered to None at decode
Tag blocks Manual stripping Built into frame layer
Dependencies nom (AIS) Zero

Features

[dependencies]
nmea-kit = "0.2"
Feature Default Description
nmea yes All 23 NMEA sentence types
ais yes AIS message decoding
dbk, dbs, dbt, dpt, gbs, gga, gll, gns, gst, hdg, hdm, hdt, mtw, mwd, mwv, rmb, rmc, rot, vbw, vhw, vtg, xdr, zda via nmea Individual sentence types

Cherry-pick only the sentences you need (no AIS, minimal code):

[dependencies]
nmea-kit = { version = "0.2", default-features = false, features = ["rmc", "mwd"] }

NMEA-only (no AIS, all sentences, zero dependencies):

[dependencies]
nmea-kit = { version = "0.2", default-features = false, features = ["nmea"] }

Adding a new sentence type

Create src/nmea/sentences/xyz.rs:

use crate::nmea::field::{FieldReader, FieldWriter, NmeaEncodable};

#[derive(Debug, Clone, PartialEq)]
pub struct Xyz {
    pub some_value: Option<f32>,
    pub some_flag: Option<char>,
}

impl Xyz {
    pub fn parse(fields: &[&str]) -> Option<Self> {
        let mut r = FieldReader::new(fields);
        Some(Self {
            some_value: r.f32(),
            some_flag: r.char(),
        })
    }
}

impl NmeaEncodable for Xyz {
    const SENTENCE_TYPE: &str = "XYZ";

    fn encode(&self) -> Vec<String> {
        let mut w = FieldWriter::new();
        w.f32(self.some_value);
        w.char(self.some_flag);
        w.finish()
    }
}

Then add mod xyz; pub use xyz::*; to sentences/mod.rs and a variant to NmeaSentence in nmea/mod.rs.

License

MIT OR Apache-2.0