/*
 * This file is Copyright 2010 by the GPSD project
 * SPDX-License-Identifier: BSD-2-clause
 */

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

#include <ctype.h>              /* for isdigit() */
#include <dirent.h>             /* for DIR */
#include <errno.h>
#include <fcntl.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/param.h>          /* defines BSD */
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#ifdef HAVE_SYS_SYSMACROS_H
#include <sys/sysmacros.h>      /* defines major() */
#endif  /* HAVE_SYS_SYSMACROS_H */

#ifdef ENABLE_BLUEZ
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>
#include <bluetooth/rfcomm.h>
#endif /* ENABLE_BLUEZ */

#include "../include/compiler.h"   // for FALLTHROUGH
#include "../include/gpsd.h"

/* Workaround for HP-UX 11.23, which is missing CRTSCTS */
#ifndef CRTSCTS
#  ifdef CNEW_RTSCTS
#    define CRTSCTS CNEW_RTSCTS
#  else
#    define CRTSCTS 0
#  endif /* CNEW_RTSCTS */
#endif /* !CRTSCTS */

static sourcetype_t gpsd_classify(const char *path)
/* figure out what kind of device we're looking at */
{
    struct stat sb;

    if (stat(path, &sb) == -1)
        return source_unknown;
    else if (S_ISREG(sb.st_mode))
        return source_blockdev;
    /* this assumes we won't get UDP from a filesystem socket */
    else if (S_ISSOCK(sb.st_mode))
        return source_tcp;
    /* OS-independent check for ptys using Unix98 naming convention */
    else if (strncmp(path, "/dev/pts/", 9) == 0)
        return source_pty;
    else if (strncmp(path, "/dev/pps", 8) == 0)
        return source_pps;
    else if (S_ISFIFO(sb.st_mode))
        return source_pipe;
    else if (S_ISCHR(sb.st_mode)) {
        sourcetype_t devtype = source_rs232;
#ifdef __linux__
        /* Linux major device numbers live here
         *
         * https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/admin-guide/devices.txt
         *
         * Note: This code works because Linux major device numbers are
         * stable and architecture-independent.  It is *not* a good model
         * for other Unixes where either or both assumptions may break.
         */
        int devmajor = major(sb.st_rdev);
        /* 207 are Freescale i.MX UARTs (ttymxc*) */
        if (devmajor == 4 || devmajor == 204 || devmajor == 207)
            devtype = source_rs232;
        else if (devmajor == 188 || devmajor == 166)
            devtype = source_usb;
        else if (devmajor == 216 || devmajor == 217)
            devtype = source_bluetooth;
        else if (devmajor == 3 || (devmajor >= 136 && devmajor <= 143))
            devtype = source_pty;
#endif /* __linux__ */
        /*
         * See http://nadeausoftware.com/articles/2012/01/c_c_tip_how_use_compiler_predefined_macros_detect_operating_system
         * for discussion how this works.  Key graphs:
         *
         * Compilers for the old BSD base for these distributions
         * defined the __bsdi__ macro, but none of these distributions
         * define it now. This leaves no generic "BSD" macro defined
         * by the compiler itself, but all UNIX-style OSes provide a
         * <sys/param.h> file. On BSD distributions, and only on BSD
         * distributions, this file defines a BSD macro that's set to
         * the OS version. Checking for this generic macro is more
         * robust than looking for known BSD distributions with
         * __DragonFly__, __FreeBSD__, __NetBSD__, and __OpenBSD__
         * macros.
         *
         * Apple's OSX for the Mac and iOS for iPhones and iPads are
         * based in part on a fork of FreeBSD distributed as
         * Darwin. As such, OSX and iOS also define the BSD macro
         * within <sys/param.h>. However, compilers for OSX, iOS, and
         * Darwin do not define __unix__. To detect all BSD OSes,
         * including OSX, iOS, and Darwin, use an #if/#endif that
         * checks for __unix__ along with __APPLE__ and __MACH__ (see
         * the later section on OSX and iOS).
         */
#ifdef BSD
        /*
         * Hacky check for pty, which is what really matters for avoiding
         * adaptive delay.
         */
        if (strncmp(path, "/dev/ttyp", 9) == 0 ||
            strncmp(path, "/dev/ttyq", 9) == 0)
            devtype = source_pty;
        else if (strncmp(path, "/dev/ttyU", 9) == 0 ||
            strncmp(path, "/dev/dtyU", 9) == 0)
            devtype = source_usb;
        /* XXX bluetooth */
#endif /* BSD */
        return devtype;
    } else
        return source_unknown;
}

