/*
 * The generic GPS packet monitor.
 *
 * This file is Copyright (c) 2010-2018 by the GPSD project
 * SPDX-License-Identifier: BSD-2-clause
 */

#include "gpsd_config.h"  /* must be before all includes */

#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <setjmp.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include "gpsd.h"
#include "gps_json.h"
#include "gpsmon.h"
#include "gpsdclient.h"
#include "revision.h"
#include "strfuncs.h"
#include "timespec.h"

#define BUFLEN		2048

/* needed under FreeBSD */
#ifndef HOST_NAME_MAX
#define HOST_NAME_MAX	255
#endif /* HOST_NAME_MAX */

/* external capability tables */
extern struct monitor_object_t nmea_mmt, sirf_mmt, ashtech_mmt;
extern struct monitor_object_t garmin_mmt, garmin_bin_ser_mmt;
extern struct monitor_object_t italk_mmt, ubx_mmt, superstar2_mmt;
extern struct monitor_object_t fv18_mmt, gpsclock_mmt, mtk3301_mmt;
extern struct monitor_object_t oncore_mmt, tnt_mmt, aivdm_mmt;
#ifdef NMEA0183_ENABLE
extern const struct gps_type_t driver_nmea0183;
#endif /* NMEA0183_ENABLE */

/* These are public */
struct gps_device_t session;
WINDOW *devicewin;
bool serial;

/* These are private */
static struct gps_context_t context;
static bool curses_active;
static WINDOW *statwin, *cmdwin;
static WINDOW *packetwin;
static FILE *logfile;
static char *type_name;
static size_t promptlen = 0;
static struct termios cooked, rare;
static struct fixsource_t source;
static char hostname[HOST_NAME_MAX];
static struct timedelta_t time_offset;

#ifdef PASSTHROUGH_ENABLE
/* no methods, it's all device window */
extern const struct gps_type_t driver_json_passthrough;
const struct monitor_object_t json_mmt = {
    .initialize = NULL,
    .update = NULL,
    .command = NULL,
    .wrap = NULL,
    .min_y = 0, .min_x = 80,	/* no need for a device window */
    .driver = &driver_json_passthrough,
};
#endif /* PASSTHROUGH_ENABLE */

static const struct monitor_object_t *monitor_objects[] = {
#ifdef NMEA0183_ENABLE
    &nmea_mmt,
#if defined(GARMIN_ENABLE) && defined(NMEA0183_ENABLE)
    &garmin_mmt,
#endif /* GARMIN_ENABLE && NMEA0183_ENABLE */
#if defined(GARMIN_ENABLE) && defined(BINARY_ENABLE)
    &garmin_bin_ser_mmt,
#endif /* defined(GARMIN_ENABLE) && defined(BINARY_ENABLE) */
#ifdef ASHTECH_ENABLE
    &ashtech_mmt,
#endif /* ASHTECH_ENABLE */
#ifdef FV18_ENABLE
    &fv18_mmt,
#endif /* FV18_ENABLE */
#ifdef GPSCLOCK_ENABLE
    &gpsclock_mmt,
#endif /* GPSCLOCK_ENABLE */
#ifdef MTK3301_ENABLE
    &mtk3301_mmt,
#endif /* MTK3301_ENABLE */
#ifdef AIVDM_ENABLE
    &aivdm_mmt,
#endif /* AIVDM_ENABLE */
#endif /* NMEA0183_ENABLE */
#if defined(SIRF_ENABLE) && defined(BINARY_ENABLE)
    &sirf_mmt,
#endif /* defined(SIRF_ENABLE) && defined(BINARY_ENABLE) */
#if defined(UBLOX_ENABLE) && defined(BINARY_ENABLE)
    &ubx_mmt,
#endif /* defined(UBLOX_ENABLE) && defined(BINARY_ENABLE) */
#if defined(ITRAX_ENABLE) && defined(BINARY_ENABLE)
    &italk_mmt,
#endif /* defined(ITALK_ENABLE) && defined(BINARY_ENABLE) */
#if defined(SUPERSTAR2_ENABLE) && defined(BINARY_ENABLE)
    &superstar2_mmt,
#endif /* defined(SUPERSTAR2_ENABLE) && defined(BINARY_ENABLE) */
#if defined(ONCORE_ENABLE) && defined(BINARY_ENABLE)
    &oncore_mmt,
#endif /* defined(ONCORE_ENABLE) && defined(BINARY_ENABLE) */
#ifdef TNT_ENABLE
    &tnt_mmt,
#endif /* TNT_ENABLE */
#ifdef PASSTHROUGH_ENABLE
    &json_mmt,
#endif /* PASSTHROUGH_ENABLE */
    NULL,
};

static const struct monitor_object_t **active;
static const struct gps_type_t *fallback;

static jmp_buf terminate;

#define display	(void)mvwprintw

/* termination codes */
#define TERM_SELECT_FAILED	1
#define TERM_DRIVER_SWITCH	2
#define TERM_EMPTY_READ 	3
#define TERM_READ_ERROR 	4
#define TERM_SIGNAL		5
#define TERM_QUIT		6

/* PPS monitoring */
static inline void report_lock(void)
{
    gpsd_acquire_reporting_lock();
}

static inline void report_unlock(void)
{
    gpsd_release_reporting_lock();
}

#define PPSBAR "-------------------------------------" \
	       " PPS " \
	       "-------------------------------------\n"

/* Dummy conditional for *display* of (possibly remote) PPS events */
#define PPS_DISPLAY_ENABLE 1

/******************************************************************************
 *
 * Visualization helpers
 *
 ******************************************************************************/

static void visibilize(char *buf2, size_t len2, const char *buf)
/* string is mostly printable, dress up the nonprintables a bit */
{
    const char *sp;

    buf2[0] = '\0';
    for (sp = buf; *sp != '\0' && strlen(buf2)+4 < len2; sp++)
	if (isprint((unsigned char) *sp) || (sp[0] == '\n' && sp[1] == '\0')
	  || (sp[0] == '\r' && sp[2] == '\0'))
	    (void)snprintf(buf2 + strlen(buf2), 2, "%c", *sp);
	else
	    (void)snprintf(buf2 + strlen(buf2), 6, "\\x%02x",
			   (unsigned)(*sp & 0xff));
}

