Ability to parse a NMEA GGA sentence
This commit is contained in:
commit
4f1ce65e2d
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
7
Cargo.lock
generated
Normal file
7
Cargo.lock
generated
Normal 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
21
Cargo.toml
Normal 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
98
README.md
Normal 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
145
docs/usage.md
Normal 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
44
examples/basic_usage.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
examples/multiple_sentences.rs
Normal file
37
examples/multiple_sentences.rs
Normal 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
54
src/error.rs
Normal 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
384
src/gps.rs
Normal 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
87
src/lib.rs
Normal 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
180
src/main.rs
Normal 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
63
src/position.rs
Normal 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
113
src/time.rs
Normal 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
106
tests/integration_test.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user