TinyRobotics
Loading...
Searching...
No Matches
NMEAParser.h
1#pragma once
2#include <sstream>
3#include <string>
4#include <vector>
5
7#include "TinyRobotics/utils/LoggerClass.h"
8
9namespace tinyrobotics {
10
11enum GPSFormat {
12 GGA, // Fix data
13 RMC // Recommended minimum
14};
15
16/**
17 * @class NMEAParser
18 * @ingroup coordinates
19 * @brief Parses NMEA sentences from GPS modules and extracts GPS data.
20 *
21 * The NMEAParser class supports parsing of common NMEA sentences such as GGA
22 * (fix data) and RMC (recommended minimum), converting them into structured
23 * GPSCoordinate objects.
24 *
25 * Features:
26 * - Supports GGA and RMC NMEA sentence formats
27 * - Converts latitude/longitude from NMEA (degrees/minutes) to decimal
28 * degrees
29 * - Extracts altitude, fix quality, and accuracy metrics
30 * - Handles both std::string and Arduino String (if ARDUINO is defined)
31 * - Can be used for navigation, mapping, logging, or any application
32 * requiring GPS data
33 *
34 * Example usage:
35 * @code
36 * NMEAParser parser(GPSFormat::GGA);
37 * GPSCoordinate gps;
38 * if
39 * (parser.parse("$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",
40 * gps)) {
41 * // gps now contains parsed data
42 * }
43 * @endcode
44 * @ingroup coordinates
45 */
46
47class NMEAParser {
48 public:
49 NMEAParser(GPSFormat fmt) : format(fmt) {}
50
51 bool parse(const char* sentence, GPSCoordinate& gps) const {
52 return parse(std::string(sentence), gps);
53 }
54
55 bool parse(const std::string& sentence, GPSCoordinate& gps) const {
56 switch (format) {
57 case GPSFormat::GGA:
58 return parseGGA(sentence, gps);
59 case GPSFormat::RMC:
60 return parseRMC(sentence, gps);
61 default:
62 return false;
63 }
64 }
65
66#ifdef ARDUINO
67 bool parse(const String& sentence, GPSCoordinate& gps) const {
68 return parse(std::string(sentence.c_str()), gps);
69 }
70#endif
71
72 protected:
73 GPSFormat format;
74
75 // Helper: safe string to float
76 static bool safe_stof(const std::string& s, float& out) {
77 if (s.empty()) return false;
78 char* endptr = nullptr;
79 out = strtof(s.c_str(), &endptr);
80 return endptr != nullptr && *endptr == '\0';
81 }
82 // Helper: safe string to int
83 static bool safe_stoi(const std::string& s, int& out) {
84 if (s.empty()) return false;
85 char* endptr = nullptr;
86 out = strtol(s.c_str(), &endptr, 10);
87 return endptr != nullptr && *endptr == '\0';
88 }
89 // Parse GGA sentence (fix data)
90 bool parseGGA(const std::string& sentence, GPSCoordinate& gps) const {
91 // Format: $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
92 std::vector<std::string> fields;
93 size_t start = 0;
94 size_t end = sentence.find(',');
95
96 while (end != std::string::npos) {
97 fields.push_back(sentence.substr(start, end - start));
98 start = end + 1;
99 end = sentence.find(',', start);
100 }
101 fields.push_back(sentence.substr(start));
102
103 if (fields.size() < 15) {
104 TRLogger.error("NMEAParser: GGA sentence has too few fields: %d" +
105 fields.size());
106 return false;
107 }
108
109 // // Time
110 // if (!fields[1].empty()) {
111 // float time = 0;
112 // if (!safe_stof(fields[1], time)) {
113 // TRLogger.error("NMEAParser: Invalid GGA time: %s", fields[1]);
114 // return false;
115 // }
116 // gps.timestamp = time; // HHMMSS.SS
117 // }
118
119 // Latitude
120 if (!fields[2].empty() && !fields[3].empty()) {
121 float lat_deg = 0, lat_min = 0;
122 if (fields[2].size() < 4 || !safe_stof(fields[2].substr(0, 2), lat_deg) ||
123 !safe_stof(fields[2].substr(2), lat_min)) {
124 TRLogger.error("NMEAParser: Invalid GGA latitude: %s, %s", fields[2],
125 fields[3]);
126 return false;
127 }
128 gps.latitude = lat_deg + lat_min / 60.0f;
129 if (fields[3] == "S") gps.latitude = -gps.latitude;
130 }
131
132 // Longitude
133 if (!fields[4].empty() && !fields[5].empty()) {
134 float lon_deg = 0, lon_min = 0;
135 if (fields[4].size() < 5 || !safe_stof(fields[4].substr(0, 3), lon_deg) ||
136 !safe_stof(fields[4].substr(3), lon_min)) {
137 TRLogger.error("NMEAParser: Invalid GGA longitude: %s, %s", fields[4],
138 fields[5]);
139 return false;
140 }
141 gps.longitude = lon_deg + lon_min / 60.0f;
142 if (fields[5] == "W") gps.longitude = -gps.longitude;
143 }
144
145 // // Fix quality
146 // if (!fields[6].empty()) {
147 // int fixq = 0;
148 // if (!safe_stoi(fields[6], fixq)) {
149 // TRLogger.error("NMEAParser: Invalid GGA fix quality: %s", fields[6]);
150 // return false;
151 // }
152 // gps.fix_quality = fixq;
153 // }
154
155 // // HDOP (Horizontal Dilution of Precision)
156 // if (!fields[8].empty()) {
157 // float hdop = 0;
158 // if (!safe_stof(fields[8], hdop)) {
159 // TRLogger.error("NMEAParser: Invalid GGA HDOP: %s", fields[8]);
160 // return false;
161 // }
162 // gps.horizontal_accuracy = hdop * 5.0; // Approximate 1-sigma
163 // }
164
165 // Altitude
166 if (!fields[9].empty()) {
167 float alt = 0;
168 if (!safe_stof(fields[9], alt)) {
169 TRLogger.error("NMEAParser: Invalid GGA altitude: %s", fields[9]);
170 return false;
171 }
172 gps.altitude = alt;
173 }
174
175 return true;
176 }
177
178 // Parse RMC sentence (recommended minimum)
179 bool parseRMC(const std::string& sentence, GPSCoordinate& gps) const {
180 // Format:
181 // $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
182 std::vector<std::string> fields;
183 size_t start = 0;
184 size_t end = sentence.find(',');
185
186 while (end != std::string::npos) {
187 fields.push_back(sentence.substr(start, end - start));
188 start = end + 1;
189 end = sentence.find(',', start);
190 }
191 fields.push_back(sentence.substr(start));
192
193 if (fields.size() < 12) {
194 TRLogger.error("NMEAParser: RMC sentence has too few fields: %d",
195 fields.size());
196 return false;
197 }
198
199 // // Status
200 // if (fields[2] == "A") {
201 // gps.fix_quality = 1; // Active fix
202 // } else {
203 // gps.fix_quality = 0; // Invalid
204 // TRLogger.error("NMEAParser: RMC status not active: %s", fields[2]);
205 // return false;
206 // }
207
208 // Latitude
209 if (!fields[3].empty() && !fields[4].empty()) {
210 float lat_deg = 0, lat_min = 0;
211 if (fields[3].size() < 4 || !safe_stof(fields[3].substr(0, 2), lat_deg) ||
212 !safe_stof(fields[3].substr(2), lat_min)) {
213 TRLogger.error("NMEAParser: Invalid RMC latitude: %s, %s", fields[3],
214 fields[4]);
215 return false;
216 }
217 gps.latitude = lat_deg + lat_min / 60.0f;
218 if (fields[4] == "S") gps.latitude = -gps.latitude;
219 }
220
221 // Longitude
222 if (!fields[5].empty() && !fields[6].empty()) {
223 float lon_deg = 0, lon_min = 0;
224 if (fields[5].size() < 5 || !safe_stof(fields[5].substr(0, 3), lon_deg) ||
225 !safe_stof(fields[5].substr(3), lon_min)) {
226 TRLogger.error("NMEAParser: Invalid RMC longitude: %s/%s", fields[5],
227 fields[6]);
228 return false;
229 }
230 gps.longitude = lon_deg + lon_min / 60.0f;
231 if (fields[6] == "W") gps.longitude = -gps.longitude;
232 }
233
234 // Speed over ground (knots)
235 if (!fields[7].empty()) {
236 float speed_knots = 0;
237 if (!safe_stof(fields[7], speed_knots)) {
238 TRLogger.error("NMEAParser: Invalid RMC speed: %s", fields[7]);
239 return false;
240 }
241 // speed_ms = speed_knots * 0.514444;
242 }
243
244 return true;
245 }
246
247 std::vector<std::string> split(const std::string& str, char delim) {
248 std::vector<std::string> tokens;
249 std::stringstream ss{str};
250 std::string token;
251 while (std::getline(ss, token, delim)) {
252 tokens.push_back(token);
253 }
254 return tokens;
255 }
256};
257
258} // namespace tinyrobotics
Represents a geodetic GPS coordinate with latitude, longitude, and optional altitude.
Definition: GPSCoordinate.h:52
Parses NMEA sentences from GPS modules and extracts GPS data.
Definition: NMEAParser.h:47