static void cond_hexdump(char *buf2, size_t len2,
			 const char *buf, size_t len)
/* pass through visibilized if all printable, hexdump otherwise */
{
    size_t i;
    bool printable = true;
    for (i = 0; i < len; i++)
	if (!isprint((unsigned char) buf[i]) && !isspace((unsigned char) buf[i]))
	    printable = false;
    if (printable) {
	size_t j;
	for (i = j = 0; i < len && j < len2 - 1; i++)
	    if (isprint((unsigned char) buf[i])) {
		buf2[j++] = buf[i];
		buf2[j] = '\0';
	    }
	    else {
		if (TEXTUAL_PACKET_TYPE(session.lexer.type)) {
		    if (i == len - 1 && buf[i] == '\n')
			continue;
		    if (i == len - 2 && buf[i] == '\r')
			continue;
		}
		(void)snprintf(&buf2[j], len2-strlen(buf2), "\\x%02x", (unsigned int)(buf[i] & 0xff));
		j = strlen(buf2);
	    }
    } else {
	buf2[0] = '\0';
	for (i = 0; i < len; i++)
	    str_appendf(buf2, len2, "%02x", (unsigned int)(buf[i] & 0xff));
    }
}

void toff_update(WINDOW *win, int y, int x)
{
    assert(win != NULL);
    if (time_offset.real.tv_sec != 0)
    {
	/* NOTE: can not use double here due to precision requirements */
	struct timespec timedelta;
	int i, ymax, xmax;
	getmaxyx(win, ymax, xmax);
	assert(ymax > 0);  /* squash a compiler warning */
	(void)wmove(win, y, x);
	/*
	 * The magic number shortening the field works because
	 * we know we'll never see more than 5 digits of seconds
	 * rather than 10.
	 */
	for (i = 0; i < TIMESPEC_LEN-4 && x + i < xmax - 1; i++)
	    (void)waddch(win, ' ');
	TS_SUB(&timedelta, &time_offset.clock, &time_offset.real);
        // (long long) for 32-bit CPU with 64-bit time_t
	if ( 86400 < llabs(timedelta.tv_sec) ) {
	    /* more than one day off, overflow */
	    /* need a bigger field to show it */
	    (void)mvwaddstr(win, y, x, "> 1 day");
	} else {
	    char buf[TIMESPEC_LEN];
	    (void)mvwaddstr(win, y, x,
                            timespec_str(&timedelta, buf, sizeof(buf)));
	}
    }
}

/* FIXME:  Decouple this reporting from local PPS monitoring. */
void pps_update(WINDOW *win, int y, int x)
{
    struct timedelta_t ppstimes;

    assert(win != NULL);
    if (pps_thread_ppsout(&session.pps_thread, &ppstimes) > 0) {
	/* NOTE: can not use double here due to precision requirements */
	struct timespec timedelta;
	int i, ymax, xmax;
	getmaxyx(win, ymax, xmax);
	assert(ymax > 0);  /* squash a compiler warning */
	(void)wmove(win, y, x);
	/* see toff_update() for explanation of the magic number */
	for (i = 0; i < TIMESPEC_LEN-4 && x + i < xmax - 1; i++)
	    (void)waddch(win, ' ');
	TS_SUB( &timedelta, &ppstimes.clock, &ppstimes.real);
        // (long long) for 32-bit CPU with 64-bit time_t
        if ( 86400 < llabs(timedelta.tv_sec) ) {
	    /* more than one day off, overflow */
            /* need a bigger field to show it */
	    (void)mvwaddstr(win, y, x, "> 1 day");
        } else {
	    char buf[TIMESPEC_LEN];
	    (void)mvwaddstr(win, y, x,
	                    timespec_str(&timedelta, buf, sizeof(buf)));
        }
	(void)wnoutrefresh(win);
    }
}

/******************************************************************************
 *
 * Curses I/O
 *
 ******************************************************************************/

void monitor_fixframe(WINDOW * win)
{
    int ymax, xmax, ycur, xcur;

    assert(win != NULL);
    getyx(win, ycur, xcur);
    getmaxyx(win, ymax, xmax);
    assert(xcur > -1 && ymax > 0);  /* squash a compiler warning */
    (void)mvwaddch(win, ycur, xmax - 1, ACS_VLINE);
}

#if defined(CONTROLSEND_ENABLE) || defined(RECONFIGURE_ENABLE)
static void packet_dump(const char *buf, size_t buflen)
{
    if (packetwin != NULL) {
	char buf2[MAX_PACKET_LENGTH * 2];
	cond_hexdump(buf2, buflen * 2, buf, buflen);
	(void)waddstr(packetwin, buf2);
	(void)waddch(packetwin, (chtype)'\n');
    }
}

static void monitor_dump_send(const char *buf, size_t len)
{
    if (packetwin != NULL) {
	report_lock();
	(void)wattrset(packetwin, A_BOLD);
	(void)wprintw(packetwin, ">>>");
	packet_dump(buf, len);
	(void)wattrset(packetwin, A_NORMAL);
	report_unlock();
    }
}
#endif /* defined(CONTROLSEND_ENABLE) || defined(RECONFIGURE_ENABLE) */

static void gpsmon_report(const char *buf)
/* log to the packet window if curses is up, otherwise stdout */
{
    /* report locking is left to caller */
    if (!curses_active)
	(void)fputs(buf, stdout);
    else if (packetwin != NULL)
	(void)waddstr(packetwin, buf);
    if (logfile != NULL)
	(void)fputs(buf, logfile);
}

static void packet_vlog(char *buf, size_t len, const char *fmt, va_list ap)
{
    char buf2[BUFSIZ];

    visibilize(buf2, sizeof(buf2), buf);

    report_lock();
    (void)vsnprintf(buf2 + strlen(buf2), len, fmt, ap);
    gpsmon_report(buf2);
    report_unlock();
}

