Coverage Report

Created: 2025-07-11 06:47

/src/gpsd/gpsd-3.26.2~dev/drivers/driver_garmin_txt.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * Handle the Garmin simple text format supported by some Garmins.
3
 * Tested with the 'Garmin eTrex Legend' device working in 'Text Out' mode.
4
 *
5
 * Protocol info from:
6
 *       http://www8.garmin.com/support/text_out.html
7
 *       http://www.garmin.com/support/commProtocol.html
8
 *
9
 * Code by: Petr Slansky <slansky@usa.net>
10
 * all rights abandoned, a thank would be nice if you use this code.
11
 *
12
 * -D 3 = packet trace
13
 * -D 4 = packet details
14
 * -D 5 = more packet details
15
 * -D 6 = very excessive details
16
 *
17
 * limitations:
18
 *  very simple protocol, only very basic information
19
 * TODO
20
 * do not have from garmin:
21
 *      pdop
22
 *      vdop
23
 *      magnetic variation
24
 *      satellite information
25
 *
26
 * This file is Copyright 2010 by the GPSD project
27
 * SPDX-License-Identifier: BSD-2-clause
28
 *
29
 */
30
31
/***************************************************
32
Garmin Simple Text Output Format:
33
34
The simple text (ASCII) output contains time, position, and velocity data in
35
the fixed width fields (not delimited) defined in the following table:
36
37
    FIELD DESCRIPTION:      WIDTH:  NOTES:
38
    ----------------------- ------- ------------------------
39
    Sentence start          1       Always '@'
40
    ----------------------- ------- ------------------------
41
   /Year                    2       Last two digits of UTC year
42
  | ----------------------- ------- ------------------------
43
  | Month                   2       UTC month, "01".."12"
44
T | ----------------------- ------- ------------------------
45
i | Day                     2       UTC day of month, "01".."31"
46
m | ----------------------- ------- ------------------------
47
e | Hour                    2       UTC hour, "00".."23"
48
  | ----------------------- ------- ------------------------
49
  | Minute                  2       UTC minute, "00".."59"
50
  | ----------------------- ------- ------------------------
51
   \Second                  2       UTC second, "00".."59"
52
    ----------------------- ------- ------------------------
53
   /Latitude hemisphere     1       'N' or 'S'
54
  | ----------------------- ------- ------------------------
55
  | Latitude position       7       WGS84 ddmmmmm, with an implied
56
  |                                 decimal after the 4th digit
57
  | ----------------------- ------- ------------------------
58
  | Longitude hemisphere    1       'E' or 'W'
59
  | ----------------------- ------- ------------------------
60
  | Longitude position      8       WGS84 dddmmmmm with an implied
61
P |                                 decimal after the 5th digit
62
o | ----------------------- ------- ------------------------
63
s | Position status         1       'd' if current 2D differential GPS position
64
i |                                 'D' if current 3D differential GPS position
65
t |                                 'g' if current 2D GPS position
66
i |                                 'G' if current 3D GPS position
67
o |                                 'S' if simulated position
68
n |                                 '_' if invalid position
69
  | ----------------------- ------- ------------------------
70
  | Horizontal posn error   3       EPH in meters
71
  | ----------------------- ------- ------------------------
72
  | Altitude sign           1       '+' or '-'
73
  | ----------------------- ------- ------------------------
74
  | Altitude                5       Height above or below mean
75
   \                                sea level in meters
76
    ----------------------- ------- ------------------------
77
   /East/West velocity      1       'E' or 'W'
78
  |     direction
79
  | ----------------------- ------- ------------------------
80
  | East/West velocity      4       Meters per second in tenths,
81
  |     magnitude                   ("1234" = 123.4 m/s)
82
V | ----------------------- ------- ------------------------
83
e | North/South velocity    1       'N' or 'S'
84
l |     direction
85
o | ----------------------- ------- ------------------------
86
c | North/South velocity    4       Meters per second in tenths,
87
i |     magnitude                   ("1234" = 123.4 m/s)
88
t | ----------------------- ------- ------------------------
89
y | Vertical velocity       1       'U' or 'D' (up/down)
90
  |     direction
91
  | ----------------------- ------- ------------------------
92
  | Vertical velocity       4       Meters per second in hundredths,
93
   \    magnitude                   ("1234" = 12.34 m/s)
94
    ----------------------- ------- ------------------------
95
    Sentence end            2       Carriage return, '0x0D', and
96
                                    line feed, '0x0A'
97
    ----------------------- ------- ------------------------
98
99
If a numeric value does not fill its entire field width, the field is padded
100
with leading '0's (eg. an altitude of 50 meters above MSL will be output as
101
"+00050").
102
103
Any or all of the data in the text sentence (except for the sentence start
104
and sentence end fields) may be replaced with underscores to indicate
105
invalid data.
106
107
***************************************************/
108
109
110
#include "../include/gpsd_config.h"  // must be before all includes
111
112
#include <math.h>
113
#include <stdbool.h>
114
#include <stdlib.h>
115
#include <string.h>
116
#include <strings.h>
117
118
#include "../include/gpsd.h"
119
120
#ifdef GARMINTXT_ENABLE
121
122
// Simple text message is fixed length, 55 chars text data + 2 characters EOL
123
// buffer for text processing
124
#define TXT_BUFFER_SIZE 13
125
126
/**************************************************************************
127
 * decode text string to double number, translate prefix to sign
128
 * return 0: OK
129
 *       -1: data error
130
 *       -2: data not valid
131
 *
132
 * examples with context->errout.debug == 0:
133
 *
134
 *  gar_decode(context, cbuf, 9, "EW", 100000.0, &result);
135
 *  E01412345 -> +14.12345
136
 *
137
 *  gar_decode(context, cbuf, 9, "EW", 100000.0, &result);
138
 *  W01412345 -> -14.12345
139
 *
140
 *  gar_decode(context, cbuf, 3, "", 10.0, &result);
141
 *  123 -> +12.3
142
 *
143
**************************************************************************/
144
static int gar_decode(const struct gps_context_t *context,
145
                      const char *data, const size_t length,
146
                      const char *prefix, const double divisor,
147
                      double *result)
