#!/usr/pkg/bin/python3.13
# This code is generated by scons.  Do not hand-hack it!
# -*- coding: utf-8 -*-
# This file is Copyright 2010 by the GPSD project
# SPDX-License-Identifier: BSD-2-clause

# This code runs compatibly under Python 2 and 3.x for x >= 2.
# Preserve this property!
#
"""gpscsv -- convert gpsd JSON streams into csv files."""

from __future__ import print_function

import argparse
import socket
import sys
import time            # for time.time()


def _do_one_line(data):
    """dump one report line."""

    if options.json_fields is None:
        # no fields specified, use the 1st ones found
        fields = data.keys()
        options.json_fields = []
        for fld in fields:
            if isinstance(data[fld], (dict, list)):
                # skip dictionary fields
                continue

            options.json_fields.append(fld)

        if 0 < options.header:
            fld = options.separator.join(options.json_fields)
            if 2 == options.header:
                fld = '# ' + fld
            print(fld)

    out = []
    for fld in options.json_fields:
        if 'time' == fld and options.cvtisotime:
            # convert 2020-08-17T23:58:01.000Z to 1597708682.0
            data[fld] = gps.isotime(data[fld])
        if fld not in data or 'null' == data[fld]:
            out.append('')
        else:
            out.append(str(data[fld]))
    print(options.separator.join(out))


# pylint wants local modules last
try:
    import gps
except ImportError as e:
    sys.stderr.write(
        "%s: can't load Python gps libraries -- check PYTHONPATH.\n" %
        (sys.argv[0]))
    sys.stderr.write("%s\n" % e)
    sys.exit(1)

gps_version = '3.27.5'
if gps.__version__ != gps_version:
    sys.stderr.write("%s: ERROR: need gps module version %s, got %s\n" %
                     (sys.argv[0], gps_version, gps.__version__))
    sys.exit(1)

description = 'Convert one gpsd JSON message class to csv format.'
usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
epilog = ('  -c ALMANAC to dump the Almanacs from Subframes 4/5.\n'
          '  -c HEALTH to dump health data from Subframe 4, page 25\n'
          '  -c HEALTH2 to dump health data from Subframe 5, page 25\n'
          '  -c IONO to dump the Iono/UTC data from Subframe 4, page 18\n'
          '  -c NMCT to dump the ERD data from Subframe 4, page 13\n'
          '  -c SAT to dump the satellite records from the SKY messages.\n'
          '  -c SUBFRAME1 to dump Ephemeris1 from Subframe 1\n'
          '  -c SUBFRAME2 to dump Ephemeris2 from Subframe 2\n'
          '  -c SUBFRAME3 to dump Ephermeris3 from Subframe 3\n\n'
          'BSD terms apply: see the file COPYING in the distribution root'
          ' for details.')

parser = argparse.ArgumentParser(
    description=description,
    epilog=epilog,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    usage=usage)
parser.add_argument(
    '-?',
    action="help",
    help='show this help message and exit'
)
parser.add_argument(
    '-c',
    '--class',
    dest='mclass',         # class is a reserved word
    default='TPV',
    help='Message class to process. [Default %(default)s]'
)
parser.add_argument(
    '--cvt-isotime',
    dest='cvtisotime',
    default=False,
    action="store_true",
    help='Convert ISO time to UNIX time'
)
parser.add_argument(
    '-D',
    '--debug',
    dest='debug',
    default=0,
    type=int,
    help='Set level of debug. Must be integer. [Default %(default)s]'
)
parser.add_argument(
    '--device',
    dest='device',
    default='',
    help='The device to connect. [Default %(default)s]'
)
parser.add_argument(
    '-f',
    '--fields',
    dest='fields',
    default=None,
    help='Fields to process. '' for all.  Default varies by class.'
)
parser.add_argument(
    '--file',
    dest='input_file_name',
    default=None,
    metavar='FILE',
    help='Read gpsd JSON from FILE instead of a gpsd instance.',
)
parser.add_argument(
    '--header',
    dest='header',
    default=1,
    type=int,
    help='0: No header, 1: fields, 2: comment. [Default %(default)s]'
)
parser.add_argument(
    '--host',
    dest='host',
    default='localhost',
    help='The host to connect. [Default %(default)s]'
)
parser.add_argument(
    '-n',
    '--count',
    dest='count',
    default=0,
    type=int,
    help='Count of messages to parse. 0 to disable. [Default %(default)s]'
)
parser.add_argument(
    '--port',
    dest='port',
    default=gps.GPSD_PORT,
    help='The port to connect. [Default %(default)s]'
)
parser.add_argument(
    '--separator',
    dest='separator',
    default=',',
    type=str,
    help='CSV field separator character. [Default %(default)s]'
)
parser.add_argument(
    '-V', '--version',
    action='version',
    version="%(prog)s: Version " + gps_version + "\n",
    help='Output version to stderr, then exit'
)
parser.add_argument(
    '-x',
    '--seconds',
    dest='seconds',
    default=0,
    type=int,
    help='Seconds of messages to parse. 0 to disable. [Default %(default)s]'
)
parser.add_argument(
    'target',
    nargs='?',
    help='[host[:port[:device]]]'
)
options = parser.parse_args()

