2 * Handle the Garmin simple text format supported by some Garmins.
3 * Tested with the 'Garmin eTrex Legend' device working in 'Text Out' mode.
6 * http://gpsd.berlios.de/vendor-docs/garmin/garmin_simpletext.txt
7 * http://www.garmin.com/support/commProtocol.html
9 * Code by: Petr Slansky <slansky@usa.net>
10 * all rights abandoned, a thank would be nice if you use this code.
13 * -D 4 = packet details
14 * -D 5 = more packet details
15 * -D 6 = very excessive details
18 * very simple protocol, only very basic information
20 * do not have from garmin:
24 * satellite information
26 * This file is Copyright (c) 2010 by the GPSD project
27 * BSD terms apply: see the file COPYING in the distribution root for details.
31 /***************************************************
32 Garmin Simple Text Output Format:
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:
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 hemishpere 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
76 ----------------------- ------- ------------------------
77 /East/West velocity 1 'E' or 'W'
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'
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)
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
97 ----------------------- ------- ------------------------
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
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
107 ***************************************************/
109 #include <sys/types.h>
118 #endif /* S_SPLINT_S */
120 #include <inttypes.h>
122 #include "gpsd_config.h"
123 #if defined (HAVE_SYS_SELECT_H)
124 #include <sys/select.h>
127 #if defined(HAVE_STRINGS_H)
133 #include "timebase.h"
135 #ifdef GARMINTXT_ENABLE
137 /* Simple text message is fixed length, 55 chars text data + 2 characters EOL */
138 /* buffer for text processing */
139 #define TXT_BUFFER_SIZE 13
141 /**************************************************************************
142 * decode text string to double number, translate prefix to sign
149 * gar_decode(cbuf, 9, "EW", 100000.0, &result);
150 * E01412345 -> +14.12345
152 * gar_decode(cbuf, 9, "EW", 100000.0, &result);
153 * W01412345 -> -14.12345
155 * gar_decode(cbuf, 3, "", 10.0, &result);
158 **************************************************************************/
159 static int gar_decode(const char *data, const size_t length, const char *prefix, const double dividor, /*@out@*/
164 int preflen = (int)strlen(prefix);
165 int offset = 1; /* assume one character prefix (E,W,S,N,U,D, etc) */
168 /* splint is buggy here, thinks buf can be a null pointer */
169 /*@ -mustdefine -nullderef -nullpass @*/
170 if (length >= sizeof(buf)) {
171 gpsd_report(LOG_ERROR, "internal buffer too small\n");
175 bzero(buf, (int)sizeof(buf));
176 (void)strncpy(buf, data, length);
177 gpsd_report(LOG_RAW + 2, "Decoded string: %s\n", buf);
179 if (strchr(buf, '_') != NULL) {
180 /* value is not valid, ignore it */
187 offset = 0; /* only number, no prefix */
190 /* second character in prefix is flag for negative number */
192 if (buf[0] == prefix[1]) {
197 /* first character in prefix is flag for positive number */
199 if (buf[0] == prefix[0]) {
204 gpsd_report(LOG_WARN, "Unexpected char \"%c\" in data \"%s\"\n",
209 if (strspn(buf + offset, "0123456789") != length - offset) {
210 gpsd_report(LOG_WARN, "Invalid value %s\n", buf);
213 /*@ +mustdefine +nullderef +nullpass @*/
215 intresult = atol(buf + offset);
217 sign = 0.0; /* don't create negative zero */
219 *result = (double)intresult / dividor * sign;
221 return 0; /* SUCCESS */
224 /**************************************************************************
225 * decode integer from string, check if the result is in expected range
229 **************************************************************************/
230 static int gar_int_decode(const char *data, const size_t length,
231 const unsigned int min, const unsigned int max,
232 /*@out@*/ unsigned int *result)
238 if (length >= sizeof(buf)) {
239 gpsd_report(LOG_ERROR, "internal buffer too small\n");
243 bzero(buf, (int)sizeof(buf));
244 (void)strncpy(buf, data, length);
245 gpsd_report(LOG_RAW + 2, "Decoded string: %s\n", buf);
247 if (strchr(buf, '_') != NULL) {
248 /* value is not valid, ignore it */
252 /*@ -nullpass @*//* splint bug */
253 if (strspn(buf, "0123456789") != length) {
254 gpsd_report(LOG_WARN, "Invalid value %s\n", buf);
258 res = (unsigned)atoi(buf);
259 if ((res >= min) && (res <= max)) {
261 return 0; /* SUCCESS */
263 gpsd_report(LOG_WARN, "Value %u out of range <%u, %u>\n", res, min,
267 /*@ +mustdefine +nullpass @*/
271 /**************************************************************************
273 * Entry points begin here
275 **************************************************************************/
277 gps_mask_t garmintxt_parse(struct gps_device_t * session)
279 /* parse GARMIN Simple Text sentence, unpack it into a session structure */
283 gpsd_report(LOG_PROG, "Garmin Simple Text packet, len %zd\n",
284 session->packet.outbuflen);
285 gpsd_report(LOG_RAW, "%s\n",
286 gpsd_hexdump_wrapper(session->packet.outbuffer,
287 session->packet.outbuflen, LOG_RAW));
289 if (session->packet.outbuflen < 54) {
290 /* trailing CR and LF can be ignored; ('@' + 54x 'DATA' + '\r\n') has length 57 */
291 gpsd_report(LOG_WARN, "Message is too short, rejected.\n");
295 session->packet.type = GARMINTXT_PACKET;
296 /* TAG message as GTXT, Garmin Simple Text Message */
297 strncpy(session->gpsdata.tag, "GTXT", MAXTAGLEN);
299 /* only one message, set cycle start */
300 session->cycle_end_reliable = true;
303 char *buf = (char *)session->packet.outbuffer + 1;
304 gpsd_report(LOG_PROG, "Timestamp: %.12s\n", buf);
307 if (0 != gar_int_decode(buf + 0, 2, 0, 99, &result))
309 session->driver.garmintxt.date.tm_year =
310 (CENTURY_BASE + (int)result) - 1900;
312 if (0 != gar_int_decode(buf + 2, 2, 1, 12, &result))
314 session->driver.garmintxt.date.tm_mon = (int)result - 1;
316 if (0 != gar_int_decode(buf + 4, 2, 1, 31, &result))
318 session->driver.garmintxt.date.tm_mday = (int)result;
320 if (0 != gar_int_decode(buf + 6, 2, 0, 23, &result))
322 session->driver.garmintxt.date.tm_hour = (int)result; /* mday update?? */
324 if (0 != gar_int_decode(buf + 8, 2, 0, 59, &result))
326 session->driver.garmintxt.date.tm_min = (int)result;
328 /* second value can be even 60, occasional leap second */
329 if (0 != gar_int_decode(buf + 10, 2, 0, 60, &result))
331 session->driver.garmintxt.date.tm_sec = (int)result;
332 session->driver.garmintxt.subseconds = 0;
333 session->newdata.time =
334 (double)mkgmtime(&session->driver.garmintxt.date) +
335 session->driver.garmintxt.subseconds;
339 /* assume that possition is unknown; if the position is known we will fix status information later */
340 session->newdata.mode = MODE_NO_FIX;
341 session->gpsdata.status = STATUS_NO_FIX;
342 mask |= MODE_IS | STATUS_IS | CLEAR_IS | REPORT_IS;
344 /* process position */
348 unsigned int degfrag;
351 /* Latitude, [NS]ddmmmmm */
352 /* decode degrees of Latitude */
354 gar_decode((char *)session->packet.outbuffer + 13, 3, "NS", 1.0,
357 /* decode minutes of Latitude */
359 gar_int_decode((char *)session->packet.outbuffer + 16, 5, 0,
362 lat += degfrag * 100.0 / 60.0 / 100000.0;
363 session->newdata.latitude = lat;
365 /* Longitude, [EW]dddmmmmm */
366 /* decode degrees of Longitude */
368 gar_decode((char *)session->packet.outbuffer + 21, 4, "EW", 1.0,
371 /* decode minutes of Longitude */
373 gar_int_decode((char *)session->packet.outbuffer + 25, 5, 0,
376 lon += degfrag * 100.0 / 60.0 / 100000.0;
377 session->newdata.longitude = lon;
379 /* fix mode, GPS status, [gGdDS_] */
380 status = (char)session->packet.outbuffer[30];
384 case 'S': /* 'S' is DEMO mode, assume 3D position */
385 session->newdata.mode = MODE_3D;
386 session->gpsdata.status = STATUS_FIX;
389 session->newdata.mode = MODE_3D;
390 session->gpsdata.status = STATUS_DGPS_FIX;
393 session->newdata.mode = MODE_2D;
394 session->gpsdata.status = STATUS_FIX;
397 session->newdata.mode = MODE_2D;
398 session->gpsdata.status = STATUS_DGPS_FIX;
401 session->newdata.mode = MODE_NO_FIX;
402 session->gpsdata.status = STATUS_NO_FIX;
404 mask |= MODE_IS | STATUS_IS | LATLON_IS;
411 gar_decode((char *)session->packet.outbuffer + 31, 3, "", 1.0,
414 /* eph is a circular error, sqrt(epx**2 + epy**2) */
415 session->newdata.epx = session->newdata.epy =
416 eph * (1 / sqrt(2)) * (GPSD_CONFIDENCE / CEP50_SIGMA);
424 gar_decode((char *)session->packet.outbuffer + 34, 6, "+-", 1.0,
427 session->newdata.altitude = alt;
433 double ewvel, nsvel, speed, track;
435 gar_decode((char *)session->packet.outbuffer + 40, 5, "EW", 10.0,
439 gar_decode((char *)session->packet.outbuffer + 45, 5, "NS", 10.0,
442 speed = sqrt(ewvel * ewvel + nsvel * nsvel); /* is this correct formula? Result is in mps */
443 session->newdata.speed = speed;
444 track = atan2(ewvel, nsvel) * RAD_2_DEG; /* is this correct formula? Result is in degrees */
447 session->newdata.track = track;
448 mask |= SPEED_IS | TRACK_IS;
452 /* Climb (vertical velocity) */
456 gar_decode((char *)session->packet.outbuffer + 50, 5, "UD", 100.0,
459 session->newdata.climb = climb; /* climb in mps */
463 gpsd_report(LOG_DATA,
464 "GTXT: time=%.2f, lat=%.2f lon=%.2f alt=%.2f speed=%.2f track=%.2f climb=%.2f exp=%.2f epy=%.2f mode=%d status=%d mask=%s\n",
465 session->newdata.time, session->newdata.latitude,
466 session->newdata.longitude, session->newdata.altitude,
467 session->newdata.speed, session->newdata.track,
468 session->newdata.climb, session->newdata.epx,
469 session->newdata.epy, session->newdata.mode,
470 session->gpsdata.status, gpsd_maskdump(mask));
474 #endif /* GARMINTXT_ENABLE */