nmea0183-parser

Crates.ionmea0183-parser
lib.rsnmea0183-parser
version0.3.2
created_at2025-07-19 21:53:23.347194+00
updated_at2025-07-23 11:53:21.40907+00
descriptionA zero-allocation NMEA 0183 parser that separates message framing from content parsing
homepagehttps://github.com/sdeor/nmea0183-parser
repositoryhttps://github.com/sdeor/nmea0183-parser
max_upload_size
id1760560
size164,944
(ziv-sdeor)

documentation

https://docs.rs/nmea0183-parser

README

A Flexible NMEA Framing Parser for Rust

License: MIT Apache License 2.0 docs.rs Crates.io Version

A zero-allocation NMEA 0183 parser that separates message framing from content parsing, giving you full control over data handling.

This Rust crate provides a generic and configurable parser for NMEA 0183-style messages, with the typical format:

$HHH,D1,D2,...,Dn*CC\r\n

It focuses on parsing and validating the framing of NMEA 0183-style sentences (start character, optional checksum, and optional CRLF), allowing you to plug in your own domain-specific content parsers — or use built-in ones for common NMEA sentence types.


✨ Why Use This Crate?

Unlike traditional NMEA crates that tightly couple format and content parsing, nmea0183_parser lets you:

  • ✅ Choose your compliance level (strict vs lenient)
  • ✅ Plug in your own payload parser (GNSS, marine, custom protocols)
  • ✅ Support both &str and &[u8] inputs
  • ✅ Parse without allocations, built on top of nom, a parser combinator library in Rust.

Perfect for:

  • GNSS/GPS receiver integration
  • Marine electronics parsing
  • IoT devices consuming NMEA 0183-style protocols
  • Debugging or testing tools for embedded equipment
  • Legacy formats that resemble NMEA but don’t strictly comply

📦 Key Features

  • ✅ ASCII-only validation
  • ✅ Required or optional checksum validation
  • ✅ Required or forbidden CRLF ending enforcement
  • ✅ Zero-allocation parsing
  • ✅ Built on nom combinators
  • ✅ Fully pluggable content parser (you bring the domain logic)
  • ✅ Optional built-in support for common NMEA sentences

⚡ Quick Start

Here's a minimal example to get you started with parsing NMEA 0183-style sentences:

use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
use nom::Parser;

// Simple content parser that splits fields by comma
fn parse_fields(input: &str) -> IResult<&str, Vec<&str>> {
    Ok(("", input.split(',').collect()))
}

// Create parser with strict validation (checksum + CRLF required)
let parser_factory = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Required)
    .line_ending_mode(LineEndingMode::Required);

let mut parser = parser_factory.build(parse_fields);

// Parse a GPS sentence
let result =
    parser.parse("$GPGGA,123456.00,4916.29,N,12311.76,W,1,08,0.9,545.4,M,46.9,M,,*73\r\n");

match result {
    Ok((_remaining, fields)) => {
        println!("Success! Parsed {} fields", fields.len()); // 15 fields
        println!("Sentence type: {}", fields[0]); // "GPGGA"
    }
    Err(e) => println!("Parse error: {:?}", e),
}

For custom parsing logic, you can define your own content parser. The Nmea0183ParserBuilder creates a parser factory that you then call with your content parser:

use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
use nom::Parser;

// Your custom logic for the inner data portion (after "$" and before "*CC").
// The `parse_content` function should return an IResult<Input, Output> type
// so it can be used with the framing parser.
// The `Output` type can be any Rust type you define, such as a struct or an enum.
fn parse_content(input: &str) -> IResult<&str, Vec<&str>> {
    // You can decode fields here. In this example, we split the input by commas.
    Ok(("", input.split(',').collect()))
}

// The `Nmea0183ParserBuilder` creates a parser factory that you then call
// with your content parser.
let parser_factory = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Required)
    .line_ending_mode(LineEndingMode::Required);

let mut parser = parser_factory.build(parse_content);

// Or combine into one line:
// let mut parser = Nmea0183ParserBuilder::new()
//     .checksum_mode(ChecksumMode::Required)
//     .line_ending_mode(LineEndingMode::Required)
//     .build(parse_content);