# the options host, port, device are set by the defaults
if options.target:
    # override host, port and device with target
    arg = options.target.split(':')
    len_arg = len(arg)
    if 1 == len_arg:
        (options.host,) = arg
    elif 2 == len_arg:
        (options.host, options.port) = arg
    elif 3 == len_arg:
        (options.host, options.port, options.device) = arg
    else:
        parser.print_help()
        sys.exit(0)

# Fields to parse
# Python dicts are unordered, so try to clean things up a little
options.json_fields = None
if options.fields is None:
    default_fields = {'ALMANAC': ('TOW17', 'tSV', 'ID', 'Health', 'e',
                                  'toa', 'deltai', 'Omegad', 'sqrtA',
                                  'Omega0', 'omega', 'M0', 'af0', 'af1'),
                      'HEALTH': ('TOW17', 'tSV',
                                 'SV1', 'SV2', 'SV3', 'SV4', 'SV5',
                                 'SV6', 'SV7', 'SV8', 'SV9', 'SV10',
                                 'SV11', 'SV12', 'SV13', 'SV14', 'SV15',
                                 'SV16', 'SV17', 'SV18', 'SV19', 'SV20',
                                 'SV21', 'SV22', 'SV23', 'SV24', 'SV25',
                                 'SV26', 'SV27', 'SV28', 'SV29', 'SV30',
                                 'SV31', 'SV32',
                                 'SVH25', 'SVH26', 'SVH27', 'SVH28', 'SVH29',
                                 'SVH30', 'SVH31', 'SVH32'),
                      'HEALTH2': ('TOW17', 'tSV', 'toa', 'WNa',
                                  'SVH1', 'SVH2', 'SVH3', 'SVH4', 'SVH5',
                                  'SVH6', 'SVH7', 'SVH8', 'SVH9', 'SVH10',
                                  'SVH11', 'SVH12', 'SVH13', 'SVH14', 'SVH15',
                                  'SVH16', 'SVH17', 'SVH18', 'SVH19', 'SVH20',
                                  'SVH21', 'SVH22', 'SVH23', 'SVH24'),
                      'IONO': ('TOW17', 'tSV', 'WNt', 'tot',
                               'lsf', 'ls', 'WNlsf', 'DN',
                               'A0', 'A1', 'a0', 'a1', 'a2', 'a3',
                               'b0', 'b1', 'b2', 'b3'),
                      'NMCT': ('TOW17', 'tSV', 'ai',
                               'ERD1', 'ERD2', 'ERD3', 'ERD4', 'ERD5',
                               'ERD6', 'ERD7', 'ERD8', 'ERD9', 'ERD10',
                               'ERD11', 'ERD12', 'ERD13', 'ERD14', 'ERD15',
                               'ERD16', 'ERD17', 'ERD18', 'ERD19', 'ERD20',
                               'ERD21', 'ERD22', 'ERD23', 'ERD24', 'ERD25',
                               'ERD26', 'ERD27', 'ERD28', 'ERD29', 'ERD30',
                               'ERD31'),
                      'SAT': ('time', 'gnssid', 'svid', 'PRN', 'sigid', 'az',
                               'el', 'ss', 'used', 'health'),
                      'SKY': ('time', 'xdop', 'ydop', 'vdop', 'tdop',
                              'hdop', 'gdop', 'pdop'),
                      'SUBFRAME1': ('TOW17', 'tSV', 'IODC', 'WN', 'ura',
                                    'hlth', 'L2', 'L2P', 'Tgd',
                                    'toc', 'af0', 'af1', 'af2'),
                      'SUBFRAME2': ('TOW17', 'tSV', 'IODE', 'M0',
                                    'deltan', 'e', 'sqrtA', 'FIT', 'AODO',
                                    'Crs', 'Cuc', 'Cus', 'toe'),
                      'SUBFRAME3': ('TOW17', 'tSV', 'IODE',
                                    'Crc', 'Cic', 'Cis', 'Omega0', 'i0',
                                    'omega', 'Omegad', 'IDOT'),
                      'TPV': ('time', 'lat', 'lon', 'altHAE'),
                      }
    # None specified, use defaults, if they exist
    if options.mclass in default_fields:
        options.json_fields = default_fields[options.mclass]
    else:
        # autodetect, read one message, use those fields
        options.json_fields = None