#ifdef RECONFIGURE_ENABLE
static void announce_log(const char *fmt, ...)
{
    char buf[BUFSIZ];
    va_list ap;
    va_start(ap, fmt);
    (void)vsnprintf(buf, sizeof(buf) - 5, fmt, ap);
    va_end(ap);

   if (packetwin != NULL) {
	report_lock();
	(void)wattrset(packetwin, A_BOLD);
	(void)wprintw(packetwin, ">>>");
	(void)waddstr(packetwin, buf);
	(void)wattrset(packetwin, A_NORMAL);
	(void)wprintw(packetwin, "\n");
	report_unlock();
   }
   if (logfile != NULL) {
       (void)fprintf(logfile, ">>>%s\n", buf);
   }
}
#endif /* RECONFIGURE_ENABLE */

static void monitor_vcomplain(const char *fmt, va_list ap)
{
    assert(cmdwin!=NULL);
    (void)wmove(cmdwin, 0, (int)promptlen);
    (void)wclrtoeol(cmdwin);
    (void)wattrset(cmdwin, A_BOLD);
    (void)vw_printw(cmdwin, (char *)fmt, ap);
    (void)wattrset(cmdwin, A_NORMAL);
    (void)wrefresh(cmdwin);
    (void)doupdate();

    (void)wgetch(cmdwin);
    (void)wmove(cmdwin, 0, (int)promptlen);
    (void)wclrtoeol(cmdwin);
    (void)wrefresh(cmdwin);
    (void)wmove(cmdwin, 0, (int)promptlen);
    (void)doupdate();
}

void monitor_complain(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    monitor_vcomplain(fmt, ap);
    va_end(ap);
}

void monitor_log(const char *fmt, ...)
{
    if (packetwin != NULL) {
	va_list ap;
	report_lock();
	va_start(ap, fmt);
	(void)vw_printw(packetwin, (char *)fmt, ap);
	va_end(ap);
	report_unlock();
    }
}

static const char *promptgen(void)
{
    static char buf[sizeof(session.gpsdata.dev.path) + HOST_NAME_MAX + 20];

    if (serial)
	(void)snprintf(buf, sizeof(buf),
		       "%s:%s %u %u%c%u",
		       hostname,
		       session.gpsdata.dev.path,
		       session.gpsdata.dev.baudrate,
		       9 - session.gpsdata.dev.stopbits,
		       session.gpsdata.dev.parity,
		       session.gpsdata.dev.stopbits);
    else {
	(void)strlcpy(buf, session.gpsdata.dev.path, sizeof(buf));
	if (source.device != NULL) {
	    (void) strlcat(buf, ":", sizeof(buf));
	    (void) strlcat(buf, source.device, sizeof(buf));
	}
    }
    return buf;
}

static void refresh_statwin(void)
/* refresh the device-identification window */
{
    /* *INDENT-OFF* */
    type_name =
	session.device_type ? session.device_type->type_name : "Unknown device";
    /* *INDENT-ON* */
    (void)wclear(statwin);
    (void)wattrset(statwin, A_BOLD);
    (void)mvwaddstr(statwin, 0, 0, promptgen());
    (void)wattrset(statwin, A_NORMAL);
    (void)wnoutrefresh(statwin);
}

static void refresh_cmdwin(void)
/* refresh the command window */
{
    (void)wmove(cmdwin, 0, 0);
    (void)wprintw(cmdwin, type_name);
    promptlen = strlen(type_name);
    if (fallback != NULL && strcmp(fallback->type_name, type_name) != 0) {
	(void)waddch(cmdwin, (chtype)' ');
	(void)waddch(cmdwin, (chtype)'(');
	(void)waddstr(cmdwin, fallback->type_name);
	(void)waddch(cmdwin, (chtype)')');
	promptlen += strlen(fallback->type_name) + 3;
    }
    (void)wprintw(cmdwin, "> ");
    promptlen += 2;
    (void)wclrtoeol(cmdwin);
    (void)wnoutrefresh(cmdwin);
}

static bool curses_init(void)
{
    (void)initscr();
    (void)cbreak();
    (void)intrflush(stdscr, FALSE);
    (void)keypad(stdscr, true);
    (void)clearok(stdscr, true);
    (void)clear();
    (void)noecho();
    curses_active = true;

#define CMDWINHEIGHT	1

    statwin = newwin(CMDWINHEIGHT, 30, 0, 0);
    cmdwin = newwin(CMDWINHEIGHT, 0, 0, 30);
    packetwin = newwin(0, 0, CMDWINHEIGHT, 0);
    if (statwin == NULL || cmdwin == NULL || packetwin == NULL)
	return false;
    (void)scrollok(packetwin, true);
    (void)wsetscrreg(packetwin, 0, LINES - CMDWINHEIGHT);

    (void)wmove(packetwin, 0, 0);

    refresh_statwin();
    refresh_cmdwin();
    return true;
}

static bool switch_type(const struct gps_type_t *devtype)
{
    const struct monitor_object_t **trial, **newobject;
    newobject = NULL;
    for (trial = monitor_objects; *trial; trial++) {
	if (strcmp((*trial)->driver->type_name, devtype->type_name)==0) {
	    newobject = trial;
	    break;
	}
    }
    if (newobject) {
	if (LINES < (*newobject)->min_y + 1 || COLS < (*newobject)->min_x) {
	    monitor_complain("%s requires %dx%d screen",
			     (*newobject)->driver->type_name,
			     (*newobject)->min_x, (*newobject)->min_y + 1);
	} else {
	    int leftover;

	    if (active != NULL) {
		if ((*active)->wrap != NULL)
		    (*active)->wrap();
		(void)delwin(devicewin);
	    }
	    active = newobject;
	    if (devicewin)
		delwin(devicewin);
	    devicewin = newwin((*active)->min_y, (*active)->min_x, 1, 0);
	    /* screen might have JSON on it from the init sequence */
	    (void)clearok(stdscr, true);
	    (void)clear();
	    if ((devicewin == NULL) || ((*active)->initialize != NULL && !(*active)->initialize())) {
		monitor_complain("Internal initialization failure - screen "
				 "must be at least 80x24. Aborting.");
		return false;
	    }

	    leftover = LINES - 1 - (*active)->min_y;
	    report_lock();
	    if (leftover <= 0) {
		if (packetwin != NULL)
		    (void)delwin(packetwin);
		packetwin = NULL;
	    } else if (packetwin == NULL) {
		packetwin = newwin(leftover, COLS, (*active)->min_y + 1, 0);
		(void)scrollok(packetwin, true);
		(void)wsetscrreg(packetwin, 0, leftover - 1);
	    } else {
		(void)wresize(packetwin, leftover, COLS);
		(void)mvwin(packetwin, (*active)->min_y + 1, 0);
		(void)wsetscrreg(packetwin, 0, leftover - 1);
	    }
	    report_unlock();
	}
	return true;
    }

    monitor_complain("No monitor matches %s.", devtype->type_name);
    return false;
}

