/*
 * This is the gpsd driver for GeoStar Navigation receivers
 * operating in binary mode.
 *
 * Tested with GeoS-1M GPS/GLONASS receiver.
 *
 * By Viktar Palstsiuk, viktar.palstsiuk@promwad.com
 *
 * This file is Copyright (c) 2010 by the GPSD project
 * BSD terms apply: see the file COPYING in the distribution root for details.
 */

#include <sys/time.h>		/* for select() */
#include <stdbool.h>
#include <stdio.h>
#include <math.h>
#include <string.h>

#include "gpsd.h"
#include "bits.h"

#ifdef GEOSTAR_ENABLE
#define GEOSTAR_CHANNELS	24

#define JAN_2008		0x47798280 /* 1199145600 = 2008 - 1970 in seconds */

#define OFFSET(n)		((n)*4+4)

static int decode_channel_id (uint32_t ch_id) {
	int num = 0;
	num = (int)(ch_id & 0x1F);		/* SV ID */
	if((ch_id & (1<<30)) == 0) num += 32;	/* GLONASS SV */
	else if (num == 0 ) num = 32;		/* GPS SV */
	return num;
}

static int geostar_write(struct gps_device_t *session,
			 unsigned int id, unsigned char *data, size_t len)
{
    int i;
    unsigned long cs = 0;

    putbyte(session->msgbuf, 0, 'P');
    putbyte(session->msgbuf, 1, 'S');
    putbyte(session->msgbuf, 2, 'G');
    putbyte(session->msgbuf, 3, 'G');

    /*@-shiftimplementation +ignoresigns@*/
    putbe16(session->msgbuf, 4, id);
    putbe16(session->msgbuf, 6, len);
    /*@+shiftimplementation -ignoresigns@*/

    /* Copy content */
    memcpy(session->msgbuf + 8, data, len * 4);

    len += 2; /* PSGG + id + len */

    /* Calculate checksum */
    for (i = 0; (size_t)i < len; i++) {
	cs ^= getleu32(session->msgbuf, i * 4);
    }

    /*@-shiftimplementation +ignoresigns@*/
    putle32(session->msgbuf, len * 4, cs);
    /*@+shiftimplementation -ignoresigns@*/

    len += 1; /* Checksum */

    session->msgbuflen = len * 4;

    gpsd_report(LOG_IO, "Sent GeoStar packet id 0x%x: %s\n", id,
		gpsd_hexdump_wrapper(session->msgbuf, session->msgbuflen, LOG_IO));

    if (gpsd_write(session, session->msgbuf, session->msgbuflen) !=
	(ssize_t) session->msgbuflen)
	return -1;

    return 0;
}

/* geostar_detect()
 *
 * see if it looks like a GeoStar device is listening and
 * return 1 if found, 0 if not
 */
static bool geostar_detect(struct gps_device_t *session)
{
    unsigned char buf[1 * 4];
    unsigned int n;
    bool ret = false;
    int myfd;
    fd_set fdset;
    struct timeval to;

    gpsd_report(LOG_PROG, "Probing GeoStar\n");

    myfd = session->gpsdata.gps_fd;

    /* request firmware revision and look for a valid response */
    /*@-shiftimplementation +ignoresigns@*/
    putbe32(buf, 0, 0);
    /*@+shiftimplementation +ignoresigns@*/
    if (geostar_write(session, 0xc1, buf, 1) == 0) {
	for (n = 0; n < 3; n++) {
	    FD_ZERO(&fdset);
	    FD_SET(myfd, &fdset);
	    to.tv_sec = 1;
	    to.tv_usec = 0;
	    if (select(myfd + 1, &fdset, NULL, NULL, &to) != 1)
		break;
	    if (generic_get(session) >= 0) {
		if (session->packet.type == GEOSTAR_PACKET) {
		    gpsd_report(LOG_RAW, "geostar_detect found\n");
		    ret = true;
		    break;
		}
	    }
	}
    }

    return ret;
}

