/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, °frag)) { |
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, °frag)) { |
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 |