mirror of
https://github.com/geoffsee/gpyes-stream.git
synced 2025-09-08 22:56:47 +00:00
working
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/gnss-reader.iml
|
||||
/.idea/
|
||||
/target/
|
217
Cargo.lock
generated
Normal file
217
Cargo.lock
generated
Normal file
@@ -0,0 +1,217 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "gnss-reader"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serialport",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-kit-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
|
||||
[[package]]
|
||||
name = "libudev"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serialport"
|
||||
version = "4.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb0bc984f6af6ef8bab54e6cf2071579ee75b9286aa9f2319a0d220c28b0a2b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"io-kit-sys",
|
||||
"libudev",
|
||||
"mach2",
|
||||
"nix",
|
||||
"scopeguard",
|
||||
"unescaper",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unescaper"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "gnss-reader"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
serialport = "4.2"
|
115
README.md
Normal file
115
README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# GNSS Reader
|
||||
|
||||
A Rust-based GNSS (Global Navigation Satellite System) data parser that reads GPS location data from serial devices and converts NMEA sentences into structured, usable location information.
|
||||
|
||||
## Features
|
||||
|
||||
- **NMEA Sentence Parsing**: Supports GPGGA and GPRMC sentence formats
|
||||
- **Live GPS Reading**: Real-time data streaming from USB GPS devices
|
||||
- **Demo Mode**: Built-in demonstration with sample GPS data
|
||||
- **Cross-Platform**: Works on macOS, Linux, and Windows
|
||||
- **Structured Output**: Converts raw NMEA data into organized LocationData structures
|
||||
- **Comprehensive Testing**: Includes extensive test coverage for various GPS scenarios
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 2024 edition or later
|
||||
- USB GPS device (tested with Stratux GPYes 2.0 u-blox 8)
|
||||
- Serial port access permissions
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd gpyes-stream
|
||||
```
|
||||
|
||||
2. Build the project:
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
3. Run the application:
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The application automatically detects and connects to your GPS device. On startup, it will:
|
||||
|
||||
1. Attempt to connect to the GPS device via serial port
|
||||
2. Parse incoming NMEA sentences (GPGGA and GPRMC formats)
|
||||
3. Display structured location data including:
|
||||
- Latitude and Longitude coordinates
|
||||
- Altitude information
|
||||
- GPS fix quality and satellite count
|
||||
- Speed and course data
|
||||
- UTC timestamp
|
||||
|
||||
### Demo Mode
|
||||
|
||||
To run the application with sample data without a physical GPS device:
|
||||
|
||||
```bash
|
||||
cargo run -- --demo
|
||||
```
|
||||
|
||||
## Supported Hardware
|
||||
|
||||
### Tested Devices
|
||||
- **Stratux GPYes 2.0 u-blox 8 USB GPS Unit** (~$20)
|
||||
- Amazon: https://www.amazon.com/Stratux-GPYes-2-0-u-blox-unit/dp/B0716BK5NT
|
||||
- Device paths on macOS: `/dev/tty.usbmodem2101` or `/dev/cu.usbmodem2101`
|
||||
|
||||
### Compatibility
|
||||
The application should work with any USB GPS device that outputs standard NMEA sentences via serial communication.
|
||||
|
||||
## NMEA Sentence Support
|
||||
|
||||
Currently supports the following NMEA sentence types:
|
||||
- **GPGGA**: Global Positioning System Fix Data
|
||||
- **GPRMC**: Recommended Minimum Course
|
||||
- **GNGGA**: GNSS Fix Data (multi-constellation)
|
||||
- **GNRMC**: GNSS Recommended Minimum Course
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Building for Release
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The compiled binary will be available in `target/release/gnss-reader`.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is open source. Please refer to the LICENSE file for details.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Device Not Found
|
||||
- Ensure your GPS device is properly connected
|
||||
- Check device permissions (you may need to add your user to the `dialout` group on Linux)
|
||||
- Verify the device path matches your system configuration
|
||||
|
||||
### No GPS Signal
|
||||
- Ensure you have a clear view of the sky
|
||||
- Wait for the GPS device to acquire satellite lock (may take several minutes on first use)
|
||||
- Check that your GPS device is functioning properly
|
460
src/main.rs
Normal file
460
src/main.rs
Normal file
@@ -0,0 +1,460 @@
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct LocationData {
|
||||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
pub altitude: Option<f64>,
|
||||
pub speed: Option<f64>,
|
||||
pub timestamp: Option<String>,
|
||||
pub fix_quality: Option<u8>,
|
||||
pub satellites: Option<u8>,
|
||||
}
|
||||
|
||||
impl Default for LocationData {
|
||||
fn default() -> Self {
|
||||
LocationData {
|
||||
latitude: None,
|
||||
longitude: None,
|
||||
altitude: None,
|
||||
speed: None,
|
||||
timestamp: None,
|
||||
fix_quality: None,
|
||||
satellites: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GnssParser;
|
||||
|
||||
impl GnssParser {
|
||||
pub fn new() -> Self {
|
||||
GnssParser
|
||||
}
|
||||
|
||||
pub fn parse_sentence(&self, sentence: &str) -> Option<LocationData> {
|
||||
if sentence.is_empty() || !sentence.starts_with('$') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = sentence.split(',').collect();
|
||||
if parts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sentence_type = parts[0];
|
||||
|
||||
match sentence_type {
|
||||
"$GPGGA" | "$GNGGA" => self.parse_gpgga(&parts),
|
||||
"$GPRMC" | "$GNRMC" => self.parse_gprmc(&parts),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_gpgga(&self, parts: &[&str]) -> Option<LocationData> {
|
||||
if parts.len() < 15 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut location = LocationData::default();
|
||||
|
||||
// Parse timestamp (field 1)
|
||||
if !parts[1].is_empty() {
|
||||
location.timestamp = Some(parts[1].to_string());
|
||||
}
|
||||
|
||||
// Parse latitude (fields 2 and 3)
|
||||
if !parts[2].is_empty() && !parts[3].is_empty() {
|
||||
if let Ok(lat_raw) = parts[2].parse::<f64>() {
|
||||
let degrees = (lat_raw / 100.0).floor();
|
||||
let minutes = lat_raw - (degrees * 100.0);
|
||||
let mut latitude = degrees + (minutes / 60.0);
|
||||
|
||||
if parts[3] == "S" {
|
||||
latitude = -latitude;
|
||||
}
|
||||
location.latitude = Some(latitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse longitude (fields 4 and 5)
|
||||
if !parts[4].is_empty() && !parts[5].is_empty() {
|
||||
if let Ok(lon_raw) = parts[4].parse::<f64>() {
|
||||
let degrees = (lon_raw / 100.0).floor();
|
||||
let minutes = lon_raw - (degrees * 100.0);
|
||||
let mut longitude = degrees + (minutes / 60.0);
|
||||
|
||||
if parts[5] == "W" {
|
||||
longitude = -longitude;
|
||||
}
|
||||
location.longitude = Some(longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse fix quality (field 6)
|
||||
if !parts[6].is_empty() {
|
||||
if let Ok(quality) = parts[6].parse::<u8>() {
|
||||
location.fix_quality = Some(quality);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse number of satellites (field 7)
|
||||
if !parts[7].is_empty() {
|
||||
if let Ok(sats) = parts[7].parse::<u8>() {
|
||||
location.satellites = Some(sats);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse altitude (field 9)
|
||||
if !parts[9].is_empty() {
|
||||
if let Ok(alt) = parts[9].parse::<f64>() {
|
||||
location.altitude = Some(alt);
|
||||
}
|
||||
}
|
||||
|
||||
Some(location)
|
||||
}
|
||||
|
||||
fn parse_gprmc(&self, parts: &[&str]) -> Option<LocationData> {
|
||||
if parts.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut location = LocationData::default();
|
||||
|
||||
// Parse timestamp (field 1)
|
||||
if !parts[1].is_empty() {
|
||||
location.timestamp = Some(parts[1].to_string());
|
||||
}
|
||||
|
||||
// Check if data is valid (field 2)
|
||||
if parts[2] != "A" {
|
||||
return None; // Invalid data
|
||||
}
|
||||
|
||||
// Parse latitude (fields 3 and 4)
|
||||
if !parts[3].is_empty() && !parts[4].is_empty() {
|
||||
if let Ok(lat_raw) = parts[3].parse::<f64>() {
|
||||
let degrees = (lat_raw / 100.0).floor();
|
||||
let minutes = lat_raw - (degrees * 100.0);
|
||||
let mut latitude = degrees + (minutes / 60.0);
|
||||
|
||||
if parts[4] == "S" {
|
||||
latitude = -latitude;
|
||||
}
|
||||
location.latitude = Some(latitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse longitude (fields 5 and 6)
|
||||
if !parts[5].is_empty() && !parts[6].is_empty() {
|
||||
if let Ok(lon_raw) = parts[5].parse::<f64>() {
|
||||
let degrees = (lon_raw / 100.0).floor();
|
||||
let minutes = lon_raw - (degrees * 100.0);
|
||||
let mut longitude = degrees + (minutes / 60.0);
|
||||
|
||||
if parts[6] == "W" {
|
||||
longitude = -longitude;
|
||||
}
|
||||
location.longitude = Some(longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse speed (field 7) - in knots
|
||||
if !parts[7].is_empty() {
|
||||
if let Ok(speed_knots) = parts[7].parse::<f64>() {
|
||||
location.speed = Some(speed_knots);
|
||||
}
|
||||
}
|
||||
|
||||
Some(location)
|
||||
}
|
||||
}
|
||||
|
||||
use std::env;
|
||||
use std::io::{BufRead, BufReader, ErrorKind};
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
println!("GNSS Reader - Parsing GNSS sentences into usable location data");
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let parser = GnssParser::new();
|
||||
|
||||
if args.len() > 1 && args[1] == "--live" {
|
||||
// Try to connect to live GPS device
|
||||
run_live_gps(&parser);
|
||||
} else {
|
||||
// Run demonstration mode
|
||||
run_demo(&parser);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_live_gps(parser: &GnssParser) {
|
||||
println!("Attempting to connect to GPS device...");
|
||||
|
||||
let device_paths = ["/dev/tty.usbmodem2101", "/dev/cu.usbmodem2101"];
|
||||
|
||||
for device_path in &device_paths {
|
||||
println!("Trying to connect to: {}", device_path);
|
||||
|
||||
match serialport::new(*device_path, 9600)
|
||||
.timeout(Duration::from_millis(1000))
|
||||
.open()
|
||||
{
|
||||
Ok(mut port) => {
|
||||
println!("Successfully connected to GPS device at {}", device_path);
|
||||
println!("Reading NMEA sentences... (Press Ctrl+C to stop)");
|
||||
println!();
|
||||
|
||||
let mut reader = BufReader::new(port.as_mut());
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
match reader.read_line(&mut line) {
|
||||
Ok(0) => break, // EOF
|
||||
Ok(_) => {
|
||||
let sentence = line.trim();
|
||||
if !sentence.is_empty() {
|
||||
println!("Raw: {}", sentence);
|
||||
|
||||
if let Some(location) = parser.parse_sentence(sentence) {
|
||||
print_location_data(&location);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
ErrorKind::TimedOut => {
|
||||
// Handle timeout gracefully - just continue reading
|
||||
println!("Read timeout - continuing to listen for GPS data...");
|
||||
continue;
|
||||
}
|
||||
ErrorKind::Interrupted => {
|
||||
// Handle interrupted system call - continue reading
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Error reading from GPS device: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to connect to {}: {}", device_path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Could not connect to any GPS device.");
|
||||
println!("Make sure the GPS device is connected and available at:");
|
||||
for path in &device_paths {
|
||||
println!(" {}", path);
|
||||
}
|
||||
println!();
|
||||
println!("Running demonstration mode instead...");
|
||||
println!();
|
||||
|
||||
let demo_parser = GnssParser::new();
|
||||
run_demo(&demo_parser);
|
||||
}
|
||||
|
||||
fn run_demo(parser: &GnssParser) {
|
||||
println!("GPS sensor should be available at:");
|
||||
println!(" tty.usbmodem2101");
|
||||
println!(" cu.usbmodem2101");
|
||||
println!();
|
||||
println!("Run with --live flag to connect to actual GPS device");
|
||||
println!();
|
||||
|
||||
// Demonstrate parsing with example GNSS sentences
|
||||
println!("Demonstrating GNSS sentence parsing:");
|
||||
println!();
|
||||
|
||||
// Example GPGGA sentence
|
||||
let gpgga_sentence = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
|
||||
println!("Parsing GPGGA sentence:");
|
||||
println!(" {}", gpgga_sentence);
|
||||
|
||||
if let Some(location) = parser.parse_sentence(gpgga_sentence) {
|
||||
print_location_data(&location);
|
||||
}
|
||||
println!();
|
||||
|
||||
// Example GPRMC sentence
|
||||
let gprmc_sentence = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
|
||||
println!("Parsing GPRMC sentence:");
|
||||
println!(" {}", gprmc_sentence);
|
||||
|
||||
if let Some(location) = parser.parse_sentence(gprmc_sentence) {
|
||||
print_location_data(&location);
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("To connect to a real GPS device, run:");
|
||||
println!(" cargo run -- --live");
|
||||
}
|
||||
|
||||
fn print_location_data(location: &LocationData) {
|
||||
println!(" Parsed location data:");
|
||||
if let Some(lat) = location.latitude {
|
||||
println!(" Latitude: {:.6}°", lat);
|
||||
}
|
||||
if let Some(lon) = location.longitude {
|
||||
println!(" Longitude: {:.6}°", lon);
|
||||
}
|
||||
if let Some(alt) = location.altitude {
|
||||
println!(" Altitude: {:.1} m", alt);
|
||||
}
|
||||
if let Some(speed) = location.speed {
|
||||
println!(" Speed: {:.1} knots", speed);
|
||||
}
|
||||
if let Some(quality) = location.fix_quality {
|
||||
println!(" Fix Quality: {}", quality);
|
||||
}
|
||||
if let Some(sats) = location.satellites {
|
||||
println!(" Satellites: {}", sats);
|
||||
}
|
||||
if let Some(time) = &location.timestamp {
|
||||
println!(" Timestamp: {}", time);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parser_creation() {
|
||||
let parser = GnssParser::new();
|
||||
// Parser should be created successfully
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gpgga_sentence() {
|
||||
let parser = GnssParser::new();
|
||||
|
||||
// Example GPGGA sentence: Global Positioning System Fix Data
|
||||
let sentence = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
|
||||
|
||||
let result = parser.parse_sentence(sentence);
|
||||
assert!(result.is_some());
|
||||
|
||||
let location = result.unwrap();
|
||||
assert!(location.latitude.is_some());
|
||||
assert!(location.longitude.is_some());
|
||||
assert!(location.altitude.is_some());
|
||||
assert!(location.fix_quality.is_some());
|
||||
assert!(location.satellites.is_some());
|
||||
|
||||
// Check specific values
|
||||
assert!((location.latitude.unwrap() - 48.1173).abs() < 0.001); // 4807.038N
|
||||
assert!((location.longitude.unwrap() - 11.5167).abs() < 0.001); // 01131.000E
|
||||
assert!((location.altitude.unwrap() - 545.4).abs() < 0.1);
|
||||
assert_eq!(location.fix_quality.unwrap(), 1);
|
||||
assert_eq!(location.satellites.unwrap(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gprmc_sentence() {
|
||||
let parser = GnssParser::new();
|
||||
|
||||
// Example GPRMC sentence: Recommended Minimum Navigation Information
|
||||
let sentence = "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A";
|
||||
|
||||
let result = parser.parse_sentence(sentence);
|
||||
assert!(result.is_some());
|
||||
|
||||
let location = result.unwrap();
|
||||
assert!(location.latitude.is_some());
|
||||
assert!(location.longitude.is_some());
|
||||
assert!(location.speed.is_some());
|
||||
assert!(location.timestamp.is_some());
|
||||
|
||||
// Check specific values
|
||||
assert!((location.latitude.unwrap() - 48.1173).abs() < 0.001);
|
||||
assert!((location.longitude.unwrap() - 11.5167).abs() < 0.001);
|
||||
assert!((location.speed.unwrap() - 22.4).abs() < 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_sentence() {
|
||||
let parser = GnssParser::new();
|
||||
|
||||
let invalid_sentence = "invalid sentence";
|
||||
let result = parser.parse_sentence(invalid_sentence);
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_empty_sentence() {
|
||||
let parser = GnssParser::new();
|
||||
|
||||
let result = parser.parse_sentence("");
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gngga_sentence() {
|
||||
let parser = GnssParser::new();
|
||||
|
||||
// Example GNGGA sentence (modern GNSS format)
|
||||
let sentence = "$GNGGA,144751.00,3708.15162,N,07621.52868,W,1,06,1.39,-14.3,M,-35.8,M,,*69";
|
||||
|
||||
let result = parser.parse_sentence(sentence);
|
||||
assert!(result.is_some());
|
||||
|
||||
let location = result.unwrap();
|
||||
assert!(location.latitude.is_some());
|
||||
assert!(location.longitude.is_some());
|
||||
assert!(location.altitude.is_some());
|
||||
assert!(location.fix_quality.is_some());
|
||||
assert!(location.satellites.is_some());
|
||||
|
||||
// Check specific values
|
||||
assert!((location.latitude.unwrap() - 37.1359).abs() < 0.001); // 3708.15162N
|
||||
assert!((location.longitude.unwrap() - (-76.3588)).abs() < 0.001); // 07621.52868W
|
||||
assert!((location.altitude.unwrap() - (-14.3)).abs() < 0.1);
|
||||
assert_eq!(location.fix_quality.unwrap(), 1);
|
||||
assert_eq!(location.satellites.unwrap(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gnrmc_sentence() {
|
||||
let parser = GnssParser::new();
|
||||
|
||||
// Example GNRMC sentence (modern GNSS format)
|
||||
let sentence = "$GNRMC,144751.00,A,3708.15162,N,07621.52868,W,0.009,,200725,,,A,V*01";
|
||||
|
||||
let result = parser.parse_sentence(sentence);
|
||||
assert!(result.is_some());
|
||||
|
||||
let location = result.unwrap();
|
||||
assert!(location.latitude.is_some());
|
||||
assert!(location.longitude.is_some());
|
||||
assert!(location.speed.is_some());
|
||||
assert!(location.timestamp.is_some());
|
||||
|
||||
// Check specific values
|
||||
assert!((location.latitude.unwrap() - 37.1359).abs() < 0.001);
|
||||
assert!((location.longitude.unwrap() - (-76.3588)).abs() < 0.001);
|
||||
assert!((location.speed.unwrap() - 0.009).abs() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_location_data_default() {
|
||||
let location = LocationData::default();
|
||||
assert!(location.latitude.is_none());
|
||||
assert!(location.longitude.is_none());
|
||||
assert!(location.altitude.is_none());
|
||||
assert!(location.speed.is_none());
|
||||
assert!(location.timestamp.is_none());
|
||||
assert!(location.fix_quality.is_none());
|
||||
assert!(location.satellites.is_none());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user