QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
NMEASentence.cc
Go to the documentation of this file.
1#include "NMEASentence.h"
2
3#include <chrono>
4#include <cmath>
5#include <cstdint>
6#include <limits>
7
8#include "Checksums.h"
9
10namespace {
11constexpr double DEGREE_MINUTE_SCALE = 100.0;
12constexpr double MAX_LATITUDE_DEGREES = 90.0;
13constexpr double MAX_LONGITUDE_DEGREES = 180.0;
14constexpr unsigned MAX_GGA_SATELLITES = std::numeric_limits<uint8_t>::max();
15constexpr size_t UTC_COMPONENT_DIGITS = 2;
16constexpr size_t UTC_INTEGER_DIGITS = 3 * UTC_COMPONENT_DIGITS;
17constexpr size_t UTC_FRACTION_OFFSET = UTC_INTEGER_DIGITS + 1;
18constexpr unsigned HOURS_PER_DAY = 24;
19constexpr unsigned MINUTES_PER_HOUR = 60;
20constexpr unsigned SECONDS_PER_MINUTE = 60;
21constexpr size_t FIRST_MILLISECOND_DIGIT_WEIGHT = 100;
22constexpr double KNOTS_TO_METERS_PER_SECOND = 0.5144444444444445;
23constexpr double KILOMETERS_PER_HOUR_TO_METERS_PER_SECOND = 1.0 / 3.6;
24
25std::optional<NMEA::UtcDate> dateFromParts(unsigned day, unsigned month, int year)
26{
27 if (!std::chrono::year_month_day{std::chrono::year{year}, std::chrono::month{month}, std::chrono::day{day}}.ok()) {
28 return {};
29 }
30 return NMEA::UtcDate{year, month, day};
31}
32
33std::optional<double> nonnegativeDegrees(std::string_view field)
34{
35 const auto value = NMEA::number<double>(field);
36 return value && *value >= 0.0 && *value <= 360.0 ? value : std::nullopt;
37}
38
39std::optional<double> nonnegativeSpeed(std::string_view field, double scale)
40{
41 const auto value = NMEA::number<double>(field);
42 return value && *value >= 0.0 ? std::optional<double>(*value * scale) : std::nullopt;
43}
44
45constexpr int MIN_REPAIRABLE_SENTENCE_LENGTH = 5;
46constexpr int MINUTE_DECIMAL_PLACES = 4;
47constexpr double MINUTE_FRACTION_SCALE = 10000.0;
48constexpr int LATITUDE_DEGREE_DIGITS = 2;
49constexpr int LONGITUDE_DEGREE_DIGITS = 3;
50constexpr int ALTITUDE_DECIMAL_PLACES = 1;
51constexpr int DOP_DECIMAL_PLACES = 1;
52} // namespace
53
54namespace NMEA {
55unsigned char checksum(std::string_view body)
56{
57 return QGC::nmeaChecksum({reinterpret_cast<const uint8_t*>(body.data()), body.size()});
58}
59
60std::optional<uint32_t> hexNumber(std::string_view digits)
61{
62 uint32_t value = 0;
63 const auto parsed = std::from_chars(digits.data(), digits.data() + digits.size(), value, HEX_BASE);
64 if (parsed.ec != std::errc{} || parsed.ptr != digits.data() + digits.size()) {
65 return std::nullopt;
66 }
67 return value;
68}
69
70size_t splitFields(std::string_view text, std::span<std::string_view> fields)
71{
72 size_t count = 0;
73 for (;;) {
74 if (count == fields.size()) {
75 return 0;
76 }
77 const auto comma = text.find(',');
78 fields[count++] = text.substr(0, comma);
79 if (comma == std::string_view::npos) {
80 return count;
81 }
82 text.remove_prefix(comma + 1);
83 }
84}
85
87{
89}
90
91std::optional<Frame> frame(std::string_view text)
92{
93 if (text.ends_with('\n')) {
94 text.remove_suffix(1);
95 if (text.ends_with('\r')) {
96 text.remove_suffix(1);
97 }
98 }
99 if (text.empty() || text.front() != '$') {
100 return {};
101 }
102 text.remove_prefix(Sentence::PREFIX_LENGTH);
103 const auto star = text.find('*');
104 const auto body = text.substr(0, star);
105 if (body.empty()) {
106 return {};
107 }
108 for (char byte : body) {
109 if (byte < ' ' || byte > '~' || byte == '$') {
110 return {};
111 }
112 }
113 return Frame{body, star == std::string_view::npos ? std::string_view() : text.substr(star + 1)};
114}
115
116bool isStandardSentence(std::string_view text)
117{
118 const auto checked = sentence(text);
119 return checked && !checked->talker().starts_with('P');
120}
121
122std::optional<Sentence> sentence(std::string_view text)
123{
124 const auto wire = frame(text);
125 if (!wire || !wire->hasValidChecksum()) {
126 return {};
127 }
128 Sentence result;
129 text = text.substr(0, Sentence::PREFIX_LENGTH + wire->body.size());
130 result.count = splitFields(text, result.fields);
131 if (result.count == 0 || result.fields[0].size() != Sentence::HEADER_LENGTH) {
132 return {};
133 }
134 return result;
135}
136
137double degreesFromDegreesMinutes(double ddmm)
138{
139 if (!std::isfinite(ddmm) || std::abs(ddmm) > MAX_LONGITUDE_DEGREES * DEGREE_MINUTE_SCALE) {
140 return NAN;
141 }
142 const double degrees = std::trunc(ddmm / DEGREE_MINUTE_SCALE);
143 const double minutes = ddmm - degrees * DEGREE_MINUTE_SCALE;
144 return std::abs(minutes) < MINUTES_PER_DEGREE ? degrees + minutes / MINUTES_PER_DEGREE : NAN;
145}
146
147std::optional<double> coordinate(std::string_view field, std::string_view hemisphere, bool latitude)
148{
149 const auto value = number<double>(field);
150 if (!value || *value < 0 || hemisphere.size() != 1) {
151 return {};
152 }
153 const double degrees = std::trunc(*value / DEGREE_MINUTE_SCALE);
154 const double minutes = *value - degrees * DEGREE_MINUTE_SCALE;
155 const double limit = latitude ? MAX_LATITUDE_DEGREES : MAX_LONGITUDE_DEGREES;
156 if (minutes >= MINUTES_PER_DEGREE || degrees + minutes / MINUTES_PER_DEGREE > limit) {
157 return {};
158 }
159 const char positive = latitude ? 'N' : 'E';
160 const char negative = latitude ? 'S' : 'W';
161 if (hemisphere[0] != positive && hemisphere[0] != negative) {
162 return {};
163 }
164 return (degrees + minutes / MINUTES_PER_DEGREE) * (hemisphere[0] == negative ? -1 : 1);
165}
166
167std::optional<GGA> gga(const Sentence& input)
168{
169 if (input.type() != "GGA" || input.count < Field::GGA_MIN_FIELDS) {
170 return {};
171 }
172 const auto& f = input.fields;
175 const auto quality = number<unsigned>(f[Field::GGA_QUALITY]);
176 const auto satellites = number<unsigned>(f[Field::GGA_SATELLITES_USED]);
177 if (!quality || *quality > GgaQuality::MAX_VALUE || (satellites && *satellites > MAX_GGA_SATELLITES)) {
178 return {};
179 }
180 const auto latitudeHemisphere = f[Field::GGA_LATITUDE_HEMISPHERE];
181 const auto longitudeHemisphere = f[Field::GGA_LONGITUDE_HEMISPHERE];
182 const bool missingLatitude = f[Field::GGA_LATITUDE].empty() &&
183 (latitudeHemisphere.empty() || latitudeHemisphere == "N" || latitudeHemisphere == "S");
184 const bool missingLongitude =
185 f[Field::GGA_LONGITUDE].empty() &&
186 (longitudeHemisphere.empty() || longitudeHemisphere == "E" || longitudeHemisphere == "W");
187 if ((!latitude && !(*quality == GgaQuality::INVALID && missingLatitude)) ||
188 (!longitude && !(*quality == GgaQuality::INVALID && missingLongitude))) {
189 return {};
190 }
191 GGA result{latitude.value_or(NAN), longitude.value_or(NAN)};
192 result.quality = *quality;
193 result.satellitesUsed = satellites;
194 if (const auto hdop = number<double>(f[Field::GGA_HDOP]); hdop && *hdop >= 0) {
195 result.hdop = *hdop;
196 }
197 if (f[Field::GGA_ALTITUDE_UNITS] == "M") {
198 result.altitude = number<double>(f[Field::GGA_ALTITUDE]).value_or(NAN);
199 }
200 if (f[Field::GGA_GEOID_UNITS] == "M") {
201 result.geoidSeparation = number<double>(f[Field::GGA_GEOID_SEPARATION]).value_or(NAN);
202 }
203 return result;
204}
205
206std::optional<int> utcMilliseconds(std::string_view field)
207{
208 if (field.size() < UTC_INTEGER_DIGITS ||
209 (field.size() > UTC_INTEGER_DIGITS &&
210 (field[UTC_INTEGER_DIGITS] != '.' || field.size() == UTC_FRACTION_OFFSET))) {
211 return {};
212 }
213 for (size_t index = 0; index < field.size(); ++index) {
214 if (index != UTC_INTEGER_DIGITS && (field[index] < '0' || field[index] > '9')) {
215 return {};
216 }
217 }
218 const auto hours = number<unsigned>(field.substr(0, UTC_COMPONENT_DIGITS));
219 const auto minutes = number<unsigned>(field.substr(UTC_COMPONENT_DIGITS, UTC_COMPONENT_DIGITS));
220 const auto seconds = number<unsigned>(field.substr(2 * UTC_COMPONENT_DIGITS, UTC_COMPONENT_DIGITS));
221 if (!hours || !minutes || !seconds || *hours >= HOURS_PER_DAY || *minutes >= MINUTES_PER_HOUR ||
222 *seconds >= SECONDS_PER_MINUTE) {
223 return {};
224 }
225 int milliseconds = 0;
226 // Truncate sub-millisecond precision without floating-point rounding into the preceding epoch.
227 for (size_t index = UTC_FRACTION_OFFSET, scale = FIRST_MILLISECOND_DIGIT_WEIGHT; index < field.size() && scale > 0;
228 ++index, scale /= DECIMAL_BASE) {
229 milliseconds += static_cast<int>((field[index] - '0') * scale);
230 }
231 const auto wholeSeconds =
232 std::chrono::hours(*hours) + std::chrono::minutes(*minutes) + std::chrono::seconds(*seconds);
233 return static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(wholeSeconds).count()) + milliseconds;
234}
235
236std::optional<UtcDate> rmcDate(std::string_view field)
237{
238 if (field.size() != 6) {
239 return {};
240 }
241 for (const char digit : field) {
242 if (digit < '0' || digit > '9') {
243 return {};
244 }
245 }
246 const auto day = number<unsigned>(field.substr(0, 2));
247 const auto month = number<unsigned>(field.substr(2, 2));
248 const auto year = number<unsigned>(field.substr(4, 2));
249 if (!day || !month || !year) {
250 return {};
251 }
252 return dateFromParts(*day, *month, 2000 + static_cast<int>(*year));
253}
254
255std::optional<RMC> rmc(const Sentence& input)
256{
257 if (input.type() != "RMC" || input.count < Field::RMC_MIN_FIELDS || input.fields[Field::RMC_STATUS] != "A") {
258 return {};
259 }
260 const auto& f = input.fields;
263 if (!latitude || !longitude) {
264 return {};
265 }
266 RMC result;
267 result.latitude = *latitude;
268 result.longitude = *longitude;
270 result.speedMetersPerSecond = nonnegativeSpeed(f[Field::RMC_SPEED_KNOTS], KNOTS_TO_METERS_PER_SECOND).value_or(NAN);
271 result.courseDegrees = nonnegativeDegrees(f[Field::RMC_COURSE]).value_or(NAN);
272 return result;
273}
274
275std::optional<GLL> gll(const Sentence& input)
276{
277 if (input.type() != "GLL" || input.count < Field::GLL_MIN_FIELDS || input.fields[Field::GLL_STATUS] != "A") {
278 return {};
279 }
280 const auto& f = input.fields;
283 if (!latitude || !longitude) {
284 return {};
285 }
287}
288
289std::optional<VTG> vtg(const Sentence& input)
290{
291 if (input.type() != "VTG" || input.count < Field::VTG_MIN_FIELDS) {
292 return {};
293 }
294 VTG result;
295 result.courseDegrees = nonnegativeDegrees(input.fields[Field::VTG_TRUE_COURSE]).value_or(NAN);
296 const auto speedKmh =
297 nonnegativeSpeed(input.fields[Field::VTG_SPEED_KMH], KILOMETERS_PER_HOUR_TO_METERS_PER_SECOND);
298 const auto speedKnots = nonnegativeSpeed(input.fields[Field::VTG_SPEED_KNOTS], KNOTS_TO_METERS_PER_SECOND);
299 result.speedMetersPerSecond = speedKmh.value_or(speedKnots.value_or(NAN));
300 return result;
301}
302
303std::optional<ZDA> zda(const Sentence& input)
304{
305 if (input.type() != "ZDA" || input.count < Field::ZDA_MIN_FIELDS) {
306 return {};
307 }
308 const auto day = number<unsigned>(input.fields[Field::ZDA_DAY]);
309 const auto month = number<unsigned>(input.fields[Field::ZDA_MONTH]);
310 const auto year = number<int>(input.fields[Field::ZDA_YEAR]);
311 if (!day || !month || !year) {
312 return {};
313 }
314 const auto date = dateFromParts(*day, *month, *year);
315 if (!date) {
316 return {};
317 }
318 return ZDA{utcMilliseconds(input.fields[Field::UTC_TIME]), *date};
319}
320
321GPSFixQuality fixQuality(unsigned quality, GPSFixQuality autonomous)
322{
323 switch (quality) {
325 return GPSFixQuality::NoFix;
326 case GgaQuality::GPS:
327 return autonomous;
329 return GPSFixQuality::Differential;
331 return GPSFixQuality::RTKFixed;
333 return GPSFixQuality::RTKFloat;
335 return GPSFixQuality::Extrapolated;
337 return GPSFixQuality::Fix3D;
338 default:
339 return GPSFixQuality::Unknown;
340 }
341}
342
343std::optional<NavigationStatus> navigationStatus(const Sentence& input)
344{
345 const auto type = input.type();
346 const auto& fields = input.fields;
347 if (type == "GGA" && input.count > Field::GGA_QUALITY) {
348 const auto quality = number<unsigned>(fields[Field::GGA_QUALITY]);
349 if (quality && *quality <= GgaQuality::MAX_VALUE) {
351 }
352 } else if (type == "GSA" && input.count > Field::GSA_DIMENSION) {
353 const auto dimension = number<unsigned>(fields[Field::GSA_DIMENSION]);
354 if (dimension && *dimension >= FixDimension::NO_FIX && *dimension <= FixDimension::THREE_D) {
355 return NavigationStatus{*dimension != FixDimension::NO_FIX, std::nullopt};
356 }
357 } else if (type == "RMC" || type == "GLL") {
358 const auto status = type == "RMC" ? Field::RMC_STATUS : Field::GLL_STATUS;
359 const auto time = type == "RMC" ? Field::UTC_TIME : Field::GLL_TIME;
360 if (input.count > status && (fields[status] == "A" || fields[status] == "V")) {
361 return NavigationStatus{fields[status] == "A", utcMilliseconds(fields[time])};
362 }
363 }
364 return std::nullopt;
365}
366
367std::optional<GST> gst(const Sentence& input)
368{
369 // Receivers may omit the trailing altitude error or append proprietary fields.
370 if (input.type() != "GST" || input.count <= Field::GST_LONGITUDE_ERROR) {
371 return {};
372 }
373 const auto latitude = number<double>(input.fields[Field::GST_LATITUDE_ERROR]);
374 const auto longitude = number<double>(input.fields[Field::GST_LONGITUDE_ERROR]);
375 GST result;
376 if (latitude && longitude && *latitude >= 0 && *longitude >= 0) {
377 result.horizontalAccuracy = std::hypot(*latitude, *longitude);
378 }
379 if (input.count > Field::GST_ALTITUDE_ERROR) {
380 const auto altitude = number<double>(input.fields[Field::GST_ALTITUDE_ERROR]);
381 if (altitude && *altitude >= 0) {
382 result.verticalAccuracy = *altitude;
383 }
384 }
385 return result;
386}
387} // namespace NMEA
388
389namespace NMEAUtils {
390QByteArray frame(QByteArrayView body)
391{
392 const char checksum = static_cast<char>(NMEA::checksum(body));
393 return '$' + body.toByteArray() + '*' + QByteArray(1, checksum).toHex().toUpper() + "\r\n";
394}
395
396QByteArray repairChecksum(const QByteArray& sentence)
397{
398 const auto wire = NMEA::frame(QByteArrayView(sentence));
399 if (wire && wire->body.size() + NMEA::Sentence::PREFIX_LENGTH >= MIN_REPAIRABLE_SENTENCE_LENGTH) {
400 return frame(QByteArrayView(wire->body));
401 }
402 QByteArray line = sentence;
403 if (line.endsWith('\n')) {
404 line.chop(1);
405 if (line.endsWith('\r')) {
406 line.chop(1);
407 }
408 }
409 return line + "\r\n";
410}
411
412QByteArray makeGGA(const NMEA::GGA& fix, const QTime& utc)
413{
414 if (!utc.isValid() || !std::isfinite(fix.latitude) || fix.latitude < -90.0 || fix.latitude > 90.0 ||
415 !std::isfinite(fix.longitude) || fix.longitude < -180.0 || fix.longitude > 180.0 || std::isinf(fix.altitude) ||
416 std::isinf(fix.geoidSeparation) || std::isinf(fix.hdop) || fix.hdop < 0.0 ||
418 return {};
419 }
420 const QByteArray hhmmss = utc.toString(u"hhmmss").toLatin1();
421
422 auto dmm = [](double deg, bool lat) -> QByteArray {
423 const double a = qFabs(deg);
424 int d = static_cast<int>(a);
425 double m = (a - d) * NMEA::MINUTES_PER_DEGREE;
426
427 const int scaledMinutes = static_cast<int>(m * MINUTE_FRACTION_SCALE + 0.5);
428 double mRounded = scaledMinutes / MINUTE_FRACTION_SCALE;
429 if (mRounded >= NMEA::MINUTES_PER_DEGREE) {
430 mRounded -= NMEA::MINUTES_PER_DEGREE;
431 d += 1;
432 }
433
434 QByteArray mm = QByteArray::number(mRounded, 'f', MINUTE_DECIMAL_PLACES);
435 if (mRounded < NMEA::DECIMAL_BASE) {
436 mm.prepend('0');
437 }
438
439 const int dWidth = lat ? LATITUDE_DEGREE_DIGITS : LONGITUDE_DEGREE_DIGITS;
440 return QByteArray::number(d).rightJustified(dWidth, '0') + mm;
441 };
442
443 const bool latNorth = fix.latitude >= 0.0;
444 const bool lonEast = fix.longitude >= 0.0;
445
446 const QByteArray latField = dmm(fix.latitude, true);
447 const QByteArray lonField = dmm(fix.longitude, false);
448 const auto measurement = [](double value, int decimalPlaces) {
449 return std::isnan(value) ? QByteArray() : QByteArray::number(value, 'f', decimalPlaces);
450 };
451
452 QByteArray core;
453 core += "GPGGA,";
454 core += hhmmss + ',';
455 core += latField + ',';
456 core += (latNorth ? "N" : "S");
457 core += ',';
458 core += lonField + ',';
459 core += (lonEast ? "E" : "W");
460 core += ',' + QByteArray::number(fix.quality) + ',';
461 if (fix.satellitesUsed) {
462 core += QByteArray::number(*fix.satellitesUsed);
463 }
464 core += ',' + measurement(fix.hdop, DOP_DECIMAL_PLACES) + ',';
465 core += measurement(fix.altitude, ALTITUDE_DECIMAL_PLACES);
466 core += ",M," + measurement(fix.geoidSeparation, ALTITUDE_DECIMAL_PLACES) + ",M,,";
467 return frame(core);
468}
469
470} // namespace NMEAUtils
int32_t latitude
Definition UBXPlan.cc:40
int32_t longitude
Definition UBXPlan.cc:42
QByteArray repairChecksum(const QByteArray &sentence)
Rebuild a valid frame with its checksum and CRLF; short or malformed bodies are only terminated.
QByteArray makeGGA(const NMEA::GGA &fix, const QTime &utc)
QByteArray frame(QByteArrayView body)
body as one "$<body>*hh\r\n" sentence, with uppercase checksum digits.
constexpr size_t VTG_SPEED_KNOTS
constexpr size_t GST_LONGITUDE_ERROR
constexpr size_t GLL_STATUS
constexpr size_t GSA_DIMENSION
constexpr size_t ZDA_DAY
constexpr size_t GGA_QUALITY
constexpr size_t RMC_MIN_FIELDS
constexpr size_t RMC_SPEED_KNOTS
constexpr size_t GLL_LONGITUDE_HEMISPHERE
constexpr size_t GGA_GEOID_UNITS
constexpr size_t GGA_LATITUDE_HEMISPHERE
constexpr size_t RMC_LONGITUDE
constexpr size_t VTG_MIN_FIELDS
constexpr size_t RMC_LONGITUDE_HEMISPHERE
constexpr size_t GGA_LONGITUDE_HEMISPHERE
constexpr size_t GGA_HDOP
constexpr size_t ZDA_MONTH
constexpr size_t ZDA_MIN_FIELDS
constexpr size_t GGA_ALTITUDE_UNITS
constexpr size_t RMC_COURSE
constexpr size_t GLL_MIN_FIELDS
constexpr size_t GLL_TIME
constexpr size_t GLL_LATITUDE_HEMISPHERE
constexpr size_t GGA_SATELLITES_USED
constexpr size_t VTG_TRUE_COURSE
constexpr size_t GST_ALTITUDE_ERROR
constexpr size_t GGA_ALTITUDE
constexpr size_t ZDA_YEAR
constexpr size_t GGA_LONGITUDE
constexpr size_t RMC_STATUS
constexpr size_t RMC_LATITUDE
constexpr size_t GLL_LONGITUDE
constexpr size_t GST_LATITUDE_ERROR
constexpr size_t VTG_SPEED_KMH
constexpr size_t GGA_MIN_FIELDS
constexpr size_t RMC_LATITUDE_HEMISPHERE
constexpr size_t GLL_LATITUDE
constexpr size_t GGA_LATITUDE
constexpr size_t UTC_TIME
constexpr size_t GGA_GEOID_SEPARATION
constexpr unsigned THREE_D
constexpr unsigned NO_FIX
constexpr unsigned MAX_VALUE
constexpr unsigned RTK_FLOAT
constexpr unsigned DIFFERENTIAL
constexpr unsigned MANUAL
Manual input: the position a base station was fixed at.
constexpr unsigned ESTIMATED
constexpr unsigned RTK_FIXED
constexpr unsigned GPS
constexpr unsigned INVALID
std::optional< ZDA > zda(const Sentence &input)
std::optional< Sentence > sentence(std::string_view text)
constexpr int HEX_BASE
std::optional< double > coordinate(std::string_view field, std::string_view hemisphere, bool latitude)
std::optional< UtcDate > rmcDate(std::string_view field)
GPSFixQuality fixQuality(unsigned quality, GPSFixQuality autonomous)
Autonomous GGA does not distinguish 2D/3D; the caller supplies its independently resolved quality.
constexpr int DECIMAL_BASE
std::optional< int > utcMilliseconds(std::string_view field)
std::optional< RMC > rmc(const Sentence &input)
std::optional< GLL > gll(const Sentence &input)
double degreesFromDegreesMinutes(double ddmm)
Convert a signed ddmm.mmmm (or dddmm.mmmm) value; invalid values return NaN.
std::optional< uint32_t > hexNumber(std::string_view digits)
The value of digits when every one is a hexadecimal digit, with no sign or prefix; nullopt otherwise.
constexpr int CHECKSUM_DIGITS
unsigned char checksum(std::string_view body)
constexpr double MINUTES_PER_DEGREE
std::optional< GGA > gga(const Sentence &input)
Quality zero is a valid fix-loss observation with empty coordinates and optional hemisphere fields.
std::optional< VTG > vtg(const Sentence &input)
std::optional< NavigationStatus > navigationStatus(const Sentence &input)
std::optional< GST > gst(const Sentence &input)
bool isStandardSentence(std::string_view text)
A checked sentence from a standard talker, not a proprietary $P... sentence.
size_t splitFields(std::string_view text, std::span< std::string_view > fields)
Preserve empty fields, including the final field. Return zero on storage overflow.
std::optional< Frame > frame(std::string_view text)
constexpr uint8_t nmeaChecksum(std::span< const uint8_t > body)
XOR of all bytes, the checksum NMEA 0183 sentences carry over their body.
Definition Checksums.h:56
Views into a single wire frame; the caller owns the backing bytes.
bool hasValidChecksum() const
std::string_view checksum
std::string_view body
double altitude
double hdop
unsigned quality
double geoidSeparation
double latitude
std::optional< unsigned > satellitesUsed
double longitude
double horizontalAccuracy
double verticalAccuracy
double latitude
double courseDegrees
double longitude
double speedMetersPerSecond
std::optional< int > utcMilliseconds
std::string_view type() const
static constexpr int PREFIX_LENGTH
std::array< std::string_view, MAX_FIELDS > fields
static constexpr int HEADER_LENGTH
double speedMetersPerSecond
double courseDegrees