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;
25std::optional<NMEA::UtcDate> dateFromParts(
unsigned day,
unsigned month,
int year)
27 if (!std::chrono::year_month_day{std::chrono::year{year}, std::chrono::month{month}, std::chrono::day{day}}.ok()) {
33std::optional<double> nonnegativeDegrees(std::string_view field)
35 const auto value = NMEA::number<double>(field);
36 return value && *value >= 0.0 && *value <= 360.0 ? value : std::nullopt;
39std::optional<double> nonnegativeSpeed(std::string_view field,
double scale)
41 const auto value = NMEA::number<double>(field);
42 return value && *value >= 0.0 ? std::optional<double>(*value * scale) : std::nullopt;
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;
57 return QGC::nmeaChecksum({
reinterpret_cast<const uint8_t*
>(body.data()), body.size()});
60std::optional<uint32_t>
hexNumber(std::string_view digits)
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()) {
70size_t splitFields(std::string_view text, std::span<std::string_view> fields)
74 if (count == fields.size()) {
77 const auto comma = text.find(
',');
78 fields[count++] = text.substr(0, comma);
79 if (comma == std::string_view::npos) {
82 text.remove_prefix(comma + 1);
91std::optional<Frame>
frame(std::string_view text)
93 if (text.ends_with(
'\n')) {
94 text.remove_suffix(1);
95 if (text.ends_with(
'\r')) {
96 text.remove_suffix(1);
99 if (text.empty() || text.front() !=
'$') {
103 const auto star = text.find(
'*');
104 const auto body = text.substr(0, star);
108 for (
char byte : body) {
109 if (
byte <
' ' ||
byte >
'~' ||
byte ==
'$') {
113 return Frame{body, star == std::string_view::npos ? std::string_view() : text.substr(star + 1)};
118 const auto checked =
sentence(text);
119 return checked && !checked->talker().starts_with(
'P');
122std::optional<Sentence>
sentence(std::string_view text)
124 const auto wire =
frame(text);
125 if (!wire || !wire->hasValidChecksum()) {
139 if (!std::isfinite(ddmm) || std::abs(ddmm) > MAX_LONGITUDE_DEGREES * DEGREE_MINUTE_SCALE) {
142 const double degrees = std::trunc(ddmm / DEGREE_MINUTE_SCALE);
143 const double minutes = ddmm - degrees * DEGREE_MINUTE_SCALE;
149 const auto value = number<double>(field);
150 if (!value || *value < 0 || hemisphere.size() != 1) {
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;
159 const char positive =
latitude ?
'N' :
'E';
160 const char negative =
latitude ?
'S' :
'W';
161 if (hemisphere[0] != positive && hemisphere[0] != negative) {
164 return (degrees + minutes /
MINUTES_PER_DEGREE) * (hemisphere[0] == negative ? -1 : 1);
172 const auto& f = input.
fields;
177 if (!quality || *quality >
GgaQuality::MAX_VALUE || (satellites && *satellites > MAX_GGA_SATELLITES)) {
183 (latitudeHemisphere.empty() || latitudeHemisphere ==
"N" || latitudeHemisphere ==
"S");
184 const bool missingLongitude =
186 (longitudeHemisphere.empty() || longitudeHemisphere ==
"E" || longitudeHemisphere ==
"W");
193 result.satellitesUsed = satellites;
194 if (
const auto hdop = number<double>(f[
Field::GGA_HDOP]); hdop && *hdop >= 0) {
208 if (field.size() < UTC_INTEGER_DIGITS ||
209 (field.size() > UTC_INTEGER_DIGITS &&
210 (field[UTC_INTEGER_DIGITS] !=
'.' || field.size() == UTC_FRACTION_OFFSET))) {
213 for (
size_t index = 0; index < field.size(); ++index) {
214 if (index != UTC_INTEGER_DIGITS && (field[index] <
'0' || field[index] >
'9')) {
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) {
225 int milliseconds = 0;
227 for (
size_t index = UTC_FRACTION_OFFSET, scale = FIRST_MILLISECOND_DIGIT_WEIGHT; index < field.size() && scale > 0;
229 milliseconds +=
static_cast<int>((field[index] -
'0') * scale);
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;
236std::optional<UtcDate>
rmcDate(std::string_view field)
238 if (field.size() != 6) {
241 for (
const char digit : field) {
242 if (digit <
'0' || digit >
'9') {
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) {
252 return dateFromParts(*day, *month, 2000 +
static_cast<int>(*year));
260 const auto& f = input.
fields;
280 const auto& f = input.
fields;
296 const auto speedKmh =
311 if (!day || !month || !year) {
314 const auto date = dateFromParts(*day, *month, *year);
325 return GPSFixQuality::NoFix;
329 return GPSFixQuality::Differential;
331 return GPSFixQuality::RTKFixed;
333 return GPSFixQuality::RTKFloat;
335 return GPSFixQuality::Extrapolated;
337 return GPSFixQuality::Fix3D;
339 return GPSFixQuality::Unknown;
345 const auto type = input.
type();
346 const auto& fields = input.
fields;
357 }
else if (type ==
"RMC" || type ==
"GLL") {
360 if (input.
count > status && (fields[status] ==
"A" || fields[status] ==
"V")) {
381 if (altitude && *altitude >= 0) {
390QByteArray
frame(QByteArrayView body)
393 return '$' + body.toByteArray() +
'*' + QByteArray(1, checksum).toHex().toUpper() +
"\r\n";
398 const auto wire =
NMEA::frame(QByteArrayView(sentence));
400 return frame(QByteArrayView(wire->body));
402 QByteArray line = sentence;
403 if (line.endsWith(
'\n')) {
405 if (line.endsWith(
'\r')) {
409 return line +
"\r\n";
420 const QByteArray hhmmss = utc.toString(u
"hhmmss").toLatin1();
422 auto dmm = [](
double deg,
bool lat) -> QByteArray {
423 const double a = qFabs(deg);
424 int d =
static_cast<int>(a);
427 const int scaledMinutes =
static_cast<int>(m * MINUTE_FRACTION_SCALE + 0.5);
428 double mRounded = scaledMinutes / MINUTE_FRACTION_SCALE;
434 QByteArray mm = QByteArray::number(mRounded,
'f', MINUTE_DECIMAL_PLACES);
439 const int dWidth = lat ? LATITUDE_DEGREE_DIGITS : LONGITUDE_DEGREE_DIGITS;
440 return QByteArray::number(d).rightJustified(dWidth,
'0') + mm;
443 const bool latNorth = fix.
latitude >= 0.0;
444 const bool lonEast = fix.
longitude >= 0.0;
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);
454 core += hhmmss +
',';
455 core += latField +
',';
456 core += (latNorth ?
"N" :
"S");
458 core += lonField +
',';
459 core += (lonEast ?
"E" :
"W");
460 core +=
',' + QByteArray::number(fix.
quality) +
',';
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,,";
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 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 INVALID
std::optional< ZDA > zda(const Sentence &input)
std::optional< Sentence > sentence(std::string_view text)
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.
Views into a single wire frame; the caller owns the backing bytes.
bool hasValidChecksum() const
std::string_view checksum
std::optional< unsigned > satellitesUsed
double horizontalAccuracy
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