#ifdef __linux__

/* return true if any process has the specified path open */
static int fusercount(const char *path)
{
    DIR *procd, *fdd;
    struct dirent *procentry, *fdentry;
    char procpath[GPS_PATH_MAX], fdpath[GPS_PATH_MAX], linkpath[GPS_PATH_MAX];
    int cnt = 0;

    if (NULL == (procd = opendir("/proc")))
        return -1;
    while (NULL != (procentry = readdir(procd))) {
        if (0 == isdigit(procentry->d_name[0]))
            continue;
        /* longest procentry->d_name I could find was 12 */
        (void)snprintf(procpath, sizeof(procpath),
                       "/proc/%.20s/fd/", procentry->d_name);
        if (NULL == (fdd = opendir(procpath)))
            continue;
        while (NULL != (fdentry = readdir(fdd))) {
            ssize_t rd;

            (void)strlcpy(fdpath, procpath, sizeof(fdpath));
            (void)strlcat(fdpath, fdentry->d_name, sizeof(fdpath));
            // readlink does not NUL terminate.
            rd = readlink(fdpath, linkpath, sizeof(linkpath) - 1);
            if (0 > rd)
                continue;
            // pacify coverity by setting NUL
            linkpath[rd] = '\0';
            if (0 == strcmp(linkpath, path)) {
                ++cnt;
            }
        }
        (void)closedir(fdd);
    }
    (void)closedir(procd);

    return cnt;
}
#endif /* __linux__ */

void gpsd_tty_init(struct gps_device_t *session)
/* to be called on allocating a device */
{
    /* mark GPS fd closed and its baud rate unknown */
    session->gpsdata.gps_fd = -1;
    session->saved_baud = -1;
    session->zerokill = false;
    session->reawake = (time_t)0;
}

#if !defined(HAVE_CFMAKERAW)
/*
 * Local implementation of cfmakeraw (which is not specified by
 * POSIX; see matching declaration in gpsd.h).
 */
/* Pasted from man page; added in serial.c arbitrarily */
void cfmakeraw(struct termios *termios_p)
{
    termios_p->c_iflag &=
        ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
    termios_p->c_oflag &= ~OPOST;
    termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    termios_p->c_cflag &= ~(CSIZE | PARENB);
    termios_p->c_cflag |= CS8;
}
#endif /* !defined(HAVE_CFMAKERAW) */

static speed_t gpsd_get_speed_termios(const struct termios *ttyctl)
{
    speed_t code = cfgetospeed(ttyctl);
    switch (code) {
    case B300:
        return (300);
    case B1200:
        return (1200);
    case B2400:
        return (2400);
    case B4800:
        return (4800);
    case B9600:
        return (9600);
    case B19200:
        return (19200);
    case B38400:
        return (38400);
    case B57600:
        return (57600);
    case B115200:
        return (115200);
    case B230400:
        return (230400);
#ifdef B460800
    case B460800:
        // not a valid POSIX speed
        return (460800);
#endif
    default: /* B0 */
        return 0;
    }
}

speed_t gpsd_get_speed(const struct gps_device_t *dev)
{
    return gpsd_get_speed_termios(&dev->ttyset);
}

speed_t gpsd_get_speed_old(const struct gps_device_t *dev)
{
    return gpsd_get_speed_termios(&dev->ttyset_old);
}

