Compare commits

...

9 Commits

Author SHA1 Message Date
mattbk 06324c3183 Working snapshot. 2026-09-11 18:23:19 -05:00
mattbk 408b94d962 Make it work, add format argument. 2026-09-07 18:38:08 -05:00
mattbk 80ba685a95 Wait for one location and then exit. 2026-09-07 18:04:48 -05:00
mattbk 86c5720fb4 Update readme. 2026-09-07 15:16:44 -05:00
mattbk 011fc62b9b Update readme. 2026-09-07 15:11:55 -05:00
W1CDN 2a9c2a4e19 Merge pull request 'Cut the program down to absolute minimum I need.' (#1) from cut into main
Reviewed-on: #1
2026-09-07 15:06:15 -05:00
mattbk 24100ffb26 Fix typos. 2026-09-07 15:06:01 -05:00
mattbk 8b2804fe1f Show datetime; take port from an argument. 2026-09-07 15:02:13 -05:00
mattbk eb37737bda Write lat/lon to terminal. 2026-09-07 12:51:32 -05:00
3 changed files with 147 additions and 342 deletions
+5 -5
View File
@@ -1,11 +1,11 @@
[package]
name = "RUST_NMEA_PARSER"
name = "gpsll"
version = "0.1.0"
edition = "2021"
[dependencies]
eframe = { version = "0.27", features = ["wgpu", "persistence"] }
egui = "0.27"
egui_plot = "0.27"
serialport = "4.2"
nmea = "0.6"
nmea = {version = "0.6", features = ["GGA"]}
clap = { version = "4", features = ["derive"] }
chrono = "0.4.45"
+26 -24
View File
@@ -1,39 +1,41 @@
# Rust GPS GUI COM Port Reader
# gpsll
Using Rust and the following packages
```Rust
[dependencies]
eframe = { version = "0.27", features = ["wgpu", "persistence"] }
egui = "0.27"
egui_plot = "0.27"
serialport = "4.2"
nmea = "0.6"
nmea = {version = "0.6", features = ["GGA"]}
clap = { version = "4", features = ["derive"] }
chrono = "0.4.45"
```
I was able to open the U-Blox port on my GPS dongle and parse the longitude and latitude, printing the information in the streaming output for the user to see.
Also showing the Satellites its fixed to, and the satellites its attached to in the circle.
I wanted a simple way to export latitude and longitude from a GPS USB dongle to the terminal.
## Sample GUI Output
## Usage
```
A tool for echoing latitude and longitude from a GPS USB dongle.
![GUI_Output](GUI_output.png)
Usage: gpsll [OPTIONS]
## Acknowledgements
Options:
-c, --com <COM> COM port
-h, --help Print help
-V, --version Print version
```
- Professors at Kean University
- Professors at NJIT
- Research Mentors at Kean University
- StackOverflow Q&A Discussion
- ChatGPT
- ClaudeAI
```
Matt@computer % gpsll -c "/dev/tty.usbmodem1301"
Reading from "/dev/tty.usbmodem1301"
2026-09-07 15:14:43, 47.11, -97.22
2026-09-07 15:14:44, 47.11, -97.22
2026-09-07 15:14:46, 47.11, -97.22
2026-09-07 15:14:48, 47.11, -97.22
2026-09-07 15:14:49, 47.11, -97.22
2026-09-07 15:14:50, 47.11, -97.22
```
## Acknowledgments
## Authors
- [@FPyC639](https://github.com/FPyC639)
## Appendix
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/joseserra8x)
- Inspiration from https://github.com/FPyC639/Rust_GPS_GUI_COM_Port_Reader.
+106 -303
View File
@@ -1,267 +1,37 @@
use eframe::egui;
//use serialport::available_ports;
use std::sync::{Arc, Mutex};
use std::thread;
use chrono::Local;
use clap::Parser;
use nmea::Nmea;
use nmea::SentenceType;
use std::time::Duration;
// use egui_plot::{Plot, PlotPoints, Points, Text, Line};
/// Command-line arguments using Clap.
#[derive(Parser, Debug)]
#[command(
author,
version,
about = "A tool for echoing latitude and longitude from a GPS USB dongle."
)]
struct Cli {
/// COM port
#[arg(short = 'c', long)]
com: Option<String>,
#[derive(Default, Clone)]
// struct Satellite {
// id: String,
// latitude: f64,
// longitude: f64,
// strength: u8,
// }
//#[derive(Default)]
struct AppState {
//ports: Vec<String>,
//selected_port: Option<String>,
//satellites: Vec<Satellite>,
is_reading: bool,
// 🔵 NEW: live NMEA data buffer
nmea_log: Vec<String>,
/// Format
#[arg(short = 'f', long, default_value = "csv")]
format: Option<String>,
}
pub struct MyApp {
state: Arc<Mutex<AppState>>,
}
impl Default for MyApp {
fn default() -> Self {
// let ports = available_ports()
// .map(|ps| ps.into_iter().map(|p| p.port_name).collect())
// .unwrap_or_default();
Self {
state: Arc::new(Mutex::new(AppState {
//ports,
..Default::default()
})),
}
}
}
// =====================================================================
// Satellite Map Drawing Method
// =====================================================================
// impl MyApp {
// fn draw_satellite_map(&self, ui: &mut egui::Ui, sats: &[Satellite]) {
// Plot::new("satellite_map")
// .width(300.0)
// .height(300.0)
// .view_aspect(1.0)
// .show(ui, |plot_ui| {
// // Draw outline circle
// let circle: PlotPoints = (0..360)
// .map(|deg| {
// let rad = (deg as f64).to_radians();
// [rad.cos(), rad.sin()]
// })
// .collect::<Vec<_>>()
// .into();
// plot_ui.line(Line::new(circle));
// // Draw satellites
// for sat in sats {
// let az = sat.longitude.to_radians();
// let el = sat.latitude.to_radians();
// let x = el.cos() * az.cos();
// let y = el.cos() * az.sin();
// plot_ui.points(Points::new(vec![[x, y]]).radius(3.0));
// plot_ui.text(Text::new([x, y].into(), sat.id.clone()));
// }
// });
// }
// }
// =====================================================================
// Main App UI
// =====================================================================
// impl eframe::App for MyApp {
// fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// let mut state = self.state.lock().unwrap();
// // Main panel
// // egui::CentralPanel::default().show(ctx, |ui| {
// // ui.heading("Select COM Port");
// // let ports = state.ports.clone();
// // egui::ComboBox::from_label("COM Port")
// // .selected_text(state.selected_port.as_deref().unwrap_or("Select a Port"))
// // .show_ui(ui, |cb| {
// // for port in ports {
// // cb.selectable_value(
// // &mut state.selected_port,
// // Some(port.clone()),
// // port,
// // );
// // }
// // });
// // if ui.button("Start Reading").clicked() && !state.is_reading {
// // if let Some(port_name) = state.selected_port.clone() {
// // let state_clone = Arc::clone(&self.state);
// // Harcode port for dev
// let port_name = "/dev/tty.usbmodem1301";
// // Thread for GPS streaming
// thread::spawn(move || {
// let port = serialport::new(port_name, 9600)
// .timeout(Duration::from_millis(1000))
// .open();
// if let Ok(mut serial) = port {
// let mut buf = [0u8; 1024];
// loop {
// match serial.read(&mut buf) {
// Ok(n) => {
// let data = String::from_utf8_lossy(&buf[..n]);
// // let mut satellites = Vec::new();
// for line in data.lines() {
// println!("{}", line.to_string());
// // 🔵 Append NMEA line to log
// // {
// // let mut st = state_clone.lock().unwrap();
// // st.nmea_log.push(line.to_string());
// // // Keep log trimmed
// // if st.nmea_log.len() > 500 {
// // st.nmea_log.remove(0);
// // }
// // }
// // Parse GSV
// // if line.starts_with("$GPGSV") {
// //let fields: Vec<&str> = line.split(',').collect();
// //let mut i = 4;
// // while i + 3 < fields.len() {
// // satellites.push(Satellite {
// // id: fields[i].to_string(),
// // latitude: fields[i + 1].parse().unwrap_or(0.0),
// // longitude: fields[i + 2].parse().unwrap_or(0.0),
// // strength: fields[i + 3].parse().unwrap_or(0),
// // });
// // i += 4;
// // }
// // }
// }
// // Update satellites
// //let mut st = state_clone.lock().unwrap();
// // st.satellites = satellites;
// }
// Err(_) => break,
// }
// thread::sleep(Duration::from_millis(200));
// }
// }
// });
// state.is_reading = true;
// }
// }
// ui.separator();
// ui.heading("Satellites");
// egui::ScrollArea::vertical().show(ui, |ui| {
// for sat in &state.satellites {
// ui.horizontal(|ui| {
// ui.label(format!("ID: {}", sat.id));
// ui.label(format!("Elv: {:.2}", sat.latitude));
// ui.label(format!("Azm: {:.2}", sat.longitude));
// ui.label(format!("Strength: {}", sat.strength));
// });
// }
// });
// });
// =====================================================================
// NEW: Live GPS Stream Window
// =====================================================================
// egui::Window::new("GPS Stream")
// .default_width(400.0)
// .default_height(300.0)
// .resizable(true)
// .show(ctx, |ui| {
// ui.label("Live NMEA Data:");
// egui::ScrollArea::vertical()
// .stick_to_bottom(true)
// .show(ui, |ui| {
// for line in &state.nmea_log {
// ui.monospace(line);
// }
// });
// });
// Write to console
// for line in &state.nmea_log {
// println!("{line}");
// }
// =====================================================================
// Mini floating sky map
// =====================================================================
// egui::Area::new("mini_sky_map".into())
// .anchor(egui::Align2::RIGHT_TOP, [-10.0, 10.0])
// .show(ctx, |ui| {
// self.draw_satellite_map(ui, &state.satellites);
// });
// }
// }
// =====================================================================
// Run
// =====================================================================
fn main() {
// let options = eframe::NativeOptions::default();
while 1 == 1 {
// Main panel
// egui::CentralPanel::default().show(ctx, |ui| {
// ui.heading("Select COM Port");
// let ports = state.ports.clone();
// egui::ComboBox::from_label("COM Port")
// .selected_text(state.selected_port.as_deref().unwrap_or("Select a Port"))
// .show_ui(ui, |cb| {
// for port in ports {
// cb.selectable_value(
// &mut state.selected_port,
// Some(port.clone()),
// port,
// );
// }
// });
// if ui.button("Start Reading").clicked() && !state.is_reading {
// if let Some(port_name) = state.selected_port.clone() {
// let state_clone = Arc::clone(&self.state);
// Function to get the location
fn get_loc(port: String, fmt: String) -> String {
let mut nmea = Nmea::default();
// Harcode port for dev
let port_name = "/dev/tty.usbmodem1301";
//let port_name = "/dev/tty.usbmodem1301";
let port_name = port;
//fn read_loc(com: &str) -> &str {
//println!("Reading from {port_name}");
// Thread for GPS streaming
thread::spawn(move || {
let port = serialport::new(port_name, 9600)
.timeout(Duration::from_millis(1000))
.open();
@@ -273,58 +43,91 @@ fn main() {
match serial.read(&mut buf) {
Ok(n) => {
let data = String::from_utf8_lossy(&buf[..n]);
// let mut satellites = Vec::new();
for line in data.lines() {
println!("{}", line.to_string());
// Check sentence type
let sentence_type = match nmea.parse(line) {
Ok(sentence_type) => sentence_type,
Err(_) => {
break;
}
};
// Only keep the GLL sentences
if sentence_type == SentenceType::GLL {
let lat = match nmea.latitude {
Some(lat) => lat,
None => -9999.25,
};
let lon = match nmea.longitude {
Some(lon) => lon,
None => -9999.25,
};
let date = Local::now();
//println!("{}, {lat}, {lon}", date.format("%Y-%m-%d %H:%M:%S"));
if fmt == "csv" {
return format!(
"{},{:.8},{:.8}",
date.format("%Y-%m-%d %H:%M:%S"),
lat,
lon
);
} else if fmt == "ssv" {
return format!(
"\"{}\" {:.8} {:.8}",
date.format("%Y-%m-%d %H:%M:%S"),
lat,
lon
);
}
}
}
}
Err(_) => {} // lol that I can just not put a return in?
}
}
} else {
// https://github.com/serialport/serialport-rs#listing-available-ports
let ports = serialport::available_ports().expect("No ports found!");
println!("Available Ports:");
for p in ports {
println!("{}", p.port_name);
}
// 🔵 Append NMEA line to log
// {
// let mut st = state_clone.lock().unwrap();
// st.nmea_log.push(line.to_string());
return format!("\nCould not connect to port. Is it listed above?");
}
}
// // Keep log trimmed
// if st.nmea_log.len() > 500 {
// st.nmea_log.remove(0);
// }
fn main() {
// Arguments
let cli = Cli::parse();
let com = match cli.com {
Some(com) => com,
None => {
// https://github.com/serialport/serialport-rs#listing-available-ports
let ports = serialport::available_ports().expect("No ports found!");
println!("Available Ports:");
for p in ports {
println!("{}", p.port_name);
}
println!("\nMissing `-c` port argument, use one listed above.");
return;
} //todo!()
};
let fmt = match cli.format {
Some(fmt) => fmt,
None => {
return;
} //todo!()
};
// Run the function
// Max rate is about 1 measurement per second
//loop {
let result = get_loc(com.clone(), fmt.clone());
println!("{result}");
//}
// Parse GSV
// if line.starts_with("$GPGSV") {
//let fields: Vec<&str> = line.split(',').collect();
//let mut i = 4;
// while i + 3 < fields.len() {
// satellites.push(Satellite {
// id: fields[i].to_string(),
// latitude: fields[i + 1].parse().unwrap_or(0.0),
// longitude: fields[i + 2].parse().unwrap_or(0.0),
// strength: fields[i + 3].parse().unwrap_or(0),
// });
// i += 4;
// }
// }
}
// Update satellites
//let mut st = state_clone.lock().unwrap();
// st.satellites = satellites;
}
Err(_) => break,
}
thread::sleep(Duration::from_millis(200));
}
}
});
// eframe::run_native(
// "NMEA GPS Viewer",
// options,
// Box::new(|_cc| Box::new(MyApp::default())),
// )
}
}