148
0
{
149
0
    char buf[10];
150
0
    float sign = 1.0;
151
0
    int offset = 1;      // assume one character prefix (E,W,S,N,U,D, etc)
152
0
    long int intresult;
153
154
0
    if (1 > length) {
155
0
        GPSD_LOG(LOG_ERROR, &context->errout, "GTXT: field too short %zu\n",
156
0
                 length);
157
0
        return -1;
158
0
    }
159
160
0
    if (sizeof(buf) <= length) {
161
0
        GPSD_LOG(LOG_ERROR, &context->errout,
162
0
                 "GTXT: internal buffer too small\n");
163
0
        return -1;
164
0
    }
165
166
0
    memset(buf, 0, sizeof(buf));
167
0
    (void)strlcpy(buf, data, length);
168
0
    GPSD_LOG(LOG_RAW, &context->errout, "GTXT: Decoded string: %s\n", buf);
169
170
0
    if (NULL != strchr(buf, '_')) {
171
        // value is not valid, ignore it
172
0
        return -2;
173
0
    }
174
175
    // parse prefix
176
0
    do {
177
0
        if ('\0' == prefix[0]) {
178
0
            offset = 0;         // only number, no prefix
179
0
            break;
180
0
        }
181
        // second character in prefix is flag for negative number
182
0
        if ('\0' != prefix[1]) {
183
0
            if (buf[0] == prefix[1]) {
184
0
                sign = -1.0;
185
0
                break;
186
0
            }
187
            // 2nd prefix char not match
188
0
        }
189
        // first character in prefix is flag for positive number
190
0
        if (buf[0] == prefix[0]) {
191
0
            sign = 1.0;
192
0
            break;
193
0
        }
194
0
        GPSD_LOG(LOG_WARN, &context->errout,
195
0
                 "GTXT: Unexpected char \"%c\" in data \"%s\"\n",
196
0
                 buf[0], buf);
197
0
        return -1;
198
0
    } while (0);
199
200
0
    if (strspn(buf + offset, "0123456789") != length - offset) {
201
0
        GPSD_LOG(LOG_WARN, &context->errout, "GTXT: Invalid value %s\n", buf);
202
0
        return -1;
203
0
    }
204
205
0
    intresult = atol(buf + offset);
206
0
    if (0L == intresult) {
207
0
        sign = 0.0;             //  don't create negative zero
208
0
    }
209
210
0
    *result = (double)intresult / divisor * sign;
211
212
0
    return 0;                   // SUCCESS
213
0
}
214
215
/**************************************************************************
216
 * decode integer from string, check if the result is in expected range
217
 * return 0: OK
218
 *       -1: data error
219
 *       -2: data not valid
220
**************************************************************************/
221
static int gar_int_decode(const struct gps_context_t *context,
222
                          const char *data, const size_t length,
223
                          const unsigned int min, const unsigned int max,
224
                          unsigned int *result)