static void select_packet_monitor(struct gps_device_t *device)
{
    static int last_type = BAD_PACKET;

    /*
     * Switch display types on packet receipt.  Note, this *doesn't*
     * change the selection of the current device driver; that's done
     * within gpsd_multipoll() before this hook is called.
     */
    if (device->lexer.type != last_type) {
	const struct gps_type_t *active_type = device->device_type;
#ifdef NMEA0183_ENABLE
	if (device->lexer.type == NMEA_PACKET
	    && ((device->device_type->flags & DRIVER_STICKY) != 0))
	    active_type = &driver_nmea0183;
#endif /* NMEA0183_ENABLE */
	if (!switch_type(active_type))
	    longjmp(terminate, TERM_DRIVER_SWITCH);
	else {
	    refresh_statwin();
	    refresh_cmdwin();
	}
	last_type = device->lexer.type;
    }

    if (active != NULL
	&& device->lexer.outbuflen > 0
	&& (*active)->update != NULL)
	(*active)->update();
    if (devicewin != NULL)
	(void)wnoutrefresh(devicewin);
}

/* Control-L character */
#define CTRL_L 0x0C

static char *curses_get_command(void)
/* char-by-char nonblocking input, return accumulated command line on \n */
{
    static char input[80];
    static char line[80];
    int c;

    c = wgetch(cmdwin);
    if (CTRL_L == c) {
        /* ^L is to repaint the screen */
	(void)clearok(stdscr, true);
	if (active != NULL && (*active)->initialize != NULL)
	    (void)(*active)->initialize();
    } else if (c != '\r' && c != '\n') {
	size_t len = strlen(input);

	if (c == '\b' || c == KEY_LEFT || c == (int)erasechar()) {
	    input[len--] = '\0';
	} else if (isprint(c)) {
	    input[len] = (char)c;
	    input[++len] = '\0';
	    (void)waddch(cmdwin, (chtype)c);
	    (void)wrefresh(cmdwin);
	    (void)doupdate();
	}

	return NULL;
    }

    (void)wmove(cmdwin, 0, (int)promptlen);
    (void)wclrtoeol(cmdwin);
    (void)wrefresh(cmdwin);
    (void)doupdate();

    /* user finished entering a command */
    if (input[0] == '\0')
	return NULL;
    else {
	(void) strlcpy(line, input, sizeof(line));
	input[0] = '\0';
    }

    /* handle it in the currently selected monitor object if possible */
    if (serial && active != NULL && (*active)->command != NULL) {
	int status = (*active)->command(line);
	if (status == COMMAND_TERMINATE)
	    longjmp(terminate, TERM_QUIT);
	else if (status == COMMAND_MATCH)
	    return NULL;
	assert(status == COMMAND_UNKNOWN);
    }

    return line;
}

/******************************************************************************
 *
 * Mode-independent I/O
 *
 * Below this line, all calls to curses-dependent functions are guarded
 * by curses_active and have ttylike alternatives.
 *
 ******************************************************************************/

static void packet_log(const char *fmt, ...)
{
    char buf[BUFSIZ];
    va_list ap;

    buf[0] = '\0';
    va_start(ap, fmt);
    packet_vlog(buf, sizeof(buf), fmt, ap);
    va_end(ap);
}

static ssize_t gpsmon_serial_write(struct gps_device_t *session,
		   const char *buf,
		   const size_t len)
/* pass low-level data to devices, echoing it to the log window */
{
#if defined(CONTROLSEND_ENABLE) || defined(RECONFIGURE_ENABLE)
    monitor_dump_send((const char *)buf, len);
#endif /* defined(CONTROLSEND_ENABLE) || defined(RECONFIGURE_ENABLE) */
    return gpsd_serial_write(session, buf, len);
}

#ifdef CONTROLSEND_ENABLE
bool monitor_control_send( unsigned char *buf, size_t len)
{
    if (!serial)
	return false;
    else {
	ssize_t st;

	context.readonly = false;
	st = session.device_type->control_send(&session, (char *)buf, len);
	context.readonly = true;
	return (st != -1);
    }
}

static bool monitor_raw_send( unsigned char *buf, size_t len)
{
    ssize_t st = gpsd_write(&session, (char *)buf, len);
    return (st > 0 && (size_t) st == len);
}
#endif /* CONTROLSEND_ENABLE */

static void complain(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    if (curses_active)
	monitor_vcomplain(fmt, ap);
    else {
	(void)vfprintf(stderr, fmt, ap);
	(void)fputc('\n', stderr);
    }

    va_end(ap);
}

/*****************************************************************************
 *
 * Main sequence
 *
 *****************************************************************************/

