1use std::collections::HashMap;
26
27#[derive(Debug, Default, Clone)]
29pub struct SatelliteInfo {
30 pub prn: u16,
32 pub elevation: Option<u8>,
34 pub azimuth: Option<u16>,
36 pub snr: Option<u8>,
38}
39
40#[derive(Debug, Default, Clone)]
42pub struct GnssSystemData {
43 pub satellites_used: Vec<u16>,
45 pub satellites_info: HashMap<u16, SatelliteInfo>,
47 pub pdop: Option<f64>,
49 pub hdop: Option<f64>,
51 pub vdop: Option<f64>,
53 pub latitude: Option<f64>,
55 pub longitude: Option<f64>,
57}
58
59#[derive(Debug, Default, Clone)]
61pub struct GnssData {
62 pub time: Option<String>,
64 pub latitude: Option<f64>,
66 pub longitude: Option<f64>,
68 pub fix_quality: Option<u8>,
70 pub num_satellites: Option<u8>,
72 pub altitude: Option<f64>,
74 pub speed_knots: Option<f64>,
76 pub track_angle: Option<f64>,
78 pub date: Option<String>,
80 pub systems: HashMap<&'static str, GnssSystemData>,
82 pub fused_position: Option<FusedPosition>,
84}
85
86#[derive(Debug, Clone)]
88pub struct FusedPosition {
89 pub latitude: f64,
91 pub longitude: f64,
93 pub estimated_accuracy: f64,
95 pub contributing_systems: Vec<String>,
97}
98
99impl GnssData {
100 pub fn new() -> Self {
109 let mut systems = HashMap::new();
110 systems.insert("GPS", GnssSystemData::default());
111 systems.insert("GLONASS", GnssSystemData::default());
112 systems.insert("GALILEO", GnssSystemData::default());
113 systems.insert("BEIDOU", GnssSystemData::default());
114 Self { systems, ..Default::default() }
115 }
116
117 fn update_gga(&mut self, parts: &[&str]) {
119 let lat = parse_lat(parts.get(2), parts.get(3));
120 let lon = parse_lon(parts.get(4), parts.get(5));
121 self.time = parts.get(1).map(|s| s.to_string());
122 self.latitude = lat;
123 self.longitude = lon;
124 self.fix_quality = parts.get(6).and_then(|s| s.parse().ok());
125 self.num_satellites = parts.get(7).and_then(|s| s.parse().ok());
126 self.altitude = parts.get(9).and_then(|s| s.parse().ok());
127
128 for (_, system_data) in self.systems.iter_mut() {
130 if system_data.satellites_info.len() > 0 {
131 system_data.latitude = lat;
132 system_data.longitude = lon;
133 } else {
134 system_data.latitude = None;
135 system_data.longitude = None;
136 }
137 }
138 }
139
140 fn update_rmc(&mut self, parts: &[&str]) {
142 let lat = parse_lat(parts.get(3), parts.get(4));
143 let lon = parse_lon(parts.get(5), parts.get(6));
144 self.time = parts.get(1).map(|s| s.to_string());
145 self.latitude = lat;
146 self.longitude = lon;
147 self.speed_knots = parts.get(7).and_then(|s| s.parse().ok());
148 self.track_angle = parts.get(8).and_then(|s| s.parse().ok());
149 self.date = parts.get(9).map(|s| s.to_string());
150
151 for (_, system_data) in self.systems.iter_mut() {
153 if system_data.satellites_info.len() > 0 {
154 system_data.latitude = lat;
155 system_data.longitude = lon;
156 } else {
157 system_data.latitude = None;
158 system_data.longitude = None;
159 }
160 }
161 }
162
163 fn update_vtg(&mut self, parts: &[&str]) {
165 self.speed_knots = parts.get(5).and_then(|s| s.parse().ok());
166 }
167
168 fn update_gsa(&mut self, parts: &[&str]) {
170 let mut gps_ids = Vec::new();
171 for i in 3..=14 {
172 if let Some(Ok(prn)) = parts.get(i).map(|s| s.parse()) {
173 gps_ids.push(prn);
174 }
175 }
176
177 let mut dop_values = Vec::new();
179 for i in 15..parts.len() {
180 if let Some(part) = parts.get(i) {
181 let clean_part = part.split('*').next().unwrap_or(part);
182 if !clean_part.is_empty() {
183 if let Ok(value) = clean_part.parse::<f64>() {
184 dop_values.push(value);
185 if dop_values.len() == 3 {
186 break;
187 }
188 }
189 }
190 }
191 }
192
193 let pdop = dop_values.get(0).copied();
194 let hdop = dop_values.get(1).copied();
195 let vdop = dop_values.get(2).copied();
196
197 let mut updated_systems = Vec::new();
198 for prn in &gps_ids {
199 match prn {
200 1..=32 => {
201 self.systems.get_mut("GPS").unwrap().satellites_used.push(*prn as u16);
202 if !updated_systems.contains(&"GPS") {
203 updated_systems.push("GPS");
204 }
205 },
206 65..=96 => {
207 self.systems.get_mut("GLONASS").unwrap().satellites_used.push(*prn as u16);
208 if !updated_systems.contains(&"GLONASS") {
209 updated_systems.push("GLONASS");
210 }
211 },
212 201..=236 => {
213 self.systems.get_mut("BEIDOU").unwrap().satellites_used.push(*prn as u16);
214 if !updated_systems.contains(&"BEIDOU") {
215 updated_systems.push("BEIDOU");
216 }
217 },
218 301..=336 => {
219 self.systems.get_mut("GALILEO").unwrap().satellites_used.push(*prn as u16);
220 if !updated_systems.contains(&"GALILEO") {
221 updated_systems.push("GALILEO");
222 }
223 },
224 _ => {}
225 }
226 }
227 for sys_name in updated_systems {
229 if let Some(sys) = self.systems.get_mut(sys_name) {
230 sys.pdop = pdop;
231 sys.hdop = hdop;
232 sys.vdop = vdop;
233 }
234 }
235 }
236
237 fn update_gsv(&mut self, parts: &[&str], system: &str) {
239 if let Some(sys_data) = self.systems.get_mut(system) {
240 let mut i = 4;
241 while i + 3 < parts.len() {
242 if let Some(Ok(prn)) = parts.get(i).map(|s| s.parse()) {
243 let elevation = parts.get(i + 1).and_then(|s| s.parse().ok());
244 let azimuth = parts.get(i + 2).and_then(|s| s.parse().ok());
245 let snr = parts.get(i + 3).and_then(|s| s.trim_end_matches('*').parse().ok());
246 sys_data.satellites_info.insert(
247 prn,
248 SatelliteInfo {
249 prn,
250 elevation,
251 azimuth,
252 snr,
253 },
254 );
255 }
256 i += 4;
257 }
258 }
259 }
260
261 fn update_gll(&mut self, parts: &[&str], system: &str) {
263 let lat = parse_lat(parts.get(1), parts.get(2));
264 let lon = parse_lon(parts.get(3), parts.get(4));
265 self.latitude = lat;
266 self.longitude = lon;
267 if let Some(sys) = self.systems.get_mut(system) {
268 if sys.satellites_info.len() > 0 {
269 sys.latitude = lat;
270 sys.longitude = lon;
271 } else {
272 sys.latitude = None;
273 sys.longitude = None;
274 }
275 }
276 }
277
278 pub fn feed_nmea(&mut self, sentence: &str) {
290 let sentence = sentence.trim_start_matches('$');
291 let parts: Vec<&str> = sentence.split(',').collect();
292 match parts.first().filter(|s| s.len() >= 5).map(|s| &s[0..5]) {
293 Some("GNGGA") => self.update_gga(&parts),
294 Some("GNRMC") => self.update_rmc(&parts),
295 Some("GNVTG") => self.update_vtg(&parts),
296 Some("GNGSA") => self.update_gsa(&parts),
297 Some("GPGSV") => self.update_gsv(&parts, "GPS"),
298 Some("GLGSV") => self.update_gsv(&parts, "GLONASS"),
299 Some("GAGSV") => self.update_gsv(&parts, "GALILEO"),
300 Some("BDGSV") => self.update_gsv(&parts, "BEIDOU"),
301 Some("GPGLL") => self.update_gll(&parts, "GPS"),
302 Some("GLGLL") => self.update_gll(&parts, "GLONASS"),
303 Some("GAGLL") => self.update_gll(&parts, "GALILEO"),
304 Some("BDGLL") => self.update_gll(&parts, "BEIDOU"),
305 _ => {}
306 }
307 }
308
309 pub fn calculate_fused_position(&mut self) {
323 let mut valid_positions = Vec::new();
324
325 for (system_name, system_data) in &self.systems {
326 if let (Some(lat), Some(lon), Some(hdop)) = (system_data.latitude, system_data.longitude, system_data.hdop) {
327 if system_data.satellites_info.len() >= 4 { valid_positions.push((system_name.to_string(), lat, lon, hdop));
329 }
330 }
331 }
332
333 if valid_positions.is_empty() {
334 self.fused_position = None;
335 return;
336 }
337
338 if valid_positions.len() == 1 {
339 let (system, lat, lon, hdop) = &valid_positions[0];
340 self.fused_position = Some(FusedPosition {
341 latitude: *lat,
342 longitude: *lon,
343 estimated_accuracy: hdop * 3.0, contributing_systems: vec![system.clone()],
345 });
346 return;
347 }
348
349 let mut weighted_lat = 0.0;
351 let mut weighted_lon = 0.0;
352 let mut total_weight = 0.0;
353 let mut contributing_systems = Vec::new();
354
355 for (system, lat, lon, hdop) in &valid_positions {
356 let weight = 1.0 / (hdop + 0.1); weighted_lat += lat * weight;
358 weighted_lon += lon * weight;
359 total_weight += weight;
360 contributing_systems.push(system.clone());
361 }
362
363 if total_weight > 0.0 {
364 let fused_lat = weighted_lat / total_weight;
365 let fused_lon = weighted_lon / total_weight;
366
367 let weighted_hdop: f64 = valid_positions.iter()
369 .map(|(_, _, _, hdop)| {
370 let weight = 1.0 / (hdop + 0.1);
371 hdop * weight
372 })
373 .sum::<f64>() / total_weight;
374
375 self.fused_position = Some(FusedPosition {
376 latitude: fused_lat,
377 longitude: fused_lon,
378 estimated_accuracy: weighted_hdop * 2.0, contributing_systems,
380 });
381 } else {
382 self.fused_position = None;
383 }
384 }
385
386 pub fn calculate_advanced_fused_position(&mut self) {
390 let mut valid_positions = Vec::new();
391
392 for (system_name, system_data) in &self.systems {
393 if let (Some(lat), Some(lon), Some(hdop), Some(pdop)) =
394 (system_data.latitude, system_data.longitude, system_data.hdop, system_data.pdop) {
395 if system_data.satellites_info.len() >= 4 {
396 valid_positions.push((system_name.to_string(), lat, lon, hdop, pdop));
397 }
398 }
399 }
400
401 if valid_positions.is_empty() {
402 self.fused_position = None;
403 return;
404 }
405
406 let mut weighted_lat = 0.0;
408 let mut weighted_lon = 0.0;
409 let mut total_weight = 0.0;
410 let mut contributing_systems = Vec::new();
411
412 for (system, lat, lon, hdop, pdop) in &valid_positions {
413 let combined_dop = (hdop * hdop + pdop * pdop).sqrt();
415 let weight = 1.0 / (combined_dop + 0.1);
416
417 weighted_lat += lat * weight;
418 weighted_lon += lon * weight;
419 total_weight += weight;
420 contributing_systems.push(system.clone());
421 }
422
423 if total_weight > 0.0 {
424 let fused_lat = weighted_lat / total_weight;
425 let fused_lon = weighted_lon / total_weight;
426
427 let variance: f64 = valid_positions.iter()
429 .map(|(_, lat, lon, hdop, _)| {
430 let weight = 1.0 / (*hdop + 0.1);
431 let lat_diff = lat - fused_lat;
432 let lon_diff = lon - fused_lon;
433 weight * (lat_diff * lat_diff + lon_diff * lon_diff)
434 })
435 .sum::<f64>() / total_weight;
436
437 let estimated_accuracy = variance.sqrt() * 111000.0; self.fused_position = Some(FusedPosition {
440 latitude: fused_lat,
441 longitude: fused_lon,
442 estimated_accuracy: estimated_accuracy.max(1.0), contributing_systems,
444 });
445 } else {
446 self.fused_position = None;
447 }
448 }
449}
450
451fn parse_lat(value: Option<&&str>, hemi: Option<&&str>) -> Option<f64> {
460 let val = value?.parse::<f64>().ok()?;
461 let deg = (val / 100.0).floor();
462 let min = val % 100.0;
463 let mut result = deg + min / 60.0;
464 if hemi? == &"S" { result *= -1.0; }
465 Some(result)
466}
467
468fn parse_lon(value: Option<&&str>, hemi: Option<&&str>) -> Option<f64> {
477 let val = value?.parse::<f64>().ok()?;
478 let deg = (val / 100.0).floor();
479 let min = val % 100.0;
480 let mut result = deg + min / 60.0;
481 if hemi? == &"W" { result *= -1.0; }
482 Some(result)
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_gnssdata_new() {
491 let gnss = GnssData::new();
492 assert!(gnss.systems.contains_key("GPS"));
493 assert!(gnss.systems.contains_key("GLONASS"));
494 assert!(gnss.systems.contains_key("GALILEO"));
495 assert!(gnss.systems.contains_key("BEIDOU"));
496 }
497
498 #[test]
499 fn test_feed_nmea_gga() {
500 let mut gnss = GnssData::new();
501 let gga = "$GNGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
502 gnss.feed_nmea(gga);
503 assert_eq!(gnss.time, Some("123519".to_string()));
504 assert!(gnss.latitude.is_some());
505 assert!(gnss.longitude.is_some());
506 assert_eq!(gnss.fix_quality, Some(1));
507 assert_eq!(gnss.num_satellites, Some(8));
508 assert_eq!(gnss.altitude, Some(545.4));
509 }
510
511 #[test]
512 fn test_feed_nmea_gps_gsv() {
513 let mut gnss = GnssData::new();
514 let gsv = "$GPGSV,2,1,08,01,40,083,41,02,17,308,43,03,13,172,42,04,09,020,39*7C";
515 gnss.feed_nmea(gsv);
516 let gps_info = &gnss.systems["GPS"].satellites_info;
517 assert_eq!(gps_info.len(), 4);
518 assert!(gps_info.contains_key(&1));
519 assert!(gps_info.contains_key(&2));
520 assert!(gps_info.contains_key(&3));
521 assert!(gps_info.contains_key(&4));
522 let sat1 = gps_info.get(&1).unwrap();
523 assert_eq!(sat1.prn, 1);
524 assert_eq!(sat1.elevation, Some(40));
525 assert_eq!(sat1.azimuth, Some(83));
526 assert_eq!(sat1.snr, Some(41));
527 }
528
529 #[test]
530 fn test_feed_nmea_glonass_gsv() {
531 let mut gnss = GnssData::new();
532 let gsv = "$GLGSV,2,1,08,67,14,186,09,68,49,228,26,69,42,308,,77,15,064,17*61";
533 gnss.feed_nmea(gsv);
534 let glonass_info = &gnss.systems["GLONASS"].satellites_info;
535 assert_eq!(glonass_info.len(), 4);
536 assert!(glonass_info.contains_key(&67));
537 assert!(glonass_info.contains_key(&68));
538 assert!(glonass_info.contains_key(&69));
539 assert!(glonass_info.contains_key(&77));
540 let sat67 = glonass_info.get(&67).unwrap();
541 assert_eq!(sat67.prn, 67);
542 assert_eq!(sat67.elevation, Some(14));
543 assert_eq!(sat67.azimuth, Some(186));
544 assert_eq!(sat67.snr, Some(9));
545 }
546
547 #[test]
548 fn test_feed_nmea_galileo_gsv() {
549 let mut gnss = GnssData::new();
550 let gsv = "$GAGSV,1,1,04,301,45,123,35,302,30,045,40,303,60,234,45,304,25,156,38*XX";
551 gnss.feed_nmea(gsv);
552 let galileo_info = &gnss.systems["GALILEO"].satellites_info;
553 assert_eq!(galileo_info.len(), 4);
554 assert!(galileo_info.contains_key(&301));
555 assert!(galileo_info.contains_key(&302));
556 assert!(galileo_info.contains_key(&303));
557 assert!(galileo_info.contains_key(&304));
558 let sat301 = galileo_info.get(&301).unwrap();
559 assert_eq!(sat301.prn, 301);
560 assert_eq!(sat301.elevation, Some(45));
561 assert_eq!(sat301.azimuth, Some(123));
562 assert_eq!(sat301.snr, Some(35));
563 }
564
565 #[test]
566 fn test_feed_nmea_beidou_gsv() {
567 let mut gnss = GnssData::new();
568 let gsv = "$BDGSV,1,1,04,201,45,123,35,202,30,045,40,203,60,234,45,204,25,156,38*XX";
569 gnss.feed_nmea(gsv);
570 let beidou_info = &gnss.systems["BEIDOU"].satellites_info;
571 assert_eq!(beidou_info.len(), 4);
572 assert!(beidou_info.contains_key(&201));
573 assert!(beidou_info.contains_key(&202));
574 assert!(beidou_info.contains_key(&203));
575 assert!(beidou_info.contains_key(&204));
576 let sat201 = beidou_info.get(&201).unwrap();
577 assert_eq!(sat201.prn, 201);
578 assert_eq!(sat201.elevation, Some(45));
579 assert_eq!(sat201.azimuth, Some(123));
580 assert_eq!(sat201.snr, Some(35));
581 }
582
583 #[test]
584 fn test_feed_nmea_gsa_gps() {
585 let mut gnss = GnssData::new();
586 let gsa = "$GNGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.2,0.9,2.1*39";
587 gnss.feed_nmea(gsa);
588 let gps_used = &gnss.systems["GPS"].satellites_used;
589 assert!(gps_used.contains(&1));
590 assert!(gps_used.contains(&2));
591 assert!(gps_used.contains(&3));
592 assert!(gps_used.contains(&4));
593 assert_eq!(gnss.systems["GPS"].pdop, Some(1.2));
594 assert_eq!(gnss.systems["GPS"].hdop, Some(0.9));
595 assert_eq!(gnss.systems["GPS"].vdop, Some(2.1));
596 }
597
598 #[test]
599 fn test_feed_nmea_gsa_glonass() {
600 let mut gnss = GnssData::new();
601 let gsa = "$GNGSA,A,3,67,68,69,77,78,79,86,87,88,,,,,1.8,1.1,1.4*3F";
602 gnss.feed_nmea(gsa);
603
604 println!("GLONASS satellites_used: {:?}", gnss.systems["GLONASS"].satellites_used);
606 println!("GLONASS pdop: {:?}", gnss.systems["GLONASS"].pdop);
607 println!("GLONASS hdop: {:?}", gnss.systems["GLONASS"].hdop);
608 println!("GLONASS vdop: {:?}", gnss.systems["GLONASS"].vdop);
609
610 let glonass_used = &gnss.systems["GLONASS"].satellites_used;
611 assert!(glonass_used.contains(&67));
612 assert!(glonass_used.contains(&68));
613 assert!(glonass_used.contains(&69));
614 assert!(glonass_used.contains(&77));
615 assert_eq!(gnss.systems["GLONASS"].pdop, Some(1.8));
616 assert_eq!(gnss.systems["GLONASS"].hdop, Some(1.1));
617 assert_eq!(gnss.systems["GLONASS"].vdop, Some(1.4));
618 }
619
620 #[test]
621 fn test_feed_nmea_gsa_galileo() {
622 let mut gnss = GnssData::new();
623 let gsa = "$GNGSA,A,3,301,302,303,304,305,306,,,,,,,2.1,1.3,1.6*XX";
624 gnss.feed_nmea(gsa);
625 let galileo_used = &gnss.systems["GALILEO"].satellites_used;
626 assert!(galileo_used.contains(&301));
627 assert!(galileo_used.contains(&302));
628 assert!(galileo_used.contains(&303));
629 assert!(galileo_used.contains(&304));
630 assert_eq!(gnss.systems["GALILEO"].pdop, Some(2.1));
631 assert_eq!(gnss.systems["GALILEO"].hdop, Some(1.3));
632 assert_eq!(gnss.systems["GALILEO"].vdop, Some(1.6));
633 }
634
635 #[test]
636 fn test_feed_nmea_gsa_beidou() {
637 let mut gnss = GnssData::new();
638 let gsa = "$GNGSA,A,3,201,202,203,204,205,206,,,,,,,1.5,0.8,1.2*XX";
639 gnss.feed_nmea(gsa);
640 let beidou_used = &gnss.systems["BEIDOU"].satellites_used;
641 assert!(beidou_used.contains(&201));
642 assert!(beidou_used.contains(&202));
643 assert!(beidou_used.contains(&203));
644 assert!(beidou_used.contains(&204));
645 assert_eq!(gnss.systems["BEIDOU"].pdop, Some(1.5));
646 assert_eq!(gnss.systems["BEIDOU"].hdop, Some(0.8));
647 assert_eq!(gnss.systems["BEIDOU"].vdop, Some(1.2));
648 }
649
650 #[test]
651 fn test_coordinates_update_for_systems_with_satellites() {
652 let mut gnss = GnssData::new();
653
654 let gps_gsv = "$GPGSV,1,1,04,01,40,083,41,02,17,308,43,03,13,172,42,04,09,020,39*XX";
656 gnss.feed_nmea(gps_gsv);
657
658 let glonass_gsv = "$GLGSV,1,1,04,67,14,186,09,68,49,228,26,69,42,308,,77,15,064,17*XX";
660 gnss.feed_nmea(glonass_gsv);
661
662 let gga = "$GNGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
664 gnss.feed_nmea(gga);
665
666 assert!(gnss.systems["GPS"].latitude.is_some());
668 assert!(gnss.systems["GPS"].longitude.is_some());
669 assert!(gnss.systems["GLONASS"].latitude.is_some());
670 assert!(gnss.systems["GLONASS"].longitude.is_some());
671
672 assert!(gnss.systems["GALILEO"].latitude.is_none());
674 assert!(gnss.systems["GALILEO"].longitude.is_none());
675 assert!(gnss.systems["BEIDOU"].latitude.is_none());
676 assert!(gnss.systems["BEIDOU"].longitude.is_none());
677 }
678
679 #[test]
680 fn test_fused_position_calculation() {
681 let mut gnss = GnssData::new();
682
683 let gps_gsv = "$GPGSV,1,1,04,01,40,083,41,02,17,308,43,03,13,172,42,04,09,020,39*XX";
685 gnss.feed_nmea(gps_gsv);
686 let gps_gsa = "$GNGSA,A,3,01,02,03,04,05,06,07,08,,,,,1.2,0.9,2.1*39";
687 gnss.feed_nmea(gps_gsa);
688
689 let glonass_gsv = "$GLGSV,1,1,04,67,14,186,09,68,49,228,26,69,42,308,,77,15,064,17*XX";
691 gnss.feed_nmea(glonass_gsv);
692 let glonass_gsa = "$GNGSA,A,3,67,68,69,77,78,79,86,87,,,,,1.8,1.1,1.4*3F";
693 gnss.feed_nmea(glonass_gsa);
694
695 let gga = "$GNGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
697 gnss.feed_nmea(gga);
698
699 gnss.calculate_fused_position();
701
702 assert!(gnss.fused_position.is_some());
703 let fused = gnss.fused_position.as_ref().unwrap();
704 assert!(fused.contributing_systems.contains(&"GPS".to_string()));
705 assert!(fused.contributing_systems.contains(&"GLONASS".to_string()));
706 assert!(fused.estimated_accuracy > 0.0);
707 }
708
709 #[test]
710 fn test_parse_lat_lon() {
711 let lat = parse_lat(Some(&"4807.038"), Some(&"N"));
712 let lon = parse_lon(Some(&"01131.000"), Some(&"E"));
713 assert!(lat.is_some());
714 assert!(lon.is_some());
715 let lat_val = lat.unwrap();
716 let lon_val = lon.unwrap();
717 assert!((lat_val - 48.1173).abs() < 0.0001);
718 assert!((lon_val - 11.5166667).abs() < 0.0001);
719 }
720}