char gpsd_get_parity(const struct gps_device_t *dev)
{
    char parity = 'N';
    if ((dev->ttyset.c_cflag & (PARENB | PARODD)) == (PARENB | PARODD))
        parity = 'O';
    else if ((dev->ttyset.c_cflag & PARENB) == PARENB)
        parity = 'E';
    return parity;
}

int gpsd_get_stopbits(const struct gps_device_t *dev)
{
    int stopbits = 0;
    if ((dev->ttyset.c_cflag & CS8) == CS8)
        stopbits = 1;
    else if ((dev->ttyset.c_cflag & (CS7 | CSTOPB)) == (CS7 | CSTOPB))
        stopbits = 2;
    return stopbits;
}

bool gpsd_set_raw(struct gps_device_t * session)
{
    (void)cfmakeraw(&session->ttyset);
    if (-1 == tcsetattr(session->gpsdata.gps_fd, TCIOFLUSH, &session->ttyset)) {
        GPSD_LOG(LOG_ERROR, &session->context->errout,
                 "SER: error changing port attributes: %s\n", strerror(errno));
        return false;
    }

    return true;
}

void gpsd_set_speed(struct gps_device_t *session,
                    speed_t speed, char parity, unsigned int stopbits)
{
    speed_t rate;
    struct timespec delay;

    if (0 < session->context->fixed_port_speed) {
        speed = session->context->fixed_port_speed;
    }
    if ('\0' != session->context->fixed_port_framing[0]) {
        // ignore length, stopbits=2 forces length 7.
        parity = session->context->fixed_port_framing[1];
        stopbits = session->context->fixed_port_framing[2] - '0';
    }

    /*
     * Yes, you can set speeds that aren't in the hunt loop.  If you
     * do this, and you aren't on Linux where baud rate is preserved
     * across port closings, you've screwed yourself. Don't do that!
     */
    if (speed < 300)
        // in POSIX B0 means do not touch the speed
        rate = B0;
    else if (speed < 1200)
        rate = B300;
    else if (speed < 2400)
        rate = B1200;
    else if (speed < 4800)
        rate = B2400;
    else if (speed < 9600)
        rate = B4800;
    else if (speed < 19200)
        rate = B9600;
    else if (speed < 38400)
        rate = B19200;
    else if (speed < 57600)
        rate = B38400;
    else if (speed < 115200)
        rate = B57600;
    else if (speed < 230400)
        rate = B115200;
#ifdef B460800
    else if (speed < 460800)
        rate = B230400;
    // not a valid POSIX speed
    else
        rate = B460800;
#else
    else
        rate = B230400;
#endif


    /* backward-compatibility hack */
    switch (parity) {
    case 'E':
        FALLTHROUGH
    case (char)2:
        parity = 'E';
        break;
    case 'O':
        FALLTHROUGH
    case (char)1:
        parity = 'O';
        break;
    case 'N':
        FALLTHROUGH
    case (char)0:
        FALLTHROUGH
    default:
        parity = 'N';   /* without this we might emit malformed JSON */
        break;
    }

    if (rate != cfgetispeed(&session->ttyset)
        || parity != session->gpsdata.dev.parity
        || stopbits != session->gpsdata.dev.stopbits) {

        /*
         * Don't mess with this conditional! Speed zero is supposed to mean
         * to leave the port speed at whatever it currently is. This leads
         * to excellent behavior on Linux, which preserves baudrate across
         * serial device closes - it means that if you've opened this
         * device before you typically don't have to hunt at all because
         * it's still at the same speed you left it - you'll typically
         * get packet lock within 1.5 seconds.  Alas, the BSDs and OS X
         * aren't so nice.
         */
        if (rate != B0) {
            (void)cfsetispeed(&session->ttyset, rate);
            (void)cfsetospeed(&session->ttyset, rate);
        }
        session->ttyset.c_iflag &= ~(PARMRK | INPCK);
        session->ttyset.c_cflag &= ~(CSIZE | CSTOPB | PARENB | PARODD);
        session->ttyset.c_cflag |= (stopbits == 2 ? CS7 | CSTOPB : CS8);
        switch (parity) {
        case 'E':
            session->ttyset.c_iflag |= INPCK;
            session->ttyset.c_cflag |= PARENB;
            break;
        case 'O':
            session->ttyset.c_iflag |= INPCK;
            session->ttyset.c_cflag |= PARENB | PARODD;
            break;
        }
        if (0 != tcsetattr(session->gpsdata.gps_fd, TCSANOW,
                           &session->ttyset)) {
            /* strangely this fails on non-serial ports, but if
             * we do not try, we get other failures.
             * so ignore for now, as we always have, until it can
             * be nailed down.
             *
             * GPSD_LOG(&session->context->errout, LOG_ERROR,
             *       "SER: error setting port attributes: %s, sourcetype: %d\n",
             *       strerror(errno), session->sourcetype);
             * return;
             */
        }

        /*
         * Serious black magic begins here.  Getting this code wrong can cause
         * failures to lock to a correct speed, and not clean reproducible
         * failures but flukey hardware- and timing-dependent ones.  So
         * be very sure you know what you're doing before hacking it, and
         * test thoroughly.
         *
         * The fundamental problem here is that serial devices take time
         * to settle into a new baud rate after tcsetattr() is issued. Until
         * they do so, input will be arbitrarily garbled.  Normally this
         * is not a big problem, but in our hunt loop the garbling can trash
         * a long enough prefix of each sample to prevent detection of a
         * packet header.  We could address the symptom by making the sample
         * size enough larger that subtracting the maximum length of garble
         * would still leave a sample longer than the maximum packet size.
         * But it's better (and more efficient) to address the disease.
         *
         * In theory, one might think that not even a tcflush() call would
         * be needed, with tcsetattr() delaying its return until the device
         * is in a good state.  For simple devices like a 14550 UART that
         * have fixed response timings this may even work, if the driver
         * writer was smart enough to delay the return by the right number
         * of milliseconds after poking the device port(s).
         *
         * Problems may arise if the driver's timings are off.  Or we may
         * be talking to a USB device like the pl2303 commonly used in GPS
         * mice; on these, the change will not happen immediately because
         * it has to be sent as a message to the external processor that
         * has to act upon it, and that processor may still have buffered
         * data in its own FIFO.  In this case the expected delay may be
         * too large and too variable (depending on the details of how the
         * USB device is integrated with its symbiont hardware) to be put
         * in the driver.
         *
         * So, somehow, we have to introduce a delay after tcsatattr()
         * returns sufficient to allow *any* device to settle.  On the other
         * hand, a really long delay will make gpsd device registration
         * unpleasantly laggy.
         *
         * The classic way to address this is with a tcflush(), counting
         * on it to clear the device FIFO. But that call may clear only the
         * kernel buffers, not the device's hardware FIFO, so it may not
         * be sufficient by itself.
         *
         * flush followed by a 200-millisecond delay followed by flush has
         * been found to work reliably on the pl2303.  It is also known
         * from testing that a 100-millisec delay is too short, allowing
         * occasional failure to lock.
         */
        (void)tcflush(session->gpsdata.gps_fd, TCIOFLUSH);

        /* wait 200,000 uSec */
        delay.tv_sec = 0;
        delay.tv_nsec = 200000000L;
        nanosleep(&delay, NULL);
        (void)tcflush(session->gpsdata.gps_fd, TCIOFLUSH);
    }
    GPSD_LOG(LOG_INF, &session->context->errout,
             "SER: speed %lu, %d%c%d\n",
             (unsigned long)gpsd_get_speed(session), 9 - stopbits, parity,
             stopbits);

    session->gpsdata.dev.baudrate = (unsigned int)speed;
    session->gpsdata.dev.parity = parity;
    session->gpsdata.dev.stopbits = stopbits;

    /*
     * The device might need a wakeup string before it will send data.
     * If we don't know the device type, ship it every driver's wakeup
     * in hopes it will respond.  But not to USB or Bluetooth, because
     * shipping probe strings to unknown USB serial adaptors or
     * Bluetooth devices may spam devices that aren't GPSes at all and
     * could become confused.
     */
    if (!session->context->readonly
                && session->sourcetype != source_usb
                && session->sourcetype != source_bluetooth) {
        if (isatty(session->gpsdata.gps_fd) != 0
            && !session->context->readonly) {
            if (session->device_type == NULL) {
                const struct gps_type_t **dp;
                for (dp = gpsd_drivers; *dp; dp++)
                    if ((*dp)->event_hook != NULL)
                        (*dp)->event_hook(session, event_wakeup);
            } else if (session->device_type->event_hook != NULL)
                session->device_type->event_hook(session, event_wakeup);
        }
    }
    packet_reset(&session->lexer);
}

