commit d239e3175bce5baa3074e9739d81cb1cb77e0bc9 Author: Jose M Serra Jr Date: Thu Nov 27 16:18:01 2025 -0500 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b24f6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target/ + +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8966f60 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "RUST_NMEA_PARSER" +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" diff --git a/GUI_output.png b/GUI_output.png new file mode 100644 index 0000000..d5a148f Binary files /dev/null and b/GUI_output.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..21339c8 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ + +# Rust GPS GUI COM Port Reader + +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" +``` + +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. + +## Sample Terminal Output + +![GUI_Output](GUI_output.png) + +## Acknowledgements + + - Professors at Kean University + - Professors at NJIT + - Research Mentors at Kean University + - StackOverflow Q&A Discussion + - ChatGPT + - ClaudeAI + + +## 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) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6488039 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,229 @@ +use eframe::egui; +use serialport::available_ports; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use egui_plot::{Plot, PlotPoints, Points, Text, Line}; + +#[derive(Default, Clone)] +struct Satellite { + id: String, + latitude: f64, + longitude: f64, + strength: u8, +} + +#[derive(Default)] +struct AppState { + ports: Vec, + selected_port: Option, + satellites: Vec, + is_reading: bool, + + // 🔵 NEW: live NMEA data buffer + nmea_log: Vec, +} + +pub struct MyApp { + state: Arc>, +} + +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::>() + .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(); + + 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() { + + // 🔵 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); + } + }); + }); + + // ===================================================================== + // 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())), + ) +}