// Now you can use the parser to parse your NMEA sentences.
match parser.parse("$Header,field1,field2*3C\r\n") {
    Ok((remaining, fields)) => {
        assert_eq!(remaining, "");
        assert_eq!(fields, vec!["Header", "field1", "field2"]);
    }
    Err(e) => println!("Parse error: {:?}", e),
}

🧐 How It Works

  1. Framing parser handles the outer structure:

    • ASCII-only validation
    • Start delimiter ($)
    • Optional checksum validation (*CC)
    • Optional CRLF endings (\r\n)
  2. Your content parser, or built-in ones, handle the inner data (D1,D2,...,Dn):

    • Field parsing and validation
    • Type conversion
    • Domain-specific logic

You have full control over sentence content interpretation.

In the above example, parse_content is your custom logic that processes the inner data of the sentence. The Nmea0183ParserBuilder creates a parser that handles the framing, while you focus on the content.


🔧 Configuration Options

You can configure the parser's behavior using ChecksumMode and LineEndingMode:

use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
use nom::Parser;

fn content_parser(input: &str) -> IResult<&str, bool> {
    Ok((input, true))
}

// Strict: checksum and CRLF both required
let mut strict_parser = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Required)
    .line_ending_mode(LineEndingMode::Required)
    .build(content_parser);

assert!(strict_parser.parse("$GPGGA,data*6A\r\n").is_ok());
assert!(strict_parser.parse("$GPGGA,data*6A").is_err()); // (missing CRLF)
assert!(strict_parser.parse("$GPGGA,data\r\n").is_err()); // (missing checksum)

// Checksum required, no CRLF allowed
let mut no_crlf_parser = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Required)
    .line_ending_mode(LineEndingMode::Forbidden)
    .build(content_parser);

assert!(no_crlf_parser.parse("$GPGGA,data*6A").is_ok());
assert!(no_crlf_parser.parse("$GPGGA,data*6A\r\n").is_err()); // (CRLF present)
assert!(no_crlf_parser.parse("$GPGGA,data").is_err()); // (missing checksum)

// Checksum optional, CRLF required
let mut optional_checksum_parser = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Optional)
    .line_ending_mode(LineEndingMode::Required)
    .build(content_parser);

assert!(optional_checksum_parser.parse("$GPGGA,data*6A\r\n").is_ok()); // (with valid checksum)
assert!(optional_checksum_parser.parse("$GPGGA,data\r\n").is_ok()); // (without checksum)
assert!(optional_checksum_parser.parse("$GPGGA,data*99\r\n").is_err()); // (invalid checksum)
assert!(optional_checksum_parser.parse("$GPGGA,data*6A").is_err()); // (missing CRLF)

// Lenient: checksum optional, CRLF forbidden
let mut lenient_parser = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Optional)
    .line_ending_mode(LineEndingMode::Forbidden)
    .build(content_parser);

assert!(lenient_parser.parse("$GPGGA,data*6A").is_ok()); // (with valid checksum)
assert!(lenient_parser.parse("$GPGGA,data").is_ok()); // (without checksum)
assert!(lenient_parser.parse("$GPGGA,data*99").is_err()); // (invalid checksum)
assert!(lenient_parser.parse("$GPGGA,data\r\n").is_err()); // (CRLF present)

🔍 Parsing Both String and Byte Inputs

The parser can handle both &str and &[u8] inputs. You can define your content parser for either type; the factory will adapt accordingly.

use nmea0183_parser::{ChecksumMode, IResult, LineEndingMode, Nmea0183ParserBuilder};
use nom::Parser;

fn parse_content_str(input: &str) -> IResult<&str, Vec<&str>> {
    Ok(("", input.split(',').collect()))
}

let mut parser_str = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Required)
    .line_ending_mode(LineEndingMode::Required)
    .build(parse_content_str);

// Parse from string
let string_input = "$Header,field1,field2*3C\r\n";
let result = parser_str.parse(string_input);

assert!(result.is_ok());
assert_eq!(result.unwrap().1, vec!["Header", "field1", "field2"]);

