Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qlocationutils.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 Jolla Ltd.
2// Copyright (C) 2016 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:critical reason:data-parser
5
9
10#include <QTime>
11#include <QList>
12#include <QByteArray>
13#include <QDateTime>
14#include <QDebug>
15#include <QTimeZone>
16
17#include <math.h>
18
19QT_BEGIN_NAMESPACE
20
21// converts e.g. 15306.0235 from NMEA sentence to 153.100392
22static double qlocationutils_nmeaDegreesToDecimal(double nmeaDegrees)
23{
24 double deg;
25 double min = 100.0 * modf(nmeaDegrees / 100.0, &deg);
26 return deg + (min / 60.0);
27}
28
29static void qlocationutils_readGga(QByteArrayView bv, QGeoPositionInfo *info, double uere,
30 bool *hasFix)
31{
32 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(), bv.size()).split(',');
33 QGeoCoordinate coord;
34
35 if (hasFix && parts.size() > 6 && !parts[6].isEmpty())
36 *hasFix = parts[6].toInt() > 0;
37
38 if (parts.size() > 1 && !parts[1].isEmpty()) {
39 QTime time;
40 if (QLocationUtils::getNmeaTime(parts[1], &time))
41 info->setTimestamp(QDateTime(QDate(), time, QTimeZone::UTC));
42 }
43
44 if (parts.size() > 5 && parts[3].size() == 1 && parts[5].size() == 1) {
45 double lat;
46 double lng;
47 if (QLocationUtils::getNmeaLatLong(parts[2], parts[3][0], parts[4], parts[5][0], &lat, &lng)) {
48 coord.setLatitude(lat);
49 coord.setLongitude(lng);
50 }
51 }
52
53 if (parts.size() > 8 && !parts[8].isEmpty()) {
54 bool hasHdop = false;
55 double hdop = parts[8].toDouble(&hasHdop);
56 if (hasHdop)
57 info->setAttribute(QGeoPositionInfo::HorizontalAccuracy, 2 * hdop * uere);
58 }
59
60 if (parts.size() > 9 && !parts[9].isEmpty()) {
61 bool hasAlt = false;
62 double alt = parts[9].toDouble(&hasAlt);
63 if (hasAlt)
64 coord.setAltitude(alt);
65 }
66
67 if (coord.type() != QGeoCoordinate::InvalidCoordinate)
68 info->setCoordinate(coord);
69}
70
71static void qlocationutils_readGsa(QByteArrayView bv, QGeoPositionInfo *info, double uere,
72 bool *hasFix)
73{
74 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(), bv.size()).split(',');
75
76 // The GSA sentence is different from GGA:
77 // 1 - no fix;
78 // 2 - 2D fix;
79 // 3 - 3D fix.
80 if (hasFix && parts.size() > 2 && !parts[2].isEmpty())
81 *hasFix = parts[2].toInt() > 1;
82
83 if (parts.size() > 16 && !parts[16].isEmpty()) {
84 bool hasHdop = false;
85 double hdop = parts[16].toDouble(&hasHdop);
86 if (hasHdop)
87 info->setAttribute(QGeoPositionInfo::HorizontalAccuracy, 2 * hdop * uere);
88 }
89
90 if (parts.size() > 17 && !parts[17].isEmpty()) {
91 bool hasVdop = false;
92 double vdop = parts[17].toDouble(&hasVdop);
93 if (hasVdop)
94 info->setAttribute(QGeoPositionInfo::VerticalAccuracy, 2 * vdop * uere);
95 }
96}
97
98static void qlocationutils_readGsa(QByteArrayView bv, QList<int> &pnrsInUse)
99{
100 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(), bv.size()).split(',');
101 pnrsInUse.clear();
102 if (parts.size() <= 2)
103 return;
104 bool ok;
105 for (qsizetype i = 3; i < qMin(15, parts.size()); ++i) {
106 const QByteArray &pnrString = parts.at(i);
107 if (pnrString.isEmpty())
108 continue;
109 int pnr = pnrString.toInt(&ok);
110 if (ok)
111 pnrsInUse.append(pnr);
112 }
113}
114
115static void qlocationutils_readGll(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
116{
117 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(), bv.size()).split(',');
118 QGeoCoordinate coord;
119
120 if (hasFix && parts.size() > 6 && !parts[6].isEmpty())
121 *hasFix = (parts[6][0] == 'A');
122
123 if (parts.size() > 5 && !parts[5].isEmpty()) {
124 QTime time;
125 if (QLocationUtils::getNmeaTime(parts[5], &time))
126 info->setTimestamp(QDateTime(QDate(), time, QTimeZone::UTC));
127 }
128
129 if (parts.size() > 4 && parts[2].size() == 1 && parts[4].size() == 1) {
130 double lat;
131 double lng;
132 if (QLocationUtils::getNmeaLatLong(parts[1], parts[2][0], parts[3], parts[4][0], &lat, &lng)) {
133 coord.setLatitude(lat);
134 coord.setLongitude(lng);
135 }
136 }
137
138 if (coord.type() != QGeoCoordinate::InvalidCoordinate)
139 info->setCoordinate(coord);
140}
141
142static void qlocationutils_readRmc(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
143{
144 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(), bv.size()).split(',');
145 QGeoCoordinate coord;
146 QDate date;
147 QTime time;
148
149 if (hasFix && parts.size() > 2 && !parts[2].isEmpty())
150 *hasFix = (parts[2][0] == 'A');
151
152 if (parts.size() > 9 && parts[9].size() == 6) {
153 date = QDate::fromString(QString::fromLatin1(parts[9]), QStringLiteral("ddMMyy"));
154 if (date.isValid())
155 date = date.addYears(100); // otherwise starts from 1900
156 }
157
158 if (parts.size() > 1 && !parts[1].isEmpty())
159 QLocationUtils::getNmeaTime(parts[1], &time);
160
161 if (parts.size() > 6 && parts[4].size() == 1 && parts[6].size() == 1) {
162 double lat;
163 double lng;
164 if (QLocationUtils::getNmeaLatLong(parts[3], parts[4][0], parts[5], parts[6][0], &lat, &lng)) {
165 coord.setLatitude(lat);
166 coord.setLongitude(lng);
167 }
168 }
169
170 bool parsed = false;
171 double value = 0.0;
172 if (parts.size() > 7 && !parts[7].isEmpty()) {
173 value = parts[7].toDouble(&parsed);
174 if (parsed)
175 info->setAttribute(QGeoPositionInfo::GroundSpeed, qreal(value * 1.852 / 3.6)); // knots -> m/s
176 }
177 if (parts.size() > 8 && !parts[8].isEmpty()) {
178 value = parts[8].toDouble(&parsed);
179 if (parsed)
180 info->setAttribute(QGeoPositionInfo::Direction, qreal(value));
181 }
182 if (parts.size() > 11 && parts[11].size() == 1
183 && (parts[11][0] == 'E' || parts[11][0] == 'W')) {
184 value = parts[10].toDouble(&parsed);
185 if (parsed) {
186 if (parts[11][0] == 'W')
187 value *= -1;
188 info->setAttribute(QGeoPositionInfo::MagneticVariation, qreal(value));
189 }
190 }
191
192 if (coord.type() != QGeoCoordinate::InvalidCoordinate)
193 info->setCoordinate(coord);
194
195 info->setTimestamp(QDateTime(date, time, QTimeZone::UTC));
196}
197
198static void qlocationutils_readVtg(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
199{
200 if (hasFix)
201 *hasFix = false;
202
203 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(), bv.size()).split(',');
204
205 bool parsed = false;
206 double value = 0.0;
207 if (parts.size() > 1 && !parts[1].isEmpty()) {
208 value = parts[1].toDouble(&parsed);
209 if (parsed)
210 info->setAttribute(QGeoPositionInfo::Direction, qreal(value));
211 }
212 if (parts.size() > 7 && !parts[7].isEmpty()) {
213 value = parts[7].toDouble(&parsed);
214 if (parsed)
215 info->setAttribute(QGeoPositionInfo::GroundSpeed, qreal(value / 3.6)); // km/h -> m/s
216 }
217}
218
219static void qlocationutils_readZda(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
220{
221 if (hasFix)
222 *hasFix = false;
223
224 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(), bv.size()).split(',');
225 QDate date;
226 QTime time;
227
228 if (parts.size() > 1 && !parts[1].isEmpty())
229 QLocationUtils::getNmeaTime(parts[1], &time);
230
231 if (parts.size() > 4 && !parts[2].isEmpty() && !parts[3].isEmpty()
232 && parts[4].size() == 4) { // must be full 4-digit year
233 int day = parts[2].toInt();
234 int month = parts[3].toInt();
235 int year = parts[4].toInt();
236 if (day > 0 && month > 0 && year > 0)
237 date.setDate(year, month, day);
238 }
239
240 info->setTimestamp(QDateTime(date, time, QTimeZone::UTC));
241}
242
243QLocationUtils::NmeaSentence QLocationUtils::getNmeaSentenceType(QByteArrayView bv)
244{
245 if (bv.size() < 6 || bv[0] != '$' || !hasValidNmeaChecksum(bv))
246 return NmeaSentenceInvalid;
247
248 QByteArrayView key = bv.sliced(3);
249
250 if (key.startsWith("GGA"))
251 return NmeaSentenceGGA;
252
253 if (key.startsWith("GSA"))
254 return NmeaSentenceGSA;
255
256 if (key.startsWith("GSV"))
257 return NmeaSentenceGSV;
258
259 if (key.startsWith("GLL"))
260 return NmeaSentenceGLL;
261
262 if (key.startsWith("RMC"))
263 return NmeaSentenceRMC;
264
265 if (key.startsWith("VTG"))
266 return NmeaSentenceVTG;
267
268 if (key.startsWith("ZDA"))
269 return NmeaSentenceZDA;
270
271 return NmeaSentenceInvalid;
272}
273
274QGeoSatelliteInfo::SatelliteSystem QLocationUtils::getSatelliteSystem(QByteArrayView bv)
275{
276 if (bv.size() < 6 || bv[0] != '$' || !hasValidNmeaChecksum(bv))
277 return QGeoSatelliteInfo::Undefined;
278
279 QByteArrayView key = bv.sliced(1);
280
281 // GPS: GP
282 if (key.startsWith("GP"))
283 return QGeoSatelliteInfo::GPS;
284
285 // GLONASS: GL
286 if (key.startsWith("GL"))
287 return QGeoSatelliteInfo::GLONASS;
288
289 // GALILEO: GA
290 if (key.startsWith("GA"))
291 return QGeoSatelliteInfo::GALILEO;
292
293 // BeiDou: BD or GB
294 if (key.startsWith("BD") || key.startsWith("GB"))
295 return QGeoSatelliteInfo::BEIDOU;
296
297 // QZSS: GQ, PQ, QZ
298 if (key.startsWith("GQ") || key.startsWith("PQ") || key.startsWith("QZ"))
299 return QGeoSatelliteInfo::QZSS;
300
301 // IRNSS: GI
302 if (key.startsWith("GI"))
303 return QGeoSatelliteInfo::IRNSS;
304
305 // SBAS: no talker id, should use satellite id range
306
307 // Multiple: GN
308 if (key.startsWith("GN"))
309 return QGeoSatelliteInfo::Multiple;
310
311 return QGeoSatelliteInfo::Undefined;
312}
313
314QGeoSatelliteInfo::SatelliteSystem QLocationUtils::getSatelliteSystemBySatelliteId(int satId)
315{
316 if (satId >= 1 && satId <= 32)
317 return QGeoSatelliteInfo::GPS;
318
319 if (satId >= 33 && satId <= 64)
320 return QGeoSatelliteInfo::SBAS;
321
322 if (satId >= 65 && satId <= 96) // including future extensions
323 return QGeoSatelliteInfo::GLONASS;
324
325 if (satId >= 193 && satId <= 200) // including future extensions
326 return QGeoSatelliteInfo::QZSS;
327
328 if ((satId >= 201 && satId <= 235) || (satId >= 401 && satId <= 437))
329 return QGeoSatelliteInfo::BEIDOU;
330
331 if (satId >= 301 && satId <= 336)
332 return QGeoSatelliteInfo::GALILEO;
333
334 return QGeoSatelliteInfo::Undefined;
335}
336
337bool QLocationUtils::getPosInfoFromNmea(QByteArrayView bv, QGeoPositionInfo *info,
338 double uere, bool *hasFix)
339{
340 if (!info)
341 return false;
342
343 if (hasFix)
344 *hasFix = false;
345
346 NmeaSentence nmeaType = getNmeaSentenceType(bv);
347 if (nmeaType == NmeaSentenceInvalid)
348 return false;
349
350 // Adjust size so that * and following characters are not parsed by the following functions.
351 qsizetype idx = bv.indexOf('*');
352 QByteArrayView key = idx < 0 ? bv : bv.first(idx);
353
354 switch (nmeaType) {
355 case NmeaSentenceGGA:
356 qlocationutils_readGga(key, info, uere, hasFix);
357 return true;
358 case NmeaSentenceGSA:
359 qlocationutils_readGsa(key, info, uere, hasFix);
360 return true;
361 case NmeaSentenceGLL:
362 qlocationutils_readGll(key, info, hasFix);
363 return true;
364 case NmeaSentenceRMC:
365 qlocationutils_readRmc(key, info, hasFix);
366 return true;
367 case NmeaSentenceVTG:
368 qlocationutils_readVtg(key, info, hasFix);
369 return true;
370 case NmeaSentenceZDA:
371 qlocationutils_readZda(key, info, hasFix);
372 return true;
373 default:
374 return false;
375 }
376}
377
378QNmeaSatelliteInfoSource::SatelliteInfoParseStatus
379QLocationUtils::getSatInfoFromNmea(QByteArrayView bv, QList<QGeoSatelliteInfo> &infos, QGeoSatelliteInfo::SatelliteSystem &system)
380{
381 if (bv.isEmpty())
382 return QNmeaSatelliteInfoSource::NotParsed;
383
384 NmeaSentence nmeaType = getNmeaSentenceType(bv);
385 if (nmeaType != NmeaSentenceGSV)
386 return QNmeaSatelliteInfoSource::NotParsed;
387
388 // Standard forbids using $GN talker id for GSV messages, so the system
389 // type here will be uniquely identified.
390 system = getSatelliteSystem(bv);
391
392 // Adjust size so that * and following characters are not parsed by the
393 // following code.
394 qsizetype idx = bv.indexOf('*');
395
396 const QList<QByteArray> parts = QByteArray::fromRawData(bv.data(),
397 idx < 0 ? bv.size() : idx).split(',');
398
399 if (parts.size() <= 3) {
400 infos.clear();
401 return QNmeaSatelliteInfoSource::FullyParsed; // Malformed sentence.
402 }
403
404 // The hard max limit for the number of sentences in one message is used
405 // to prevent malicious messages. The protocol does not seem to define any
406 // maximum, but this value provides an upper limit that is more than enough
407 // in practice.
408 static constexpr quint32 MaxTotalSentences = 250;
409 bool ok;
410 const quint32 totalSentences = parts.at(1).toUInt(&ok);
411 if (!ok || totalSentences > MaxTotalSentences) {
412 infos.clear();
413 return QNmeaSatelliteInfoSource::FullyParsed; // Malformed sentence.
414 }
415
416 const quint32 sentence = parts.at(2).toUInt(&ok);
417 if (!ok || sentence > totalSentences) {
418 infos.clear();
419 return QNmeaSatelliteInfoSource::FullyParsed; // Malformed sentence.
420 }
421
422 const quint32 totalSats = parts.at(3).toUInt(&ok);
423 if (!ok) {
424 infos.clear();
425 return QNmeaSatelliteInfoSource::FullyParsed; // Malformed sentence.
426 }
427
428 if (sentence == 1)
429 infos.clear();
430 else if (infos.isEmpty()) // sentences out of order or DDoS spotted earlier
431 return QNmeaSatelliteInfoSource::FullyParsed; // Malformed sentence.
432
433 static constexpr qsizetype MaxTotalSatellites = 1000;
434 const auto currentSatsSize = infos.size();
435 if (currentSatsSize > qint64(totalSats) || currentSatsSize > MaxTotalSatellites) {
436 // Maybe DDoS: otherwise, probably something is wrong; reject, either way.
437 infos.clear();
438 return QNmeaSatelliteInfoSource::FullyParsed; // Malformed sentence.
439 }
440
441 const qint64 numSatInSentence = qMin(sentence * 4, totalSats) - (sentence - 1) * 4;
442 if (parts.size() < (4 + numSatInSentence * 4)) {
443 infos.clear();
444 return QNmeaSatelliteInfoSource::FullyParsed; // Malformed sentence.
445 }
446
447 int field = 4;
448 for (qint64 i = 0; i < numSatInSentence; ++i) {
449 QGeoSatelliteInfo info;
450 info.setSatelliteSystem(system);
451 int prn = parts.at(field++).toInt(&ok);
452 // Quote from: https://gpsd.gitlab.io/gpsd/NMEA.html#_satellite_ids
453 // GLONASS satellite numbers come in two flavors. If a sentence has a GL
454 // talker ID, expect the skyviews to be GLONASS-only and in the range
455 // 1-32; you must add 64 to get a globally-unique NMEA ID. If the
456 // sentence has a GN talker ID, the device emits a multi-constellation
457 // skyview with GLONASS IDs already in the 65-96 range.
458 //
459 // However I don't observe such behavior with my device. So implementing
460 // a safe scenario.
461 if (ok && (system == QGeoSatelliteInfo::GLONASS)) {
462 if (prn <= 64)
463 prn += 64;
464 }
465
466 // If the system is unknown, try to determine it based on prn.
467 // Note that the new value can vary among the satellites, so *do not*
468 // update the out-variable 'system'.
469 if (ok && system == QGeoSatelliteInfo::Undefined)
470 info.setSatelliteSystem(getSatelliteSystemBySatelliteId(prn));
471
472 info.setSatelliteIdentifier((ok) ? prn : 0);
473 const int elevation = parts.at(field++).toInt(&ok);
474 info.setAttribute(QGeoSatelliteInfo::Elevation, (ok) ? elevation : 0);
475 const int azimuth = parts.at(field++).toInt(&ok);
476 info.setAttribute(QGeoSatelliteInfo::Azimuth, (ok) ? azimuth : 0);
477 const int snr = parts.at(field++).toInt(&ok);
478 info.setSignalStrength((ok) ? snr : -1);
479 infos.append(info);
480 }
481
482 if (sentence == totalSentences)
483 return QNmeaSatelliteInfoSource::FullyParsed;
484
485 return QNmeaSatelliteInfoSource::PartiallyParsed;
486}
487
488QGeoSatelliteInfo::SatelliteSystem QLocationUtils::getSatInUseFromNmea(QByteArrayView bv,
489 QList<int> &pnrsInUse)
490{
491 if (bv.isEmpty())
492 return QGeoSatelliteInfo::Undefined;
493
494 NmeaSentence nmeaType = getNmeaSentenceType(bv);
495 if (nmeaType != NmeaSentenceGSA)
496 return QGeoSatelliteInfo::Undefined;
497
498 auto systemType = getSatelliteSystem(bv);
499 if (systemType == QGeoSatelliteInfo::Undefined)
500 return systemType;
501
502 // The documentation states that we do not modify pnrsInUse if we could not
503 // parse the data
504 pnrsInUse.clear();
505
506 // Adjust size so that * and following characters are not parsed by the following functions.
507 qsizetype idx = bv.indexOf('*');
508 QByteArrayView key = idx < 0 ? bv : bv.first(idx);
509
510 qlocationutils_readGsa(key, pnrsInUse);
511
512 // Quote from: https://gpsd.gitlab.io/gpsd/NMEA.html#_satellite_ids
513 // GLONASS satellite numbers come in two flavors. If a sentence has a GL
514 // talker ID, expect the skyviews to be GLONASS-only and in the range 1-32;
515 // you must add 64 to get a globally-unique NMEA ID. If the sentence has a
516 // GN talker ID, the device emits a multi-constellation skyview with
517 // GLONASS IDs already in the 65-96 range.
518 //
519 // However I don't observe such behavior with my device. So implementing a
520 // safe scenario.
521 if (systemType == QGeoSatelliteInfo::GLONASS) {
522 std::for_each(pnrsInUse.begin(), pnrsInUse.end(), [](int &id) {
523 if (id <= 64)
524 id += 64;
525 });
526 }
527
528 if ((systemType == QGeoSatelliteInfo::Multiple) && !pnrsInUse.isEmpty()) {
529 // Standard claims that in case of multiple system types we will receive
530 // several GSA messages, each containing data from only one satellite
531 // system, so we can pick the first id to determine the system type.
532 auto tempSystemType = getSatelliteSystemBySatelliteId(pnrsInUse.front());
533 if (tempSystemType != QGeoSatelliteInfo::Undefined)
534 systemType = tempSystemType;
535 }
536
537 return systemType;
538}
539
540bool QLocationUtils::hasValidNmeaChecksum(QByteArrayView bv)
541{
542 qsizetype asteriskIndex = bv.indexOf('*');
543
544 constexpr qsizetype CSUM_LEN = 2;
545 if (asteriskIndex < 0 || asteriskIndex >= bv.size() - CSUM_LEN)
546 return false;
547
548 // XOR byte value of all characters between '$' and '*'
549 int result = 0;
550 for (qsizetype i = 1; i < asteriskIndex; ++i)
551 result ^= bv[i];
552 /*
553 char calc[CSUM_LEN + 1];
554 ::snprintf(calc, CSUM_LEN + 1, "%02x", result);
555 return ::strncmp(calc, &data[asteriskIndex+1], 2) == 0;
556 */
557
558 QByteArrayView checkSumBytes = bv.sliced(asteriskIndex + 1, 2);
559 bool ok = false;
560 int checksum = checkSumBytes.toInt(&ok,16);
561 return ok && checksum == result;
562}
563
564bool QLocationUtils::getNmeaTime(const QByteArray &bytes, QTime *time)
565{
566 QTime tempTime = QTime::fromString(QString::fromLatin1(bytes),
567 QStringView(bytes.size() > 6 && bytes[6] == '.'
568 ? u"hhmmss.z"
569 : u"hhmmss"));
570
571 if (tempTime.isValid()) {
572 *time = tempTime;
573 return true;
574 }
575 return false;
576}
577
578bool QLocationUtils::getNmeaLatLong(const QByteArray &latString, char latDirection, const QByteArray &lngString, char lngDirection, double *lat, double *lng)
579{
580 if ((latDirection != 'N' && latDirection != 'S')
581 || (lngDirection != 'E' && lngDirection != 'W')) {
582 return false;
583 }
584
585 bool hasLat = false;
586 bool hasLong = false;
587 double tempLat = latString.toDouble(&hasLat);
588 double tempLng = lngString.toDouble(&hasLong);
589 if (hasLat && hasLong) {
590 tempLat = qlocationutils_nmeaDegreesToDecimal(tempLat);
591 if (latDirection == 'S')
592 tempLat *= -1;
593 tempLng = qlocationutils_nmeaDegreesToDecimal(tempLng);
594 if (lngDirection == 'W')
595 tempLng *= -1;
596
597 if (isValidLat(tempLat) && isValidLong(tempLng)) {
598 *lat = tempLat;
599 *lng = tempLng;
600 return true;
601 }
602 }
603 return false;
604}
605
606QT_END_NAMESPACE
static void qlocationutils_readZda(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
static void qlocationutils_readVtg(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
static void qlocationutils_readRmc(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
static void qlocationutils_readGll(QByteArrayView bv, QGeoPositionInfo *info, bool *hasFix)
static void qlocationutils_readGsa(QByteArrayView bv, QList< int > &pnrsInUse)
static void qlocationutils_readGga(QByteArrayView bv, QGeoPositionInfo *info, double uere, bool *hasFix)
static void qlocationutils_readGsa(QByteArrayView bv, QGeoPositionInfo *info, double uere, bool *hasFix)