1use chrono::{Timelike, Utc};
7
8#[derive(Debug, Clone)]
10pub struct GgaSentence {
11 pub latitude: f64,
13 pub longitude: f64,
15 pub altitude: f64,
17 pub quality: u8,
19 pub num_satellites: u8,
21 pub hdop: f32,
23}
24
25impl GgaSentence {
26 pub fn new(latitude: f64, longitude: f64, altitude: f64) -> Self {
28 Self {
29 latitude,
30 longitude,
31 altitude,
32 quality: 1, num_satellites: 10,
34 hdop: 1.0,
35 }
36 }
37
38 pub fn with_quality(mut self, quality: u8) -> Self {
40 self.quality = quality;
41 self
42 }
43
44 pub fn with_satellites(mut self, num: u8) -> Self {
46 self.num_satellites = num;
47 self
48 }
49
50 pub fn with_hdop(mut self, hdop: f32) -> Self {
52 self.hdop = hdop;
53 self
54 }
55
56 pub fn to_nmea(&self) -> String {
60 let now = Utc::now();
61 let time = format!("{:02}{:02}{:02}.00", now.hour(), now.minute(), now.second());
62
63 let (lat_str, lat_dir) = self.format_latitude();
65 let (lon_str, lon_dir) = self.format_longitude();
67
68 let sentence = format!(
70 "GPGGA,{},{},{},{},{},{},{:02},{:.1},{:.1},M,0.0,M,,",
71 time,
72 lat_str,
73 lat_dir,
74 lon_str,
75 lon_dir,
76 self.quality,
77 self.num_satellites.min(99),
78 self.hdop,
79 self.altitude
80 );
81
82 let checksum = sentence.bytes().fold(0u8, |acc, b| acc ^ b);
84
85 format!("${}*{:02X}\r\n", sentence, checksum)
86 }
87
88 fn format_latitude(&self) -> (String, char) {
90 let dir = if self.latitude >= 0.0 { 'N' } else { 'S' };
91 let lat_abs = self.latitude.abs();
92 let degrees = lat_abs.floor() as u32;
93 let minutes = (lat_abs - degrees as f64) * 60.0;
94 (format!("{:02}{:07.4}", degrees, minutes), dir)
95 }
96
97 fn format_longitude(&self) -> (String, char) {
99 let dir = if self.longitude >= 0.0 { 'E' } else { 'W' };
100 let lon_abs = self.longitude.abs();
101 let degrees = lon_abs.floor() as u32;
102 let minutes = (lon_abs - degrees as f64) * 60.0;
103 (format!("{:03}{:07.4}", degrees, minutes), dir)
104 }
105}
106
107impl Default for GgaSentence {
108 fn default() -> Self {
109 Self {
110 latitude: 0.0,
111 longitude: 0.0,
112 altitude: 0.0,
113 quality: 0,
114 num_satellites: 0,
115 hdop: 99.9,
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_gga_creation() {
126 let gga = GgaSentence::new(-27.4698, 153.0251, 50.0);
127 assert_eq!(gga.latitude, -27.4698);
128 assert_eq!(gga.longitude, 153.0251);
129 assert_eq!(gga.altitude, 50.0);
130 }
131
132 #[test]
133 fn test_gga_builder() {
134 let gga = GgaSentence::new(0.0, 0.0, 0.0)
135 .with_quality(4)
136 .with_satellites(15)
137 .with_hdop(0.8);
138
139 assert_eq!(gga.quality, 4);
140 assert_eq!(gga.num_satellites, 15);
141 assert_eq!(gga.hdop, 0.8);
142 }
143
144 #[test]
145 fn test_latitude_format_north() {
146 let gga = GgaSentence::new(27.4698, 0.0, 0.0);
147 let (lat, dir) = gga.format_latitude();
148 assert_eq!(dir, 'N');
149 assert!(lat.starts_with("27")); }
151
152 #[test]
153 fn test_latitude_format_south() {
154 let gga = GgaSentence::new(-27.4698, 0.0, 0.0);
155 let (lat, dir) = gga.format_latitude();
156 assert_eq!(dir, 'S');
157 assert!(lat.starts_with("27")); }
159
160 #[test]
161 fn test_longitude_format_east() {
162 let gga = GgaSentence::new(0.0, 153.0251, 0.0);
163 let (lon, dir) = gga.format_longitude();
164 assert_eq!(dir, 'E');
165 assert!(lon.starts_with("153")); }
167
168 #[test]
169 fn test_longitude_format_west() {
170 let gga = GgaSentence::new(0.0, -122.4194, 0.0);
171 let (lon, dir) = gga.format_longitude();
172 assert_eq!(dir, 'W');
173 assert!(lon.starts_with("122")); }
175
176 #[test]
177 fn test_nmea_format() {
178 let gga = GgaSentence::new(-27.4698, 153.0251, 50.0)
179 .with_quality(4)
180 .with_satellites(12);
181
182 let nmea = gga.to_nmea();
183
184 assert!(nmea.starts_with("$GPGGA,"));
186 assert!(nmea.ends_with("\r\n"));
188 assert!(nmea.contains('*'));
190 assert!(nmea.contains(",S,"));
192 assert!(nmea.contains(",E,"));
194 }
195
196 #[test]
197 fn test_nmea_checksum() {
198 let gga = GgaSentence::new(0.0, 0.0, 0.0);
199 let nmea = gga.to_nmea();
200
201 let parts: Vec<&str> = nmea.trim().split('*').collect();
203 assert_eq!(parts.len(), 2);
204
205 let calculated_checksum = parts[0][1..].bytes().fold(0u8, |acc, b| acc ^ b);
206 let reported_checksum = u8::from_str_radix(parts[1], 16).unwrap();
207
208 assert_eq!(calculated_checksum, reported_checksum);
209 }
210}