static gps_mask_t geostar_analyze(struct gps_device_t *session)
{
    int i, j, len;
    gps_mask_t mask = 0;
    unsigned int id;
    int16_t s1, s2, s3;
    uint16_t uw1, uw2;
    uint32_t ul1, ul2, ul3, ul4, ul5;
    double d1, d2, d3, d4, d5;
    union long_double l_d;
    unsigned char buf[BUFSIZ];
    char buf2[BUFSIZ];

    if (session->packet.type != GEOSTAR_PACKET) {
	gpsd_report(LOG_INF, "geostar_analyze packet type %d\n",
		    session->packet.type);
	return 0;
    }

    /*@ +charint @*/
    if (session->packet.outbuflen < 12 || session->packet.outbuffer[0] != 'P')
	return 0;

    /* put data part of message in buf */

    memset(buf, 0, sizeof(buf));
    memcpy(buf, session->packet.outbuffer, session->packet.outbuflen);

    buf2[len = 0] = '\0';
    for (i = 0; i < (int)session->packet.outbuflen; i++) {
	(void)snprintf(buf2 + strlen(buf2),
		       sizeof(buf2) - strlen(buf2),
		       "%02x", buf[len++] = session->packet.outbuffer[i]);
    }
    /*@ -charint @*/

    id = (unsigned int)getleu16(session->packet.outbuffer, OFFSET(0));

    (void)snprintf(session->gpsdata.tag, sizeof(session->gpsdata.tag), "ID%02x", id);

    gpsd_report(LOG_IO, "GeoStar packet id 0x%02x length %d: %s\n", id, len, buf2);

    session->cycle_end_reliable = true;

    switch (id) {
    case 0x10:
	gpsd_report(LOG_INF, "Raw measurements\n");
	break;
    case 0x11:
	gpsd_report(LOG_INF, "GPS sub-frame data\n");
	break;
    case 0x12:
	gpsd_report(LOG_INF, "GLONASS sub-frame data\n");
	break;
    case 0x13:
	d1 = getled(buf, OFFSET(1));
	d2 = getled(buf, OFFSET(3));
	d3 = getled(buf, OFFSET(5));
	d4 = getled(buf, OFFSET(29)); /* GPS time */
	d5 = getled(buf, OFFSET(31)); /* GLONASS time */
	gpsd_report(LOG_INF, "ECEF coordinates %g %g %g %f %f\n", d1, d2, d3, d4, d5);
	break;
    case 0x20:
	d1 = getled(buf, OFFSET(1)); /* time */

	session->newdata.time = d1 + JAN_2008;
	session->newdata.latitude = getled(buf, OFFSET(3)) * RAD_2_DEG;
	session->newdata.longitude = getled(buf, OFFSET(5)) * RAD_2_DEG;
	session->newdata.altitude = getled(buf, OFFSET(7));
	session->gpsdata.separation = getled(buf, OFFSET(9));
	session->gpsdata.satellites_used = getles32(buf, OFFSET(11));
	session->gpsdata.dop.gdop = getled(buf, OFFSET(13));
	session->gpsdata.dop.pdop = getled(buf, OFFSET(15));
	session->gpsdata.dop.tdop = getled(buf, OFFSET(17));
	session->gpsdata.dop.hdop = getled(buf, OFFSET(19));
	session->gpsdata.dop.vdop = getled(buf, OFFSET(21));
	session->newdata.speed = getled(buf, OFFSET(31));
	session->newdata.track = getled(buf, OFFSET(33)) * RAD_2_DEG;

	ul1 = getleu32(buf, OFFSET(29)); /* status */

	if (ul1 != (uint8_t) 0) {
	    session->gpsdata.status = STATUS_NO_FIX;
	    mask |= STATUS_IS;
	} else {
	    if (session->gpsdata.status < STATUS_FIX) {
		session->gpsdata.status = STATUS_FIX;
		mask |= STATUS_IS;
	    }
	}
	mask |= TIME_IS | LATLON_IS | ALTITUDE_IS | SPEED_IS | TRACK_IS | DOP_IS | USED_IS | REPORT_IS;

	gpsd_report(LOG_INF, "Geographic coordinates %f %g %g %g %g %g\n",
		d1,
		session->newdata.latitude,
		session->newdata.longitude,
		session->newdata.altitude,
		session->newdata.speed,
		session->newdata.track);
	gpsd_report(LOG_INF, "Dilution of precision %g %g %g %g %g\n",
		session->gpsdata.dop.gdop,
		session->gpsdata.dop.pdop,
		session->gpsdata.dop.tdop,
		session->gpsdata.dop.hdop,
		session->gpsdata.dop.vdop);
	break;
    case 0x21:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	uw1 = getleu16(buf, OFFSET(3));
	uw2 = getleu16(buf, OFFSET(3) + 2);
	gpsd_report(LOG_INF, "Current receiver telemetry %x %d %d %d\n", ul1, ul2, uw1, uw2);
	if(ul1 & (1<<3)) {
	    session->newdata.mode = MODE_2D;
	}
	else {
	    session->newdata.mode = MODE_3D;
	}
	if(ul1 & (1<<2)) {
	    session->gpsdata.status = STATUS_FIX;
	}
	else {
	    session->gpsdata.status = STATUS_NO_FIX;
	    session->newdata.mode = MODE_NO_FIX;
	}

	mask |= MODE_IS | STATUS_IS;
	break;
    case 0x22:
	ul1 = getleu32(buf, OFFSET(1));
	gpsd_report(LOG_INF, "SVs in view %d\n", ul1);
	session->gpsdata.satellites_visible = (int)ul1;
	if(ul1 > GEOSTAR_CHANNELS) ul1 = GEOSTAR_CHANNELS;
	for(i = 0, j = 0; (uint32_t)i < ul1; i++) {
	    ul2 = getleu32(buf, OFFSET(2) + i * 3 * 4);
	    s1 = getles16(buf, OFFSET(3) + i * 3 * 4);
	    s2 = getles16(buf, OFFSET(3) + 2 + i * 3 * 4);
	    s3 = getles16(buf, OFFSET(4) + 2 + i * 3 * 4);
	    gpsd_report(LOG_INF, "ID %d Az %g El %g SNR %g\n",
			decode_channel_id(ul2), s1*0.001*RAD_2_DEG, s2*0.001*RAD_2_DEG, s3*0.1);
	    session->gpsdata.PRN[i] = decode_channel_id(ul2);
	    session->gpsdata.azimuth[i] = (int)round(s1*0.001 * RAD_2_DEG);
	    session->gpsdata.elevation[i] = (int)round(s2*0.001 * RAD_2_DEG);
	    session->gpsdata.ss[i] = s3*0.1;
	    if(ul2 & (1<<27)) {
		session->gpsdata.used[j++] = decode_channel_id(ul2);
	    }
	}
	session->gpsdata.skyview_time = NAN;
	mask |= SATELLITE_IS | USED_IS;
	break;
    case 0x3e:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	ul3 = getleu32(buf, OFFSET(3));
	gpsd_report(LOG_INF, "Receiver power-up message %d %d %d\n", ul1, ul2, ul3);
	break;
    case 0x3f:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	gpsd_report(LOG_WARN, "Negative acknowledge %x %d\n", ul1, ul2);
	break;
    case 0x40:
	gpsd_report(LOG_INF, "Response to Set initial parameters\n");
	break;
    case 0x41:
	gpsd_report(LOG_INF, "Response to Set serial ports parameters\n");
	break;
    case 0x42:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	ul3 = getleu32(buf, OFFSET(3));
	gpsd_report(LOG_INF, "Response to Set receiver operation mode %d %d %d\n",
		    ul1, ul2, ul3);
	break;
    case 0x43:
	gpsd_report(LOG_INF, "Response to Set navigation task solution parameters\n");
	break;
    case 0x44:
	gpsd_report(LOG_INF, "Response to Set output data rate\n");
	break;
    case 0x46:
	gpsd_report(LOG_INF, "Response to Assign data protocol to communication port\n");
	break;
    case 0x48:
	gpsd_report(LOG_INF, "Response to Set GPS almanac\n");
	break;
    case 0x49:
	gpsd_report(LOG_INF, "Response to Set GLONASS almanac\n");
	break;
    case 0x4a:
	gpsd_report(LOG_INF, "Response to Set GPS ephemeris\n");
	break;
	case 0x4b:
	gpsd_report(LOG_INF, "Response to Set GLONASS ephemeris\n");
	break;
    case 0x4c:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	ul3 = getleu32(buf, OFFSET(3));
	ul4 = getleu32(buf, OFFSET(4));
	ul5 = getleu32(buf, OFFSET(5));
	gpsd_report(LOG_INF, "Response to Set PPS paramters %d %d %d %d %d\n",
		    ul1, ul2, ul3, ul4, ul5);
	break;
    case 0x4d:
	gpsd_report(LOG_INF, "Response to Enable/disable SV in position fix\n");
	break;
    case 0x4e:
	gpsd_report(LOG_INF, "Response to Enable/disable NMEA messages\n");
	break;
    case 0x4f:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	gpsd_report(LOG_INF, "Response to Enable/disable binary messages %x %x\n",
		    ul1, ul2);
	break;
    case 0x80:
	gpsd_report(LOG_INF, "Response to Query initial parameters\n");
	break;
    case 0x81:
	gpsd_report(LOG_INF, "Response to Query serial ports parameters\n");
	break;
    case 0x82:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	ul3 = getleu32(buf, OFFSET(3));
	gpsd_report(LOG_INF, "Response to Query receiver operation mode %d %d %d\n",
		    ul1, ul2, ul3);
	break;
    case 0x83:
	gpsd_report(LOG_INF, "Response to Query navigation task solution parameters\n");
	break;
    case 0x84:
	gpsd_report(LOG_INF, "Response to Query output data rate\n");
	break;
    case 0x86:
	session->driver.geostar.physical_port = (unsigned int)getleu32(buf, OFFSET(1));
	gpsd_report(LOG_INF, "Response to Query data protocol assignment to communication port\n");
	gpsd_report(LOG_INF, "Connected to physical port %d\n",
		    session->driver.geostar.physical_port);
	break;
    case 0x88:
	gpsd_report(LOG_INF, "Response to Query GPS almanac\n");
	break;
    case 0x89:
	gpsd_report(LOG_INF, "Response to Query GLONASS almanac\n");
	break;
    case 0x8a:
	gpsd_report(LOG_INF, "Response to Query GPS ephemerides\n");
	break;
    case 0x8b:
	d1 = getled(buf, OFFSET(23));
	d2 = getled(buf, OFFSET(25));
	d3 = getled(buf, OFFSET(27));
	gpsd_report(LOG_INF, "Response to Query GLONASS ephemerides %g %g %g\n",
		    d1, d2, d3);
	break;
    case 0x8c:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	ul3 = getleu32(buf, OFFSET(3));
	ul4 = getleu32(buf, OFFSET(4));
	ul5 = getleu32(buf, OFFSET(5));
	gpsd_report(LOG_INF, "Response to Query PPS parameters %d %d %d %d %d\n",
		    ul1, ul2, ul3, ul4, ul5);
	break;
    case 0x8d:
	gpsd_report(LOG_INF, "Response to Query enable/disable status of the SV in position fix\n");
	break;
    case 0x8e:
	gpsd_report(LOG_INF, "Response to Query enable NMEA messages\n");
	break;
    case 0x8f:
	ul1 = getleu32(buf, OFFSET(1));
	ul2 = getleu32(buf, OFFSET(2));
	gpsd_report(LOG_INF, "Response to Query enable binary messages %x %x\n",
		    ul1, ul2);
	break;
    case 0xc0:
	gpsd_report(LOG_INF, "Response to Change operation mode command\n");
	break;
    case 0xc1:
	ul4 = getleu32(buf, OFFSET(1));
	ul1 = getleu32(buf, OFFSET(2));
	ul2 = getleu32(buf, OFFSET(3));
	ul3 = getleu32(buf, OFFSET(4));
	/*@ -formattype @*/
	(void)snprintf(session->subtype, sizeof(session->subtype), "%d.%d %d.%d.%d %x %c-%d\n",
		ul4>>16, ul4&0xFFFF, ul1>>9, (ul1>>5)&0xF, ul1&0x1F, ul2, ul3>>24, ul3&0x00FFFFFF);
	/*@ +formattype @*/
	gpsd_report(LOG_INF, "Response to Request FW version command: %s\n",
		    session->subtype);
	mask |= DEVICEID_IS;
	break;
    case 0xc2:
	gpsd_report(LOG_INF, "Response to Restart receiver command\n");
	break;
    case 0xc3:
	gpsd_report(LOG_INF, "Response to Store parameters to Flash command\n");
	break;
    case 0xd0:
	gpsd_report(LOG_INF, "Response to Erase Flash sector command\n");
	break;
    case 0xd1:
	gpsd_report(LOG_INF, "Response to Write data to Flash command\n");
	break;
    case 0xd2:
	gpsd_report(LOG_INF, "Response to Store Serial Number command\n");
	break;
    default:
	gpsd_report(LOG_WARN, "Unhandled GeoStar packet type 0x%02x\n", id);
	break;
    }

    return mask;
}

