Compare commits

...

12 Commits

Author SHA1 Message Date
W1CDN 0b7fcb2e93 Merge pull request 'Build function that waits for one location, returns it, and exits.' (#4) from function into main
Reviewed-on: #4
2026-09-11 18:23:35 -05:00
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
mattbk 88f39df488 Rip out the GUI, run forever, log to terminal. 2026-09-07 10:40:40 -05:00
mattbk 05fe57bf24 Rip out all GUI except skeleton. 2026-09-06 22:29:24 -05:00
3 changed files with 142 additions and 236 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.
+101 -197
View File
@@ -1,116 +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,
/// Format
#[arg(short = 'f', long, default_value = "csv")]
format: Option<String>,
}
#[derive(Default)]
struct AppState {
ports: Vec<String>,
selected_port: Option<String>,
satellites: Vec<Satellite>,
is_reading: bool,
// Function to get the location
fn get_loc(port: String, fmt: String) -> String {
let mut nmea = Nmea::default();
// 🔵 NEW: live NMEA data buffer
nmea_log: Vec<String>,
}
// Harcode port for dev
//let port_name = "/dev/tty.usbmodem1301";
let port_name = port;
pub struct MyApp {
state: Arc<Mutex<AppState>>,
}
//fn read_loc(com: &str) -> &str {
//println!("Reading from {port_name}");
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);
// Thread for GPS streaming
thread::spawn(move || {
let port = serialport::new(port_name, 9600)
.timeout(Duration::from_millis(1000))
.open();
@@ -122,108 +43,91 @@ impl eframe::App for MyApp {
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() {
// Check sentence type
let sentence_type = match nmea.parse(line) {
Ok(sentence_type) => sentence_type,
Err(_) => {
break;
}
};
// 🔵 Append NMEA line to log
{
let mut st = state_clone.lock().unwrap();
st.nmea_log.push(line.to_string());
// 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);
}
// Keep log trimmed
if st.nmea_log.len() > 500 {
st.nmea_log.remove(0);
return format!("\nCould not connect to port. Is it listed above?");
}
}
// Parse GSV
if line.starts_with("$GPGSV") {
let fields: Vec<&str> = line.split(',').collect();
let mut i = 4;
fn main() {
// Arguments
let cli = Cli::parse();
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;
}
}
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!()
};
// Update satellites
let mut st = state_clone.lock().unwrap();
st.satellites = satellites;
}
Err(_) => break,
}
let fmt = match cli.format {
Some(fmt) => fmt,
None => {
return;
} //todo!()
};
thread::sleep(Duration::from_millis(200));
}
}
});
// Run the function
// Max rate is about 1 measurement per second
//loop {
let result = get_loc(com.clone(), fmt.clone());
println!("{result}");
//}
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);
}
});
});
// =====================================================================
// 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() -> eframe::Result<()> {
let options = eframe::NativeOptions::default();
eframe::run_native(
"NMEA GPS Viewer",
options,
Box::new(|_cc| Box::new(MyApp::default())),
)
}