/* open a device for access to its data
 * return: the opened file descriptor
 *         PLACEHOLDING_FD - for /dev/ppsX
 *         UNALLOCATED_FD - for open failure
 */
int gpsd_serial_open(struct gps_device_t *session)
{
    mode_t mode = (mode_t) O_RDWR;

    session->sourcetype = gpsd_classify(session->gpsdata.dev.path);
    session->servicetype = service_sensor;

    /* we may need to hold on to this slot without opening the device */
    if (source_pps == session->sourcetype) {
        (void)gpsd_switch_driver(session, "PPS");
        return PLACEHOLDING_FD;
    }

    if (session->context->readonly
        || (session->sourcetype <= source_blockdev)) {
        mode = (mode_t) O_RDONLY;
        GPSD_LOG(LOG_INF, &session->context->errout,
                 "SER: opening read-only GPS data source type %d and at '%s'\n",
                 (int)session->sourcetype, session->gpsdata.dev.path);
    } else {
        GPSD_LOG(LOG_INF, &session->context->errout,
                 "SER: opening GPS data source type %d at '%s'\n",
                 (int)session->sourcetype, session->gpsdata.dev.path);
    }
#ifdef ENABLE_BLUEZ
    if (bachk(session->gpsdata.dev.path) == 0) {
        struct sockaddr_rc addr = { 0, *BDADDR_ANY, 0};
        session->gpsdata.gps_fd = socket(AF_BLUETOOTH,
                                         SOCK_STREAM,
                                         BTPROTO_RFCOMM);
        if (0 > session->gpsdata.gps_fd) {
            GPSD_LOG(LOG_ERROR, &session->context->errout,
                     "SER: bluetooth socket() failed: %s\n",
                     strerror(errno));
            return UNALLOCATED_FD;
        }
        addr.rc_family = AF_BLUETOOTH;
        addr.rc_channel = (uint8_t) 1;
        (void) str2ba(session->gpsdata.dev.path, &addr.rc_bdaddr);
        if (-1 == connect(session->gpsdata.gps_fd,
                          (struct sockaddr *) &addr,
                          sizeof (addr))) {
            if (errno != EINPROGRESS && errno != EAGAIN) {
                (void)close(session->gpsdata.gps_fd);
                GPSD_LOG(LOG_ERROR, &session->context->errout,
                         "SER: bluetooth socket connect failed: %s\n",
                         strerror(errno));
                return UNALLOCATED_FD;
            }
            GPSD_LOG(LOG_ERROR, &session->context->errout,
                     "SER: bluetooth socket connect in progress or "
                     "EAGAIN: %s\n",
                     strerror(errno));
        }
        (void)fcntl(session->gpsdata.gps_fd, F_SETFL, (int)mode);
        GPSD_LOG(LOG_PROG, &session->context->errout,
                 "SER: bluez device open success: %s %s\n",
                 session->gpsdata.dev.path, strerror(errno));
    } else
#endif /* BLUEZ */
    {
        /*
         * We open with O_NONBLOCK because we want to not get hung if
         * the CLOCAL flag is off.  Need to keep O_NONBLOCK so the main
         * loop does not clock on an unresponsive read() from a receiver.
         */
        errno = 0;
        if ((session->gpsdata.gps_fd =
             open(session->gpsdata.dev.path,
                  (int)(mode | O_NONBLOCK | O_NOCTTY))) == -1) {
            GPSD_LOG(LOG_ERROR, &session->context->errout,
                     "SER: device open of %s failed: %s - "
                     "retrying read-only\n",
                     session->gpsdata.dev.path,
                     strerror(errno));
            if ((session->gpsdata.gps_fd =
                 open(session->gpsdata.dev.path,
                      O_RDONLY | O_NONBLOCK | O_NOCTTY)) == -1) {
                GPSD_LOG(LOG_ERROR, &session->context->errout,
                         "SER: read-only device open of %s failed: %s\n",
                         session->gpsdata.dev.path,
                         strerror(errno));
                return UNALLOCATED_FD;
            }

            GPSD_LOG(LOG_PROG, &session->context->errout,
                     "SER: file device open of %s succeeded\n",
                     session->gpsdata.dev.path);
        }
    }

    /*
     * Ideally we want to exclusion-lock the device before doing any reads.
     * It would have been best to do this at open(2) time, but O_EXCL
     * doesn't work without O_CREAT.
     *
     * We have to make an exception for ptys, which are intentionally
     * opened by another process on the master side, otherwise we'll
     * break all our regression tests.
     *
     * We also exclude bluetooth device because the bluetooth daemon opens them.
     */
    if (!(session->sourcetype == source_pty ||
          session->sourcetype == source_bluetooth)) {
#ifdef TIOCEXCL
        /*
         * Try to block other processes from using this device while we
         * have it open (later opens should return EBUSY).  Won't work
         * against anything with root privileges, alas.
         */
        (void)ioctl(session->gpsdata.gps_fd, (unsigned long)TIOCEXCL);
#endif /* TIOCEXCL */

#ifdef __linux__
        /*
         * Don't touch devices already opened by another process.
         */
        if (fusercount(session->gpsdata.dev.path) > 1) {
            GPSD_LOG(LOG_ERROR, &session->context->errout,
                     "SER: %s already opened by another process\n",
                     session->gpsdata.dev.path);
            (void)close(session->gpsdata.gps_fd);
            session->gpsdata.gps_fd = UNALLOCATED_FD;
            return UNALLOCATED_FD;
        }
#endif /* __linux__ */
    }

    if (0 < session->context->fixed_port_speed) {
        session->saved_baud = session->context->fixed_port_speed;
    }

    if (session->saved_baud != -1) {
        (void)cfsetispeed(&session->ttyset, (speed_t)session->saved_baud);
        (void)cfsetospeed(&session->ttyset, (speed_t)session->saved_baud);
        if (0 != tcsetattr(session->gpsdata.gps_fd, TCSANOW,
                           &session->ttyset)) {
            GPSD_LOG(LOG_ERROR, &session->context->errout,
                     "SER: Error setting port attributes: %s\n",
                     strerror(errno));
        }
        (void)tcflush(session->gpsdata.gps_fd, TCIOFLUSH);
    }

    session->lexer.type = BAD_PACKET;
    if ( 0 != isatty(session->gpsdata.gps_fd) ) {
        speed_t new_speed;
        char new_parity;   // E, N, O
        unsigned int new_stop;

        /* Save original terminal parameters */
        if (tcgetattr(session->gpsdata.gps_fd, &session->ttyset_old) != 0)
            return UNALLOCATED_FD;
        session->ttyset = session->ttyset_old;
        /* twiddle the speed, parity, etc. but only on real serial ports */
        memset(session->ttyset.c_cc, 0, sizeof(session->ttyset.c_cc));
        //session->ttyset.c_cc[VTIME] = 1;
        /*
         * Tip from Chris Kuethe: the FIDI chip used in the Trip-Nav
         * 200 (and possibly other USB GPSes) gets completely hosed
         * in the presence of flow control.  Thus, turn off CRTSCTS.
         *
         * This is not ideal.  Setting no parity here will mean extra
         * initialization time for some devices, like certain Trimble
         * boards, that want 7O2 or other non-8N1 settings. But starting
         * the hunt loop at 8N1 will minimize the average sync time
         * over all devices.
         */
        session->ttyset.c_cflag &= ~(PARENB | PARODD | CRTSCTS | CSTOPB);
        session->ttyset.c_cflag |= CREAD | CLOCAL;
        session->ttyset.c_iflag = session->ttyset.c_oflag =
            session->ttyset.c_lflag = (tcflag_t) 0;

        session->baudindex = 0;
        if (0 < session->context->fixed_port_speed) {
            new_speed = session->context->fixed_port_speed;
        } else {
            new_speed = gpsd_get_speed_old(session);
        }
        if ('\0' == session->context->fixed_port_framing[0]) {
            new_parity = 'N';
            new_stop = 1;
        } else {
            // ignore length, stopbits=2 forces length 7.
            new_parity = session->context->fixed_port_framing[1];
            new_stop = session->context->fixed_port_framing[2] - '0';
        }
        gpsd_set_speed(session, new_speed, new_parity, new_stop);
    }

    /* Used to turn off O_NONBLOCK here, but best not to block trying
     * to read from an unresponsive receiver. */

    /* required so parity field won't be '\0' if saved speed matches */
    if (session->sourcetype <= source_blockdev) {
        session->gpsdata.dev.parity = 'N';
        session->gpsdata.dev.stopbits = 1;
    }

    GPSD_LOG(LOG_SPIN, &session->context->errout,
             "SER: open(%s) -> %d in gpsd_serial_open()\n",
             session->gpsdata.dev.path, session->gpsdata.gps_fd);
    return session->gpsdata.gps_fd;
}