225
0
{
226
0
    char buf[10];
227
0
    unsigned int res;
228
229
0
    if (sizeof(buf) <= length ) {
230
0
        GPSD_LOG(LOG_ERROR, &context->errout,
231
0
                 "GTXT: internal buffer too small\n");
232
0
        return -1;
233
0
    }
234
235
0
    memset(buf, 0, sizeof(buf));
236
0
    (void)strlcpy(buf, data, length);
237
0
    GPSD_LOG(LOG_RAW, &context->errout, "GTXT: Decoded string: %s\n", buf);
238
239
0
    if (NULL != strchr(buf, '_')) {
240
        // value is not valid, ignore it
241
0
        return -2;
242
0
    }
243
244
0
    if (strspn(buf, "0123456789") != length) {
245
0
        GPSD_LOG(LOG_WARN, &context->errout, "GTXT: Invalid value %s\n", buf);
246
0
        return -1;
247
0
    }
248
249
0
    res = (unsigned)atoi(buf);
250
0
    if (IN(min, res, max)) {
251
0
        *result = res;
252
0
        return 0;               // SUCCESS
253
0
    }
254
0
    GPSD_LOG(LOG_WARN, &context->errout,
255
0
             "GTXT: Value %u out of range <%u, %u>\n", res, min,
256
0
             max);
257
0
    return -1;
258
0
}
259
260
261
/**************************************************************************
262
 *
263
 * Entry points begin here
264
 *
265
 **************************************************************************/