static gps_mask_t geostar_parse_input(struct gps_device_t *session)
{
    gps_mask_t st;

    if (session->packet.type == GEOSTAR_PACKET) {
	st = geostar_analyze(session);
	session->gpsdata.dev.driver_mode = MODE_BINARY;
	return st;
    } else
	return 0;
}

#ifdef ALLOW_CONTROLSEND
static ssize_t geostar_control_send(struct gps_device_t *session,
				 char *buf, size_t buflen)
/* not used by the daemon, it's for gpsctl and friends */
{
    return (ssize_t) geostar_write(session,
				(unsigned int)buf[0],
				(unsigned char *)buf + 1, (buflen - 1)/4);
}
#endif /* ALLOW_CONTROLSEND */

static void geostar_event_hook(struct gps_device_t *session, event_t event)
{
    unsigned char buf[2 * 4];

    /*@-shiftimplementation +ignoresigns@*/
    if (event == event_identified && event == event_reactivate) {
	/* Select binary packets */
	putbe32(buf, 0, 0xffff0000);
	putbe32(buf, 4, 0);
	(void)geostar_write(session, 0x4f, buf, 2);

	/* Poll Ports params */
	putbe32(buf, 0, 1);
	(void)geostar_write(session, 0x81, buf, 1);
	putbe32(buf, 0, 0);
	(void)geostar_write(session, 0x81, buf, 1);
	/* Poll Init params */
	(void)geostar_write(session, 0x80, buf, 1);
	/* Poll Mode */
	(void)geostar_write(session, 0x82, buf, 1);
	/* Poll Solution params */
	(void)geostar_write(session, 0x83, buf, 1);
	/* Poll Output rate */
	(void)geostar_write(session, 0x84, buf, 1);
	/* Poll Protocols assignment */
	(void)geostar_write(session, 0x86, buf, 1);
	/* Poll PPS params */
	(void)geostar_write(session, 0x8c, buf, 1);
	/* Poll NMEA packets selected */
	(void)geostar_write(session, 0x8e, buf, 1);
	/* Poll binary packets selected */
	(void)geostar_write(session, 0x8f, buf, 1);
	/* Poll Software Version */
	(void)geostar_write(session, 0xc1, buf, 1);
    }

    if (event == event_deactivate) {
	/* Perform cold restart */
	putbe32(buf, 0, 3);
	(void)geostar_write(session, 0xc2, buf, 1);
    }
    /*@+shiftimplementation -ignoresigns@*/
}

