1 /*
2     Communicate Thales/Magellan serial protocol.
3 
4     Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007,
5       2008, 2010  Robert Lipe, robertlipe+source@gpsbabel.org
6 
7     This program is free software; you can redistribute it and/or modify
8     it under the terms of the GNU General Public License as published by
9     the Free Software Foundation; either version 2 of the License, or
10     (at your option) any later version.
11 
12     This program is distributed in the hope that it will be useful,
13     but WITHOUT ANY WARRANTY; without even the implied warranty of
14     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15     GNU General Public License for more details.
16 
17     You should have received a copy of the GNU General Public License
18     along with this program; if not, write to the Free Software
19     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20 
21  */
22 
23 #include <cassert>                 // for assert
24 #include <cctype>                  // for isprint, toupper
25 #include <cmath>                   // for fabs, lround
26 #include <cstdio>                  // for sscanf, size_t
27 #include <cstdlib>                 // for atoi, atof, strtoul
28 #include <cstring>                 // for strchr, strncmp, strlen, memmove, strrchr, memset
29 #include <ctime>                   // for gmtime
30 
31 #include <QtCore/QByteArray>       // for QByteArray
32 #include <QtCore/QDateTime>        // for QDateTime
33 #include <QtCore/QDir>             // for QDir, operator|, QDir::Files, QDir::Name, QDir::Readable
34 #include <QtCore/QFileInfo>        // for QFileInfo
35 #include <QtCore/QFileInfoList>    // for QFileInfoList
36 #include <QtCore/QLatin1String>    // for QLatin1String
37 #include <QtCore/QList>            // for QList
38 #include <QtCore/QScopedPointer>   // for QScopedPointer
39 #include <QtCore/QString>          // for QString, operator==
40 #include <QtCore/QStringList>      // for QStringList
41 #include <QtCore/QTime>            // for QTime
42 #include <QtCore/QVector>          // for QVector
43 #include <QtCore/Qt>               // for CaseInsensitive
44 #include <QtCore/QtGlobal>         // for qPrintable, foreach
45 
46 #include "defs.h"
47 #include "explorist_ini.h"         // for explorist_ini_done, explorist_ini_get, mag_info
48 #include "format.h"                // for Format
49 #include "gbfile.h"                // for gbfclose, gbfeof, gbfgets, gbfopen, gbfwrite, gbfile
50 #include "gbser.h"                 // for gbser_deinit, gbser_init, gbser_is_serial, gbser_read_line, gbser_set_port, gbser_write, gbser_OK
51 #include "magellan.h"              // for mm_meridian, mm_sportrak, magellan_icon_mapping_t, mm_gps315320, mm_unknown, mm_map330, mm_map410, pid_to_model_t, mm_gps310, m330_cleanse, mag_checksum, mag_find_descr_from_token, mag_find_token_from_descr, mag_rteparse, mag_trkparse
52 #include "src/core/datetime.h"     // for DateTime
53 #include "vecs.h"                  // for Vecs
54 
55 
56 static int bitrate = 4800;
57 static int wptcmtcnt;
58 static int wptcmtcnt_max;
59 static int explorist;
60 static int broken_sportrak;
61 #define MYNAME "MAGPROTO"
62 #define MAXCMTCT 200
63 
64 #define debug_serial  (global_opts.debug_level > 1)
65 
66 static QString termread(char* ibuf, int size);
67 static void termwrite(const char* obuf, int size);
68 static void mag_readmsg(gpsdata_type objective);
69 static void mag_handon();
70 static void mag_handoff();
71 static short_handle mkshort_handle = nullptr;
72 static char* deficon = nullptr;
73 static char* bs = nullptr;
74 static char* cmts = nullptr;
75 static char* noack = nullptr;
76 static char* nukewpt = nullptr;
77 static int route_out_count;
78 static int waypoint_read_count;
79 static int wpt_len = 8;
80 static QString curfname;
81 static int extension_hint;
82 // For Explorist GC/510/610/710 families, bludgeon in GPX support.
83 // (This has nothing to do with the Explorist 100...600 products.)
84 static Format* gpx_vec;
85 static mag_info* explorist_info;
86 static QStringList os_gpx_files(const char* dirname);
87 
88 /*
89  * Magellan's firmware is *horribly* slow to send the next packet after
90  * we turn around an ack while we are reading from the device.  It's
91  * quite spiffy when we're writing to the device.   Since we're *way*
92  * less likely to lose data while reading from it than it is to lose data
93  * when we write to it, we turn off the acks when we are predominantly
94  * reading.
95  */
96 static int suppress_ack;
97 
98 enum mag_rxstate {
99   mrs_handoff = 0,
100   mrs_handon,
101   mrs_awaiting_ack
102 };
103 
104 /*
105  *   An individual element of a route.
106  */
107 struct mag_rte_elem {
108   QString wpt_name;
109   QString wpt_icon;
110 };
111 
112 /*
113  *  A header of a route.  Related elements of a route belong to this.
114  */
115 struct mag_rte_head_t {
116   QList<mag_rte_elem*> elem_list; /* list of child rte_elems */
117   char* rte_name{nullptr};
118   int nelems{0};
119 };
120 
121 static QList<Waypoint*> rte_wpt_tmp; /* temporary PGMNWPL msgs for routes */
122 
123 static gbfile* magfile_h;
124 static mag_rxstate magrxstate;
125 static int mag_error;
126 static unsigned int last_rx_csum;
127 static int found_done;
128 static int got_version;
129 static int is_file = 0;
130 static route_head* trk_head;
131 static int ignore_unable;
132 
133 static Waypoint* mag_wptparse(char*);
134 using cleanse_fn = QString (const char*);
135 static cleanse_fn* mag_cleanse;
136 static const char** os_get_magellan_mountpoints();
137 
138 static const magellan_icon_mapping_t gps315_icon_table[] = {
139   { "a", "filled circle" },
140   { "b", "box" },
141   { "c", "red buoy" },
142   { "d", "green buoy" },
143   { "e", "buoy" },
144   { "f", "rocks" },
145   { "g", "red daymark" },
146   { "h", "green daymark" },
147   { "i", "bell" },
148   { "j", "danger" },
149   { "k", "diver down" },
150   { "l", "fish" },
151   { "m", "house" },
152   { "n", "mark" },
153   { "o", "car" },
154   { "p", "tent" },
155   { "q", "boat" },
156   { "r", "food" },
157   { "s", "fuel" },
158   { "t", "tree" },
159   { nullptr, nullptr }
160 };
161 
162 static const magellan_icon_mapping_t map330_icon_table[] = {
163   { "a", "crossed square" },
164   { "b", "box" },
165   { "c", "house" },
166   { "d", "aerial" },
167   { "e", "airport" },
168   { "f", "amusement park" },
169   { "g", "ATM" },
170   { "g", "Bank" },
171   { "h", "auto repair" },
172   { "i", "boating" },
173   { "j", "camping" },
174   { "k", "exit ramp" },
175   { "l", "first aid" },
176   { "m", "nav aid" },
177   { "n", "buoy" },
178   { "o", "fuel" },
179   { "p", "garden" },
180   { "q", "golf" },
181   { "r", "hotel" },
182   { "s", "hunting/fishing" },
183   { "t", "large city" },
184   { "u", "lighthouse" },
185   { "v", "major city" },
186   { "w", "marina" },
187   { "x", "medium city" },
188   { "y", "museum" },
189   { "z", "obstruction" },
190   { "aa", "park" },
191   { "ab", "resort" },
192   { "ac", "restaurant" },
193   { "ad", "rock" },
194   { "ae", "scuba" },
195   { "af", "RV service" },
196   { "ag", "shooting" },
197   { "ah", "sight seeing" },
198   { "ai", "small city" },
199   { "aj", "sounding" },
200   { "ak", "sports arena" },
201   { "al", "tourist info" },
202   { "am", "truck service" },
203   { "an", "winery" },
204   { "ao", "wreck" },
205   { "ap", "zoo" },
206   { "ah", "Virtual cache"}, 	/* Binos: because you "see" them. */
207   { "ak", "Micro-Cache" },	/* Looks like a film canister. */
208   { "an", "Multi-Cache"}, 	/* Winery: grapes 'coz they "bunch" */
209   { "s",  "Unknown Cache"}, 	/* 'Surprise' cache: use a target. */
210   { "ac",  "Event Cache"}, 	/* Event caches.  May be food. */
211   { nullptr, nullptr }
212 };
213 
214 pid_to_model_t pid_to_model[] = {
215   { mm_gps315320, 19, "ColorTrak" },
216   { mm_gps315320, 24, "GPS 315/320" },
217   { mm_map410, 25, "Map 410" },
218   { mm_map330, 30, "Map 330" },
219   { mm_gps310, 31, "GPS 310" },
220   { mm_meridian, 33, "Meridian" },
221   { mm_meridian, 35, "ProMark 2" },
222   { mm_sportrak, 36, "SporTrak Map/Pro" },
223   { mm_sportrak, 37, "SporTrak" },
224   { mm_meridian, 38, "FX324 Plotter" },
225   { mm_meridian, 39, "Meridian Color" },
226   { mm_meridian, 40, "FX324C Plotter" },
227   { mm_sportrak, 41, "Sportrak Color" },
228   { mm_sportrak, 42, "Sportrak Marine" },
229   { mm_meridian, 43, "Meridian Marine" },
230   { mm_sportrak, 44, "Sportrak Topo" },
231   { mm_sportrak, 45, "Mystic" },
232   { mm_meridian, 46, "MobileMapper" },
233   { mm_meridian, 110, "Explorist 100" },
234   { mm_meridian, 111, "Explorist 200" },
235   { mm_unknown, 0, nullptr }
236 };
237 
238 static const magellan_icon_mapping_t* icon_mapping = map330_icon_table;
239 
240 /*
241  *   For each receiver type, return a "cleansed" version of the string
242  *   that's valid for a waypoint name or comment.   The string should be
243  *   freed when you're done with it.
244  */
245 static QString
m315_cleanse(const char * istring)246 m315_cleanse(const char* istring)
247 {
248   char* rstring = (char*) xmalloc(strlen(istring)+1);
249   char* o;
250   const char* i;
251   static char m315_valid_chars[] =
252     "ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789";
253   for (o=rstring,i=istring; *i; i++) {
254     if (strchr(m315_valid_chars, toupper(*i))) {
255       *o++ = toupper(*i);
256     }
257   }
258   *o = 0;
259   QString rv(rstring);
260   xfree(rstring);
261   return rv;
262 }
263 
264 /*
265  * Do same for 330, Meridian, and SportTrak.
266  */
267 QString
m330_cleanse(const char * istring)268 m330_cleanse(const char* istring)
269 {
270   static char m330_valid_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ "
271                                    "abcdefghijklmnopqrstuvwxyz"
272                                    "0123456789+-.'/!@#<%^&>()=:\\";
273   char* rstring = (char*) xmalloc(strlen(istring)+1);
274   char* o;
275   const char* i;
276 
277   for (o=rstring,i=istring; *i; i++) {
278     if (strchr(m330_valid_chars, *i)) {
279       *o++ = *i;
280     }
281   }
282   *o = 0;
283   QString rv(rstring);
284   xfree(rstring);
285   return rv;
286 }
287 
288 /*
289  * Given a protocol message, compute the checksum as needed by
290  * the Magellan protocol.
291  */
292 unsigned int
mag_checksum(const char * const buf)293 mag_checksum(const char* const buf)
294 {
295   int csum = 0;
296 
297   for (const char* p = buf; *p; p++) {
298     csum  ^= *p;
299   }
300 
301   return csum;
302 }
303 static unsigned int
mag_pchecksum(const char * const buf,int len)304 mag_pchecksum(const char* const buf, int len)
305 {
306   int csum = 0;
307   const char* p = buf;
308   for (; len ; len--) {
309     csum ^= *p++;
310   }
311   return csum;
312 }
313 
314 static void
mag_writemsg(const char * const buf)315 mag_writemsg(const char* const buf)
316 {
317   unsigned int osum = mag_checksum(buf);
318   int retry_cnt = 5;
319   QScopedPointer<char, QScopedPointerPodDeleter> obuf;
320 
321   if (debug_serial) {
322     warning("WRITE: $%s*%02X\r\n",buf, osum);
323   }
324 
325 retry:
326 
327   int i = xasprintf(obuf, "$%s*%02X\r\n",buf, osum);
328   termwrite(obuf.data(), i);
329   if (magrxstate == mrs_handon || magrxstate == mrs_awaiting_ack) {
330     magrxstate = mrs_awaiting_ack;
331     mag_readmsg(trkdata);
332     if (last_rx_csum != osum) {
333       if (debug_serial) {
334         warning("COMM ERROR: Expected %02x, got %02x",
335                 osum, last_rx_csum);
336       }
337       if (retry_cnt--) {
338         goto retry;
339       } else {
340         mag_handoff();
341         fatal(MYNAME
342               ": Too many communication errors.\n");
343       }
344     }
345   }
346 }
347 
348 static void
mag_writeack(int osum)349 mag_writeack(int osum)
350 {
351   QScopedPointer<char, QScopedPointerPodDeleter> nbuf;
352   QScopedPointer<char, QScopedPointerPodDeleter> obuf;
353 
354   if (is_file) {
355     return;
356   }
357 
358   (void) xasprintf(nbuf, "PMGNCSM,%02X", osum);
359   unsigned int nsum = mag_checksum(nbuf.data());
360   int i = xasprintf(obuf, "$%s*%02X\r\n",nbuf.data(), nsum);
361 
362   if (debug_serial) {
363     warning("ACK WRITE: %s",obuf.data());
364   }
365   /*
366    * Don't call mag_writemsg here so we don't get into ack feedback
367    * loops.
368    */
369   termwrite(obuf.data(), i);
370 }
371 
372 static void
mag_handon()373 mag_handon()
374 {
375   if (!is_file) {
376     mag_writemsg("PMGNCMD,HANDON");
377   }
378   magrxstate = mrs_handon;
379 
380 }
381 
382 static void
mag_handoff()383 mag_handoff()
384 {
385   if (!is_file) {
386     mag_writemsg("PMGNCMD,HANDOFF");
387   }
388   magrxstate = mrs_handoff;
389 }
390 
391 static void
mag_verparse(char * ibuf)392 mag_verparse(char* ibuf)
393 {
394   int prodid = mm_unknown;
395   char version[1024];
396   pid_to_model_t* pp = pid_to_model;
397 
398   got_version = 1;
399   sscanf(ibuf,"$PMGNVER,%d,%[^,]", &prodid, version);
400 
401   for (pp = pid_to_model; pp->model != mm_unknown; pp++) {
402     if (pp->pid == prodid) {
403       break;
404     }
405   }
406 
407   if (prodid == 37) {
408     broken_sportrak = 1;
409   }
410 
411   switch (pp->model) {
412   case mm_gps315320:
413   case mm_map410:
414     icon_mapping = gps315_icon_table;
415     setshort_length(mkshort_handle, 6);
416     setshort_mustupper(mkshort_handle, 1);
417     mag_cleanse = m315_cleanse;
418     break;
419   case mm_map330:
420   case mm_meridian:
421   case mm_sportrak:
422     icon_mapping = map330_icon_table;
423     setshort_length(mkshort_handle, wpt_len);
424     setshort_mustupper(mkshort_handle, 0);
425     mag_cleanse = m330_cleanse;
426     break;
427   default:
428     fatal(MYNAME ": Unknown receiver type %d, model version '%s'.\n", prodid, version);
429   }
430 }
431 
432 #define IS_TKN(x) (strncmp(ibuf,x, sizeof(x)-1) == 0)
433 
434 static void
mag_readmsg(gpsdata_type objective)435 mag_readmsg(gpsdata_type objective)
436 {
437   char ibuf[512];	/* oliskoli: corrupted data (I've seen descr with a lot
438 				     of escaped FFFFFFFF) may need more size  */
439   int retrycnt = 20;
440 
441 retry:
442   QString gr = termread(ibuf, sizeof(ibuf));
443 
444   if (gr.isEmpty()) {
445     if (!got_version) {
446       /*
447        * The 315 can take up to six seconds to respond to
448        * a VERSION command.   Since this is on startup,
449        * we'll be fairly persistent in retrying.
450        */
451       if (retrycnt--) {
452         goto retry;
453       } else {
454         fatal(MYNAME ": No data received from GPS.\n");
455       }
456     } else {
457       if (is_file)  {
458         found_done = 1;
459       }
460       return;
461     }
462   }
463 
464   /* If column zero isn't a dollar sign, it's not for us */
465   if (ibuf[0] != '$') {
466     fatal(MYNAME ": line doesn't start with '$'.\n");
467   }
468 
469 
470   int isz = strlen(ibuf);
471 
472   if (isz < 5) {
473     if (debug_serial) {
474       warning("SHORT READ %d\n", isz);
475     }
476     return;
477   }
478   mag_error = 0;
479   while (!isprint(ibuf[isz])) {
480     isz--;
481   }
482   char* isump = &ibuf[isz-1];
483   unsigned int isum = strtoul(isump, nullptr,16);
484   if (isum != mag_pchecksum(&ibuf[1], isz-3)) {
485     if (debug_serial) {
486       warning("RXERR %02x/%02x: '%s'\n", isum, mag_pchecksum(&ibuf[1],isz-5), ibuf);
487     }
488     /* Special case receive errors early on. */
489     if (!got_version) {
490       fatal(MYNAME ": bad communication.  Check bit rate.\n");
491     }
492   }
493   if (debug_serial) {
494     warning("READ: %s\n", ibuf);
495   }
496   if (IS_TKN("$PMGNCSM,")) {
497     last_rx_csum = strtoul(&ibuf[9], nullptr, 16);
498     magrxstate = mrs_handon;
499     return;
500   }
501   if (strncmp(ibuf, "$PMGNWPL,", 7) == 0) {
502     Waypoint* wpt = mag_wptparse(ibuf);
503     waypoint_read_count++;
504     if (global_opts.verbose_status) {
505       waypt_status_disp(waypoint_read_count,
506                         waypoint_read_count);
507     }
508 
509     if (extension_hint) {
510       if (extension_hint == WPTDATAMASK) {
511         waypt_add(wpt);
512       } else if (extension_hint == RTEDATAMASK) {
513         rte_wpt_tmp.append(wpt);
514       }
515     } else {
516       switch (objective) {
517       case wptdata:
518         waypt_add(wpt);
519         break;
520       case rtedata:
521         rte_wpt_tmp.append(wpt);
522         break;
523       default:
524         break;
525       }
526     }
527   }
528   if (strncmp(ibuf, "$PMGNTRK,", 7) == 0) {
529     Waypoint* wpt = mag_trkparse(ibuf);
530     /*
531      * Allow lazy allocation of track head.
532      */
533     if (trk_head == nullptr) {
534       /* These tracks don't have names, so derive one
535        * from input filename.
536        */
537 
538       trk_head = new route_head;
539 
540       /* Whack trailing extension if present. */
541       QString s = get_filename(curfname);
542       int idx = s.indexOf('.');
543       if (idx > 0) {
544         s.truncate(idx);
545       }
546 
547       trk_head->rte_name = s;
548       track_add_head(trk_head);
549     }
550 
551     track_add_wpt(trk_head, wpt);
552   }
553   if (strncmp(ibuf, "$PMGNRTE,", 7) == 0) {
554     mag_rteparse(ibuf);
555   }
556   if (IS_TKN("$PMGNVER,")) {
557     mag_verparse(ibuf);
558   }
559   mag_error = 0;
560   if (!ignore_unable && IS_TKN("$PMGNCMD,UNABLE")) {
561     warning("Unable to send\n");
562     found_done = 1;
563     mag_error = 1;
564     ignore_unable = 0;
565     return;
566   }
567   if (IS_TKN("$PMGNCMD,END") || (is_file && (gbfeof(magfile_h)))) {
568     found_done = 1;
569     return;
570   }
571 
572   if (magrxstate != mrs_handoff) {
573     mag_writeack(isum);
574   }
575 }
576 
577 static void* serial_handle = nullptr;
578 
579 static int
terminit(const QString & portname,int create_ok)580 terminit(const QString& portname, int create_ok)
581 {
582   if (gbser_is_serial(qPrintable(portname))) {
583     if (serial_handle = gbser_init(qPrintable(portname)), nullptr != serial_handle) {
584       int rc;
585       if (rc = gbser_set_port(serial_handle, bitrate, 8, 0, 1), gbser_OK != rc) {
586         fatal(MYNAME ": Can't configure port\n");
587       }
588     }
589     is_file = 0;
590     if (serial_handle == nullptr) {
591       fatal(MYNAME ": Could not open serial port %s\n", qPrintable(portname));
592     }
593     return 1;
594   } else {
595     /* Does this check for an error? */
596     magfile_h = gbfopen(portname, create_ok ? "w+b" : "rb", MYNAME);
597     is_file = 1;
598     icon_mapping = map330_icon_table;
599     mag_cleanse = m330_cleanse;
600     got_version = 1;
601     return 0;
602   }
603 }
604 
termread(char * ibuf,int size)605 static QString termread(char* ibuf, int size)
606 {
607   if (is_file) {
608     return gbfgets(ibuf, size, magfile_h);
609   } else {
610     int rc = gbser_read_line(serial_handle, ibuf, size, 2000, 0x0a, 0x0d);
611     if (rc != gbser_OK) {
612       fatal(MYNAME ": Read error\n");
613     }
614     return ibuf;
615   }
616 }
617 
618 /* Though not documented in the protocol spec, if the unit itself
619  * wants to create a field containing a comma, it will encode it
620  * as <escape>2C.  We extrapolate that any 2 digit hex encoding may
621  * be valid.  We don't do this in termread() since we need to do it
622  * after the scanf.  This means we have to do it field-by-field
623  * basis.
624  *
625  * The buffer is modified in place and shortened by copying the remaining
626  * string including the terminator.
627  */
628 static
629 void
mag_dequote(char * ibuf)630 mag_dequote(char* ibuf)
631 {
632   char* esc = nullptr;
633 
634   while ((esc = strchr(ibuf, 0x1b))) {
635     int nremains = strlen(esc);
636     if (nremains >= 3) {
637       static const char hex[17] = "0123456789ABCDEF";
638       const char* c1 = strchr(hex, esc[1]);
639       const char* c2 = strchr(hex, esc[2]);
640       if (c1 && c2) {
641         int escv = (c1 - hex) * 16 + (c2 - hex);
642         if (escv == 255) {	/* corrupted data */
643           char* tmp = esc + 1;
644           while (*tmp == 'F') {
645             tmp++;
646           }
647           memmove(esc, tmp, strlen(tmp) + 1);
648         } else {
649           *esc++ = (isprint(escv)) ? escv : '$';
650           /* buffers overlap */
651           memmove(esc, esc+2, nremains - 2);
652         }
653       }
654     } else {
655       *esc = '\0';	/* trim corrupted data,
656 					   otherwise we get an endless loop */
657     }
658   }
659 }
660 
661 static void
termwrite(const char * obuf,int size)662 termwrite(const char* obuf, int size)
663 {
664   if (is_file) {
665     size_t nw;
666     if (nw = gbfwrite(obuf, 1, size, magfile_h), nw < (size_t) size) {
667       fatal(MYNAME ": Write error");
668     }
669   } else {
670     int rc;
671     if (rc = gbser_write(serial_handle, obuf, size), rc < 0) {
672       fatal(MYNAME ": Write error");
673     }
674   }
675 }
676 
termdeinit()677 static void termdeinit()
678 {
679   if (is_file) {
680     gbfclose(magfile_h);
681     magfile_h = nullptr;
682   } else {
683     gbser_deinit(serial_handle);
684     serial_handle = nullptr;
685   }
686 }
687 
688 /*
689  *  Arg tables are doubled up so that -? can output appropriate help
690  */
691 static
692 QVector<arglist_t> mag_sargs = {
693   {
694     "deficon", &deficon, "Default icon name", nullptr, ARGTYPE_STRING,
695     ARG_NOMINMAX, nullptr
696   },
697   {
698     "maxcmts", &cmts, "Max number of comments to write (maxcmts=200)",
699     "200", ARGTYPE_INT, ARG_NOMINMAX, nullptr
700   },
701   {
702     "baud", &bs, "Numeric value of bitrate (baud=4800)", "4800",
703     ARGTYPE_INT, ARG_NOMINMAX, nullptr
704   },
705   {
706     "noack", &noack, "Suppress use of handshaking in name of speed",
707     nullptr, ARGTYPE_BOOL, ARG_NOMINMAX, nullptr
708   },
709   {
710     "nukewpt", &nukewpt, "Delete all waypoints", nullptr, ARGTYPE_BOOL,
711     ARG_NOMINMAX, nullptr
712   },
713 };
714 
715 static
716 QVector<arglist_t> mag_fargs = {
717   {
718     "deficon", &deficon, "Default icon name", nullptr, ARGTYPE_STRING,
719     ARG_NOMINMAX, nullptr
720   },
721   {
722     "maxcmts", &cmts, "Max number of comments to write (maxcmts=200)",
723     nullptr, ARGTYPE_INT, ARG_NOMINMAX, nullptr
724   },
725 };
726 
727 /*
728  * The part of the serial init that's common to read and write.
729  */
730 static void
mag_serial_init_common(const QString & portname)731 mag_serial_init_common(const QString& portname)
732 {
733   if (is_file) {
734     return;
735   }
736 
737   mag_handoff();
738   if (!noack && !suppress_ack) {
739     mag_handon();
740   }
741 
742   time_t now = current_time().toTime_t();
743   /*
744    * The 315 can take up to 4.25 seconds to respond to initialization
745    * commands.   Time out on the side of caution.
746    */
747   time_t later = now + 6;
748   got_version = 0;
749   mag_writemsg("PMGNCMD,VERSION");
750 
751   while (!got_version) {
752     mag_readmsg(trkdata);
753     if (current_time().toTime_t() > later) {
754       fatal(MYNAME ": No acknowledgment from GPS on %s\n",
755             qPrintable(portname));
756     }
757   }
758 
759   if ((icon_mapping != gps315_icon_table)) {
760     /*
761      * The 315 can't handle this command, so we set a global
762      * to ignore the NAK on it.
763      */
764     ignore_unable = 1;
765     mag_writemsg("PMGNCMD,NMEAOFF");
766     ignore_unable = 0;
767   }
768 
769   if (nukewpt) {
770     /* The unit will send us an "end" message upon completion */
771     mag_writemsg("PMGNCMD,DELETE,WAYPOINT");
772     mag_readmsg(trkdata);
773     if (!found_done) {
774       fatal(MYNAME ": Unexpected response to waypoint delete command.\n");
775     }
776     found_done = 0;
777   }
778 
779 }
780 static void
mag_rd_init_common(const QString & portname)781 mag_rd_init_common(const QString& portname)
782 {
783   waypoint_read_count = 0;
784   // For Explorist GC, intercept the device access and redirect to GPX.
785   // We actually do the rd_init() inside read as we may have multiple
786   // files that we have to read.
787   if (portname == "usb:") {
788     const char** dlist = os_get_magellan_mountpoints();
789     explorist_info = explorist_ini_get(dlist);
790     if (explorist_info) {
791       gpx_vec = Vecs::Instance().find_vec("gpx");
792     }
793     return;
794   }
795 
796   if (bs) {
797     bitrate=atoi(bs);
798   }
799 
800   if (!mkshort_handle) {
801     mkshort_handle = mkshort_new_handle();
802   }
803 
804   terminit(portname, 0);
805   mag_serial_init_common(portname);
806 
807   rte_wpt_tmp.clear();
808 
809   /* find the location of the tail of the path name,
810    * make a copy of it, then lop off the file extension
811    */
812 
813   curfname = get_filename(portname);
814 
815   /*
816    * I'd rather not derive behaviour from filenames but since
817    * we can't otherwise tell if we should put a WPT on the route
818    * queue or the WPT queue in the presence of (-w -r -t) we
819    * divine a hint from the filename extension when we can.
820    */
821   QString exten = QFileInfo(curfname).suffix();
822   if (exten.length() > 0) {
823     if (0 == exten.compare(QLatin1String("upt"), Qt::CaseInsensitive)) {
824       extension_hint = WPTDATAMASK;
825     } else if (0 == exten.compare(QLatin1String("log"), Qt::CaseInsensitive)) {
826       extension_hint = TRKDATAMASK;
827     } else if (0 == exten.compare(QLatin1String("rte"), Qt::CaseInsensitive)) {
828       extension_hint = RTEDATAMASK;
829     }
830   }
831 
832 }
833 
834 static void
mag_rd_init(const QString & portname)835 mag_rd_init(const QString& portname)
836 {
837   explorist = 0;
838   suppress_ack = 1;
839   mag_rd_init_common(portname);
840 }
841 
842 static void
magX_rd_init(const QString & portname)843 magX_rd_init(const QString& portname)
844 {
845   explorist = 1;
846   mag_rd_init_common(portname);
847 }
848 
849 static void
mag_wr_init_common(const QString & portname)850 mag_wr_init_common(const QString& portname)
851 {
852   suppress_ack = 0;
853   if (bs) {
854     bitrate=atoi(bs);
855   }
856 
857   if (waypt_count() > 500) {
858     fatal(MYNAME ": Meridian/Explorist does not support more than 500 waypoints in one file. Only\n200 waypoints may have comments.\nDecrease the number of waypoints sent.\n");
859   }
860 
861   if (cmts) {
862     wptcmtcnt_max = atoi(cmts);
863   } else {
864     wptcmtcnt_max = MAXCMTCT ;
865   }
866 
867   if (!mkshort_handle) {
868     mkshort_handle = mkshort_new_handle();
869   }
870 
871   terminit(portname, 1);
872   mag_serial_init_common(portname);
873 
874   rte_wpt_tmp.clear();
875 }
876 
877 /*
878  * Entry point for extended (explorist) points.
879  */
880 static void
magX_wr_init(const QString & portname)881 magX_wr_init(const QString& portname)
882 {
883   wpt_len = 20;
884   explorist = 1;
885   mag_wr_init_common(portname);
886   setshort_length(mkshort_handle, wpt_len);
887   setshort_whitespace_ok(mkshort_handle, 1);
888 }
889 
890 static void
mag_wr_init(const QString & portname)891 mag_wr_init(const QString& portname)
892 {
893   explorist = 0;
894   wpt_len = 8;
895   mag_wr_init_common(portname);
896   /*
897    * Whitespace is actually legal, but since waypoint name length is
898    * only 8 bytes, we'll conserve them.
899    */
900 
901   setshort_whitespace_ok(mkshort_handle, 0);
902 }
903 
904 static void
mag_deinit()905 mag_deinit()
906 {
907   if (explorist_info) {
908     explorist_ini_done(explorist_info);
909     return;
910   }
911   mag_handoff();
912   termdeinit();
913   if (mkshort_handle) {
914     mkshort_del_handle(&mkshort_handle);
915   }
916 
917   while (!rte_wpt_tmp.isEmpty()) {
918     delete rte_wpt_tmp.takeFirst();
919   }
920 
921   trk_head = nullptr;
922 
923   curfname.clear();
924 }
925 
926 static void
mag_wr_deinit()927 mag_wr_deinit()
928 {
929   if (explorist) {
930     mag_writemsg("PMGNCMD,END");
931   }
932   mag_deinit();
933 }
934 
935 /*
936  * I'm tired of arguing with scanf about optional fields .  Detokenize
937  * an incoming string that may contain empty fields.
938  *
939  * Probably should be cleaned up and moved to common code, but
940  * making it deal with an arbitrary number of fields of arbitrary
941  * size is icky.  We don't have to solve the general case here...
942  */
943 
944 static char ifield[20][100];
945 static
parse_istring(char * istring)946 void parse_istring(char* istring)
947 {
948   int f = 0;
949   int n;
950   while (istring[0]) {
951     char* fp = ifield[f];
952     int x = sscanf(istring, "%[^,]%n", fp, &n);
953     f++;
954     if (x) {
955       istring += n;
956       /* IF more in this string, skip delim */
957       if (istring[0]) {
958         istring++;
959       }
960     } else {
961       istring ++;
962     }
963   }
964 }
965 
966 /*
967  * Given an incoming track messages of the form:
968  * $PMGNTRK,3605.259,N,08644.389,W,00151,M,201444.61,A,,020302*66
969  * create and return a populated waypoint.
970  */
971 Waypoint*
mag_trkparse(char * trkmsg)972 mag_trkparse(char* trkmsg)
973 {
974   int hms;
975   int fracsecs;
976   struct tm tm;
977 
978   auto* waypt = new Waypoint;
979 
980   memset(&tm, 0, sizeof(tm));
981 
982   /*
983    * As some of the fields are optional, sscanf works badly
984    * for us.
985    */
986   parse_istring(trkmsg);
987   double latdeg = atof(ifield[1]);
988   char latdir = ifield[2][0];
989   double lngdeg = atof(ifield[3]);
990   char lngdir = ifield[4][0];
991   int alt = atof(ifield[5]);
992   char altunits = ifield[6][0];
993   (void)altunits;
994   sscanf(ifield[7], "%d.%d", &hms, &fracsecs);
995   /* Field 8 is constant */
996   /* Field nine is optional track name */
997   int dmy = atoi(ifield[10]);
998 
999   tm.tm_sec = hms % 100;
1000   hms = hms / 100;
1001   tm.tm_min = hms % 100;
1002   hms = hms / 100;
1003   tm.tm_hour = hms % 100;
1004 
1005   tm.tm_year = 100 + dmy % 100;
1006   dmy = dmy / 100;
1007   tm.tm_mon =  dmy % 100 - 1;
1008   dmy = dmy / 100;
1009   tm.tm_mday = dmy % 100;
1010 
1011   waypt->SetCreationTime(mkgmtime(&tm), 10.0 * fracsecs);
1012 
1013   if (latdir == 'S') {
1014     latdeg = -latdeg;
1015   }
1016   waypt->latitude = ddmm2degrees(latdeg);
1017 
1018   if (lngdir == 'W') {
1019     lngdeg = -lngdeg;
1020   }
1021   waypt->longitude = ddmm2degrees(lngdeg);
1022 
1023   waypt->altitude = alt;
1024 
1025   return waypt;
1026 
1027 }
1028 
1029 /*
1030  * Given an incoming route messages of the form:
1031  * $PMGNRTE,4,1,c,1,DAD,a,Anna,a*61
1032  * generate a route.
1033  */
1034 void
mag_rteparse(char * rtemsg)1035 mag_rteparse(char* rtemsg)
1036 {
1037   int n;
1038   int frags,frag,rtenum;
1039   char xbuf[100],next_stop[100],abuf[100];
1040   char* currtemsg;
1041   static mag_rte_head_t* mag_rte_head;
1042   char* p;
1043 
1044 #if 0
1045   sscanf(rtemsg,"$PMGNRTE,%d,%d,%c,%d%n",
1046          &frags,&frag,xbuf,&rtenum,&n);
1047 #else
1048   sscanf(rtemsg,"$PMGNRTE,%d,%d,%c,%d%n",
1049          &frags,&frag,xbuf,&rtenum,&n);
1050 
1051   /* Explorist has a route name here */
1052   QString rte_name;
1053   if (explorist) {
1054     char* ca = rtemsg + n;
1055     is_fatal(*ca++ != ',', MYNAME ": Incorrectly formatted route line '%s'", rtemsg);
1056 
1057     char* ce = strchr(ca, ',');
1058     is_fatal(ce == nullptr, MYNAME ": Incorrectly formatted route line '%s'", rtemsg);
1059 
1060     if (ca == ce) {
1061       rte_name = "Route";
1062       rte_name += QString::number(rtenum);
1063     } else {
1064       rte_name = ca;
1065       rte_name.truncate(ce-ca);
1066     }
1067 
1068     n += ((ce - ca) + 1);
1069   }
1070 
1071 #endif
1072 
1073   /*
1074    * This is the first component of a route.  Allocate a new
1075    * head.
1076    */
1077   if (frag == 1) {
1078     mag_rte_head = new mag_rte_head_t;
1079     mag_rte_head->nelems = frags;
1080   }
1081 
1082   currtemsg = rtemsg + n;
1083 
1084   /*
1085    * The individual line may contain several route elements.
1086    * loop and pick those up.
1087    */
1088   while (sscanf(currtemsg,",%[^,],%[^,]%n",next_stop, abuf,&n)) {
1089     if ((next_stop[0] == 0) || (next_stop[0] == '*')) {
1090       break;
1091     }
1092 
1093     /* trim CRC from waypoint icon string */
1094     if ((p = strchr(abuf, '*')) != nullptr) {
1095       *p = '\0';
1096     }
1097 
1098     auto* rte_elem = new mag_rte_elem;
1099 
1100     rte_elem->wpt_name = next_stop;
1101     rte_elem->wpt_icon = abuf;
1102 
1103     mag_rte_head->elem_list.append(rte_elem);
1104 
1105     /* Sportrak (the non-mapping unit) creates malformed
1106      * RTE sentence with no icon info after the routepoint
1107      * name.  So if we saw an "icon" treat that as new
1108      * routepoint.
1109      */
1110     if (broken_sportrak && abuf[0]) {
1111       rte_elem = new mag_rte_elem;
1112       rte_elem->wpt_name = abuf;
1113 
1114       mag_rte_head->elem_list.append(rte_elem);
1115     }
1116 
1117     next_stop[0] = 0;
1118     currtemsg += n;
1119   }
1120 
1121   /*
1122    * If this was the last fragment of the route, add it to the
1123    * gpsbabel internal structs now.
1124    */
1125   if (frag == mag_rte_head->nelems) {
1126 
1127     auto* rte_head = new route_head;
1128     route_add_head(rte_head);
1129     rte_head->rte_num = rtenum;
1130     rte_head->rte_name = rte_name;
1131 
1132     /*
1133      * It is quite feasible that we have 200 waypoints,
1134      * 3 of which are used in the route.  We'll need to find
1135      * those in the queue for SD routes...
1136      */
1137 
1138     while (!mag_rte_head->elem_list.isEmpty()) {
1139       mag_rte_elem* re = mag_rte_head->elem_list.takeFirst();
1140 
1141       /*
1142        * Copy route points from temp wpt queue.
1143        */
1144       foreach (const Waypoint* waypt, rte_wpt_tmp) {
1145         if (waypt->shortname == re->wpt_name) {
1146           auto* wpt = new Waypoint(*waypt);
1147           route_add_wpt(rte_head, wpt);
1148           break;
1149         }
1150       }
1151 
1152       delete re;
1153     }
1154     delete mag_rte_head;
1155   }
1156 }
1157 
1158 QString
mag_find_descr_from_token(const char * token)1159 mag_find_descr_from_token(const char* token)
1160 {
1161   if (icon_mapping == nullptr) {
1162     return "unknown";
1163   }
1164 
1165   for (const magellan_icon_mapping_t* i = icon_mapping; i->token; i++) {
1166     if (token[0] == 0) {
1167       break;
1168     }
1169     if (case_ignore_strcmp(token, i->token) == 0) {
1170       return i->icon;
1171     }
1172   }
1173   return icon_mapping[0].icon;
1174 }
1175 
1176 QString
mag_find_token_from_descr(const QString & icon)1177 mag_find_token_from_descr(const QString& icon)
1178 {
1179   const magellan_icon_mapping_t* i = icon_mapping;
1180 
1181   if (i == nullptr || icon == nullptr) {
1182     return "a";
1183   }
1184 
1185   for (i = icon_mapping; i->token; i++) {
1186     if (icon.compare(i->icon, Qt::CaseInsensitive) == 0) {
1187       return i->token;
1188     }
1189   }
1190   return icon_mapping[0].token;
1191 }
1192 
1193 /*
1194  * Given an incoming waypoint messages of the form:
1195  * $PMGNWPL,3549.499,N,08650.827,W,0000257,M,HOME,HOME,c*4D
1196  * create and return a populated waypoint.
1197  */
1198 static Waypoint*
mag_wptparse(char * trkmsg)1199 mag_wptparse(char* trkmsg)
1200 {
1201   double latdeg, lngdeg;
1202   char latdir;
1203   char lngdir;
1204   int alt;
1205   char altunits;
1206   char shortname[100];
1207   char descr[256];
1208   char icon_token[100];
1209   int i = 0;
1210 
1211   descr[0] = 0;
1212   icon_token[0] = 0;
1213 
1214   auto* waypt = new Waypoint;
1215 
1216   sscanf(trkmsg,"$PMGNWPL,%lf,%c,%lf,%c,%d,%c,%[^,],%[^,]",
1217          &latdeg,&latdir,
1218          &lngdeg,&lngdir,
1219          &alt,&altunits,shortname,descr);
1220   char* icone = strrchr(trkmsg, '*');
1221   char* icons = strrchr(trkmsg, ',')+1;
1222 
1223   mag_dequote(descr);
1224 
1225   for (char* blah = icons ; blah < icone; blah++) {
1226     icon_token[i++] = *blah;
1227   }
1228   icon_token[i++] = '\0';
1229 
1230   if (latdir == 'S') {
1231     latdeg = -latdeg;
1232   }
1233   waypt->latitude = ddmm2degrees(latdeg);
1234 
1235   if (lngdir == 'W') {
1236     lngdeg = -lngdeg;
1237   }
1238   waypt->longitude = ddmm2degrees(lngdeg);
1239 
1240   waypt->altitude = alt;
1241   waypt->shortname = shortname;
1242   waypt->description = descr;
1243   waypt->icon_descr = mag_find_descr_from_token(icon_token);
1244 
1245   return waypt;
1246 }
1247 
1248 static void
mag_read()1249 mag_read()
1250 {
1251   if (gpx_vec) {
1252     QStringList f = os_gpx_files(explorist_info->track_path);
1253     for (const auto& file : qAsConst(f)) {
1254       gpx_vec->rd_init(file);
1255       gpx_vec->read();
1256       gpx_vec->rd_deinit();
1257     }
1258 
1259     f = os_gpx_files(explorist_info->waypoint_path);
1260     for (const auto& file : qAsConst(f)) {
1261       gpx_vec->rd_init(file);
1262       gpx_vec->read();
1263       gpx_vec->rd_deinit();
1264     }
1265 #if 0
1266     f = os_gpx_files(explorist_info->geo_path);
1267     for (const auto& file : qAsConst(f)) {
1268       gpx_vec->rd_init(file);
1269       gpx_vec->read();
1270       gpx_vec->rd_deinit();
1271     }
1272 #endif
1273     return;
1274   }
1275 
1276   found_done = 0;
1277   if (global_opts.masked_objective & TRKDATAMASK) {
1278     magrxstate = mrs_handoff;
1279     if (!is_file) {
1280       mag_writemsg("PMGNCMD,TRACK,2");
1281     }
1282 
1283     while (!found_done) {
1284       mag_readmsg(trkdata);
1285     }
1286   }
1287 
1288   found_done = 0;
1289   if (global_opts.masked_objective & WPTDATAMASK) {
1290     magrxstate = mrs_handoff;
1291     if (!is_file) {
1292       mag_writemsg("PMGNCMD,WAYPOINT");
1293     }
1294 
1295     while (!found_done) {
1296       mag_readmsg(wptdata);
1297     }
1298   }
1299 
1300   found_done = 0;
1301   if (global_opts.masked_objective & RTEDATAMASK) {
1302     magrxstate = mrs_handoff;
1303     if (!is_file) {
1304       /*
1305        * serial routes require waypoint & routes
1306        * messages commands.
1307        */
1308       mag_writemsg("PMGNCMD,WAYPOINT");
1309 
1310       while (!found_done) {
1311         mag_readmsg(rtedata);
1312       }
1313 
1314       mag_writemsg("PMGNCMD,ROUTE");
1315 
1316       found_done = 0;
1317       while (!found_done) {
1318         mag_readmsg(rtedata);
1319       }
1320     } else {
1321       /*
1322        * SD routes are a stream of PMGNWPL and
1323        * PMGNRTE messages, in that order.
1324        */
1325       while (!found_done) {
1326         mag_readmsg(rtedata);
1327       }
1328     }
1329   }
1330 }
1331 
1332 static
1333 void
mag_waypt_pr(const Waypoint * waypointp)1334 mag_waypt_pr(const Waypoint* waypointp)
1335 {
1336   QScopedPointer<char, QScopedPointerPodDeleter> obuf;
1337   QScopedPointer<char, QScopedPointerPodDeleter> ofmtdesc;
1338   QString icon_token;
1339 
1340   double ilat = waypointp->latitude;
1341   double ilon = waypointp->longitude;
1342 
1343   double lon = fabs(ilon);
1344   double lat = fabs(ilat);
1345 
1346   int lon_deg = lon;
1347   int lat_deg = lat;
1348 
1349   lon = (lon - lon_deg) * 60.0;
1350   lat = (lat - lat_deg) * 60.0;
1351 
1352   lon = (lon_deg * 100.0 + lon);
1353   lat = (lat_deg * 100.0 + lat);
1354 
1355   if (deficon)  {
1356     icon_token = mag_find_token_from_descr(deficon);
1357   } else {
1358     icon_token = mag_find_token_from_descr(waypointp->icon_descr);
1359   }
1360 
1361   if (get_cache_icon(waypointp)) {
1362     icon_token = mag_find_token_from_descr(get_cache_icon(waypointp));
1363   }
1364 
1365   QString isrc = waypointp->notes.isEmpty() ? waypointp->description : waypointp->notes;
1366   QString owpt = global_opts.synthesize_shortnames ?
1367          mkshort_from_wpt(mkshort_handle, waypointp) : waypointp->shortname;
1368   QString odesc = isrc;
1369   owpt = mag_cleanse(CSTRc(owpt));
1370 
1371   if (global_opts.smart_icons &&
1372       waypointp->gc_data->diff && waypointp->gc_data->terr) {
1373     // It's a string and compactness counts, so "1.0" is OK to be "10".
1374     xasprintf(ofmtdesc, "%ud/%ud %s", waypointp->gc_data->diff,
1375             waypointp->gc_data->terr, CSTRc(odesc));
1376     odesc = mag_cleanse(ofmtdesc.data());
1377   } else {
1378     odesc = mag_cleanse(CSTRc(odesc));
1379   }
1380 
1381   /*
1382    * For the benefit of DirectRoute (which uses waypoint comments
1383    * to deliver turn-by-turn popups for street routing) allow a
1384    * cap on the comments delivered so we leave space for it to route.
1385    */
1386   if (!odesc.isEmpty() && (wptcmtcnt++ >= wptcmtcnt_max)) {
1387     odesc.clear();
1388   }
1389 
1390   xasprintf(obuf, "PMGNWPL,%4.3f,%c,%09.3f,%c,%07.0f,M,%-.*s,%-.46s,%s",
1391           lat, ilat < 0 ? 'S' : 'N',
1392           lon, ilon < 0 ? 'W' : 'E',
1393           waypointp->altitude == unknown_alt ?
1394           0 : waypointp->altitude,
1395           wpt_len,
1396           CSTRc(owpt),
1397           CSTRc(odesc),
1398           CSTR(icon_token));
1399   mag_writemsg(obuf.data());
1400 
1401   if (!is_file) {
1402     if (mag_error) {
1403       warning("Protocol error Writing '%s'\n", obuf.data());
1404     }
1405   }
1406 }
1407 
1408 static
mag_track_disp(const Waypoint * waypointp)1409 void mag_track_disp(const Waypoint* waypointp)
1410 {
1411   QScopedPointer<char, QScopedPointerPodDeleter> obuf;
1412 
1413   double ilat = waypointp->latitude;
1414   double ilon = waypointp->longitude;
1415 
1416   QByteArray dmy("");
1417   QByteArray hms("");
1418   if (waypointp->creation_time.isValid()) {
1419     // Round to hundredths of seconds before conversion to string.
1420     // Rounding can ripple all the way from the msec to the year.
1421     QDateTime dt = waypointp->GetCreationTime().toUTC();
1422     dt = dt.addMSecs(10 * lround(dt.time().msec()/10.0) - dt.time().msec());
1423     assert((dt.time().msec() % 10) == 0);
1424     dmy = dt.toString("ddMMyy").toUtf8();
1425     hms = dt.toString("hhmmss.zzz").left(9).toUtf8();
1426   }
1427 
1428   double lon = fabs(ilon);
1429   double lat = fabs(ilat);
1430 
1431   int lon_deg = lon;
1432   int lat_deg = lat;
1433 
1434   lon = (lon - lon_deg) * 60.0;
1435   lat = (lat - lat_deg) * 60.0;
1436 
1437   lon = (lon_deg * 100.0 + lon);
1438   lat = (lat_deg * 100.0 + lat);
1439 
1440   xasprintf(obuf,"PMGNTRK,%4.3f,%c,%09.3f,%c,%05.0f,%c,%s,A,,%s",
1441           lat, ilat < 0 ? 'S' : 'N',
1442           lon, ilon < 0 ? 'W' : 'E',
1443           waypointp->altitude == unknown_alt ?
1444           0 : waypointp->altitude,
1445           'M', hms.constData(), dmy.constData());
1446   mag_writemsg(obuf.data());
1447 }
1448 
1449 static
mag_track_pr()1450 void mag_track_pr()
1451 {
1452   track_disp_all(nullptr, nullptr, mag_track_disp);
1453 }
1454 
1455 /*
1456 The spec says to stack points:
1457 	$PMGNRTE,2,1,c,1,FOO,POINT1,b,POINT2,c,POINT3,d*6C<CR><LF>
1458 
1459 Meridian SD card and serial (at least) writes in pairs:
1460 	$PMGNRTE,4,1,c,1,HOME,c,I49X73,a*15
1461 	...
1462 	$PMGNRTE,4,4,c,1,RON273,a,MYCF93,a*7B
1463 
1464 The spec also says that some units don't like single-legged pairs,
1465 and to replace the 2nd name with "<<>>", but I haven't seen one of those.
1466 */
1467 
1468 static void
mag_route_trl(const route_head * rte)1469 mag_route_trl(const route_head* rte)
1470 {
1471   QScopedPointer<char, QScopedPointerPodDeleter> obuff;
1472   QString buff1;
1473   QString buff2;
1474   QString* pbuff;
1475   QString icon_token;
1476 
1477   /* count waypoints for this route */
1478   int i = rte->rte_waypt_ct;
1479 
1480   /* number of output PMGNRTE messages at 2 points per line */
1481   int numlines = (i / 2) + (i % 2);
1482 
1483   /* increment the route counter. */
1484   route_out_count++;
1485 
1486   int thisline = i = 0;
1487   foreach (const Waypoint* waypointp, rte->waypoint_list) {
1488     i++;
1489 
1490     if (deficon) {
1491       icon_token = mag_find_token_from_descr(deficon);
1492     } else {
1493       icon_token = mag_find_token_from_descr(waypointp->icon_descr);
1494     }
1495 
1496     if (i == 1) {
1497       pbuff = &buff1;
1498     } else {
1499       pbuff = &buff2;
1500     }
1501     // Write name, icon tuple into alternating buff1/buff2 buffer.
1502     *pbuff = waypointp->shortname + ',' + icon_token;
1503 
1504     if ((waypointp == rte->waypoint_list.back()) || ((i % 2) == 0)) {
1505       QString expbuf;
1506       thisline++;
1507       if (explorist) {
1508         expbuf = rte->rte_name + ',';
1509       }
1510 
1511       xasprintf(obuff, "PMGNRTE,%d,%d,c,%d,%s%s,%s",
1512               numlines, thisline,
1513               rte->rte_num ? rte->rte_num : route_out_count,
1514               CSTRc(expbuf),
1515               CSTR(buff1), CSTR(buff2));
1516 
1517       mag_writemsg(obuff.data());
1518       buff1.clear();
1519       buff2.clear();
1520       i = 0;
1521     }
1522   }
1523 }
1524 
1525 static void
mag_route_pr()1526 mag_route_pr()
1527 {
1528   route_out_count = 0;
1529   route_disp_all(nullptr, mag_route_trl, mag_waypt_pr);
1530 
1531 }
1532 
1533 static void
mag_write()1534 mag_write()
1535 {
1536 
1537   wptcmtcnt = 0;
1538 
1539   switch (global_opts.objective) {
1540   case trkdata:
1541     mag_track_pr();
1542     break;
1543   case wptdata:
1544     waypt_disp_all(mag_waypt_pr);
1545     break;
1546   case rtedata:
1547     mag_route_pr();
1548     break;
1549   default:
1550     fatal(MYNAME ": Unknown objective.\n");
1551   }
1552 }
1553 
os_get_magellan_mountpoints()1554 const char** os_get_magellan_mountpoints()
1555 {
1556 #if __APPLE__
1557   const char** dlist = (const char**) xcalloc(2, sizeof *dlist);
1558   dlist[0] = xstrdup("/Volumes/Magellan");
1559   dlist[1] = nullptr;
1560   return dlist;
1561 #else
1562   fatal("Not implemented");
1563   return nullptr;
1564 #endif
1565 }
1566 
1567 static QStringList
os_gpx_files(const char * dirname)1568 os_gpx_files(const char* dirname)
1569 {
1570   QDir dir(dirname);
1571 
1572   const QFileInfoList filist = dir.entryInfoList(QStringList("*.gpx"), QDir::Files | QDir::Readable, QDir::Name);
1573   QStringList rv;
1574   for (const auto& fi : filist) {
1575     rv.append(fi.absoluteFilePath());
1576   }
1577   return rv;
1578 }
1579 
1580 /*
1581  *  This is repeated just so it shows up as separate menu options
1582  *  for the benefit of GUI wrappers.
1583  */
1584 ff_vecs_t mag_svecs = {
1585   ff_type_serial,
1586   FF_CAP_RW_ALL,
1587   mag_rd_init,
1588   mag_wr_init,
1589   mag_deinit,
1590   mag_deinit,
1591   mag_read,
1592   mag_write,
1593   nullptr,
1594   &mag_sargs,
1595   CET_CHARSET_ASCII, 0,	/* CET-REVIEW */
1596   NULL_POS_OPS,
1597   nullptr,
1598 };
1599 
1600 ff_vecs_t mag_fvecs = {
1601   ff_type_file,
1602   FF_CAP_RW_ALL,
1603   mag_rd_init,
1604   mag_wr_init,
1605   mag_deinit,
1606   mag_deinit,
1607   mag_read,
1608   mag_write,
1609   nullptr,
1610   &mag_fargs,
1611   CET_CHARSET_ASCII, 0,	/* CET-REVIEW */
1612   NULL_POS_OPS,
1613   nullptr,
1614 };
1615 
1616 /*
1617  * Extended (Explorist) entry tables.
1618  */
1619 ff_vecs_t magX_fvecs = {
1620   ff_type_file,
1621   FF_CAP_RW_ALL,
1622   magX_rd_init,
1623   magX_wr_init,
1624   mag_deinit,
1625   mag_wr_deinit,
1626   mag_read,
1627   mag_write,
1628   nullptr,
1629   &mag_fargs,
1630   CET_CHARSET_ASCII, 0,	/* CET-REVIEW */
1631   NULL_POS_OPS,
1632   nullptr,
1633 };
1634