Coverage Report

Created: 2026-04-12 06:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/gpsd/gpsd-3.27.6~dev/drivers/driver_garmin_txt.c
Line
Count
Source
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
116
{
149
116
    char buf[10];
150
116
    float sign = 1.0;
151
116
    int offset = 1;      // assume one character prefix (E,W,S,N,U,D, etc)
152
116
    long int intresult;
153
154
116
    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
116
    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
116
    memset(buf, 0, sizeof(buf));
167
116
    (void)strlcpy(buf, data, length);
168
116
    GPSD_LOG(LOG_RAW, &context->errout, "GTXT: Decoded string: %s\n", buf);
169
170
116
    if (NULL != strchr(buf, '_')) {
171
        // value is not valid, ignore it
172
20
        return -2;
173
20
    }
174
175
    // parse prefix
176
96
    do {
177
96
        if ('\0' == prefix[0]) {
178
23
            offset = 0;         // only number, no prefix
179
23
            break;
180
23
        }
181
        // second character in prefix is flag for negative number
182
73
        if ('\0' != prefix[1]) {
183
73
            if (buf[0] == prefix[1]) {
184
10
                sign = -1.0;
185
10
                break;
186
10
            }
187
            // 2nd prefix char not match
188
73
        }
189
        // first character in prefix is flag for positive number
190
63
        if (buf[0] == prefix[0]) {
191
8
            sign = 1.0;
192
8
            break;
193
8
        }
194
55
        GPSD_LOG(LOG_WARN, &context->errout,
195
55
                 "GTXT: Unexpected char \"%c\" in data \"%s\"\n",
196
55
                 buf[0], buf);
197
55
        return -1;
198
63
    } while (0);
199
200
41
    if (strspn(buf + offset, "0123456789") != length - offset) {
201
41
        GPSD_LOG(LOG_WARN, &context->errout, "GTXT: Invalid value %s\n", buf);
202
41
        return -1;
203
41
    }
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
41
}
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
29
{
226
29
    char buf[10];
227
29
    unsigned int res;
228
229
29
    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
29
    memset(buf, 0, sizeof(buf));
236
29
    (void)strlcpy(buf, data, length);
237
29
    GPSD_LOG(LOG_RAW, &context->errout, "GTXT: Decoded string: %s\n", buf);
238
239
29
    if (NULL != strchr(buf, '_')) {
240
        // value is not valid, ignore it
241
2
        return -2;
242
2
    }
243
244
27
    if (strspn(buf, "0123456789") != length) {
245
27
        GPSD_LOG(LOG_WARN, &context->errout, "GTXT: Invalid value %s\n", buf);
246
27
        return -1;
247
27
    }
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
29
{
270
271
29
    gps_mask_t mask = 0;
272
273
29
    GPSD_LOG(LOG_PROG, &session->context->errout,
274
29
             "GTXT: Garmin Simple Text packet, len %zd: %s\n",
275
29
             session->lexer.outbuflen, (char*)session->lexer.outbuffer);
276
277
29
    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
29
    session->lexer.type = GARMINTXT_PACKET;
286
287
    // only one message, set cycle start
288
29
    session->cycle_end_reliable = true;
289
29
    do {
290
29
        struct tm gdate = {0};            // date part of last sentence time
291
29
        unsigned int result;
292
29
        char *buf = (char *)session->lexer.outbuffer + 1;
293
294
29
        GPSD_LOG(LOG_PROG, &session->context->errout,
295
29
                 "GTXT: Timestamp: %.12s\n", buf);
296
297
        // year
298
29
        if (0 != gar_int_decode(session->context,
299
29
                                buf + 0, 2, 0, 99, &result)) {
300
29
            break;
301
29
        }
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
29
    session->newdata.mode = MODE_NO_FIX;
343
29
    session->newdata.status = STATUS_UNK;
344
29
    mask |= MODE_SET | STATUS_SET | CLEAR_IS | REPORT_IS;
345
346
    // process position
347
348
29
    do {
349
29
        double lat, lon;
350
29
        unsigned int degfrag;
351
29
        char status;
352
353
        // Latitude, [NS]ddmmmmm
354
        // decode degrees of Latitude
355
29
        if (0 !=
356
29
            gar_decode(session->context,
357
29
                (char *)session->lexer.outbuffer + 13, 3, "NS", 1.0,
358
29
                &lat)) {
359
29
            break;
360
29
        }
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
29
    do {
423
29
        double eph;
424
29
        if (0 !=
425
29
            gar_decode(session->context,
426
29
                       (char *)session->lexer.outbuffer + 31, 3, "", 1.0,
427
29
                       &eph)) {
428
29
            break;
429
29
        }
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
29
    do {
437
29
        double alt;
438
29
        if (0 !=
439
29
            gar_decode(session->context,
440
29
                       (char *)session->lexer.outbuffer + 34, 6, "+-", 1.0,
441
29
                       &alt)) {
442
29
            break;
443
29
        }
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
29
    do {
452
29
        double ewvel, nsvel;
453
29
        double climb;
454
455
29
        if (0 != gar_decode(session->context,
456
29
                            (char *)session->lexer.outbuffer + 40, 5,
457
29
                            "EW", 10.0, &ewvel)) {
458
29
            break;
459
29
        }
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
29
    GPSD_LOG(LOG_DATA, &session->context->errout,
478
29
             "GTXT: time=%lld, lat=%.2f lon=%.2f altMSL=%.2f "
479
29
             "climb=%.2f eph=%.2f mode=%d status=%d\n",
480
29
             (long long)session->newdata.time.tv_sec,
481
29
             session->newdata.latitude,
482
29
             session->newdata.longitude, session->newdata.altMSL,
483
29
             session->newdata.climb, session->newdata.eph,
484
29
             session->newdata.mode,
485
29
             session->newdata.status);
486
29
    return mask;
487
29
}
488
489
#endif  // GARMINTXT_ENABLE
490
// vim: set expandtab shiftwidth=4