1 /*
2  * monitor_nmea0183.c - gpsmon support for NMEA devices.
3  *
4  * To do: Support for GPGLL, GPGBS, GPZDA, PASHR NMEA sentences.
5  *
6  * This file is Copyright (c) 2010-2018 by the GPSD project
7  * SPDX-License-Identifier: BSD-2-clause
8  */
9 
10 #include "gpsd_config.h"  /* must be before all includes */
11 
12 #include <assert.h>
13 #include <math.h>
14 #include <stdarg.h>
15 #include <stdio.h>
16 #include <stdlib.h> /* for labs() */
17 #include <string.h>
18 #include <unistd.h>
19 
20 #include "gpsd.h"
21 #include "gpsmon.h"
22 #include "gpsdclient.h"
23 #include "strfuncs.h"
24 
25 #ifdef NMEA0183_ENABLE
26 extern const struct gps_type_t driver_nmea0183;
27 
28 static WINDOW *cookedwin, *nmeawin, *satwin, *gprmcwin;
29 static WINDOW *gpggawin, *gpgsawin, *gpgstwin;
30 static timespec_t last_tick, tick_interval;
31 static char sentences[NMEA_MAX * 2];
32 
33 /*****************************************************************************
34  *
35  * NMEA0183 support
36  *
37  *****************************************************************************/
38 
39 #define SENTENCELINE    1       /* index of sentences line in the NMEA window */
40 #define MAXSATS         12      /* max satellites we can display */
41 
nmea_initialize(void)42 static bool nmea_initialize(void)
43 {
44 
45     cookedwin = derwin(devicewin, 3, 80, 0, 0);
46     assert(cookedwin !=NULL);
47     (void)wborder(cookedwin, 0, 0, 0, 0, 0, 0, 0, 0);
48     (void)syncok(cookedwin, true);
49     (void)wattrset(cookedwin, A_BOLD);
50     (void)mvwaddstr(cookedwin, 1, 1, "Time: ");
51     (void)mvwaddstr(cookedwin, 1, 32, "Lat: ");
52     (void)mvwaddstr(cookedwin, 1, 55, "Lon: ");
53     (void)mvwaddstr(cookedwin, 2, 34, " Cooked TPV ");
54     (void)wattrset(cookedwin, A_NORMAL);
55 
56     nmeawin = derwin(devicewin, 3, 80, 3, 0);
57     assert(nmeawin !=NULL);
58     (void)wborder(nmeawin, 0, 0, 0, 0, 0, 0, 0, 0);
59     (void)syncok(nmeawin, true);
60     (void)wattrset(nmeawin, A_BOLD);
61     (void)mvwaddstr(nmeawin, 2, 34, " Sentences ");
62     (void)wattrset(nmeawin, A_NORMAL);
63 
64     satwin = derwin(devicewin, MAXSATS + 3, 20, 6, 0);
65     assert(satwin !=NULL);
66     (void)wborder(satwin, 0, 0, 0, 0, 0, 0, 0, 0), (void)syncok(satwin, true);
67     (void)wattrset(satwin, A_BOLD);
68     (void)mvwprintw(satwin, 1, 1, "PRN  Az El S/N");
69     (void)mvwprintw(satwin, 14, 7, " GSV ");
70     (void)wattrset(satwin, A_NORMAL);
71 
72     gprmcwin = derwin(devicewin, 9, 30, 6, 20);
73     assert(gprmcwin !=NULL);
74     (void)wborder(gprmcwin, 0, 0, 0, 0, 0, 0, 0, 0),
75         (void)syncok(gprmcwin, true);
76     (void)wattrset(gprmcwin, A_BOLD);
77     (void)mvwprintw(gprmcwin, 1, 1, "Time: ");
78     (void)mvwprintw(gprmcwin, 2, 1, "Latitude: ");
79     (void)mvwprintw(gprmcwin, 3, 1, "Longitude: ");
80     (void)mvwprintw(gprmcwin, 4, 1, "Speed: ");
81     (void)mvwprintw(gprmcwin, 5, 1, "Course: ");
82     (void)mvwprintw(gprmcwin, 6, 1, "Status:            FAA: ");
83     (void)mvwprintw(gprmcwin, 7, 1, "MagVar: ");
84     (void)mvwprintw(gprmcwin, 8, 12, " RMC ");
85     (void)wattrset(gprmcwin, A_NORMAL);
86 
87     gpgsawin = derwin(devicewin, 6, 30, 15, 20);
88     assert(gpgsawin !=NULL);
89     (void)wborder(gpgsawin, 0, 0, 0, 0, 0, 0, 0, 0);
90     (void)syncok(gpgsawin, true);
91     (void)wattrset(gpgsawin, A_BOLD);
92 #define MODE_LINE       1
93     (void)mvwprintw(gpgsawin, MODE_LINE, 1, "Mode: ");
94 #define SATS_LINE       1
95 #define SATS_COL        10
96     (void)mvwprintw(gpgsawin, SATS_LINE, SATS_COL, "Sats: ");
97 #define DOP_LINE        2
98     (void)mvwprintw(gpgsawin, DOP_LINE, 1, "DOP: H=      V=      P=");
99 #define TOFF_LINE       3
100     (void)mvwprintw(gpgsawin, TOFF_LINE, 1, "TOFF: ");
101     (void)mvwaddstr(gpgsawin, TOFF_LINE, 7, "N/A");
102 #define PPS_LINE        4
103     (void)mvwprintw(gpgsawin, PPS_LINE, 1, "PPS: ");
104     (void)mvwaddstr(gpgsawin, PPS_LINE, 6, "N/A");
105     (void)mvwprintw(gpgsawin, 5, 9, " GSA + PPS ");
106     (void)wattrset(gpgsawin, A_NORMAL);
107     (void)syncok(gpgsawin, true);
108 
109     gpggawin = derwin(devicewin, 9, 30, 6, 50);
110     assert(gpggawin !=NULL);
111     (void)wborder(gpggawin, 0, 0, 0, 0, 0, 0, 0, 0);
112     (void)syncok(gpggawin, true);
113     (void)wattrset(gpggawin, A_BOLD);
114     (void)mvwprintw(gpggawin, 1, 1, "Time: ");
115     (void)mvwprintw(gpggawin, 2, 1, "Latitude: ");
116     (void)mvwprintw(gpggawin, 3, 1, "Longitude: ");
117     (void)mvwprintw(gpggawin, 4, 1, "Altitude: ");
118     (void)mvwprintw(gpggawin, 5, 1, "Quality:       Sats: ");
119     (void)mvwprintw(gpggawin, 6, 1, "HDOP: ");
120     (void)mvwprintw(gpggawin, 7, 1, "Geoid: ");
121     (void)mvwprintw(gpggawin, 8, 12, " GGA ");
122     (void)wattrset(gpggawin, A_NORMAL);
123 
124     gpgstwin = derwin(devicewin, 6, 30, 15, 50);
125     assert(gpgstwin !=NULL);
126     (void)wborder(gpgstwin, 0, 0, 0, 0, 0, 0, 0, 0);
127     (void)syncok(gpgstwin, true);
128     (void)wattrset(gpgstwin, A_BOLD);
129     (void)mvwprintw(gpgstwin, 1,  1, "UTC: ");
130     (void)mvwprintw(gpgstwin, 1, 16, "RMS: ");
131     (void)mvwprintw(gpgstwin, 2,  1, "MAJ: ");
132     (void)mvwprintw(gpgstwin, 2, 16, "MIN: ");
133     (void)mvwprintw(gpgstwin, 3,  1, "ORI: ");
134     (void)mvwprintw(gpgstwin, 3, 16, "LAT: ");
135     (void)mvwprintw(gpgstwin, 4,  1, "LON: ");
136     (void)mvwprintw(gpgstwin, 4, 16, "ALT: ");
137     (void)mvwprintw(gpgstwin, 5, 12, " GST ");
138     (void)wattrset(gpgstwin, A_NORMAL);
139 
140 
141     (void)clock_gettime(CLOCK_REALTIME, &last_tick);
142 
143     sentences[0] = '\0';
144 
145     return (nmeawin != NULL);
146 }
147 
cooked_pvt(void)148 static void cooked_pvt(void)
149 {
150     char scr[128];
151 
152     if (0 < session.gpsdata.fix.time.tv_sec) {
153         (void)timespec_to_iso8601(session.gpsdata.fix.time, scr, sizeof(scr));
154     } else
155         (void)snprintf(scr, sizeof(scr), "n/a");
156     (void)mvwprintw(cookedwin, 1, 7, "%-24s", scr);
157 
158 
159     if (session.gpsdata.fix.mode >= MODE_2D) {
160         deg_to_str2(deg_ddmm, session.gpsdata.fix.latitude,
161                     scr, sizeof(scr), " N", " S");
162     } else
163         (void)strncpy(scr, "n/a", sizeof(scr));
164     (void)mvwprintw(cookedwin, 1, 37, "%-17s", scr);
165 
166     if (session.gpsdata.fix.mode >= MODE_2D) {
167         deg_to_str2(deg_ddmm, session.gpsdata.fix.longitude,
168                     scr, sizeof(scr), " E", " W");
169     } else
170         (void)strncpy(scr, "n/a", sizeof(scr));
171     (void)mvwprintw(cookedwin, 1, 60, "%-17s", scr);
172 }
173 
monitor_satlist(WINDOW * win,int y,int x)174 static void monitor_satlist(WINDOW *win, int y, int x)
175 /* display as much as we can of a satlist in a specified window */
176 {
177     int ymax, xmax;
178     char scr[128];
179     int i;
180 
181     assert(win != NULL);
182     (void)wmove(win, y, x);
183     (void)wclrtoeol(win);
184     scr[0] = '\0';
185     for (i = 0; i < MAXCHANNELS; i++) {
186         if (session.gpsdata.skyview[i].used)
187             str_appendf(scr, sizeof(scr),
188                         "%d ", session.gpsdata.skyview[i].PRN);
189     }
190     getmaxyx(win, ymax, xmax);
191     assert(ymax != 0);  /* suppress compiler warning */
192     (void)mvwaddnstr(win, y, x, scr, xmax - 2 - x);
193     if (strlen(scr) >= (size_t) (xmax - 2)) {
194         (void)mvwaddch(win, y, xmax - 2 - x, (chtype) '.');
195         (void)mvwaddch(win, y, xmax - 3 - x, (chtype) '.');
196         (void)mvwaddch(win, y, xmax - 4 - x, (chtype) '.');
197     }
198     monitor_fixframe(win);
199 }
200 
nmea_update(void)201 static void nmea_update(void)
202 {
203     char **fields;
204 
205     assert(cookedwin != NULL);
206     assert(nmeawin != NULL);
207     assert(gpgsawin != NULL);
208     assert(gpggawin != NULL);
209     assert(gprmcwin != NULL);
210     assert(gpgstwin != NULL);
211 
212     /* can be NULL if packet was overlong */
213     fields = session.nmea.field;
214 
215     if (session.lexer.outbuffer[0] == (unsigned char)'$'
216                 && fields != NULL && fields[0] != NULL) {
217         int ymax, xmax;
218         timespec_t now;
219         timespec_t ts_diff;
220 
221         getmaxyx(nmeawin, ymax, xmax);
222         assert(ymax > 0);
223         if (strstr(sentences, fields[0]) == NULL) {
224             char *s_end = sentences + strlen(sentences);
225             if ((int)(strlen(sentences) + strlen(fields[0])) < xmax - 2) {
226                 *s_end++ = ' ';
227                 (void)strlcpy(s_end, fields[0], NMEA_MAX);
228             } else {
229                 *--s_end = '.';
230                 *--s_end = '.';
231                 *--s_end = '.';
232             }
233             (void)mvwaddstr(nmeawin, SENTENCELINE, 1, sentences);
234         }
235 
236         /*
237          * If the interval between this and last update is
238          * the longest we've seen yet, boldify the corresponding
239          * tag.
240          */
241         (void)clock_gettime(CLOCK_REALTIME, &now);
242         TS_SUB(&ts_diff, &now, &last_tick);
243         if (TS_GZ(&ts_diff) && TS_GT(&ts_diff, &tick_interval)) {
244             char *findme = strstr(sentences, fields[0]);
245 
246             tick_interval = ts_diff;
247             if (findme != NULL) {
248                 (void)mvwchgat(nmeawin, SENTENCELINE, 1, xmax - 13, A_NORMAL, 0,
249                                NULL);
250                 (void)mvwchgat(nmeawin, SENTENCELINE, 1 + (findme - sentences),
251                                (int)strlen(fields[0]), A_BOLD, 0, NULL);
252             }
253         }
254         last_tick = now;
255 
256         if (strcmp(fields[0], "GPGSV") == 0
257             || strcmp(fields[0], "GNGSV") == 0
258             || strcmp(fields[0], "GLGSV") == 0) {
259             int i;
260             int nsats =
261                 (session.gpsdata.satellites_visible <
262                  MAXSATS) ? session.gpsdata.satellites_visible : MAXSATS;
263 
264             for (i = 0; i < nsats; i++) {
265                 (void)wmove(satwin, i + 2, 1);
266                 (void)wprintw(satwin, "%3d %3d%3d %3.0f",
267                               session.gpsdata.skyview[i].PRN,
268                               (int)session.gpsdata.skyview[i].azimuth,
269                               (int)session.gpsdata.skyview[i].elevation,
270                               session.gpsdata.skyview[i].ss);
271             }
272             /* add overflow mark to the display */
273             if (nsats <= MAXSATS)
274                 (void)mvwaddch(satwin, MAXSATS + 2, 18, ACS_HLINE);
275             else
276                 (void)mvwaddch(satwin, MAXSATS + 2, 18, ACS_DARROW);
277         }
278 
279         if (strcmp(fields[0], "GPRMC") == 0
280             || strcmp(fields[0], "GNRMC") == 0
281             || strcmp(fields[0], "GLRMC") == 0) {
282             /* time, lat, lon, course, speed */
283             (void)mvwaddstr(gprmcwin, 1, 12, fields[1]);
284             (void)mvwprintw(gprmcwin, 2, 12, "%12s %s", fields[3], fields[4]);
285             (void)mvwprintw(gprmcwin, 3, 12, "%12s %s", fields[5], fields[6]);
286             (void)mvwaddstr(gprmcwin, 4, 12, fields[7]);
287             (void)mvwaddstr(gprmcwin, 5, 12, fields[8]);
288             /* the status field, FAA code, and magnetic variation */
289             (void)mvwaddstr(gprmcwin, 6, 12, fields[2]);
290             (void)mvwaddstr(gprmcwin, 6, 25, fields[12]);
291             (void)mvwprintw(gprmcwin, 7, 12, "%-5s%s", fields[10],
292                             fields[11]);
293 
294             cooked_pvt();       /* cooked version of TPV */
295         }
296 
297         if (strcmp(fields[0], "GPGSA") == 0
298             || strcmp(fields[0], "GNGSA") == 0
299             || strcmp(fields[0], "GLGSA") == 0) {
300             (void)mvwprintw(gpgsawin, MODE_LINE, 7, "%1s%s", fields[1], fields[2]);
301             monitor_satlist(gpgsawin, SATS_LINE, SATS_COL+6);
302             (void)mvwprintw(gpgsawin, DOP_LINE, 8, "%-5s", fields[16]);
303             (void)mvwprintw(gpgsawin, DOP_LINE, 16, "%-5s", fields[17]);
304             (void)mvwprintw(gpgsawin, DOP_LINE, 24, "%-5s", fields[15]);
305             monitor_fixframe(gpgsawin);
306         }
307 
308         toff_update(gpgsawin, TOFF_LINE, 7);
309 
310         if (strcmp(fields[0], "GPGGA") == 0
311             || strcmp(fields[0], "GNGGA") == 0
312             || strcmp(fields[0], "GLGGA") == 0) {
313             (void)mvwprintw(gpggawin, 1, 12, "%-17s", fields[1]);
314             (void)mvwprintw(gpggawin, 2, 12, "%-17s", fields[2]);
315             (void)mvwprintw(gpggawin, 3, 12, "%-17s", fields[4]);
316             (void)mvwprintw(gpggawin, 4, 12, "%-17s", fields[9]);
317             (void)mvwprintw(gpggawin, 5, 12, "%1.1s", fields[6]);
318             (void)mvwprintw(gpggawin, 5, 22, "%2.2s", fields[7]);
319             (void)mvwprintw(gpggawin, 6, 12, "%-5.5s", fields[8]);
320             (void)mvwprintw(gpggawin, 7, 12, "%-5.5s", fields[11]);
321         }
322         if (strcmp(fields[0], "GPGST") == 0) {
323             (void)mvwprintw(gpgstwin, 1,  6, "%-10s", fields[1]);
324             (void)mvwprintw(gpgstwin, 1, 21,  "%-8s", fields[2]);
325             (void)mvwprintw(gpgstwin, 2,  6, "%-10s", fields[3]);
326             (void)mvwprintw(gpgstwin, 2, 21,  "%-8s", fields[4]);
327             (void)mvwprintw(gpgstwin, 3,  6, "%-10s", fields[5]);
328             (void)mvwprintw(gpgstwin, 3, 21,  "%-8s", fields[6]);
329             (void)mvwprintw(gpgstwin, 4,  6, "%-10s", fields[7]);
330             (void)mvwprintw(gpgstwin, 4, 21,  "%-8s", fields[8]);
331         }
332     }
333 
334     pps_update(gpgsawin, PPS_LINE, 6);
335 }
336 
337 #undef SENTENCELINE
338 
nmea_wrap(void)339 static void nmea_wrap(void)
340 {
341     (void)delwin(nmeawin);
342     (void)delwin(gpgsawin);
343     (void)delwin(gpggawin);
344     (void)delwin(gprmcwin);
345 }
346 
347 const struct monitor_object_t nmea_mmt = {
348     .initialize = nmea_initialize,
349     .update = nmea_update,
350     .command = NULL,
351     .wrap = nmea_wrap,
352     .min_y = 21,.min_x = 80,
353     .driver = &driver_nmea0183,
354 };
355 
356 /*****************************************************************************
357  *
358  * Extended NMEA support
359  *
360  *****************************************************************************/
361 
362 #if defined(CONTROLSEND_ENABLE) && defined(ASHTECH_ENABLE)
monitor_nmea_send(const char * fmt,...)363 static void monitor_nmea_send(const char *fmt, ...)
364 {
365     char buf[BUFSIZ];
366     va_list ap;
367 
368     va_start(ap, fmt);
369     (void)vsnprintf(buf, sizeof(buf) - 5, fmt, ap);
370     va_end(ap);
371     (void)monitor_control_send((unsigned char *)buf, strlen(buf));
372 }
373 #endif /* defined(CONTROLSEND_ENABLE) && defined(ASHTECH_ENABLE) */
374 
375 /*
376  * Yes, it's OK for most of these to be clones of the generic NMEA monitor
377  * object except for the pointer to the GPSD driver.  That pointer makes
378  * a difference, as it will automatically enable stuff like speed-switcher
379  * and mode-switcher commands.  It's really only necessary to write a
380  * separate monitor object if you want to change the device-window
381  * display or implement device-specific commands.
382  */
383 
384 #if defined(GARMIN_ENABLE) && defined(NMEA0183_ENABLE)
385 extern const struct gps_type_t driver_garmin;
386 
387 const struct monitor_object_t garmin_mmt = {
388     .initialize = nmea_initialize,
389     .update = nmea_update,
390     .command = NULL,
391     .wrap = nmea_wrap,
392     .min_y = 21,.min_x = 80,
393     .driver = &driver_garmin,
394 };
395 #endif /* GARMIN_ENABLE && NMEA0183_ENABLE */
396 
397 #ifdef ASHTECH_ENABLE
398 extern const struct gps_type_t driver_ashtech;
399 
400 #define ASHTECH_SPEED_9600 5
401 #define ASHTECH_SPEED_57600 8
402 
403 #ifdef CONTROLSEND_ENABLE
ashtech_command(char line[])404 static int ashtech_command(char line[])
405 {
406     switch (line[0]) {
407     case 'N':                   /* normal = 9600, GGA+GSA+GSV+RMC+ZDA */
408         monitor_nmea_send("$PASHS,NME,ALL,A,OFF");      /* silence outbound chatter */
409         monitor_nmea_send("$PASHS,NME,ALL,B,OFF");
410         monitor_nmea_send("$PASHS,NME,GGA,A,ON");
411         monitor_nmea_send("$PASHS,NME,GSA,A,ON");
412         monitor_nmea_send("$PASHS,NME,GSV,A,ON");
413         monitor_nmea_send("$PASHS,NME,RMC,A,ON");
414         monitor_nmea_send("$PASHS,NME,ZDA,A,ON");
415 
416         monitor_nmea_send("$PASHS,INI,%d,%d,,,0,",
417                           ASHTECH_SPEED_9600, ASHTECH_SPEED_9600);
418         (void)sleep(6);         /* it takes 4-6 sec for the receiver to reboot */
419         monitor_nmea_send("$PASHS,WAS,ON");     /* enable WAAS */
420         break;
421 
422     case 'R':                   /* raw = 57600, normal+XPG+POS+SAT+MCA+PBN+SNV */
423         monitor_nmea_send("$PASHS,NME,ALL,A,OFF");      /* silence outbound chatter */
424         monitor_nmea_send("$PASHS,NME,ALL,B,OFF");
425         monitor_nmea_send("$PASHS,NME,GGA,A,ON");
426         monitor_nmea_send("$PASHS,NME,GSA,A,ON");
427         monitor_nmea_send("$PASHS,NME,GSV,A,ON");
428         monitor_nmea_send("$PASHS,NME,RMC,A,ON");
429         monitor_nmea_send("$PASHS,NME,ZDA,A,ON");
430 
431         monitor_nmea_send("$PASHS,INI,%d,%d,,,0,",
432                           ASHTECH_SPEED_57600, ASHTECH_SPEED_9600);
433         (void)sleep(6);         /* it takes 4-6 sec for the receiver to reboot */
434         monitor_nmea_send("$PASHS,WAS,ON");     /* enable WAAS */
435 
436         monitor_nmea_send("$PASHS,NME,POS,A,ON");       /* Ashtech TPV solution */
437         monitor_nmea_send("$PASHS,NME,SAT,A,ON");       /* Ashtech Satellite status */
438         monitor_nmea_send("$PASHS,NME,MCA,A,ON");       /* MCA measurements */
439         monitor_nmea_send("$PASHS,NME,PBN,A,ON");       /* ECEF TPV solution */
440         monitor_nmea_send("$PASHS,NME,SNV,A,ON,10");    /* Almanac data */
441 
442         monitor_nmea_send("$PASHS,NME,XMG,A,ON");       /* exception messages */
443         break;
444 
445     default:
446         return COMMAND_UNKNOWN;
447     }
448 
449     return COMMAND_UNKNOWN;
450 }
451 #endif /* CONTROLSEND_ENABLE */
452 
453 const struct monitor_object_t ashtech_mmt = {
454     .initialize = nmea_initialize,
455     .update = nmea_update,
456 #ifdef CONTROLSEND_ENABLE
457     .command = ashtech_command,
458 #else
459     .command = NULL,
460 #endif /* CONTROLSEND_ENABLE */
461     .wrap = nmea_wrap,
462     .min_y = 21,.min_x = 80,
463     .driver = &driver_ashtech,
464 };
465 #endif /* ASHTECH_ENABLE */
466 
467 #ifdef FV18_ENABLE
468 extern const struct gps_type_t driver_fv18;
469 
470 const struct monitor_object_t fv18_mmt = {
471     .initialize = nmea_initialize,
472     .update = nmea_update,
473     .command = NULL,
474     .wrap = nmea_wrap,
475     .min_y = 21,.min_x = 80,
476     .driver = &driver_fv18,
477 };
478 #endif /* FV18_ENABLE */
479 
480 #ifdef GPSCLOCK_ENABLE
481 extern const struct gps_type_t driver_gpsclock;
482 
483 const struct monitor_object_t gpsclock_mmt = {
484     .initialize = nmea_initialize,
485     .update = nmea_update,
486     .command = NULL,
487     .wrap = nmea_wrap,
488     .min_y = 21,.min_x = 80,
489     .driver = &driver_gpsclock,
490 };
491 #endif /* GPSCLOCK_ENABLE */
492 
493 #ifdef MTK3301_ENABLE
494 extern const struct gps_type_t driver_mtk3301;
495 
496 const struct monitor_object_t mtk3301_mmt = {
497     .initialize = nmea_initialize,
498     .update = nmea_update,
499     .command = NULL,
500     .wrap = nmea_wrap,
501     .min_y = 21,.min_x = 80,
502     .driver = &driver_mtk3301,
503 };
504 #endif /* MTK3301_ENABLE */
505 
506 #ifdef AIVDM_ENABLE
507 extern const struct gps_type_t driver_aivdm;
508 
509 const struct monitor_object_t aivdm_mmt = {
510     .initialize = nmea_initialize,
511     .update = nmea_update,
512     .command = NULL,
513     .wrap = nmea_wrap,
514     .min_y = 21,.min_x = 80,
515     .driver = &driver_aivdm,
516 };
517 #endif /* AIVDM_ENABLE */
518 #endif /* NMEA0183_ENABLE */
519 
520 /* vim: set expandtab shiftwidth=4: */
521