#ifdef ALLOW_RECONFIGURE
static bool geostar_speed_switch(struct gps_device_t *session,
			      speed_t speed, char parity, int stopbits)
{
    unsigned char buf[4 * 4];

    switch (parity) {
    case 'E':
    case 2:
	parity = (char)2;
	break;
    case 'O':
    case 1:
	parity = (char)1;
	break;
    case 'N':
    case 0:
    default:
	parity = (char)0;
	break;
    }

    /*@-shiftimplementation@*/
    putbe32(buf, 0, session->driver.geostar.physical_port);
    putbe32(buf, 4, speed);
    putbe32(buf, 8, stopbits);
    putbe32(buf, 12, parity);
    /*@+shiftimplementation@*/
    (void)geostar_write(session, 0x41, buf, 4);

    return true;	/* it would be nice to error-check this */
}

static void geostar_mode(struct gps_device_t *session, int mode)
{
    unsigned char buf[1 * 4];

    /*@-shiftimplementation@*/
    if (mode == MODE_NMEA) {
	/* Switch to NMEA mode */
	putbe32(buf, 0, (session->driver.geostar.physical_port == 0) ? 1 : 0);
	(void)geostar_write(session, 0x46, buf, 1);
    } else if (mode == MODE_BINARY) {
	/* Switch to binary mode */
	(void)nmea_send(session, "$GPSGG,SWPROT");
    } else {
	gpsd_report(LOG_ERROR, "unknown mode %i requested\n", mode);
    }
    /*@+shiftimplementation@*/
}
#endif /* ALLOW_RECONFIGURE */