fn parse_content_bytes(input: &[u8]) -> IResult<&[u8], u8> {
    let (input, first_byte) = nom::number::complete::u8(input)?;
    Ok((input, first_byte))
}

let mut parser_bytes = Nmea0183ParserBuilder::new()
    .checksum_mode(ChecksumMode::Required)
    .line_ending_mode(LineEndingMode::Required)
    .build(parse_content_bytes);

// Parse from bytes
let byte_input = b"$Header,field1,field2*3C\r\n";
let result_bytes = parser_bytes.parse(byte_input);

assert!(result_bytes.is_ok());
assert_eq!(result_bytes.unwrap().1, 72); // 'H' is the first byte of the content

🧩 NmeaParse trait and #[derive(NmeaParse)] Macro

The NmeaParse trait provides a generic interface for parsing values from NMEA 0183-style content, supporting both primitive and composite types. Implementations are provided for primitive types, Option<T>, Vec<T>, and more types, and you can implement this trait for your own types to enable custom parsing logic.

Implementing the NmeaParse Trait

To implement the NmeaParse trait for your type, you need to provide a parse method that takes an input and returns an IResult with the remaining input and the parsed value.

NMEA 0183 fields are typically comma-separated. When parsing composite types (like structs), you usually want to consume the separator before parsing each subsequent field. However, for optional fields (Option<T>) or repeated fields (Vec<T>), always consuming the separator can cause issues if the field is missing.

To address this, the trait provides a parse_preceded(separator) method. This method ensures the separator is only consumed if the field is present. By default, parse_preceded is implemented as a simple wrapper around preceded(separator, Self::parse), but you can override it for custom behavior—such as the implementations for Option<T> and Vec<T>.

This design gives you fine-grained control over field parsing and separator handling, making it easy to implement robust NMEA content parsers for your own types.

Deriving the NmeaParse Trait

Based on nom-derive and with a lot of similarities, NmeaParse is a custom derive attribute to derive content parsers for your NMEA 0183-style data structures.

The NmeaParse derive macro automatically generates an implementation of the NmeaParse trait for your structs and enums using nom parsers when possible. This allows you to define your data structures and derive parsing logic without writing boilerplate code.

For example, you can define a struct and derive the NmeaParse trait like this:

use nmea0183_parser::NmeaParse;

#[derive(NmeaParse)]
struct Data {
    pub id: u8,
    pub value: f64,
    pub timestamp: u64,
}

This will generate an implementation of the NmeaParse trait for the Data struct, allowing you to parse NMEA 0183-style input into instances of Data. The generated code will look something like this (simplified):

