Coverage Report

Created: 2026-03-03 06:57

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