static void gpsmon_hook(struct gps_device_t *device, gps_mask_t changed UNUSED)
/* per-packet hook */
{
    char buf[BUFSIZ];
    char ts_buf1[TIMESPEC_LEN];
    char ts_buf2[TIMESPEC_LEN];

/* FIXME:  If the following condition is false, the display is screwed up. */
#if defined(SOCKET_EXPORT_ENABLE) && defined(PPS_DISPLAY_ENABLE)
    if (!serial && str_starts_with((char*)device->lexer.outbuffer, "{\"class\":\"TOFF\",")) {
	const char *end = NULL;
	int status = json_toff_read((const char *)device->lexer.outbuffer,
				   &session.gpsdata,
				   &end);
	if (status != 0) {
	    complain("Ill-formed TOFF packet: %d (%s)", status,
                     json_error_string(status));
	    return;
	} else {
	    if (!curses_active)
		(void)fprintf(stderr, "TOFF=%s real=%s\n",
                              timespec_str(&session.gpsdata.toff.clock,
                                           ts_buf1, sizeof(ts_buf1)),
                              timespec_str(&session.gpsdata.toff.real,
                                           ts_buf2, sizeof(ts_buf2)));
	    time_offset = session.gpsdata.toff;
	    return;
	}
    } else if (!serial && str_starts_with((char*)device->lexer.outbuffer, "{\"class\":\"PPS\",")) {
	const char *end = NULL;
	struct gps_data_t noclobber;
	int status = json_pps_read((const char *)device->lexer.outbuffer,
				   &noclobber,
				   &end);
	if (status != 0) {
	    complain("Ill-formed PPS packet: %d (%s)", status,
                     json_error_string(status));
	    return;
	} else {
	    struct timespec timedelta;
	    char timedelta_str[TIMESPEC_LEN];

	    TS_SUB( &timedelta, &noclobber.pps.clock, &noclobber.pps.real);
	    timespec_str(&timedelta, timedelta_str, sizeof(timedelta_str));

	    if (!curses_active) {
		char pps_clock_str[TIMESPEC_LEN];
		char pps_real_str[TIMESPEC_LEN];

		timespec_str(&noclobber.pps.clock, pps_clock_str,
			     sizeof(pps_clock_str));
		timespec_str(&noclobber.pps.real, pps_real_str,
			     sizeof(pps_real_str));

		(void)fprintf(stderr,
			      "PPS=%.20s clock=%.20s offset=%.20s\n",
			      pps_clock_str,
			      pps_real_str,
                              timedelta_str);
	    }

	    (void)snprintf(buf, sizeof(buf),
			"------------------- PPS offset: %.20s ------\n",
			timedelta_str);
/* FIXME:  Decouple this from the pps_thread code. */
	    /*
	     * In direct mode this would be a bad idea, but we're not actually
	     * watching for handshake events on a spawned thread here.
	     */
	    /* coverity[missing_lock] */
	    session.pps_thread.pps_out = noclobber.pps;
	    /* coverity[missing_lock] */
	    session.pps_thread.ppsout_count++;
	}
    }
    else
#endif /* SOCKET_EXPORT_ENABLE && PPS_DISPLAY_ENABLE */
    {
#ifdef __future__
	if (!serial)
	{
	    if (device->lexer.type == JSON_PACKET)
	    {
		const char *end = NULL;
		libgps_json_unpack((char *)device->lexer.outbuffer, &session.gpsdata, &end);
	    }
	}
#endif /* __future__ */

	if (curses_active)
	    select_packet_monitor(device);

	(void)snprintf(buf, sizeof(buf), "(%d) ",
		       (int)device->lexer.outbuflen);
	cond_hexdump(buf + strlen(buf), sizeof(buf) - strlen(buf),
		     (char *)device->lexer.outbuffer,device->lexer.outbuflen);
	(void)strlcat(buf, "\n", sizeof(buf));
    }

    report_lock();

    if (!curses_active)
	(void)fputs(buf, stdout);
    else {
	if (packetwin != NULL) {
	    (void)waddstr(packetwin, buf);
	    (void)wnoutrefresh(packetwin);
	}
	(void)doupdate();
    }

    if (logfile != NULL && device->lexer.outbuflen > 0) {
        UNUSED size_t written_count = fwrite
               (device->lexer.outbuffer, sizeof(char),
                device->lexer.outbuflen, logfile);
        assert(written_count >= 1);
    }

    report_unlock();

    /* Update the last fix time seen for PPS if we've actually seen one,
     * and it is a new second. */
    if (0 >= device->newdata.time.tv_sec) {
	// "NTP: bad new time
    } else if (device->newdata.time.tv_sec <=
               device->pps_thread.fix_in.real.tv_sec) {
	// "NTP: Not a new time
    } else
	ntp_latch(device, &time_offset);
}