#ifdef NTPSHM_ENABLE
static double geostar_ntp_offset(struct gps_device_t *session UNUSED)
{
    return 0.31;
}
#endif /* NTPSHM_ENABLE */

/* this is everything we export */
/* *INDENT-OFF* */
const struct gps_type_t geostar_binary =
{
    .type_name      = "GeoStar binary",	/* full name of type */
    .packet_type    = GEOSTAR_PACKET,	/* associated lexer packet type */
    .flags	    = DRIVER_NOFLAGS,	/* no flags set */
    .trigger	    = NULL,		/* no trigger */
    .channels       = GEOSTAR_CHANNELS,	/* consumer-grade GPS/GLONASS */
    .probe_detect   = geostar_detect,	/* probe for device */
    .get_packet     = generic_get,	/* use the generic packet getter */
    .parse_packet   = geostar_parse_input,	/* parse message packets */
    .rtcm_writer    = NULL,		/* doesn't accept DGPS corrections */
    .event_hook     = geostar_event_hook,	/* ifire on various lifetime events */
#ifdef ALLOW_RECONFIGURE
    .speed_switcher = geostar_speed_switch,/* change baud rate */
    .mode_switcher  = geostar_mode,	/* there is a mode switcher */
    .rate_switcher  = NULL,		/* no rate switcher */
    .min_cycle      = 1,		/* not relevant, no rate switcher */
#endif /* ALLOW_RECONFIGURE */
#ifdef ALLOW_CONTROLSEND
    .control_send   = geostar_control_send,/* how to send commands */
#endif /* ALLOW_CONTROLSEND */
#ifdef NTPSHM_ENABLE
    .ntp_offset     = geostar_ntp_offset,
#endif /* NTPSHM_ENABLE */
};
/* *INDENT-ON* */

#endif /* GEOSTAR_ENABLE */