impl NmeaParse for Data {
    fn parse(i: &'a str) -> nmea0183_parser::IResult<&'a str, Self, Error> {
        let (i, id) = <u8>::parse(i)?;
        let (i, value) = <f64>::parse_preceded(nom::character::complete::char(',')).parse(i)?;
        let (i, timestamp) = <u64>::parse_preceded(nom::character::complete::char(',')).parse(i)?;

        Ok((i, Data { id, value, timestamp }))
    }
}

You can now parse an input containing NMEA 0183-style content into a Data struct:

let input = "123,45.67,1622547800";
let result: IResult<_, _> = Data::parse(input);
let (remaining, data) = result.unwrap();
assert!(remaining.is_empty());
assert_eq!(data.id, 123);
assert_eq!(data.value, 45.67);
assert_eq!(data.timestamp, 1622547800);

The macro also supports enums, which require a selector attribute to determine which variant to parse:

use nmea0183_parser::{Error, IResult, NmeaParse};

#[derive(NmeaParse)]
#[nmea(selector(u8::parse))]
enum Data {
    #[nmea(selector(0))]
    TypeA { id: u8, value: u16 },
    #[nmea(selector(1))]
    TypeB { values: [u8; 4] },
}

let input = "0,42,100";
let result: IResult<_, _> = Data::parse(input);
assert!(matches!(result, Ok((_, Data::TypeA { id: 42, value: 100 }))));

let input = "1,2,3,4,5";
let result: IResult<_, _> = Data::parse(input);
assert!(matches!(result, Ok((_, Data::TypeB { values: [2, 3, 4, 5] }))));

let input = "2,42";
// Expecting an error because no variant matches selector 2
let error = Data::parse(input).unwrap_err();
assert!(matches!(error,
    nom::Err::Error(Error::ParsingError(nom::error::Error {
        code: nom::error::ErrorKind::Switch,
        ..
    }))
));

You can use the #[derive(NmeaParse)] attribute on your structs and enums to automatically generate parsing logic based on the field types. The macro will try to infer parsers for known types (implementors of the NmeaParse trait), but you can also customize the parsing behavior using attributes.

For more details on how to use the NmeaParse derive macro and customize parsing behavior, refer to the documentation.


🧱 Built-in NMEA Sentence Content Parser

Alongside the flexible framing parser, this crate can provide a built-in NmeaSentence content parser for common NMEA 0183 sentence types. To use it, enable the nmea-content feature in your Cargo.toml.

This parser uses the NmeaParse trait to provide content-only parsing. It does not handle framing — such as the initial $, optional checksum (*CC), or optional CRLF (\r\n). That responsibility belongs to the framing parser, which wraps around the content parser.

To parse a complete NMEA sentence, you can use the Nmea0183ParserBuilder with the built-in content parser:

use nmea0183_parser::{
    IResult, Nmea0183ParserBuilder, NmeaParse,
    nmea_content::{GGA, Location, NmeaSentence, Quality},
};
use nom::Parser;

// Defaults to strict parsing with both checksum and CRLF required
let mut nmea_parser = Nmea0183ParserBuilder::new().build(NmeaSentence::parse);

let result: IResult<_, _> =
    nmea_parser.parse("$GPGGA,123456.00,4916.29,N,12311.76,W,1,08,0.9,545.4,M,46.9,M,,*73\r\n");

assert!(
    result.is_ok(),
    "Failed to parse NMEA sentence: {:?}",
    result.unwrap_err()
);

let (_, sentence) = result.unwrap();
assert!(matches!(
    sentence,
    NmeaSentence::GGA(GGA {
        location: Some(Location {
            latitude: 49.2715,
            longitude: -123.196,
        }),
        fix_quality: Quality::GPSFix,
        satellite_count: Some(8),
        hdop: Some(0.9),
        ..
    })
));

Note: While the Nmea0183ParserBuilder framing parser can accept both &str and &[u8] inputs, the built-in content parser only accepts &str, as it is designed specifically for text-based NMEA sentences.

Supported NMEA Sentences

  • DBT - Depth Below Transducer
  • DPT - Depth of Water
  • GGA - Global Positioning System Fix Data
  • GLL - Geographic Position: Latitude/Longitude
  • GSA - GPS DOP and Active Satellites
  • GSV - Satellites in View
  • RMC - Recommended Minimum Navigation Information
  • VTG - Track made good and Ground speed
  • ZDA - Time & Date: UTC, day, month, year and local time zone

NMEA Version Support

Different NMEA versions may include additional fields in certain sentence types. You can choose the version that matches your equipment by enabling the appropriate feature flags.

Feature Flag NMEA Version When to Use
nmea-content Pre-2.3 Standard NMEA parsing
nmea-v2-3 NMEA 2.3 Older GPS/marine equipment
nmea-v3-0 NMEA 3.0 Mid-range equipment
nmea-v4-11 NMEA 4.11 Modern equipment

For specific field differences between versions, please refer to the NMEA 0183 standard documentation.


🚀 Coming Soon

  • 📘 Comprehensive examples and tutorials for common use cases
  • 🔧 Additional NMEA sentence types and protocol support

📝 License

This project is licensed under the MIT License and Apache License 2.0. You can choose either license for your use.


🛠️ Contributing

Contributions are very welcome! Open an issue or PR for:

  • Bug fixes
  • Integration tests and samples
  • Documentation improvements
  • New content parsers for additional NMEA sentences

You can submit issues or pull requests to contribute.


📚 Documentation

Commit count: 0

cargo fmt