Coverage Report

Created: 2026-05-30 06:15

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