Ability to parse a NMEA GGA sentence

This commit is contained in:
Richard Patching 2025-04-05 10:03:53 +01:00
commit 4f1ce65e2d
14 changed files with 1340 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

7
Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aniker-gps"
version = "0.1.0"

21
Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "aniker-gps"
version = "0.1.0"
edition = "2021"
authors = ["Richard Patching <richard@simaker.com>"]
description = "A Rust library for parsing NMEA GPS sentences"
license = "MIT OR Apache-2.0"
repository = "https://github.com/yourusername/aniker-gps"
readme = "README.md"
keywords = ["gps", "nmea", "parser", "navigation"]
categories = ["parsing", "science", "embedded"]
[lib]
name = "aniker_gps"
path = "src/lib.rs"
[[bin]]
name = "aniker-gps"
path = "src/main.rs"
[dependencies]

98
README.md Normal file
View File

@ -0,0 +1,98 @@
# Aniker GPS
A Rust library for parsing NMEA GPS sentences.
## Features
- Parse GGA sentences to extract latitude, longitude, altitude, and more
- Support for different units of measurement (meters, feet)
- Comprehensive error handling with custom error types
- Simple and efficient API
## Usage
Add this to your `Cargo.toml`:
```toml
[dependencies]
aniker-gps = "0.1.0"
```
### Example
```rust
use aniker_gps::{parse_gga, GpsError};
fn main() {
// Example NMEA GGA sentence
let mut gga_example = "$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
// Parse the GGA sentence and handle the result
match parse_gga(&mut gga_example) {
Ok(position) => {
println!("Latitude: {}", position.lat);
println!("Longitude: {}", position.long);
println!("Altitude: {} {}", position.alt, position.altitude_unit);
println!("Geoid Separation: {} {}", position.geoid_separation, position.geoid_unit);
}
Err(e) => {
println!("Error parsing GGA sentence:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
}
```
## Error Handling
The library provides a custom `GpsError` enum for error handling:
```rust
pub enum GpsError {
/// Invalid message type (not GGA)
InvalidMessageType,
/// Invalid sentence length
InvalidLength,
/// Error parsing a numeric field
ParseError(String),
/// Other errors
Other(String),
}
```
This allows for more detailed error handling and better error messages.
## GGA Sentence Format
The GGA sentence contains the following fields:
- Sentence type (checked for "GGA")
- Current time (UTC)
- Latitude (in DDMM.MMM format)
- Latitude compass direction (N/S)
- Longitude (in DDDMM.MMM format)
- Longitude compass direction (E/W)
- Fix type (0 for no fix, 1 for GPS, 2 for DGPS)
- Number of satellites used for fix
- Horizontal dilution of precision
- Altitude above mean sea level
- Altitude units (M for meters, F for feet)
- Height of mean sea level above WGS-84 earth ellipsoid
- Units of the above geoid separation (M for meters, F for feet)
- Time since last differential correction (if available)
- Differential station ID (if available)
- Checksum validation value
## License
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0)
* MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT)
at your option.

145
docs/usage.md Normal file
View File

@ -0,0 +1,145 @@
# Aniker GPS Library Usage Guide
This document provides detailed information on how to use the Aniker GPS library for parsing NMEA sentences.
## Installation
Add the following to your `Cargo.toml`:
```toml
[dependencies]
aniker-gps = "0.1.0"
```
## Basic Usage
### Parsing a GGA Sentence
The most common use case is parsing a GGA sentence to extract position data:
```rust
use aniker_gps::{Position, parse_gga, GpsError};
fn main() {
// Example NMEA GGA sentence
let mut gga_example = "$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
// Parse the GGA sentence and handle the result
match parse_gga(&mut gga_example) {
Ok(position) => {
println!("Latitude: {}", position.lat);
println!("Longitude: {}", position.long);
println!("Altitude: {}", position.alt);
}
Err(e) => {
println!("Error parsing GGA sentence:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
}
```
### Working with Position Data
The `Position` struct contains the following fields:
- `lat`: Latitude in decimal degrees (positive for North, negative for South)
- `long`: Longitude in decimal degrees (positive for East, negative for West)
- `alt`: Altitude in meters
You can access these fields directly:
```rust
let position = parse_gga(&mut gga_example).unwrap();
let latitude = position.lat;
let longitude = position.long;
let altitude = position.alt;
```
## Error Handling
The `parse_gga` function returns a `Result<Position, GpsError>`, which means it can either return a valid `Position` or a `GpsError`. The `GpsError` enum provides detailed error information:
- `GpsError::InvalidMessageType`: Returned when the message type is not GGA
- `GpsError::InvalidLength`: Returned when the GGA sentence has an incorrect number of fields
- `GpsError::ParseError(msg)`: Returned when parsing a specific field fails, with a description of the error
- `GpsError::Other(msg)`: Returned for other errors that don't fit into the above categories
Example of error handling:
```rust
match parse_gga(&mut gga_example) {
Ok(position) => {
// Use the position data
}
Err(e) => {
// Handle the error
match e {
GpsError::InvalidMessageType => println!("Invalid message type, expected GGA"),
GpsError::InvalidLength => println!("Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!("Parse error: {}", msg),
GpsError::Other(msg) => println!("Other error: {}", msg),
}
}
}
```
## Advanced Usage
### Parsing Multiple Sentences
When working with multiple NMEA sentences, you can process them in a loop:
```rust
let sentences = vec![
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A",
"$GPGGA,210231,3855.4488,N,09446.0072,W,1,07,1.1,371.5,M,-29.5,M,,*7B",
];
for sentence in sentences {
let mut sentence = sentence.to_string();
match parse_gga(&mut sentence) {
Ok(position) => {
// Process the position
println!("Latitude: {}, Longitude: {}", position.lat, position.long);
}
Err(e) => {
// Handle the error
match e {
GpsError::InvalidMessageType => println!("Invalid message type, expected GGA"),
GpsError::InvalidLength => println!("Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!("Parse error: {}", msg),
GpsError::Other(msg) => println!("Other error: {}", msg),
}
}
}
}
```
## Examples
Check out the examples directory for more detailed examples of how to use the library:
- `basic_usage.rs`: Simple example of parsing a GGA sentence
- `multiple_sentences.rs`: Example of parsing multiple sentences
## Running the Examples
To run the examples, use the following command:
```bash
cargo run --example basic_usage
```
## Running the Tests
To run the tests, use the following command:
```bash
cargo test
```