elif '' == options.fields:
    # autodetect, read one message, use those fields
    options.json_fields = None
else:
    options.json_fields = options.fields.split(',')

options.frames = None
if 'ALMANAC' == options.mclass:
    options.mclass = 'SUBFRAME'
    options.subclass = 'ALMANAC'
    options.frames = [4, 5]
elif 'HEALTH' == options.mclass:
    options.mclass = 'SUBFRAME'
    options.subclass = 'HEALTH'
    options.frames = [4]
elif 'HEALTH2' == options.mclass:
    options.mclass = 'SUBFRAME'
    options.subclass = 'HEALTH2'
    options.frames = [5]
elif 'IONO' == options.mclass:
    options.mclass = 'SUBFRAME'
    options.subclass = 'IONO'
    options.frames = [4]
elif 'NMCT' == options.mclass:
    # Note: ai is probably 01, which means the NMCT data is encrypted.
    options.mclass = 'SUBFRAME'
    options.subclass = 'NMCT'
    options.frames = [4]
elif 'SAT' == options.mclass:
    options.mclass = 'SKY'
    options.subclass = 'SAT'
elif 'SUBFRAME1' == options.mclass:
    options.mclass = 'SUBFRAME'
    options.subclass = 'EPHEM1'
    options.frames = [1]
elif 'SUBFRAME2' == options.mclass:
    options.mclass = 'SUBFRAME'
    options.subclass = 'EPHEM2'
    options.frames = [2]
elif 'SUBFRAME3' == options.mclass:
    options.mclass = 'SUBFRAME'
    options.subclass = 'EPHEM3'
    options.frames = [3]
else:
    options.subclass = None

try:
    session = gps.gps(host=options.host, port=options.port,
                      input_file_name=options.input_file_name,
                      verbose=options.debug)
except socket.error:
    sys.stderr.write("gpscsv: Could not connect to gpsd daemon\n")
    sys.exit(1)

session.stream(gps.WATCH_ENABLE | gps.WATCH_SCALED, devpath=options.device)


# top line is headings
if options.json_fields is not None and 0 < options.header:
    f = options.separator.join(options.json_fields)
    if 2 == options.header:
        f = '# ' + f
    print(f)

count = 0
if 0 < options.seconds:
    end_seconds = time.time() + options.seconds
else:
    end_seconds = 0

try:
    while 0 == session.read():

        if ((session.data is None or
            'class' not in session.data)):
            # Not JSON input.  Maybe NMEA, etc.
            sys.stderr.write("WARNING: Malformed input %s\n" % session.data)
            continue
        if session.data['class'] != options.mclass:
            continue
        if 'SAT' == options.subclass:
            # grab the sats, one at a time
            if 'satellites' not in session.data:
                continue
            for sat in session.data['satellites']:
                if 'time' not in session.data:
                    sys.stderr.write("WARNING: missing time in SKY\n")
                    continue
                subreport = {'time': session.data['time']}
                subreport.update(sat)
                _do_one_line(subreport)
        elif 'SUBFRAME' == options.mclass:
            if session.data['frame'] not in options.frames:
                continue
            if options.subclass not in session.data:
                # Not all subframe 4/5 have ALMANAC
                continue

            subreport = {'TOW17': session.data['TOW17'],
                         'tSV': session.data['tSV']}
            subreport.update(session.data[options.subclass])
            _do_one_line(subreport)

        else:
            _do_one_line(session.data)

        if 0 < options.count:
            count += 1
            if count >= options.count:
                break

        if 0 < options.seconds:
            if time.time() > end_seconds:
                break

except KeyboardInterrupt:
    # caught control-C
    print()
    sys.exit(1)