ssize_t gpsd_serial_write(struct gps_device_t * session,
                          const char *buf, const size_t len)
{
    ssize_t status;
    bool ok;
    if (session == NULL ||
        session->context == NULL || session->context->readonly)
        return 0;
    status = write(session->gpsdata.gps_fd, buf, len);
    ok = (status == (ssize_t) len);
    (void)tcdrain(session->gpsdata.gps_fd);
    /* extra guard prevents expensive hexdump calls */
    if (session->context->errout.debug >= LOG_IO) {
        char scratchbuf[MAX_PACKET_LENGTH*2+1];
        GPSD_LOG(LOG_IO, &session->context->errout,
                 "SER: => GPS: %s%s\n",
                 gpsd_packetdump(scratchbuf, sizeof(scratchbuf),
                                 (char *)buf, len), ok ? "" : " FAILED");
    }
    return status;
}

/*
 * This constant controls how long the packet sniffer will spend looking
 * for a packet leader before it gives up.  It *must* be larger than
 * MAX_PACKET_LENGTH or we risk never syncing up at all.  Large values
 * will produce annoying startup lag.
 */
#define SNIFF_RETRIES   (MAX_PACKET_LENGTH + 128)

/* advance to the next hunt setting  */
bool gpsd_next_hunt_setting(struct gps_device_t * session)
{
    // every rate we're likely to see on an GNSS receiver

    /* don't waste time in the hunt loop if this is not actually a tty */
    if (0 == isatty(session->gpsdata.gps_fd))
        return false;

    /* ...or if it's nominally a tty but delivers only PPS and no data */
    if (session->sourcetype == source_pps)
        return false;

    if (session->lexer.retry_counter++ >= SNIFF_RETRIES) {
        char new_parity;   // E, N, O
        unsigned int new_stop;
        // u-blox 9 can do 921600
        static unsigned int rates[] =
            {0, 4800, 9600, 19200, 38400, 57600, 115200, 230400,
             460800, 921600};

        if (0 < session->context->fixed_port_speed) {
            //  fixed speed, don't hunt
            //  this prevents framing hunt?
            return false;
        }

        if (session->baudindex++ >=
            (unsigned int)(sizeof(rates) / sizeof(rates[0])) - 1) {

            session->baudindex = 0;
            if ('\0' != session->context->fixed_port_framing[0]) {
                return false;   /* hunt is over, no sync */
            }

            // More stop bits to try?
            if (session->gpsdata.dev.stopbits++ >= 2) {
                return false;   /* hunt is over, no sync */
            }
        }

        if ('\0' == session->context->fixed_port_framing[0]) {
            new_parity = session->gpsdata.dev.parity;
            new_stop = session->gpsdata.dev.stopbits;
        } else {
            // ignore length, stopbits=2 forces length 7.
            new_parity = session->context->fixed_port_framing[1];
            new_stop = session->context->fixed_port_framing[2] - '0';
        }

        gpsd_set_speed(session, rates[session->baudindex], new_parity,
                       new_stop);
        session->lexer.retry_counter = 0;
    }
    return true;                /* keep hunting */
}