44
examples/basic_usage.rs Normal file
View File

@ -0,0 +1,44 @@
// Basic example demonstrating how to use the aniker-gps library
use aniker_gps::{parse_gga, GpsError, Position};
fn main() {
// Example NMEA GGA sentence
let mut gga_example =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
// Parse the GGA sentence and handle the result
match parse_gga(&mut gga_example) {
Ok(position) => {
println!("Successfully parsed GGA sentence:");
println!("Latitude: {}", position.lat);
println!("Longitude: {}", position.long);
println!("Altitude: {}", position.alt);
}
Err(e) => {
println!("Error parsing GGA sentence:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
// Example of handling an invalid sentence
let mut invalid_example =
"$GPRMC,210230,A,3855.4487,N,09446.0071,W,0.0,076.2,130495,003.8,E*69".to_string();
match parse_gga(&mut invalid_example) {
Ok(_) => println!("Unexpected: Successfully parsed invalid sentence"),
Err(e) => {
println!("Expected error:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
}

View File

@ -0,0 +1,37 @@
// Example demonstrating how to parse multiple NMEA sentences
use aniker_gps::{parse_gga, GpsError, Position};
fn main() {
// A collection of NMEA sentences
let sentences = vec![
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A",
"$GPGGA,210231,3855.4488,N,09446.0072,W,1,07,1.1,371.5,M,-29.5,M,,*7B",
"$GPGGA,210232,3855.4489,N,09446.0073,W,1,07,1.1,372.5,M,-29.5,M,,*7C",
"$GPRMC,210233,A,3855.4490,N,09446.0074,W,0.0,076.2,130495,003.8,E*69", // Invalid (RMC)
"$GPGGA,210234,3855.4491,N,09446.0075,W,1,07,1.1,373.5,M,-29.5,M,,*7D",
];
// Process each sentence
for (i, sentence) in sentences.iter().enumerate() {
let mut sentence = sentence.to_string();
match parse_gga(&mut sentence) {
Ok(position) => {
println!("Sentence {}: Successfully parsed", i + 1);
println!(" Latitude: {}", position.lat);
println!(" Longitude: {}", position.long);
println!(" Altitude: {}", position.alt);
}
Err(e) => {
println!("Sentence {}: Error", i + 1);
match e {
GpsError::InvalidMessageType => {
println!(" Invalid message type, expected GGA")
}
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
}
}

54
src/error.rs Normal file
View File

@ -0,0 +1,54 @@
/// Custom error type for GPS parsing operations
///
/// This enum provides detailed error information when parsing NMEA sentences fails.
/// It allows for specific error handling based on the type of error encountered.
///
/// # Examples
///
/// ```
/// use aniker_gps::GpsError;
///
/// // Handle different error types
/// fn handle_gps_error(error: GpsError) {
/// match error {
/// GpsError::InvalidMessageType => {
/// println!("The message type is not GGA");
/// },
/// GpsError::InvalidLength => {
/// println!("The GGA sentence has an incorrect number of fields");
/// },
/// GpsError::ParseError(msg) => {
/// println!("Failed to parse a field: {}", msg);
/// },
/// GpsError::Other(msg) => {
/// println!("An unexpected error occurred: {}", msg);
/// },
/// }
/// }
/// ```
#[derive(Debug, PartialEq)]
pub enum GpsError {
/// Error when the message type is not GGA
///
/// This error is returned when the NMEA sentence does not contain "GGA" in its message type field.
/// The library currently only supports parsing GGA sentences.
InvalidMessageType,
/// Error when the GGA sentence has an incorrect number of fields
///
/// GGA sentences should have exactly 15 fields. This error is returned when the number of fields
/// does not match this expectation.
InvalidLength,
/// Error when parsing a specific field fails
///
/// This error includes a message describing which field failed to parse and why.
/// Common causes include invalid formats for timestamps, coordinates, or numeric values.
ParseError(&'static str),
/// Other errors that don't fit into the above categories
///
/// This is a catch-all variant for errors that don't fit into the other categories.
/// It includes a message describing the error.
Other(&'static str),
}

384
src/gps.rs Normal file
View File

@ -0,0 +1,384 @@
use crate::error::GpsError;
use crate::position::Position;
use crate::time::Time;
// Constant for the expected number of fields in a GGA sentence
const GGA_LENGTH: usize = 15;
// Constant for the GGA sentence fields
const MSG_TYPE_INDEX: usize = 0;
const TIMESTAMP_INDEX: usize = 1;
const LAT_INDEX: usize = 2;
const LAT_HEMISPHERE_INDEX: usize = 3;
const LONG_INDEX: usize = 4;
const LONG_HEMISPHERE_INDEX: usize = 5;
const FIX_TYPE_INDEX: usize = 6;
const SATELLITES_INDEX: usize = 7;
const HDOP_INDEX: usize = 8;
const ALT_INDEX: usize = 9;
const ALT_UNITS_INDEX: usize = 10; // "M" for meters (default)
const GEOID_SEPARATION_INDEX: usize = 11;
const GEOID_UNITS_INDEX: usize = 12;
const DIFFERENTIAL_AGE_INDEX: usize = 13;
const DIFFERENTIAL_STATION_ID_INDEX: usize = 14;
// Parses a GGA (Global Positioning System Fix Data) NMEA sentence
// GGA format: $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
// Parameters:
// buffer: The NMEA sentence to parse
// Returns:
// Ok(Position) if parsing is successful
// Err(GpsError) if the sentence is invalid
//
// # Errors
//
// This function returns the following errors:
// - `GpsError::InvalidMessageType` if the message type is not GGA
// - `GpsError::InvalidLength` if the GGA sentence has an incorrect number of fields
// - `GpsError::ParseError` if parsing a specific field fails
// - `GpsError::Other` for other errors that don't fit into the above categories
pub fn parse_gga(buffer: &mut String) -> Result<Position, GpsError> {
// Split the NMEA sentence by commas into a vector of string slices
let v: Vec<&str> = buffer.split_terminator(',').collect();
// Check if the message type is GGA
if !v[MSG_TYPE_INDEX].contains("GGA") {
return Err(GpsError::InvalidMessageType);
}
// Check if the sentence has the correct number of fields
if v.len() != GGA_LENGTH {
return Err(GpsError::InvalidLength);
}
let mut position = Position::new();
// Parse the timestamp field
if !v[TIMESTAMP_INDEX].is_empty() {
match Time::parse(v[TIMESTAMP_INDEX]) {
Ok(time) => position.time = time,
Err(e) => return Err(GpsError::ParseError(e)),
}
}
// Parse fix type (0 for no fix, 1 for GPS, 2 for DGPS)
if !v[FIX_TYPE_INDEX].is_empty() {
position.fix_type = v[FIX_TYPE_INDEX].parse::<u8>().unwrap_or(0);
}
// Parse number of satellites
if !v[SATELLITES_INDEX].is_empty() {
position.satellites = v[SATELLITES_INDEX].parse::<u8>().unwrap_or(0);
}
// Parse horizontal dilution of precision
if !v[HDOP_INDEX].is_empty() {
position.hdop = v[HDOP_INDEX].parse::<f32>().unwrap_or(0.);
}
// Check if latitude field (index 2) is not empty
if !v[LAT_INDEX].is_empty() {
// Split latitude into degrees (x) and minutes (y)
// Format: DDMM.MMMM where DD is degrees and MM.MMMM is minutes
let (x, y) = v[LAT_INDEX].split_at(2);
// Parse degrees part to f32, default to 0 if parsing fails
let x_f32 = x.parse::<f32>().unwrap_or(0.);
// Parse minutes part to f32, default to 0 if parsing fails
let y_f32 = y.parse::<f32>().unwrap_or(0.);
// Check if the calculated latitude is valid (not 0)
if x_f32 + (y_f32 / 60.) != 0. {
// Convert from DDMM.MMMM format to decimal degrees
// Formula: degrees + (minutes/60)
position.lat = x_f32 + (y_f32 / 60.);
// If hemisphere is South (S), make latitude negative
if v[LAT_HEMISPHERE_INDEX] == "S" {
position.lat *= -1.;
}
}
if !v[LONG_INDEX].is_empty() {
// Split longitude into degrees (a) and minutes (b)
// Format: DDDMM.MMMM where DDD is degrees and MM.MMMM is minutes
let (a, b) = v[LONG_INDEX].split_at(3);
// Parse degrees part to f32, default to 0 if parsing fails
let a_f32 = a.parse::<f32>().unwrap_or(0.);
// Parse minutes part to f32, default to 0 if parsing fails
let b_f32 = b.parse::<f32>().unwrap_or(0.);
// Check if the calculated longitude is valid (not 0)
if a_f32 + (b_f32 / 60.) != 0. {
// Convert from DDDMM.MMMM format to decimal degrees
// Formula: degrees + (minutes/60)
position.long = a_f32 + (b_f32 / 60.);
// If hemisphere is West (W), make longitude negative
if v[LONG_HEMISPHERE_INDEX] == "W" {
position.long *= -1.;
}
}
// Check if altitude field (index 9) is not empty
if !v[ALT_INDEX].is_empty() {
let c = v[ALT_INDEX];
// Parse altitude to f32
position.alt = match c.parse::<f32>() {
Ok(c) => c,
Err(_) => position.lat, // If parsing fails, use latitude as fallback (likely a bug)
};
}
// Parse geoid separation
if !v[GEOID_SEPARATION_INDEX].is_empty() {
position.geoid_separation = v[GEOID_SEPARATION_INDEX].parse::<f32>().unwrap_or(0.);
}
// Parse differential age if available
if !v[DIFFERENTIAL_AGE_INDEX].is_empty() {
position.differential_age = v[DIFFERENTIAL_AGE_INDEX].parse::<f32>().ok();
}
// Parse differential station ID if available
if !v[DIFFERENTIAL_STATION_ID_INDEX].is_empty() {
position.differential_station_id =
Some(v[DIFFERENTIAL_STATION_ID_INDEX].to_string());
}
}
}
Ok(position)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_gga_sentence() {
// Test with a valid GGA sentence
let mut gga =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Expected values:
// Latitude: 38 degrees + 55.4487/60 = 38.924145
// Longitude: 94 degrees + 46.0071/60 = -94.766785 (negative because W)
// Altitude: 370.5
// Time: 21:02:30.00
// Fix type: 1 (GPS)
// Satellites: 7
// HDOP: 1.1
// Geoid separation: -29.5
assert!((position.lat - 38.924145).abs() < 0.000001);
assert!((position.long - (-94.766785)).abs() < 0.000001);
assert!((position.alt - 370.5).abs() < 0.000001);
assert_eq!(position.time.hours, 21);
assert_eq!(position.time.minutes, 2);
assert_eq!(position.time.seconds, 30);
assert_eq!(position.time.centiseconds, 0);
assert_eq!(position.fix_type, 1);
assert_eq!(position.satellites, 7);
assert!((position.hdop - 1.1).abs() < 0.000001);
assert!((position.geoid_separation - (-29.5)).abs() < 0.000001);
assert_eq!(position.differential_age, None);
assert_eq!(position.differential_station_id, Some("*7A".to_string()));
}
#[test]
fn test_south_west_coordinates() {
// Test with coordinates in South and West hemispheres
let mut gga =
"$GPGGA,210230,3855.4487,S,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Latitude should be negative (South)
assert!(position.lat < 0.0);
// Longitude should be negative (West)
assert!(position.long < 0.0);
}
#[test]
fn test_empty_fields() {
// Test with empty latitude field
let mut gga = "$GPGGA,210230,,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Position should remain at initial values (0.0)
assert_eq!(position.lat, 0.0);
assert_eq!(position.long, 0.0);
assert_eq!(position.alt, 0.0);
}
#[test]
fn test_invalid_altitude() {
// Test with invalid altitude value
let mut gga =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,ABC,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Altitude should fall back to latitude value
assert_eq!(position.alt, position.lat);
}
#[test]
fn test_zero_coordinates() {
// Test with zero coordinates
let mut gga =
"$GPGGA,210230,0000.0000,N,00000.0000,E,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Position should remain at initial values (0.0) due to zero check
assert_eq!(position.lat, 0.0);
assert_eq!(position.long, 0.0);
}
#[test]
fn test_wrong_sentence_type() {
// Test with a non-GGA sentence (RMC in this case)
let mut rmc = "$GPGGA,A,3855.4487N.0071,W0.0,076.2,130495,003.8,E*69".to_string();
let result = parse_gga(&mut rmc);
// Should return an error due to wrong sentence length
assert!(result.is_err());
assert_eq!(result.unwrap_err(), GpsError::InvalidLength);
}
#[test]
fn test_invalid_length() {
// Test with a sentence that has fewer fields than expected
let mut short_gga = "$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M".to_string();
let result = parse_gga(&mut short_gga);
// Should return an error due to wrong sentence length
assert!(result.is_err());
assert_eq!(result.unwrap_err(), GpsError::InvalidLength);
}
#[test]
fn test_invalid_message_type() {
// Test with a non-GGA message type
let mut rmc =
"$GPRMC,210230,A,3855.4487,N,09446.0071,W,0.0,076.2,130495,003.8,E*69".to_string();
let result = parse_gga(&mut rmc);
// Should return an error due to wrong message type
assert!(result.is_err());
assert_eq!(result.unwrap_err(), GpsError::InvalidMessageType);
}
#[test]
fn test_timestamp_parsing() {
// Test with a GGA sentence with a specific timestamp
let mut gga =
"$GPGGA,123456,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Check that the timestamp is correctly parsed
assert_eq!(position.time.hours, 12);
assert_eq!(position.time.minutes, 34);
assert_eq!(position.time.seconds, 56);
assert_eq!(position.time.centiseconds, 0);
// Test with a different timestamp including centiseconds
let mut gga2 =
"$GPGGA,235959.50,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position2 = parse_gga(&mut gga2).unwrap();
// Check that the timestamp is correctly parsed
assert_eq!(position2.time.hours, 23);
assert_eq!(position2.time.minutes, 59);
assert_eq!(position2.time.seconds, 59);
assert_eq!(position2.time.centiseconds, 50);
}
#[test]
fn test_empty_timestamp() {
// Test with an empty timestamp field
let mut gga = "$GPGGA,,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Time should have all fields set to 0
assert_eq!(position.time.hours, 0);
assert_eq!(position.time.minutes, 0);
assert_eq!(position.time.seconds, 0);
assert_eq!(position.time.centiseconds, 0);
}
#[test]
fn test_invalid_timestamp() {
// Test with an invalid timestamp format
let mut gga =
"$GPGGA,ABC,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let result = parse_gga(&mut gga);
// Should return a parse error for the timestamp field
assert!(result.is_err());
match result.unwrap_err() {
GpsError::ParseError(msg) => {
assert!(msg.contains("Invalid time format"));
}
_ => panic!("Expected ParseError for invalid timestamp"),
}
}
#[test]
fn test_fix_type() {
// Test with different fix types
let mut gga_no_fix =
"$GPGGA,210230,3855.4487,N,09446.0071,W,0,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position_no_fix = parse_gga(&mut gga_no_fix).unwrap();
assert_eq!(position_no_fix.fix_type, 0);
let mut gga_gps_fix =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position_gps_fix = parse_gga(&mut gga_gps_fix).unwrap();
assert_eq!(position_gps_fix.fix_type, 1);
let mut gga_dgps_fix =
"$GPGGA,210230,3855.4487,N,09446.0071,W,2,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position_dgps_fix = parse_gga(&mut gga_dgps_fix).unwrap();
assert_eq!(position_dgps_fix.fix_type, 2);
}
#[test]
fn test_satellites() {
// Test with different number of satellites
let mut gga =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,12,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
assert_eq!(position.satellites, 12);
}
#[test]
fn test_hdop() {
// Test with different HDOP values
let mut gga =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,2.5,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
assert!((position.hdop - 2.5).abs() < 0.000001);
}
#[test]
fn test_geoid_separation() {
// Test with different geoid separation values
let mut gga =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-35.2,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
assert!((position.geoid_separation - (-35.2)).abs() < 0.000001);
}
#[test]
fn test_differential_data() {
// Test with differential data
let mut gga =
"$GPGGA,210230,3855.4487,N,09446.0071,W,2,07,1.1,370.5,M,-29.5,M,0.5,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
assert_eq!(position.differential_age, Some(0.5));
assert_eq!(position.differential_station_id, Some("*7A".to_string()));
}
}

87
src/lib.rs Normal file
View File

@ -0,0 +1,87 @@
//! Aniker GPS - A Rust library for parsing NMEA GPS sentences
//!
//! This library provides functionality to parse NMEA GPS sentences,
//! particularly GGA sentences that contain position data.
//!
//! # Features
//!
//! - Parse GGA sentences to extract latitude, longitude, altitude, and more
//! - Support for different units of measurement (meters, feet)
//! - Comprehensive error handling with custom error types
//! - Simple and efficient API
//!
//! # Example
//!
//! ```
//! use aniker_gps::{parse_gga, GpsError};
//!
//! fn main() {
//! // Example NMEA GGA sentence
//! let mut gga_example = "$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
//!
//! // Parse the GGA sentence and handle the result
//! match parse_gga(&mut gga_example) {
//! Ok(position) => {
//! println!("Latitude: {}", position.lat);
//! println!("Longitude: {}", position.long);
//! println!("Altitude: {} {}", position.alt, position.altitude_unit);
//! println!("Geoid Separation: {} {}", position.geoid_separation, position.geoid_unit);
//! }
//! Err(e) => {
//! println!("Error parsing GGA sentence:");
//! match e {
//! GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
//! GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
//! GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
//! GpsError::Other(msg) => println!(" Other error: {}", msg),
//! }
//! }
//! }
//! }
//! ```
//!
//! # Error Handling
//!
//! The library uses a custom error type `GpsError` to provide detailed error information:
//!
//! - `GpsError::InvalidMessageType`: Returned when the message type is not GGA
//! - `GpsError::InvalidLength`: Returned when the GGA sentence has an incorrect number of fields
//! - `GpsError::ParseError(msg)`: Returned when parsing a specific field fails, with a description of the error
//! - `GpsError::Other(msg)`: Returned for other errors that don't fit into the above categories
//!
//! # GGA Sentence Format
//!
//! The GGA sentence contains the following fields:
//!
//! - Sentence type (checked for "GGA")
//! - Current time (UTC)
//! - Latitude (in DDMM.MMM format)
//! - Latitude compass direction (N/S)
//! - Longitude (in DDDMM.MMM format)
//! - Longitude compass direction (E/W)
//! - Fix type (0 for no fix, 1 for GPS, 2 for DGPS)
//! - Number of satellites used for fix
//! - Horizontal dilution of precision
//! - Altitude above mean sea level
//! - Altitude units (M for meters, F for feet)
//! - Height of mean sea level above WGS-84 earth ellipsoid
//! - Units of the above geoid separation (M for meters, F for feet)
//! - Time since last differential correction (if available)
//! - Differential station ID (if available)
//! - Checksum validation value
mod error;
mod gps;
mod position;
mod time;
pub use error::GpsError;
pub use gps::parse_gga;
pub use position::Position;
pub use time::Time;
/// Re-export the Position struct for convenience
pub use position::Position as GpsPosition;
/// Re-export the parse_gga function for convenience
pub use gps::parse_gga as parse_gps_gga;

180
src/main.rs Normal file
View File

@ -0,0 +1,180 @@
// Example binary demonstrating how to use the Aniker GPS library
use aniker_gps::{parse_gga, GpsError, Position};
fn main() {
// Example NMEA GGA sentence
let mut gga_example =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
println!("Aniker GPS - NMEA Parser Example");
println!("=================================");
println!("Parsing GGA sentence: {}", gga_example);
println!();
// Parse the GGA sentence and handle the result
match parse_gga(&mut gga_example) {
Ok(position) => {
println!("Successfully parsed GGA sentence:");
println!(
"Time: {:02}:{:02}:{:02}.{:02}",
position.time.hours,
position.time.minutes,
position.time.seconds,
position.time.centiseconds
);
println!("Latitude: {}", position.lat);
println!("Longitude: {}", position.long);
println!("Altitude: {} {}", position.alt, position.altitude_unit);
println!(
"Fix Type: {}",
match position.fix_type {
0 => "No fix",
1 => "GPS fix",
2 => "DGPS fix",
_ => "Unknown",
}
);
println!("Satellites in use: {}", position.satellites);
println!("HDOP: {}", position.hdop);
println!(
"Geoid Separation: {} {}",
position.geoid_separation, position.geoid_unit
);
if let Some(age) = position.differential_age {
println!("Differential Age: {} seconds", age);
} else {
println!("Differential Age: Not available");
}
if let Some(station_id) = &position.differential_station_id {
println!("Differential Station ID: {}", station_id);
} else {
println!("Differential Station ID: Not available");
}
}
Err(e) => {
println!("Error parsing GGA sentence:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
// Example of handling an invalid sentence
let mut invalid_example =
"$GPRMC,210230,A,3855.4487,N,09446.0071,W,0.0,076.2,130495,003.8,E*69".to_string();
println!("\nTrying to parse an invalid sentence (RMC):");
println!("{}", invalid_example);
println!();
match parse_gga(&mut invalid_example) {
Ok(_) => println!("Unexpected: Successfully parsed invalid sentence"),
Err(e) => {
println!("Expected error:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
// Example of handling a sentence with wrong length
let mut short_example = "$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M".to_string();
println!("\nTrying to parse a sentence with wrong length:");
println!("{}", short_example);
println!();
match parse_gga(&mut short_example) {
Ok(_) => println!("Unexpected: Successfully parsed invalid sentence"),
Err(e) => {
println!("Expected error:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
// Example with differential correction data
let mut diff_example =
"$GPGGA,210230,3855.4487,N,09446.0071,W,2,07,1.1,370.5,M,-29.5,M,0.5,1234*7A".to_string();
println!("\nParsing a GGA sentence with differential correction data:");
println!("{}", diff_example);
println!();
match parse_gga(&mut diff_example) {
Ok(position) => {
println!("Successfully parsed GGA sentence with differential data:");
println!(
"Fix Type: {}",
match position.fix_type {
0 => "No fix",
1 => "GPS fix",
2 => "DGPS fix",
_ => "Unknown",
}
);
println!("Altitude: {} {}", position.alt, position.altitude_unit);
println!(
"Geoid Separation: {} {}",
position.geoid_separation, position.geoid_unit
);
if let Some(age) = position.differential_age {
println!("Differential Age: {} seconds", age);
}
if let Some(station_id) = &position.differential_station_id {
println!("Differential Station ID: {}", station_id);
}
}
Err(e) => {
println!("Error parsing GGA sentence:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
// Example with altitude in feet
let mut feet_example =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,1215.5,F,-96.8,F,,*7A".to_string();
println!("\nParsing a GGA sentence with altitude in feet:");
println!("{}", feet_example);
println!();
match parse_gga(&mut feet_example) {
Ok(position) => {
println!("Successfully parsed GGA sentence with altitude in feet:");
println!("Altitude: {} {}", position.alt, position.altitude_unit);
println!(
"Geoid Separation: {} {}",
position.geoid_separation, position.geoid_unit
);
}
Err(e) => {
println!("Error parsing GGA sentence:");
match e {
GpsError::InvalidMessageType => println!(" Invalid message type, expected GGA"),
GpsError::InvalidLength => println!(" Invalid GGA sentence length"),
GpsError::ParseError(msg) => println!(" Parse error: {}", msg),
GpsError::Other(msg) => println!(" Other error: {}", msg),
}
}
}
}

63
src/position.rs Normal file
View File

@ -0,0 +1,63 @@
use crate::time::Time;
/// Structure to store GPS position data from GGA sentences
#[derive(Debug)]
pub struct Position {
pub lat: f32, // Latitude in decimal degrees
pub long: f32, // Longitude in decimal degrees
pub alt: f32, // Altitude in meters
pub altitude_unit: String, // Unit of altitude measurement (M for meters, F for feet)
pub time: Time, // Time in DDMMSS.SS format
pub fix_type: u8, // Fix type (0 for no fix, 1 for GPS, 2 for DGPS)
pub satellites: u8, // Number of satellites used for fix
pub hdop: f32, // Horizontal dilution of precision
pub geoid_separation: f32, // Height of mean sea level above WGS-84 earth ellipsoid
pub geoid_unit: String, // Unit of geoid separation measurement (M for meters, F for feet)
pub differential_age: Option<f32>, // Time since last differential correction (if available)
pub differential_station_id: Option<String>, // Differential station ID (if available)
}
impl Position {
/// Constructor for Position struct, initializes all fields to 0
pub fn new() -> Self {
Position {
lat: 0.,
long: 0.,
alt: 0.,
altitude_unit: "M".to_string(), // Default to meters
time: Time::new(),
fix_type: 0,
satellites: 0,
hdop: 0.,
geoid_separation: 0.,
geoid_unit: "M".to_string(), // Default to meters
differential_age: None,
differential_station_id: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_position_new() {
let position = Position::new();
assert_eq!(position.lat, 0.0);
assert_eq!(position.long, 0.0);
assert_eq!(position.alt, 0.0);
assert_eq!(position.altitude_unit, "M");
assert_eq!(position.time.hours, 0);
assert_eq!(position.time.minutes, 0);
assert_eq!(position.time.seconds, 0);
assert_eq!(position.time.centiseconds, 0);
assert_eq!(position.fix_type, 0);
assert_eq!(position.satellites, 0);
assert_eq!(position.hdop, 0.0);
assert_eq!(position.geoid_separation, 0.0);
assert_eq!(position.geoid_unit, "M");
assert_eq!(position.differential_age, None);
assert_eq!(position.differential_station_id, None);
}
}

113
src/time.rs Normal file
View File

@ -0,0 +1,113 @@
/// Structure to store time data in DDMMSS.SS format
#[derive(Debug, PartialEq)]
pub struct Time {
pub hours: u8, // Hours (00-23)
pub minutes: u8, // Minutes (00-59)
pub seconds: u8, // Seconds (00-59)
pub centiseconds: u8, // Centiseconds (00-99)
}
impl Time {
/// Constructor for Time struct, initializes all fields to 0
pub fn new() -> Self {
Time {
hours: 0,
minutes: 0,
seconds: 0,
centiseconds: 0,
}
}
/// Parse time from string in format "HHMMSS.SS"
pub fn parse(time_str: &str) -> Result<Self, &'static str> {
let mut time = Time::new();
if time_str.is_empty() {
return Ok(time);
}
// Split the time string into hours, minutes, seconds, and centiseconds
if time_str.len() >= 6 {
// Parse hours (first 2 characters)
time.hours = time_str[0..2]
.parse::<u8>()
.map_err(|_| "Invalid hours format")?;
// Parse minutes (next 2 characters)
time.minutes = time_str[2..4]
.parse::<u8>()
.map_err(|_| "Invalid minutes format")?;
// Parse seconds (next 2 characters)
time.seconds = time_str[4..6]
.parse::<u8>()
.map_err(|_| "Invalid seconds format")?;
// Parse centiseconds if available (after the decimal point)
if time_str.len() >= 9 && time_str.contains('.') {
time.centiseconds = time_str[7..9]
.parse::<u8>()
.map_err(|_| "Invalid centiseconds format")?;
}
} else {
return Err("Invalid time format: too short");
}
Ok(time)
}
/// Format time as a string in "HHMMSS.SS" format
pub fn to_string(&self) -> String {
format!(
"{:02}{:02}{:02}.{:02}",
self.hours, self.minutes, self.seconds, self.centiseconds
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_new() {
let time = Time::new();
assert_eq!(time.hours, 0);
assert_eq!(time.minutes, 0);
assert_eq!(time.seconds, 0);
assert_eq!(time.centiseconds, 0);
}
#[test]
fn test_time_parse() {
// Test with a valid time string
let time = Time::parse("123456.78").unwrap();
assert_eq!(time.hours, 12);
assert_eq!(time.minutes, 34);
assert_eq!(time.seconds, 56);
assert_eq!(time.centiseconds, 78);
// Test with an empty string
let time = Time::parse("").unwrap();
assert_eq!(time.hours, 0);
assert_eq!(time.minutes, 0);
assert_eq!(time.seconds, 0);
assert_eq!(time.centiseconds, 0);
// Test with an invalid string
let result = Time::parse("ABC");
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid time format: too short");
}
#[test]
fn test_time_to_string() {
let time = Time {
hours: 12,
minutes: 34,
seconds: 56,
centiseconds: 78,
};
assert_eq!(time.to_string(), "123456.78");
}
}

106
tests/integration_test.rs Normal file
View File

@ -0,0 +1,106 @@
// Integration tests for the aniker-gps library
use aniker_gps::{parse_gga, GpsError, Position};
#[test]
fn test_valid_gga_sentence() {
// Test with a valid GGA sentence
let mut gga =
"$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Expected values:
// Latitude: 38 degrees + 55.4487/60 = 38.924145
// Longitude: 94 degrees + 46.0071/60 = -94.766785 (negative because W)
// Altitude: 370.5
// Time: 21:02:30.00
assert!((position.lat - 38.924145).abs() < 0.000001);
assert!((position.long - (-94.766785)).abs() < 0.000001);
assert!((position.alt - 370.5).abs() < 0.000001);
assert_eq!(position.time.hours, 21);
assert_eq!(position.time.minutes, 2);
assert_eq!(position.time.seconds, 30);
assert_eq!(position.time.centiseconds, 0);
}
#[test]
fn test_invalid_sentence_type() {
// Test with a non-GGA sentence (RMC in this case)
let mut rmc =
"$GPRMC,210230,A,3855.4487,N,09446.0071,W,0.0,076.2,130495,003.8,E*69".to_string();
let result = parse_gga(&mut rmc);
// Should return an error due to wrong message type
assert!(result.is_err());
assert_eq!(result.unwrap_err(), GpsError::InvalidMessageType);
}
#[test]
fn test_short_sentence() {
// Test with a sentence that has fewer fields than expected
let mut short_gga = "$GPGGA,210230,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M".to_string();
let result = parse_gga(&mut short_gga);
// Should return an error due to wrong sentence length
assert!(result.is_err());
assert_eq!(result.unwrap_err(), GpsError::InvalidLength);
}
#[test]
fn test_timestamp_parsing() {
// Test with a GGA sentence with a specific timestamp
let mut gga =
"$GPGGA,123456,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position = parse_gga(&mut gga).unwrap();
// Check that the timestamp is correctly parsed
assert_eq!(position.time.hours, 12);
assert_eq!(position.time.minutes, 34);
assert_eq!(position.time.seconds, 56);
assert_eq!(position.time.centiseconds, 0);
// Test with a different timestamp including centiseconds
let mut gga2 =
"$GPGGA,235959.50,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let position2 = parse_gga(&mut gga2).unwrap();
// Check that the timestamp is correctly parsed
assert_eq!(position2.time.hours, 23);
assert_eq!(position2.time.minutes, 59);
assert_eq!(position2.time.seconds, 59);
assert_eq!(position2.time.centiseconds, 50);
}
#[test]
fn test_invalid_timestamp() {
// Test with an invalid timestamp format
let mut gga =
"$GPGGA,2359.50,3855.4487,N,09446.0071,W,1,07,1.1,370.5,M,-29.5,M,,*7A".to_string();
let result = parse_gga(&mut gga);
// Should return a parse error for the timestamp field
assert!(result.is_err());
match result.unwrap_err() {
GpsError::ParseError(msg) => {
assert!(msg.len() > 0);
}
_ => panic!("Expected ParseError for invalid timestamp"),
}
}
#[test]
fn test_invalid_altitude() {
// Test with invalid altitude value
let mut gga = "$GPGGA,21030,3855.4487,N,09446.0071,W,1,07,1.1,ABC,M,-29.5,M,,*7A".to_string();
let result = parse_gga(&mut gga);
// Should return a parse error for the altitude field
assert!(result.is_err());
match result.unwrap_err() {
GpsError::ParseError(msg) => {
// The actual error message might be different, so we'll just check that it's a ParseError
// without checking the specific message content
assert!(msg.len() > 0);
}
_ => panic!("Expected ParseError for invalid altitude"),
}
}