static bool do_command(const char *line)
{
#ifdef RECONFIGURE_ENABLE
    unsigned int v;
    struct timespec delay;
#endif /* RECONFIGURE_ENABLE */
#ifdef CONTROLSEND_ENABLE
    unsigned char buf[BUFLEN];
#endif /* CONTROLSEND_ENABLE */
    const char *arg;

    if (isspace((unsigned char) line[1])) {
	for (arg = line + 2; *arg != '\0' && isspace((unsigned char) *arg); arg++)
	    arg++;
	arg++;
    } else
	arg = line + 1;

    switch (line[0]) {
#ifdef RECONFIGURE_ENABLE
    case 'c':	/* change cycle time */
	if (session.device_type == NULL)
	    complain("No device defined yet");
	else if (!serial)
	    complain("Only available in low-level mode.");
	else {
	    double rate = strtod(arg, NULL);
	    const struct gps_type_t *switcher = session.device_type;

	    if (fallback != NULL && fallback->rate_switcher != NULL)
		switcher = fallback;
	    if (switcher->rate_switcher != NULL) {
		/* *INDENT-OFF* */
		context.readonly = false;
		if (switcher->rate_switcher(&session, rate)) {
		    announce_log("[Rate switcher called.]");
		} else
		    complain("Rate not supported.");
		context.readonly = true;
		/* *INDENT-ON* */
	    } else
		complain
		    ("Device type %s has no rate switcher",
		     switcher->type_name);
	}
#endif /* RECONFIGURE_ENABLE */
	break;
    case 'i':	/* start probing for subtype */
	if (session.device_type == NULL)
	    complain("No GPS type detected.");
	else if (!serial)
	    complain("Only available in low-level mode.");
	else {
	    if (strcspn(line, "01") == strlen(line))
		context.readonly = !context.readonly;
	    else
		context.readonly = (atoi(line + 1) == 0);
#ifdef RECONFIGURE_ENABLE
	    announce_log("[probing %sabled]", context.readonly ? "dis" : "en");
#endif /* RECONFIGURE_ENABLE */
	    if (!context.readonly)
		/* magic - forces a reconfigure */
		session.lexer.counter = 0;
	}
	break;

    case 'l':	/* open logfile */
	report_lock();
	if (logfile != NULL) {
	    if (packetwin != NULL)
		(void)wprintw(packetwin,
			      ">>> Logging off\n");
	    (void)fclose(logfile);
	}

	if ((logfile = fopen(line + 1, "a")) != NULL)
	    if (packetwin != NULL)
		(void)wprintw(packetwin,
			      ">>> Logging to %s\n", line + 1);
	report_unlock();
	break;

#ifdef RECONFIGURE_ENABLE
    case 'n':	/* change mode */
	/* if argument not specified, toggle */
	if (strcspn(line, "01") == strlen(line)) {
	    /* *INDENT-OFF* */
	    v = (unsigned int)TEXTUAL_PACKET_TYPE(
		session.lexer.type);
	    /* *INDENT-ON* */
	} else
	    v = (unsigned)atoi(line + 1);
	if (session.device_type == NULL)
	    complain("No device defined yet");
	else if (!serial)
	    complain("Only available in low-level mode.");
	else {
	    const struct gps_type_t *switcher = session.device_type;

	    if (fallback != NULL && fallback->mode_switcher != NULL)
		switcher = fallback;
	    if (switcher->mode_switcher != NULL) {
		context.readonly = false;
		announce_log("[Mode switcher to mode %d]", v);
		switcher->mode_switcher(&session, (int)v);
		context.readonly = true;
		(void)tcdrain(session.gpsdata.gps_fd);

		/* wait 50,000 uSec */
		delay.tv_sec = 0;
		delay.tv_nsec = 50000000L;
		nanosleep(&delay, NULL);

		/*
		 * Session device change will be set to NMEA when
		 * gpsmon resyncs.  So stash the current type to
		 * be restored if we do 'n' from NMEA mode.
		 */
		if (v == 0)
		    fallback = switcher;
	    } else
		complain
		    ("Device type %s has no mode switcher",
		     switcher->type_name);
	}
	break;
#endif /* RECONFIGURE_ENABLE */

    case 'q':	/* quit */
	return false;

#ifdef RECONFIGURE_ENABLE
    case 's':	/* change speed */
	if (session.device_type == NULL)
	    complain("No device defined yet");
	else if (!serial)
	    complain("Only available in low-level mode.");
	else {
	    speed_t speed;
	    char parity = session.gpsdata.dev.parity;
	    unsigned int stopbits =
		(unsigned int)session.gpsdata.dev.stopbits;
	    char *modespec;
	    const struct gps_type_t *switcher = session.device_type;

	    if (fallback != NULL && fallback->speed_switcher != NULL)
		switcher = fallback;
	    modespec = strchr(arg, ':');
	    if (modespec != NULL) {
		if (strchr("78", *++modespec) == NULL) {
		    complain
			("No support for that word length.");
		    break;
		}
		parity = *++modespec;
		if (strchr("NOE", parity) == NULL) {
		    complain("What parity is '%c'?.",
				     parity);
		    break;
		}
		stopbits = (unsigned int)*++modespec;
		if (strchr("12", (char)stopbits) == NULL) {
		    complain("Stop bits must be 1 or 2.");
		    break;
		}
		stopbits = (unsigned int)(stopbits - '0');
	    }
	    speed = (unsigned)atoi(arg);
	    /* *INDENT-OFF* */
	    if (switcher->speed_switcher) {
		context.readonly = false;
		if (switcher->speed_switcher(&session, speed,
					     parity, (int)
					     stopbits)) {
		    announce_log("[Speed switcher called.]");
		    /*
		     * See the comment attached to the 'DEVICE'
		     * command in gpsd.  Allow the control
		     * string time to register at the GPS
		     * before we do the baud rate switch,
		     * which effectively trashes the UART's
		     * buffer.
		     */
		    (void)tcdrain(session.gpsdata.gps_fd);
		    /* wait 50,000 uSec */
		    delay.tv_sec = 0;
		    delay.tv_nsec = 50000000L;
		    nanosleep(&delay, NULL);

		    (void)gpsd_set_speed(&session, speed,
					 parity, stopbits);
		} else
		    complain
			("Speed/mode combination not supported.");
		context.readonly = true;
	    } else
		complain
		    ("Device type %s has no speed switcher",
		     switcher->type_name);
	    /* *INDENT-ON* */
	    if (curses_active)
		refresh_statwin();
	}
	break;
#endif /* RECONFIGURE_ENABLE */

    case 't':	/* force device type */
	if (!serial)
	    complain("Only available in low-level mode.");
	else if (strlen(arg) > 0) {
	    int matchcount = 0;
	    const struct gps_type_t **dp, *forcetype = NULL;
	    for (dp = gpsd_drivers; *dp; dp++) {
		if (strstr((*dp)->type_name, arg) != NULL) {
		    forcetype = *dp;
		    matchcount++;
		}
	    }
	    if (matchcount == 0) {
		complain
		    ("No driver type matches '%s'.", arg);
	    } else if (matchcount == 1) {
		assert(forcetype != NULL);
		/* *INDENT-OFF* */
		if (switch_type(forcetype))
		    (void)gpsd_switch_driver(&session,
					     forcetype->type_name);
		/* *INDENT-ON* */
		if (curses_active)
		    refresh_cmdwin();
	    } else {
		complain("Multiple driver type names match '%s'.", arg);
	    }
	}
	break;
#ifdef CONTROLSEND_ENABLE
    case 'x':	/* send control packet */
	if (session.device_type == NULL)
	    complain("No device defined yet");
	else if (!serial)
	    complain("Only available in low-level mode.");
	else {
	    int st = gpsd_hexpack(arg, (char *)buf, strlen(arg));
	    if (st < 0)
		complain("Invalid hex string (error %d)", st);
	    else if (session.device_type->control_send == NULL)
		complain("Device type %s has no control-send method.",
			 session.device_type->type_name);
	    else if (!monitor_control_send(buf, (size_t) st))
		complain("Control send failed.");
	}
	break;

    case 'X':	/* send raw packet */
	if (!serial)
	    complain("Only available in low-level mode.");
	else {
	    ssize_t len = (ssize_t) gpsd_hexpack(arg, (char *)buf, strlen(arg));
	    if (len < 0)
		complain("Invalid hex string (error %d)", len);
	    else if (!monitor_raw_send(buf, (size_t) len))
		complain("Raw send failed.");
	}
	break;
#endif /* CONTROLSEND_ENABLE */

    default:
	complain("Unknown command '%c'", line[0]);
	break;
    }

    /* continue accepting commands */
    return true;
}

static char *pps_report(volatile struct pps_thread_t *pps_thread UNUSED,
			struct timedelta_t *td UNUSED) {
    packet_log(PPSBAR);
    return "gpsmon";
}

static jmp_buf assertbuf;