266
267
// parse GARMIN Simple Text sentence, unpack it into a session structure
268
gps_mask_t garmintxt_parse(struct gps_device_t * session)
269
0
{
270
271
0
    gps_mask_t mask = 0;
272
273
0
    GPSD_LOG(LOG_PROG, &session->context->errout,
274
0
             "GTXT: Garmin Simple Text packet, len %zd: %s\n",
275
0
             session->lexer.outbuflen, (char*)session->lexer.outbuffer);
276
277
0
    if (54 > session->lexer.outbuflen) {
278
        /* trailing CR and LF can be ignored; ('@' + 54x 'DATA' + '\r\n')
279
         * has length 57 */
280
0
        GPSD_LOG(LOG_WARN, &session->context->errout,
281
0
                 "GTXT: Message is too short, rejected.\n");
282
0
        return ONLINE_SET;
283
0
    }
284
285
0
    session->lexer.type = GARMINTXT_PACKET;
286
287
    // only one message, set cycle start
288
0
    session->cycle_end_reliable = true;
289
0
    do {
290
0
        struct tm gdate = {0};            // date part of last sentence time
291
0
        unsigned int result;
292
0
        char *buf = (char *)session->lexer.outbuffer + 1;
293
294
0
        GPSD_LOG(LOG_PROG, &session->context->errout,
295
0
                 "GTXT: Timestamp: %.12s\n", buf);
296
297
        // year
298
0
        if (0 != gar_int_decode(session->context,
299
0
                                buf + 0, 2, 0, 99, &result)) {
300
0
            break;
301
0
        }
302
0
        gdate.tm_year = (session->context->century + (int)result) - 1900;
303
        // month
304
0
        if (0 != gar_int_decode(session->context,
305
0
                                buf + 2, 2, 1, 12, &result)) {
306
0
            break;
307
0
        }
308
0
        gdate.tm_mon = (int)result - 1;
309
        // day
310
0
        if (0 != gar_int_decode(session->context,
311
0
                                buf + 4, 2, 1, 31, &result)) {
312
0
            break;
313
0
        }
314
0
        gdate.tm_mday = (int)result;
315
        // hour
316
0
        if (0 != gar_int_decode(session->context,
317
0
                                buf + 6, 2, 0, 23, &result)) {
318
0
            break;
319
0
        }
320
        // mday update??
321
0
        gdate.tm_hour = (int)result;
322
        // minute
323
0
        if (0 != gar_int_decode(session->context,
324
0
                                buf + 8, 2, 0, 59, &result)) {
325
0
            break;
326
0
        }
327
0
        gdate.tm_min = (int)result;
328
        // second
329
        // second value can be even 60, occasional leap second
330
0
        if (0 != gar_int_decode(session->context,
331
0
                                buf + 10, 2, 0, 60, &result)) {
332
0
            break;
333
0
        }
334
0
        gdate.tm_sec = (int)result;
335
0
        session->newdata.time.tv_sec = mkgmtime(&gdate);
336
0
        session->newdata.time.tv_nsec = 0;
337
0
        mask |= TIME_SET;
338
0
    } while (0);
339
340
    /* assume that position is unknown; if the position is known we
341
     * will fix status information later */
342
0
    session->newdata.mode = MODE_NO_FIX;
343
0
    session->newdata.status = STATUS_UNK;
344
0
    mask |= MODE_SET | STATUS_SET | CLEAR_IS | REPORT_IS;
345
346
    // process position
347
348
0
    do {
349
0
        double lat, lon;
350
0
        unsigned int degfrag;
351
0
        char status;
352
353
        // Latitude, [NS]ddmmmmm
354
        // decode degrees of Latitude
355
0
        if (0 !=
356
0
            gar_decode(session->context,
357
0
                (char *)session->lexer.outbuffer + 13, 3, "NS", 1.0,
358
0
                &lat)) {
359
0
            break;
360
0
        }
361
        // decode minutes of Latitude
362
0
        if (0 !=
363
0
            gar_int_decode(session->context,
364
0
                           (char *)session->lexer.outbuffer + 16, 5, 0,
365
0
                           99999, &degfrag)) {
366
0
            break;
367
0
        }
368
0
        lat += degfrag * 100.0 / 60.0 / 100000.0;
369
0
        session->newdata.latitude = lat;
370
371
        // Longitude, [EW]dddmmmmm
372
        // decode degrees of Longitude
373
0
        if (0 !=
374
0
            gar_decode(session->context,
375
0
                       (char *)session->lexer.outbuffer + 21, 4, "EW", 1.0,
376
0
                       &lon)) {
377
0
            break;
378
0
        }
379
        // decode minutes of Longitude
380
0
        if (0 !=
381
0
            gar_int_decode(session->context,
382
0
                           (char *)session->lexer.outbuffer + 25, 5, 0,
383
0
                           99999, &degfrag)) {
384
0
            break;
385
0
        }
386
0
        lon += degfrag * 100.0 / 60.0 / 100000.0;
387
0
        session->newdata.longitude = lon;
388
0
        session->newdata.geoid_sep = wgs84_separation(lat, lon);
389
390
        // fix mode, GPS status, [gGdDS_]
391
0
        status = (char)session->lexer.outbuffer[30];
392
393
0
        switch (status) {
394
0
        case 'D':
395
0
            session->newdata.mode = MODE_3D;
396
0
            session->newdata.status = STATUS_DGPS;
397
0
            break;
398
0
        case 'G':
399
0
            session->newdata.mode = MODE_3D;
400
0
            session->newdata.status = STATUS_GPS;
401
0
            break;
402
0
        case 'S':
403
0
            session->newdata.mode = MODE_3D;
404
0
            session->newdata.status = STATUS_SIM;
405
0
            break;
406
0
        case 'd':
407
0
            session->newdata.mode = MODE_2D;
408
0
            session->newdata.status = STATUS_DGPS;
409
0
            break;
410
0
        case 'g':
411
0
            session->newdata.mode = MODE_2D;
412
0
            session->newdata.status = STATUS_GPS;
413
0
            break;
414
0
        default:
415
0
            session->newdata.mode = MODE_NO_FIX;
416
0
            session->newdata.status = STATUS_UNK;
417
0
        }
418
0
        mask |= MODE_SET | STATUS_SET | LATLON_SET;
419
0
    } while (0);
420
421
    // EPH
422
0
    do {
423
0
        double eph;
424
0
        if (0 !=
425
0
            gar_decode(session->context,
426
0
                       (char *)session->lexer.outbuffer + 31, 3, "", 1.0,
427
0
                       &eph)) {
428
0
            break;
429
0
        }
430
        // this conversion looks dodgy...
431
0
        session->newdata.eph = eph * (GPSD_CONFIDENCE / CEP50_SIGMA);
432
0
        mask |= HERR_SET;
433
0
    } while (0);
434
435
    // Altitude
436
0
    do {
437
0
        double alt;
438
0
        if (0 !=
439
0
            gar_decode(session->context,
440
0
                       (char *)session->lexer.outbuffer + 34, 6, "+-", 1.0,
441
0
                       &alt)) {
442
0
            break;
443
0
        }
444
        // alt is MSL
445
0
        session->newdata.altMSL = alt;
446
        // Let gpsd_error_model() deal with altHAE
447
0
        mask |= ALTITUDE_SET;
448
0
    } while (0);
449
450
    // Velocities, meters per second
451
0
    do {
452
0
        double ewvel, nsvel;
453
0
        double climb;
454
455
0
        if (0 != gar_decode(session->context,
456
0
                            (char *)session->lexer.outbuffer + 40, 5,
457
0
                            "EW", 10.0, &ewvel)) {
458
0
            break;
459
0
        }
460
0
        if (0 != gar_decode(session->context,
461
0
                            (char *)session->lexer.outbuffer + 45, 5,
462
0
                            "NS", 10.0, &nsvel)) {
463
0
            break;
464
0
        }
465
0
        if (0 != gar_decode(session->context,
466
0
                            (char *)session->lexer.outbuffer + 50, 5,
467
0
                            "UD", 100.0, &climb)) {
468
0
            break;
469
0
        }
470
471
0
        session->newdata.NED.velN = ewvel;
472
0
        session->newdata.NED.velE = nsvel;
473
0
        session->newdata.NED.velD = -climb;
474
0
        mask |= VNED_SET;
475
0
    } while (0);
476
477
0
    GPSD_LOG(LOG_DATA, &session->context->errout,
478
0
             "GTXT: time=%lld, lat=%.2f lon=%.2f altMSL=%.2f "
479
0
             "climb=%.2f eph=%.2f mode=%d status=%d\n",
480
0
             (long long)session->newdata.time.tv_sec,
481
0
             session->newdata.latitude,
482
0
             session->newdata.longitude, session->newdata.altMSL,
483
0
             session->newdata.climb, session->newdata.eph,
484
0
             session->newdata.mode,
485
0
             session->newdata.status);
486
0
    return mask;
487
0
}
488
489
#endif  // GARMINTXT_ENABLE
490
// vim: set expandtab shiftwidth=4