/* to be called when we want to register that we've synced with a device */
void gpsd_assert_sync(struct gps_device_t *session)
{
    /*
     * We've achieved first sync with the device. Remember the
     * baudrate so we can try it first next time this device
     * is opened.
     */
    if (session->saved_baud == -1)
        session->saved_baud = (int)cfgetispeed(&session->ttyset);
}

void gpsd_close(struct gps_device_t *session)
{
    if (!BAD_SOCKET(session->gpsdata.gps_fd)) {
#ifdef TIOCNXCL
        (void)ioctl(session->gpsdata.gps_fd, (unsigned long)TIOCNXCL);
#endif /* TIOCNXCL */
        if (!session->context->readonly)
                (void)tcdrain(session->gpsdata.gps_fd);
        if (isatty(session->gpsdata.gps_fd) != 0) {
            /* force hangup on close on systems that don't do HUPCL properly */
            (void)cfsetispeed(&session->ttyset, (speed_t) B0);
            (void)cfsetospeed(&session->ttyset, (speed_t) B0);
            (void)tcsetattr(session->gpsdata.gps_fd, TCSANOW,
                            &session->ttyset);
        }
        /* this is the clean way to do it */
        session->ttyset_old.c_cflag |= HUPCL;
        /*
         * Don't revert the serial parameters if we didn't have to mess with
         * them the first time.  Economical, and avoids tripping over an
         * obscure Linux 2.6 kernel bug that disables threaded
         * ioctl(TIOCMIWAIT) on a device after tcsetattr() is called.
         */
        if ((cfgetispeed(&session->ttyset_old) !=
             cfgetispeed(&session->ttyset)) ||
            ((session->ttyset_old.c_cflag & CSTOPB) !=
             (session->ttyset.c_cflag & CSTOPB))) {
            /*
             * If we revert, keep the most recent baud rate.
             * Cuts down on autobaud overhead the next time.
             */
            (void)cfsetispeed(&session->ttyset_old,
                              (speed_t) session->gpsdata.dev.baudrate);
            (void)cfsetospeed(&session->ttyset_old,
                              (speed_t) session->gpsdata.dev.baudrate);
            (void)tcsetattr(session->gpsdata.gps_fd, TCSANOW,
                            &session->ttyset_old);
        }
        GPSD_LOG(LOG_SPIN, &session->context->errout,
                 "SER: close(%d) in gpsd_close(%s)\n",
                 session->gpsdata.gps_fd, session->gpsdata.dev.path);
        (void)close(session->gpsdata.gps_fd);
        session->gpsdata.gps_fd = -1;
    }
}
// vim: set expandtab shiftwidth=4