static void onsig(int sig UNUSED)
{
    if (sig == SIGABRT)
	longjmp(assertbuf, 1);
    else
	longjmp(terminate, TERM_SIGNAL);
}

#define WATCHRAW	"?WATCH={\"raw\":2,\"pps\":true}\r\n"
#define WATCHRAWDEVICE	"?WATCH={\"raw\":2,\"pps\":true,\"device\":\"%s\"}\r\n"
#define WATCHNMEA	"?WATCH={\"nmea\":true,\"pps\":true}\r\n"
#define WATCHNMEADEVICE	"?WATCH={\"nmea\":true,\"pps\":true,\"device\":\"%s\"}\r\n"

/* this placement avoids a compiler warning */
static const char *cmdline;

int main(int argc, char **argv)
{
    int option;
    char *explanation;
    int bailout = 0, matches = 0;
    bool nmea = false;
    fd_set all_fds;
    fd_set rfds;
    volatile int maxfd = 0;
    char inbuf[80];
    volatile bool nocurses = false;
    int activated = -1;

    gethostname(hostname, sizeof(hostname)-1);
    (void)putenv("TZ=UTC");	// for ctime()
    gps_context_init(&context, "gpsmon");	// initialize the report mutex
    context.serial_write = gpsmon_serial_write;
    context.errout.report = gpsmon_report;
    while ((option = getopt(argc, argv, "aD:LVhl:nt:?")) != -1) {
	switch (option) {
	case 'a':
	    nocurses = true;
	    break;
	case 'D':
	    context.errout.debug = atoi(optarg);
#if defined(CLIENTDEBUG_ENABLE) && defined(SOCKET_EXPORT_ENABLE)
	    json_enable_debug(context.errout.debug - 2, stderr);
#endif
	    break;
	case 'L':		/* list known device types */
	    (void)
		fputs
		("General commands available per type. '+' means there are private commands.\n",
		 stdout);
	    for (active = monitor_objects; *active; active++) {
		(void)fputs("i l q ^S ^Q", stdout);
		(void)fputc(' ', stdout);
#ifdef RECONFIGURE_ENABLE
		if ((*active)->driver->mode_switcher != NULL)
		    (void)fputc('n', stdout);
		else
		    (void)fputc(' ', stdout);
		(void)fputc(' ', stdout);
		if ((*active)->driver->speed_switcher != NULL)
		    (void)fputc('s', stdout);
		else
		    (void)fputc(' ', stdout);
		(void)fputc(' ', stdout);
		if ((*active)->driver->rate_switcher != NULL)
		    (void)fputc('x', stdout);
		else
		    (void)fputc(' ', stdout);
		(void)fputc(' ', stdout);
#endif /* RECONFIGURE_ENABLE */
#ifdef CONTROLSEND_ENABLE
		if ((*active)->driver->control_send != NULL)
		    (void)fputc('x', stdout);
		else
		    (void)fputc(' ', stdout);
#endif /* CONTROLSEND_ENABLE */
		(void)fputc(' ', stdout);
		if ((*active)->command != NULL)
		    (void)fputc('+', stdout);
		else
		    (void)fputc(' ', stdout);
		(void)fputs("\t", stdout);
		(void)fputs((*active)->driver->type_name, stdout);
		(void)fputc('\n', stdout);
	    }
	    exit(EXIT_SUCCESS);
	case 'V':
	    (void)printf("%s: %s (revision %s)\n", argv[0], VERSION, REVISION);
	    exit(EXIT_SUCCESS);
	case 'l':		/* enable logging at startup */
	    logfile = fopen(optarg, "w");
	    if (logfile == NULL) {
		(void)fprintf(stderr, "Couldn't open logfile for writing.\n");
		exit(EXIT_FAILURE);
	    }
	    break;
        case 'T':
        case 't':
	    fallback = NULL;
	    for (active = monitor_objects; *active; active++) {
		if (str_starts_with((*active)->driver->type_name, optarg))
		{
		    fallback = (*active)->driver;
		    matches++;
		}
	    }
	    if (matches > 1) {
		(void)fprintf(stderr, "-t option matched more than one driver.\n");
		exit(EXIT_FAILURE);
	    }
	    else if (matches == 0) {
		(void)fprintf(stderr, "-t option didn't match any driver.\n");
		exit(EXIT_FAILURE);
	    }
	    active = NULL;
	    break;
	case 'n':
	    nmea = true;
	    break;
	case 'h':
	case '?':
	default:
	    (void)
		fputs
		("usage: gpsmon [-?hVn] [-l logfile] [-D debuglevel] "
		 "[-t type] [server[:port:[device]]]\n",
		 stderr);
	    exit(EXIT_FAILURE);
	}
    }

    gpsd_time_init(&context, time(NULL));
    gpsd_init(&session, &context, NULL);

    /* Grok the server, port, and device. */
    if (optind < argc) {
	serial = str_starts_with(argv[optind], "/dev");
	gpsd_source_spec(argv[optind], &source);
    } else {
	serial = false;
	gpsd_source_spec(NULL, &source);
    }

    if (serial) {
        if (NULL == source.device) {
            /* this can happen with "gpsmon /dev:dd" */
	    (void) strlcpy(session.gpsdata.dev.path,
			   argv[optind],
			   sizeof(session.gpsdata.dev.path));
        } else {
	    (void) strlcpy(session.gpsdata.dev.path,
			   source.device,
			   sizeof(session.gpsdata.dev.path));
        }
    } else {
	if (strstr(source.server, "//") == 0)
	    (void) strlcpy(session.gpsdata.dev.path,
			   "tcp://",
			   sizeof(session.gpsdata.dev.path));
	else
	    session.gpsdata.dev.path[0] = '\0';
	str_appendf(session.gpsdata.dev.path, sizeof(session.gpsdata.dev.path),
		       "%s:%s", source.server, source.port);
    }

    activated = gpsd_activate(&session, O_PROBEONLY);
    if ( 0 > activated ) {
	if ( PLACEHOLDING_FD == activated ) {
		(void)fputs("gpsmon:ERROR: PPS device unsupported\n", stderr);
        }
	exit(EXIT_FAILURE);
    }


    if (serial) {
	/* this guard suppresses a warning on Bluetooth devices */
	if (session.sourcetype == source_rs232 || session.sourcetype == source_usb) {
	    session.pps_thread.report_hook = pps_report;
	    #ifdef MAGIC_HAT_ENABLE
	    /*
	     * The HAT kludge. If we're using the HAT GPS on a
	     * Raspberry Pi or a workalike like the ODROIDC2, and
	     * there is a static "first PPS", and we have access because
	     * we're root, assume we want to use KPPS.
	     */
	    if (strcmp(session.pps_thread.devicename, MAGIC_HAT_GPS) == 0
		|| strcmp(session.pps_thread.devicename, MAGIC_LINK_GPS) == 0) {
		char *first_pps = pps_get_first();
		if (access(first_pps, R_OK | W_OK) == 0)
			session.pps_thread.devicename = first_pps;
		}
	    #endif /* MAGIC_HAT_ENABLE */
	    pps_thread_activate(&session.pps_thread);
	}
    }
    else {
	if (source.device != NULL)
	    (void)gps_send(&session.gpsdata, nmea ? WATCHNMEADEVICE : WATCHRAWDEVICE, source.device);
	else
	    (void)gps_send(&session.gpsdata, nmea ? WATCHNMEA : WATCHRAW);
    }

    /*
     * This is a monitoring utility. Disable autoprobing, because
     * in some cases (e.g. SiRFs) there is no way to probe a chip
     * type without flipping it to native mode.
     */
    context.readonly = true;

    /* quit cleanly if an assertion fails */
    (void)signal(SIGABRT, onsig);
    if (setjmp(assertbuf) > 0) {
	if (logfile)
	    (void)fclose(logfile);
	if (curses_active)
	    (void)endwin();
	(void)fputs("gpsmon: assertion failure, probable I/O error\n",
		    stderr);
	exit(EXIT_FAILURE);
    }

    FD_ZERO(&all_fds);
#ifndef __clang_analyzer__
    FD_SET(0, &all_fds);	/* accept keystroke inputs */
#endif /* __clang_analyzer__ */


    FD_SET(session.gpsdata.gps_fd, &all_fds);
    if (session.gpsdata.gps_fd > maxfd)
	 maxfd = session.gpsdata.gps_fd;

    if ((bailout = setjmp(terminate)) == 0) {
	(void)signal(SIGQUIT, onsig);
	(void)signal(SIGINT, onsig);
	(void)signal(SIGTERM, onsig);
	if (nocurses) {
	    (void)fputs("gpsmon: ", stdout);
	    (void)fputs(promptgen(), stdout);
	    (void)fputs("\n", stdout);
	    (void)tcgetattr(0, &cooked);
	    (void)tcgetattr(0, &rare);
	    rare.c_lflag &=~ (ICANON | ECHO);
	    rare.c_cc[VMIN] = (cc_t)1;
	    (void)tcflush(0, TCIFLUSH);
	    (void)tcsetattr(0, TCSANOW, &rare);
	} else if (!curses_init())
	    goto quit;

	for (;;)
	{
	    fd_set efds;
	    switch(gpsd_await_data(&rfds, &efds, maxfd, &all_fds, &context.errout))
	    {
	    case AWAIT_GOT_INPUT:
		break;
	    case AWAIT_NOT_READY:
		/* no recovery from bad fd is possible */
		if (FD_ISSET(session.gpsdata.gps_fd, &efds))
		    longjmp(terminate, TERM_SELECT_FAILED);
		continue;
	    case AWAIT_FAILED:
		longjmp(terminate, TERM_SELECT_FAILED);
		break;
	    }

	    switch(gpsd_multipoll(FD_ISSET(session.gpsdata.gps_fd, &rfds),
				  &session, gpsmon_hook, 0))
	    {
	    case DEVICE_READY:
		FD_SET(session.gpsdata.gps_fd, &all_fds);
		break;
	    case DEVICE_UNREADY:
		longjmp(terminate, TERM_EMPTY_READ);
		break;
	    case DEVICE_ERROR:
		longjmp(terminate, TERM_READ_ERROR);
		break;
	    case DEVICE_EOF:
		longjmp(terminate, TERM_QUIT);
		break;
	    default:
		break;
	    }

	    if (FD_ISSET(0, &rfds)) {
		if (curses_active)
		    cmdline = curses_get_command();
		else
		{
		    /* coverity[string_null_argument] */
		    ssize_t st = read(0, &inbuf, 1);

		    if (st == 1) {
			report_lock();
			(void)tcflush(0, TCIFLUSH);
			(void)tcsetattr(0, TCSANOW, &cooked);
			(void)fputs("gpsmon: ", stdout);
			(void)fputs(promptgen(), stdout);
			(void)fputs("> ", stdout);
			(void)putchar(inbuf[0]);
			cmdline = fgets(inbuf+1, sizeof(inbuf)-1, stdin);
			if (cmdline)
			    cmdline--;
		    }
		}
		if (cmdline != NULL && !do_command(cmdline))
		    longjmp(terminate, TERM_QUIT);
		if (!curses_active) {
		    (void)sleep(2);
		    (void)tcsetattr(0, TCSANOW, &rare);
		    report_unlock();
		}
	    }
	}
    }

  quit:
    /* we'll fall through to here on longjmp() */

    /* Shut down PPS monitoring. */
    if (serial)
       (void)pps_thread_deactivate(&session.pps_thread);

    gpsd_close(&session);
    if (logfile)
	(void)fclose(logfile);
    if (curses_active)
	(void)endwin();
    else
	(void)tcsetattr(0, TCSANOW, &cooked);

    explanation = NULL;
    switch (bailout) {
    case TERM_SELECT_FAILED:
	explanation = "I/O wait on device failed\n";
	break;
    case TERM_DRIVER_SWITCH:
	explanation = "Driver type switch failed\n";
	break;
    case TERM_EMPTY_READ:
	explanation = "Device went offline\n";
	break;
    case TERM_READ_ERROR:
	explanation = "Read error from device\n";
	break;
    case TERM_SIGNAL:
    case TERM_QUIT:
	/* normal exit, no message */
	break;
    default:
	explanation = "Unknown error, should never happen.\n";
	break;
    }

    if (explanation != NULL)
	(void)fputs(explanation, stderr);
    exit(EXIT_SUCCESS);
}

/* gpsmon.c ends here */
