CoolFace
Datasetpublic

12Parker/python-migrations

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes92downloads
rtkbase-task-instances.jsonl.all2 linesDownload Raw Back to train
1{"repo": "Stefal/rtkbase", "pull_number": 60, "instance_id": "Stefal__rtkbase-60", "issue_numbers": "", "base_commit": "73637def7ff8219fed889fcfcb4435dd2076626e", "patch": "diff --git a/tools/gps/__init__.py b/tools/gps/__init__.py\nnew file mode 100755\n--- /dev/null\n+++ b/tools/gps/__init__.py\n@@ -0,0 +1,26 @@\n+# Make core client functions available without prefix.\n+# This code is generated by scons.  Do not hand-hack it!\n+#\n+# This file is Copyright 2010 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-Clause\n+#\n+# This code runs compatibly under Python 2 and 3.x for x >= 2.\n+# Preserve this property!\n+from __future__ import absolute_import  # Ensure Python2 behaves like Python 3\n+\n+from .gps import *\n+from .misc import *\n+\n+# Keep in sync with gpsd.h\n+api_version_major = 3   # bumped on incompatible changes\n+api_version_minor = 14   # bumped on compatible changes\n+\n+# at some point this will need an override method\n+__iconpath__ = '//usr/local/share/gpsd/icons'\n+\n+__version__ = '3.20.1~dev'\n+\n+# The 'client' module exposes some C utility functions for Python clients.\n+# The 'packet' module exposes the packet getter via a Python interface.\n+\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/aiogps.py b/tools/gps/aiogps.py\nnew file mode 100644\n--- /dev/null\n+++ b/tools/gps/aiogps.py\n@@ -0,0 +1,309 @@\n+#!/usr/bin/env python3\n+# -*- coding: utf-8 -*-\n+\n+# Copyright 2019 Grand Joldes (grandwork2@yahoo.com).\n+#\n+# This file is Copyright 2019 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-clause\n+\n+# This code run compatibly under Python 3.x for x >= 6.\n+\n+\"\"\"aiogps.py -- Asyncio Python interface to GPSD.\n+\n+This module adds asyncio support to the Python gps interface. It runs on\n+Python versions >= 3.6 and provides the following benefits:\n+    - easy integration in asyncio applications (all I/O operations done through\n+        non-blocking coroutines, async context manager, async iterator);\n+    - support for cancellation (all operations are cancellable);\n+    - support for timeouts (on both read and connect);\n+    - support for connection keep-alive (using the TCP keep alive mechanism)\n+    - support for automatic re-connection;\n+    - configurable connection parameters;\n+    - configurable exeption handling (internally or by application);\n+    - logging support (logger name: 'gps.aiogps').\n+\n+The use of timeouts, keepalive and automatic reconnection make possible easy\n+handling of GPSD connections over unreliable networks.\n+\n+Examples:\n+    import logging\n+    import gps.aiogps\n+\n+    # configuring logging\n+    logging.basicConfig()\n+    logging.root.setLevel(logging.INFO)\n+    # Example of setting up logging level for the aiogps logger\n+    logging.getLogger('gps.aiogps').setLevel(logging.ERROR)\n+\n+    # using default parameters\n+    async with gps.aiogps.aiogps() as gpsd:\n+        async for msg in gpsd:\n+            # Log last message\n+            logging.info(f'Received: {msg}')\n+            # Log updated GPS status\n+            logging.info(f'\\nGPS status:\\n{gpsd}')\n+\n+    # using custom parameters\n+    try:\n+        async with gps.aiogps.aiogps(\n+                connection_args = {\n+                    'host': '192.168.10.116',\n+                    'port': 2947\n+                },\n+                connection_timeout = 5,\n+                reconnect = 0,   # do not try to reconnect, raise exceptions\n+                alive_opts = {\n+                    'rx_timeout': 5\n+                }\n+            ) as gpsd:\n+            async for msg in gpsd:\n+                logging.info(msg)\n+    except asyncio.CancelledError:\n+        return\n+    except asyncio.IncompleteReadError:\n+        logging.info('Connection closed by server')\n+    except asyncio.TimeoutError:\n+        logging.error('Timeout waiting for gpsd to respond')\n+    except Exception as exc:\n+        logging.error(f'Error: {exc}')\n+\n+\"\"\"\n+\n+__all__ = ['aiogps', ]\n+\n+import logging\n+import asyncio\n+import socket\n+from typing import Optional, Union, Awaitable\n+\n+from .client import gpsjson, dictwrapper\n+from .gps import gps, gpsdata, WATCH_ENABLE, PACKET_SET\n+from .misc import polystr, polybytes\n+\n+\n+class aiogps(gps):  # pylint: disable=R0902\n+    \"\"\"An asyncio gps client.\n+\n+    Reimplements all gps IO methods using asyncio coros. Adds connection\n+    management, an asyncio context manager and an asyncio iterator.\n+\n+    The class uses a logger named 'gps.aiogps' to record events. The logger is\n+    configured with a NullHandler to disable any message logging until the\n+    application configures another handler.\n+    \"\"\"\n+\n+    def __init__(self,  # pylint: disable=W0231\n+                 connection_args: Optional[dict] = None,\n+                 connection_timeout: Optional[float] = None,\n+                 reconnect: Optional[float] = 2,\n+                 alive_opts: Optional[dict] = None) -> None:\n+        \"\"\"\n+        Arguments:\n+            connection_args: arguments needed for opening a connection.\n+                These will be passed directly to asyncio.open_connection.\n+                If set to None, a connection to the default gps host and port\n+                will be attempded.\n+            connection_timeout: time to wait for a connection to complete\n+                (seconds). Set to None to disable.\n+            reconnect: configures automatic reconnections:\n+                - 0: reconnection is not attempted in case of an error and the\n+                error is raised to the user;\n+                - number > 0: delay until next reconnection attempt (seconds).\n+            alive_opts: options related to detection of disconnections.\n+                Two mecanisms are supported: TCP keepalive (default, may not be\n+                available on all platforms) and Rx timeout, through the\n+                following options:\n+                - rx_timeout: Rx timeout (seconds). Set to None to disable.\n+                - SO_KEEPALIVE: socket keepalive and related parameters:\n+                - TCP_KEEPIDLE\n+                - TCP_KEEPINTVL\n+                - TCP_KEEPCNT\n+        \"\"\"\n+        # If connection_args are not specified use defaults\n+        self.connection_args = connection_args or {\n+            'host': self.host,\n+            'port': self.port\n+        }\n+        self.connection_timeout = connection_timeout\n+        assert reconnect >= 0\n+        self.reconnect = reconnect\n+        # If alive_opts are not specified use defaults\n+        self.alive_opts = alive_opts or {\n+            'rx_timeout': None,\n+            'SO_KEEPALIVE': 1,\n+            'TCP_KEEPIDLE': 2,\n+            'TCP_KEEPINTVL': 2,\n+            'TCP_KEEPCNT': 3\n+        }\n+        # Connection access streams\n+        self.reader: Optional[asyncio.StreamReader] = None\n+        self.writer: Optional[asyncio.StreamWriter] = None\n+        # Set up logging\n+        self.logger = logging.getLogger(__name__)\n+        # Set the Null handler - prevents logging message handling unless the\n+        # application sets up a handler.\n+        self.logger.addHandler(logging.NullHandler())\n+        # Init gps parents\n+        gpsdata.__init__(self)  # pylint: disable=W0233\n+        gpsjson.__init__(self)  # pylint: disable=W0233\n+        # Provide the response in both 'str' and 'bytes' form\n+        self.bresponse = b''\n+        self.response = polystr(self.bresponse)\n+        # Default stream command\n+        self.stream_command = self.generate_stream_command(WATCH_ENABLE)\n+        self.loop = self.connection_args.get('loop', asyncio.get_event_loop())\n+\n+    def __del__(self) -> None:\n+        \"\"\" Destructor \"\"\"\n+        self.close()\n+\n+    async def _open_connection(self) -> None:\n+        \"\"\"\n+        Opens a connection to the GPSD server and configures the TCP socket.\n+        \"\"\"\n+        self.logger.info(\n+            f\"Connecting to gpsd at {self.connection_args['host']}\" +\n+            (f\":{self.connection_args['port']}\"\n+             if self.connection_args['port'] else ''))\n+        self.reader, self.writer = await asyncio.wait_for(\n+            asyncio.open_connection(**self.connection_args),\n+            self.connection_timeout,\n+            loop=self.loop)\n+        # Set socket options\n+        sock = self.writer.get_extra_info('socket')\n+        if sock is not None:\n+            if 'SO_KEEPALIVE' in self.alive_opts:\n+                sock.setsockopt(socket.SOL_SOCKET,\n+                                socket.SO_KEEPALIVE,\n+                                self.alive_opts['SO_KEEPALIVE'])\n+            if hasattr(\n+                    sock,\n+                    'TCP_KEEPIDLE') and 'TCP_KEEPIDLE' in self.alive_opts:\n+                sock.setsockopt(socket.IPPROTO_TCP,\n+                                socket.TCP_KEEPIDLE,    # pylint: disable=E1101\n+                                self.alive_opts['TCP_KEEPIDLE'])\n+            if hasattr(\n+                    sock,\n+                    'TCP_KEEPINTVL') and 'TCP_KEEPINTVL' in self.alive_opts:\n+                sock.setsockopt(socket.IPPROTO_TCP,\n+                                socket.TCP_KEEPINTVL,   # pylint: disable=E1101\n+                                self.alive_opts['TCP_KEEPINTVL'])\n+            if hasattr(\n+                    sock,\n+                    'TCP_KEEPCNT') and 'TCP_KEEPCNT' in self.alive_opts:\n+                sock.setsockopt(socket.IPPROTO_TCP,\n+                                socket.TCP_KEEPCNT,\n+                                self.alive_opts['TCP_KEEPCNT'])\n+\n+    def close(self) -> None:\n+        \"\"\" Closes connection to GPSD server \"\"\"\n+        if self.writer:\n+            try:\n+                self.writer.close()\n+            except Exception:   # pylint: disable=W0703\n+                pass\n+            self.writer = None\n+\n+    def waiting(self) -> bool:   # pylint: disable=W0221\n+        \"\"\" Mask the blocking waiting method from gpscommon \"\"\"\n+        return True\n+\n+    async def read(self) -> Union[dictwrapper, str]:\n+        \"\"\" Reads data from GPSD server \"\"\"\n+        while True:\n+            await self.connect()\n+            try:\n+                rx_timeout = self.alive_opts.get('rx_timeout', None)\n+                reader = self.reader.readuntil(separator=b'\\n')\n+                self.bresponse = await asyncio.wait_for(reader,\n+                                                        rx_timeout,\n+                                                        loop=self.loop)\n+                self.response = polystr(self.bresponse)\n+                if self.response.startswith(\n+                        \"{\") and self.response.endswith(\"}\\r\\n\"):\n+                    self.unpack(self.response)\n+                    self._oldstyle_shim()\n+                    self.valid |= PACKET_SET\n+                    return self.data\n+                return self.response\n+            except asyncio.CancelledError:\n+                self.close()\n+                raise\n+            except Exception as exc:    # pylint: disable=W0703\n+                error = 'timeout' if isinstance(\n+                    exc, asyncio.TimeoutError) else exc\n+                self.logger.warning(\n+                    f'Failed to get message from GPSD: {error}')\n+                self.close()\n+                if self.reconnect:\n+                    # Try again later\n+                    await asyncio.sleep(self.reconnect)\n+                else:\n+                    raise\n+\n+    async def connect(self) -> None:    # pylint: disable=W0221\n+        \"\"\" Connects to GPSD server and starts streaming data \"\"\"\n+        while not self.writer:\n+            try:\n+                await self._open_connection()\n+                await self.stream()\n+                self.logger.info('Connected to gpsd')\n+            except asyncio.CancelledError:\n+                self.close()\n+                raise\n+            except Exception as exc:    # pylint: disable=W0703\n+                error = 'timeout' if isinstance(\n+                    exc, asyncio.TimeoutError) else exc\n+                self.logger.error(f'Failed to connect to GPSD: {error}')\n+                self.close()\n+                if self.reconnect:\n+                    # Try again later\n+                    await asyncio.sleep(self.reconnect)\n+                else:\n+                    raise\n+\n+    async def send(self, commands) -> None:\n+        \"\"\" Sends commands \"\"\"\n+        bcommands = polybytes(commands + \"\\n\")\n+        if self.writer:\n+            self.writer.write(bcommands)\n+            await self.writer.drain()\n+\n+    async def stream(self, flags: Optional[int] = 0,\n+                     devpath: Optional[str] = None) -> None:\n+        \"\"\" Creates and sends the stream command \"\"\"\n+        if flags > 0:\n+            # Update the stream command\n+            self.stream_command = self.generate_stream_command(flags, devpath)\n+\n+        if self.stream_command:\n+            self.logger.info(f'Sent stream as: {self.stream_command}')\n+            await self.send(self.stream_command)\n+        else:\n+            raise TypeError(f'Invalid streaming command: {flags}')\n+\n+    async def __aenter__(self) -> 'aiogps':\n+        \"\"\" Context manager entry \"\"\"\n+        return self\n+\n+    async def __aexit__(self, exc_type, exc, traceback) -> None:\n+        \"\"\" Context manager exit: close connection \"\"\"\n+        self.close()\n+\n+    def __aiter__(self) -> 'aiogps':\n+        \"\"\" Async iterator interface \"\"\"\n+        return self\n+\n+    async def __anext__(self) -> Union[dictwrapper, str]:\n+        \"\"\" Returns next message from GPSD \"\"\"\n+        data = await self.read()\n+        return data\n+\n+    def __next__(self) -> Awaitable:\n+        \"\"\"\n+        Reimplementation of the blocking iterator from gps.\n+        Returns an awaitable which returns the next message from GPSD.\n+        \"\"\"\n+        return self.read()\n+\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/client.py b/tools/gps/client.py\nnew file mode 100644\n--- /dev/null\n+++ b/tools/gps/client.py\n@@ -0,0 +1,342 @@\n+\"gpsd client functions\"\n+# This file is Copyright 2019 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-Clause\n+#\n+# This code run compatibly under Python 2 and 3.x for x >= 2.\n+# Preserve this property!\n+from __future__ import absolute_import, print_function, division\n+\n+import json\n+import select\n+import socket\n+import sys\n+import time\n+\n+from .misc import polystr, polybytes\n+from .watch_options import *\n+\n+GPSD_PORT = \"2947\"\n+\n+\n+class gpscommon(object):\n+    \"Isolate socket handling and buffering from the protocol interpretation.\"\n+\n+    host = \"127.0.0.1\"\n+    port = GPSD_PORT\n+\n+    def __init__(self, host=\"127.0.0.1\", port=GPSD_PORT, verbose=0,\n+                 should_reconnect=False):\n+        self.stream_command = b''\n+        self.linebuffer = b''\n+        self.received = time.time()\n+        self.reconnect = should_reconnect\n+        self.verbose = verbose\n+        self.sock = None        # in case we blow up in connect\n+        # Provide the response in both 'str' and 'bytes' form\n+        self.bresponse = b''\n+        self.response = polystr(self.bresponse)\n+\n+        if host is not None and port is not None:\n+            self.host = host\n+            self.port = port\n+            self.connect(self.host, self.port)\n+\n+    def connect(self, host, port):\n+        \"\"\"Connect to a host on a given port.\n+\n+        If the hostname ends with a colon (`:') followed by a number, and\n+        there is no port specified, that suffix will be stripped off and the\n+        number interpreted as the port number to use.\n+        \"\"\"\n+        if not port and (host.find(':') == host.rfind(':')):\n+            i = host.rfind(':')\n+            if i >= 0:\n+                host, port = host[:i], host[i + 1:]\n+            try:\n+                port = int(port)\n+            except ValueError:\n+                raise socket.error(\"nonnumeric port\")\n+        # if self.verbose > 0:\n+        #    print 'connect:', (host, port)\n+        msg = \"getaddrinfo returns an empty list\"\n+        self.sock = None\n+        for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):\n+            af, socktype, proto, _canonname, sa = res\n+            try:\n+                self.sock = socket.socket(af, socktype, proto)\n+                # if self.debuglevel > 0: print 'connect:', (host, port)\n+                self.sock.connect(sa)\n+                if self.verbose > 0:\n+                    print('connected to tcp://{}:{}'.format(host, port))\n+                break\n+            # do not use except ConnectionRefusedError\n+            # # Python 2.7 doc does have this exception\n+            except socket.error as e:\n+                if self.verbose > 1:\n+                    msg = str(e) + ' (to {}:{})'.format(host, port)\n+                    sys.stderr.write(\"error: {}\\n\".format(msg.strip()))\n+                self.close()\n+                raise  # propogate error to caller\n+\n+    def close(self):\n+        \"Close the gpsd socket\"\n+        if self.sock:\n+            self.sock.close()\n+        self.sock = None\n+\n+    def __del__(self):\n+        \"Close the gpsd socket\"\n+        self.close()\n+\n+    def waiting(self, timeout=0):\n+        \"Return True if data is ready for the client.\"\n+        if self.linebuffer:\n+            return True\n+        if self.sock is None:\n+            return False\n+\n+        (winput, _woutput, _wexceptions) = select.select(\n+            (self.sock,), (), (), timeout)\n+        return winput != []\n+\n+    def read(self):\n+        \"Wait for and read data being streamed from the daemon.\"\n+\n+        if None is self.sock:\n+            self.connect(self.host, self.port)\n+            if None is self.sock:\n+                return -1\n+            self.stream()\n+\n+        eol = self.linebuffer.find(b'\\n')\n+        if eol == -1:\n+            # RTCM3 JSON can be over 4.4k long, so go big\n+            frag = self.sock.recv(8192)\n+\n+            self.linebuffer += frag\n+            if not self.linebuffer:\n+                if self.verbose > 1:\n+                    sys.stderr.write(\n+                        \"poll: no available data: returning -1.\\n\")\n+                # Read failed\n+                return -1\n+\n+            eol = self.linebuffer.find(b'\\n')\n+            if eol == -1:\n+                if self.verbose > 1:\n+                    sys.stderr.write(\"poll: partial message: returning 0.\\n\")\n+                # Read succeeded, but only got a fragment\n+                self.response = ''  # Don't duplicate last response\n+                self.bresponse = ''  # Don't duplicate last response\n+                return 0\n+        else:\n+            if self.verbose > 1:\n+                sys.stderr.write(\"poll: fetching from buffer.\\n\")\n+\n+        # We got a line\n+        eol += 1\n+        # Provide the response in both 'str' and 'bytes' form\n+        self.bresponse = self.linebuffer[:eol]\n+        self.response = polystr(self.bresponse)\n+        self.linebuffer = self.linebuffer[eol:]\n+\n+        # Can happen if daemon terminates while we're reading.\n+        if not self.response:\n+            return -1\n+        if 1 < self.verbose:\n+            sys.stderr.write(\"poll: data is %s\\n\" % repr(self.response))\n+        self.received = time.time()\n+        # We got a \\n-terminated line\n+        return len(self.response)\n+\n+    # Note that the 'data' method is sometimes shadowed by a name\n+    # collision, rendering it unusable.  The documentation recommends\n+    # accessing 'response' directly.  Consequently, no accessor method\n+    # for 'bresponse' is currently provided.\n+\n+    def data(self):\n+        \"Return the client data buffer.\"\n+        return self.response\n+\n+    def send(self, commands):\n+        \"Ship commands to the daemon.\"\n+        lineend = \"\\n\"\n+        if isinstance(commands, bytes):\n+            lineend = polybytes(\"\\n\")\n+        if not commands.endswith(lineend):\n+            commands += lineend\n+\n+        if self.sock is None:\n+            self.stream_command = commands\n+        else:\n+            self.sock.send(polybytes(commands))\n+\n+\n+class json_error(BaseException):\n+    \"Class for JSON errors\"\n+\n+    def __init__(self, data, explanation):\n+        BaseException.__init__(self)\n+        self.data = data\n+        self.explanation = explanation\n+\n+\n+class gpsjson(object):\n+    \"Basic JSON decoding.\"\n+\n+    def __init__(self):\n+        self.data = None\n+        self.stream_command = None\n+        self.enqueued = None\n+        self.verbose = -1\n+\n+    def __iter__(self):\n+        \"Broken __iter__\"\n+        return self\n+\n+    def unpack(self, buf):\n+        \"Unpack a JSON string\"\n+        try:\n+            self.data = dictwrapper(json.loads(buf.strip(), encoding=\"ascii\"))\n+        except ValueError as e:\n+            raise json_error(buf, e.args[0])\n+        # Should be done for any other array-valued subobjects, too.\n+        # This particular logic can fire on SKY or RTCM2 objects.\n+        if hasattr(self.data, \"satellites\"):\n+            self.data.satellites = [dictwrapper(x)\n+                                    for x in self.data.satellites]\n+\n+    def stream(self, flags=0, devpath=None):\n+        \"Control streaming reports from the daemon,\"\n+\n+        if 0 < flags:\n+            self.stream_command = self.generate_stream_command(flags, devpath)\n+        else:\n+            self.stream_command = self.enqueued\n+\n+        if self.stream_command:\n+            if self.verbose > 1:\n+                sys.stderr.write(\"send: stream as:\"\n+                                 \" {}\\n\".format(self.stream_command))\n+            self.send(self.stream_command)\n+        else:\n+            raise TypeError(\"Invalid streaming command!! : \" + str(flags))\n+\n+    def generate_stream_command(self, flags=0, devpath=None):\n+        \"Generate stream command\"\n+        if flags & WATCH_OLDSTYLE:\n+            return self.generate_stream_command_old_style(flags)\n+\n+        return self.generate_stream_command_new_style(flags, devpath)\n+\n+    @staticmethod\n+    def generate_stream_command_old_style(flags=0):\n+        \"Generate stream command, old style\"\n+        if flags & WATCH_DISABLE:\n+            arg = \"w-\"\n+            if flags & WATCH_NMEA:\n+                arg += 'r-'\n+\n+        elif flags & WATCH_ENABLE:\n+            arg = 'w+'\n+            if flags & WATCH_NMEA:\n+                arg += 'r+'\n+\n+        return arg\n+\n+    @staticmethod\n+    def generate_stream_command_new_style(flags=0, devpath=None):\n+        \"Generate stream command, new style\"\n+\n+        if (flags & (WATCH_JSON | WATCH_OLDSTYLE | WATCH_NMEA |\n+                     WATCH_RAW)) == 0:\n+            flags |= WATCH_JSON\n+\n+        if flags & WATCH_DISABLE:\n+            arg = '?WATCH={\"enable\":false'\n+            if flags & WATCH_JSON:\n+                arg += ',\"json\":false'\n+            if flags & WATCH_NMEA:\n+                arg += ',\"nmea\":false'\n+            if flags & WATCH_RARE:\n+                arg += ',\"raw\":1'\n+            if flags & WATCH_RAW:\n+                arg += ',\"raw\":2'\n+            if flags & WATCH_SCALED:\n+                arg += ',\"scaled\":false'\n+            if flags & WATCH_TIMING:\n+                arg += ',\"timing\":false'\n+            if flags & WATCH_SPLIT24:\n+                arg += ',\"split24\":false'\n+            if flags & WATCH_PPS:\n+                arg += ',\"pps\":false'\n+        else:  # flags & WATCH_ENABLE:\n+            arg = '?WATCH={\"enable\":true'\n+            if flags & WATCH_JSON:\n+                arg += ',\"json\":true'\n+            if flags & WATCH_NMEA:\n+                arg += ',\"nmea\":true'\n+            if flags & WATCH_RARE:\n+                arg += ',\"raw\":1'\n+            if flags & WATCH_RAW:\n+                arg += ',\"raw\":2'\n+            if flags & WATCH_SCALED:\n+                arg += ',\"scaled\":true'\n+            if flags & WATCH_TIMING:\n+                arg += ',\"timing\":true'\n+            if flags & WATCH_SPLIT24:\n+                arg += ',\"split24\":true'\n+            if flags & WATCH_PPS:\n+                arg += ',\"pps\":true'\n+            if flags & WATCH_DEVICE:\n+                arg += ',\"device\":\"%s\"' % devpath\n+        arg += \"}\"\n+        return arg\n+\n+\n+class dictwrapper(object):\n+    \"Wrapper that yields both class and dictionary behavior,\"\n+\n+    def __init__(self, ddict):\n+        \"Init class dictwrapper\"\n+        self.__dict__ = ddict\n+\n+    def get(self, k, d=None):\n+        \"Get dictwrapper\"\n+        return self.__dict__.get(k, d)\n+\n+    def keys(self):\n+        \"Keys dictwrapper\"\n+        return self.__dict__.keys()\n+\n+    def __getitem__(self, key):\n+        \"Emulate dictionary, for new-style interface.\"\n+        return self.__dict__[key]\n+\n+    def __iter__(self):\n+        \"Iterate dictwrapper\"\n+        return self.__dict__.__iter__()\n+\n+    def __setitem__(self, key, val):\n+        \"Emulate dictionary, for new-style interface.\"\n+        self.__dict__[key] = val\n+\n+    def __contains__(self, key):\n+        \"Find key in dictwrapper\"\n+        return key in self.__dict__\n+\n+    def __str__(self):\n+        \"dictwrapper to string\"\n+        return \"<dictwrapper: \" + str(self.__dict__) + \">\"\n+    __repr__ = __str__\n+\n+    def __len__(self):\n+        \"length of dictwrapper\"\n+        return len(self.__dict__)\n+\n+#\n+# Someday a cleaner Python interface using this machinery will live here\n+#\n+\n+# End\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/clienthelpers.py b/tools/gps/clienthelpers.py\nnew file mode 100644\n--- /dev/null\n+++ b/tools/gps/clienthelpers.py\n@@ -0,0 +1,920 @@\n+#!/usr/bin/env python\n+# -*- coding: utf-8 -*-\n+#\n+# clienthelpers.py - helper functions for xgps and test_maidenhead\n+# This duplicates gpsclient.c, but in python.  Keep the two files in\n+# sync.\n+#\n+# See gpsclient.c for code comments.\n+#\n+# This file is Copyright 2019 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-Clause\n+\"\"\"GPSd client helpers submodule.\"\"\"\n+import math\n+import os\n+\n+\n+TABLE_ROWS = 37\n+TABLE_COLS = 73\n+TABLE_SPAN = 5\n+\n+GEOID_DELTA = [\n+    # -90\n+    [ -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015,\n+      -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015,\n+      -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015,\n+      -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015,\n+      -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015,\n+      -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015,\n+      -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015, -3015,\n+      -3015, -3015, -3015],\n+    # -85\n+    [ -3568, -3608, -3739, -3904, -4039, -4079, -4033, -3946, -3845, -3734,\n+      -3603, -3458, -3310, -3163, -2994, -2827, -2695, -2667, -2737, -2823,\n+      -2840, -2757, -2634, -2567, -2547, -2540, -2452, -2247, -1969, -1704,\n+      -1540, -1507, -1552, -1592, -1573, -1513, -1465, -1478, -1542, -1577,\n+      -1483, -1256, -1029,  -957, -1066, -1216, -1262, -1194, -1118, -1129,\n+      -1231, -1370, -1504, -1641, -1813, -2028, -2255, -2455, -2630, -2811,\n+      -3022, -3242, -3436, -3578, -3658, -3676, -3640, -3578, -3527, -3490,\n+      -3532, -3570, -3568],\n+    # -80\n+    [ -5232, -5276, -5275, -5301, -5286, -5276, -5218, -5001, -4775, -4580,\n+      -4319, -4064, -3854, -3691, -3523, -3205, -2910, -2608, -2337, -2355,\n+      -2417, -2445, -2471, -2350, -2230, -2136, -1869, -1689, -1732, -1748,\n+      -1540, -1236, -1048,  -794,  -569,  -603,  -501,  -305,  -166,    19,\n+        146,   274,   444,   510,   534,   550,   458,   373,   473,   575,\n+        607,   732,   562,   153,  -271,  -825, -1300, -1861, -2475, -2866,\n+      -3434, -4001, -4196, -4533, -4989, -5152, -5094, -4983, -4987, -5065,\n+      -5055, -5115, -5232],\n+    # -75\n+    [ -6155, -6339, -6266, -6344, -6282, -6100, -6009, -5492, -5088, -4547,\n+      -4187, -3901, -3586, -3234, -3051, -2886, -2577, -2289, -1981, -1655,\n+      -1435, -1096,  -557,  -617,  -998,  -961,  -655,  -464,  -170,    79,\n+       -103,   -64,   150,   223,   819,  1006,  1174,  1136,  1211,  1278,\n+       1467,  1686,  1783,  1706,  1833,  1721,  1653,  1580,  1267,   953,\n+        629,   807,   774,   607,   217,  -386,  -814, -1354, -2452, -3542,\n+      -3833, -3932, -4259, -4962, -4977, -5536, -5753, -5800, -6012, -5835,\n+      -5751, -5820, -6155],\n+    # -70\n+    [ -6218, -6432, -6333, -6150, -6021, -5948, -5705, -5480, -5213, -4789,\n+      -4365, -4003, -3757, -3514, -3250, -3000, -2672, -2541, -2138, -1220,\n+       -844,  -277,   249,   906,   458,    69,    26,    98,   166,   130,\n+        118,   253,   303,   437,  1010,  1341,  1423,  1558,  1682,  1825,\n+       1766,  1917,  2027,  2047,  2164,  2909,  2882,  2997,  3010,  2687,\n+       1749,  1703,  1799,  1438,  1099,   346,  -813, -1432, -2149, -2320,\n+      -2704, -3085, -3907, -4172, -4287, -4846, -5466, -5592, -5576, -5525,\n+      -5800, -5954, -6218],\n+    # -65\n+    [ -5152, -5115, -5049, -4943, -4858, -4714, -4580, -4369, -4202, -4060,\n+      -3806, -3454, -3210, -3007, -2749, -2484, -2264, -1928, -1501, -1113,\n+       -614,    31,   642,  1502,  1833,  1844,  1268,  1442,  1441,  1302,\n+       1164,  1041,   945,   874,   896,  1059,  1368,  1680,  1736,  1975,\n+       1891,  1979,  2131,  2338,  2672,  2861,  3114,  3097,  2801,  2695,\n+       2422,  2022,  1648,  1340,   713,   352,  -127,  -895, -1740, -2040,\n+      -2854, -3292, -3453, -3922, -4395, -4538, -4554, -4356, -4445, -4669,\n+      -4988, -5122, -5152],\n+    # -60\n+    [ -4598, -4449, -4278, -4056, -3732, -3417, -3205, -3094, -3008, -2876,\n+      -2669, -2478, -2350, -2272, -2218, -1969, -1660, -1381, -1123,  -716,\n+       -350,   247,   924,  1712,  2016,  2066,  2032,  1556,  2123,  2322,\n+       2384,  1034,  2121,  1923,  1720,  1571,  1517,  1668,  2008,  2366,\n+       2546,  2736,  2914,  3169,  3395,  3467,  3315,  3286,  3279,  3073,\n+       2930,  2727,  2502,  1783,   893,   311,  -328,  -778, -1364, -1973,\n+      -2467, -2833, -3143, -3283, -3311, -3120, -2956, -3027, -3485, -3972,\n+      -4454, -4679, -4598],\n+    # -55\n+    [ -3414, -3429, -3223, -3013, -2704, -2474, -2292, -2185, -1962, -1818,\n+      -1828, -1485, -1259, -1284, -1327, -1304, -1097, -1071,  -628,  -326,\n+        174,   340,  1331,  1217,  1712,  1441,  1467,  1578,  1654,  2179,\n+        764,  1486,  2074,  2245,  2462,  2655,  2720,  2581,  2423,  2731,\n+       3145,  3383,  3436,  3909,  4448,  4422,  4032,  3938,  3665,  3461,\n+       3465,  3317,  2487,  1908,  1311,   683,    52,  -582, -1196, -1798,\n+      -2158, -2450, -2475, -2429, -2277, -2011, -2140, -2306, -2551, -2726,\n+      -3016, -3319, -3414],\n+    # -50\n+    [ -1615, -1938, -1875, -1827, -1839, -1793, -1605, -1650, -1737, -1773,\n+      -1580, -1237, -1010,  -983, -1051, -1025,  -838,  -653,  -316,    48,\n+        502,  1382,  1186,  1114,  1264,   785,   231,   329,   353,   556,\n+       1084,  1597,  2065,  2475,  2744,  2701,  2518,  2545,  2584,  2963,\n+       3323,  3537,  3792,  4085,  4520,  4505,  4459,  4287,  3818,  4112,\n+       3975,  3293,  2748,  2043,  1272,   569,  -207,  -898, -1498, -1990,\n+      -2242, -2358, -2212, -1968, -1843, -1695, -1705, -1688, -1400, -1177,\n+      -1013, -1168, -1615],\n+    # -45\n+    [   338,   -20,  -606,  -849,  -777,  -838, -1123, -1322, -1485, -1503,\n+      -1413, -1203, -1077, -1004,  -960,  -829,  -662,  -371,   -88,   322,\n+        710,  1323,  1831,  1202,   908,    47,  -292,  -367,  -495,  -174,\n+        688,  1500,  2194,  2673,  2568,  2423,  2099,  2168,  2617,  2834,\n+       3254,  3328,  3443,  4442,  4639,  4588,  4524,  4223,  3575,  3187,\n+       3101,  2651,  2155,  1506,   774,   -55,  -961, -1719, -2355, -2719,\n+      -2731, -2670, -2430, -2026, -1715, -1477, -1144,  -901,  -646,  -303,\n+        871,   565,   338],\n+    # -40\n+    [  2048,  1283,   637,   317,   109,  -156,  -679, -1023, -1186, -1277,\n+      -1275, -1202, -1282, -1150, -1022,  -881,  -690,  -300,   -84,   130,\n+        694,   937,  2220,  1511,  1341,   558,  -266,  -623,  -670,  -209,\n+        643,  1459,  2101,  2385,  2307,  2000,  1765,  1992,  2496,  2733,\n+       2941,  3431,  3298,  3327,  3877,  4306,  4069,  3446,  2844,  2601,\n+       2333,  1786,  1318,   599,  -238, -1184, -2098, -2786, -3250, -3406,\n+      -3351, -3095, -2741, -2101, -1482,  -148,  -201,   221,   491,  1179,\n+       1877,  1206,  2048],\n+    # -35\n+    [  2833,  2556,  1700,  1059,   497,   -21,  -370,  -752,  -959, -1103,\n+      -1093, -1104, -1198, -1097,  -960,  -785,  -596,  -362,  -211,   103,\n+        739,  1300,  3029,  2021,  1712,  1269,   -23,  -616,  -701,  -255,\n+        684,  1237,  1701,  1903,  1696,  1789,  1795,  2034,  2398,  2561,\n+       3187,  2625,  2609,  2897,  2564,  3339,  3118,  3121,  2240,  2102,\n+       1529,   991,   387,  -559, -1464, -2380, -3138, -3999, -3899, -3446,\n+      -3473, -3300, -2823, -1043,   143,   970,  2058,  1555,  1940,  2621,\n+       3154,  3839,  2833],\n+    # -30\n+    [  4772,  3089,  2257,  1381,   566,    64,  -136,  -612,  -868, -1186,\n+      -1309, -1131, -1033,  -903,  -780,  -625,  -443,  -242,   100,   269,\n+        815,  1489,  3633,  2424,  1810,  1138,   297,  -720,  -847,    -2,\n+        347,   579,  1025,  1408,  1504,  1686,  2165,  2353,  2599,  3182,\n+       3332,  3254,  3094,  2042,  1369,  1945,  1468,  1487,  1505,  1048,\n+        613,    26,  -904, -1757, -2512, -3190, -3751, -3941, -3939, -2896,\n+      -2222, -1766, -1442,    70,  1262,  2229,  3189,  2910,  3371,  3608,\n+       4379,  4520,  4772],\n+    # -25\n+    [  4984,  2801,  2475,  1374,   798,   198,  -269,  -628, -1063, -1262,\n+      -1090,  -970,  -692,  -516,  -458,  -313,  -143,    19,   183,   403,\n+        837,  1650,  3640,  2990,  2084,   628,   422,  -597, -1130,  -712,\n+       -474,  -110,   446,  1043,  1349,  1571,  2008,  2572,  2405,  3175,\n+       2766,  2407,  2100,  1130,   367,   840,    89,   114,    49,   -25,\n+       -494, -1369, -2345, -3166, -3804, -4256, -4141, -3730, -3337, -1814,\n+       -901,  -388,   298,  1365,  2593,  3490,  4639,  4427,  4795,  4771,\n+       5325,  5202,  4984],\n+    # -20\n+    [  4994,  5152,  2649,  1466,   935,   427,  -115,  -518,  -838, -1135,\n+      -1134,  -917,  -525,  -280,  -218,  -310,  -396,  -306,  -137,   148,\n+        811,  1643,  3496,  4189,  1958,   358,  -784,  -684,  -740,  -800,\n+       -579,  -638,   -49,   704,  1221,  1358,  1657,  1957,  2280,  2639,\n+       2157,  1246,   728,  -364, -1021,  -586, -1098, -1055, -1032, -1244,\n+      -2065, -3158, -4028, -4660, -4802, -4817, -4599, -3523, -2561, -1260,\n+        446,  1374,  2424,  3310,  4588,  5499,  5724,  5479,  5698,  5912,\n+       6400,  6116,  4994],\n+    # -15\n+    [  4930,  4158,  2626,  1375,   902,   630,   150,  -275,  -667, -1005,\n+       -954,  -847,  -645,  -376,  -315,  -479,  -639,  -681,  -550,  -268,\n+        709,  2996,  4880,  2382,  1695,  -136,  -964, -1211, -1038, -1045,\n+       -695,  -595,    23,   733,  1107,  1318,  1348,  1376,  1630,  2240,\n+       1248,   454,  -737, -1252, -2001, -2513, -1416, -2169, -2269, -3089,\n+      -4063, -5194, -5715, -6105, -5700, -4873, -3919, -2834, -1393,  -112,\n+       1573,  3189,  3907,  4863,  5437,  6548,  6379,  6281,  6289,  5936,\n+       6501,  5794,  4930],\n+    # -10\n+    [  3525,  2747,  2135,  1489,  1078,   739,   544,   -39,  -268,  -588,\n+       -917, -1025, -1087,  -940,  -771,  -923, -1177, -1114,  -919,  -383,\n+       -108,  2135,  2818,  1929,   386, -1097, -1911, -1619, -1226, -1164,\n+       -952,  -583,   399,  1070,  1280,  1345,  1117,   993,  1306,  1734,\n+        538,  -463, -1208, -1602, -2662, -3265, -3203, -3408, -3733, -5014,\n+      -6083, -7253, -7578, -7096, -6418, -4658, -2647,  -586,   -87,  1053,\n+       3840,  3336,  5240,  6253,  6898,  7070,  7727,  7146,  6209,  5826,\n+       5068,  4161,  3525],\n+    # -5\n+    [  2454,  1869,  1656,  1759,  1404,  1263,  1012,   605,   108,  -511,\n+       -980, -1364, -1620, -1633, -1421, -1342, -1412, -1349, -1006,  -229,\n+       1711,  1293,  1960,   605,  -793, -2058, -2108, -2626, -1195,  -606,\n+       -513,  -108,   671,  1504,  1853,  1711,  1709,   940,   570,   296,\n+       -913, -1639, -1471, -1900, -3000, -4164, -4281, -4062, -5366, -6643,\n+      -7818, -8993, -9275, -8306, -6421, -4134, -1837,  1367,  2850,  4286,\n+       5551,  5599,  5402,  6773,  7736,  7024,  8161,  6307,  5946,  4747,\n+       3959,  3130,  2454],\n+    # 0\n+    [  2128,  1774,  1532,  1470,  1613,  1589,  1291,   783,    79,  -676,\n+      -1296, -1941, -2298, -2326, -2026, -1738, -1412, -1052,  -406,    82,\n+       1463,  1899,  1352,  -170, -1336, -2446, -2593, -2328, -1863,  -833,\n+        245,  1005,  1355,  1896,  1913,  1888,  1723,  1642,   940,  -127,\n+      -1668, -1919, -1078, -1633, -2762, -4357, -4885, -5143, -6260, -7507,\n+      -8947, -10042, -10259, -8865, -6329, -3424,  -692,  1445,  3354,  5132,\n+       5983,  4978,  7602,  7274,  7231,  6941,  6240,  5903,  4944,  4065,\n+       3205,  2566,  2128],\n+    # 5\n+    [  1632,  1459,  1243,  1450,  1643,  1432,   867,   283,  -420, -1316,\n+      -1993, -2614, -3012, -3016, -2555, -1933, -1256,  -688,  -133,   634,\n+       1369,  2095,   -92,  -858, -1946, -3392, -3666, -3110, -1839,  -371,\n+        674,  1221,  1657,  1994,  2689,  2577,  2020,  2126,  1997,   987,\n+       -739,  -989, -1107, -1369, -1914, -3312, -4871, -5365, -6171, -7732,\n+      -9393, -10088, -10568, -9022, -6053, -4104, -1296,   373,  2310,  4378,\n+       6279,  6294,  6999,  6852,  6573,  6302,  5473,  5208,  4502,  3445,\n+       2790,  2215,  1632],\n+    # 10\n+    [  1285,  1050,  1212,  1439,  1055,   638,   140,  -351, -1115, -2060,\n+      -2904, -3593, -3930, -3694, -2924, -2006, -1145,  -441,   164,  1059,\n+         91,  -440, -1043, -2791, -4146, -4489, -4259, -3218, -1691,  -683,\n+        306,  1160,  1735,  3081,  3275,  2807,  2373,  2309,  2151,  1245,\n+        207,  -132,  -507,  -564,  -956, -1917, -3167, -5067, -5820, -7588,\n+      -9107, -9732, -9732, -8769, -6308, -4585, -2512,  -891,  1108,  3278,\n+       5183,  6391,  5985,  5969,  6049,  5616,  4527,  4156,  3531,  2776,\n+       2456,  1904,  1285],\n+    # 15\n+    [   862,   804,   860,   969,   544,    89,  -417, -1008, -1641, -2608,\n+      -3607, -4234, -4482, -4100, -3232, -2092, -1105, -1092,   238,   330,\n+       -571, -1803, -2983, -3965, -5578, -4864, -3777, -2572, -1690,  -536,\n+        806,  2042,  2323,  3106,  3019,  2833,  2260,  2064,  2036,  1358,\n+       1030,   908,   391,   -54,  -377,  -885, -2172, -3359, -5309, -6686,\n+      -8058, -8338, -8695, -8322, -6404, -5003, -3420, -2060,  -255,  1833,\n+       4143,  4218,  4771,  5031,  5241,  5504,  4399,  3471,  2832,  2266,\n+       1643,  1190,   862],\n+    # 20\n+    [   442,   488,   986,   877,   757,  1175,  -696, -1473, -2285, -3128,\n+      -3936, -4520, -4739, -4286, -3350, -2092,  -747, -1894, -1083, -1508,\n+      -2037, -2528, -4813, -6316, -4698, -4222, -3279, -1814, -1001,   212,\n+       1714,  2273,  2535,  3367,  3112,  2736,  3086,  2742,  2679,  2071,\n+       1422,  1333,   922,   619,   183,  -945, -3070, -3680, -4245, -5461,\n+      -6064, -6652, -6806, -6210, -5947, -5177, -3814, -2589, -1319,   551,\n+       2150,  3262,  3799,  4177,  4898,  4658,  4149,  2833,  2148,  1410,\n+        899,   551,   442],\n+    # 25\n+    [  -248,    12,   716,   415,   327,  -187, -1103, -1729, -2469, -3296,\n+      -4040, -4545, -4642, -4232, -3466, -2064, -1667, -3232, -2660, -2685,\n+      -2789, -4262, -5208, -5084, -4935, -4077, -2622,  -804,   131,   946,\n+       1859,  2203,  3038,  3433,  3758,  3029,  2757,  3524,  3109,  2511,\n+       2300,  1554,  1316,  1114,   954,   -81, -2642, -3389, -3167, -4211,\n+      -4634, -5193, -6014, -6245, -5347, -5313, -3846, -3149, -2130,  -354,\n+       1573,  2760,  3310,  3713,  4594,  3862,  2827,  1939,  1019,   313,\n+       -142,  -378,  -248],\n+    # 30\n+    [  -720,  -717,  -528,  -573,  -867, -1224, -1588, -2135, -2796, -3432,\n+      -4036, -4329, -4246, -3464, -2996, -2389, -2323, -2844, -2744, -2884,\n+      -3238, -4585, -5164, -4463, -4064, -3238, -1751,   150,  1657,  2501,\n+       3023,  3007,  3404,  3976,  4354,  4648,  3440,  2708,  2813,  2968,\n+       2611,  2104,  1606,  1808,  1086,  -392, -1793,  -689, -1527, -2765,\n+      -3766, -4709, -3687, -2800, -3375, -3793, -3365, -4182, -2385, -1115,\n+        785,  2302,  3020,  3564,  4178,  2993,  1940,  1081,   331,  -364,\n+       -683,  -690,  -720],\n+    # 35\n+    [ -1004, -1222, -1315, -1304, -1463, -1680, -2160, -2675, -3233, -3746,\n+      -4021, -4053, -3373, -3012, -2447, -2184, -2780, -3219, -2825, -3079,\n+      -3181, -4284, -4548, -3867, -3123, -2302,  -785,   943,  2687,  4048,\n+       4460,  4290,  4118,  4585,  4282,  4437,  4898,  3818,  3696,  3414,\n+       2299,  2057,   627,  1915,  1833,   451,   678,  -876, -1602, -2167,\n+      -3344, -2549, -2860, -3514, -4043, -4207, -4005, -3918, -3121, -1521,\n+        471,  2023,  2980,  3679,  3465,  2405,  1475,   553,  -142,  -880,\n+      -1178,  -963, -1004],\n+    # 40\n+    [ -1223, -1218, -1076, -1116, -1298, -1541, -2085, -2648, -3120, -3473,\n+      -3679, -3342, -2334, -1912, -1787, -1756, -2482, -3182, -3322, -3429,\n+      -3395, -3374, -3372, -3341, -2654, -1509,   105,  1620,  3250,  4603,\n+       5889,  5776,  5198,  4840,  4903,  5370,  5086,  4536,  4519,  4601,\n+       3395,  4032,  3890,  3537,  3113,  2183, -1769, -1552, -2856, -3694,\n+      -4092, -3614, -5468, -6518, -6597, -5911, -5476, -4465, -2802, -1076,\n+        232,  1769,  2305,  3018,  3768,  1721,  1694,   667,  -154,  -799,\n+      -1068, -1196, -1223],\n+    # 45\n+    [  -634,  -460,  -330,  -267,  -413,  -818, -1310, -1763, -2352, -2738,\n+      -2632, -2685, -1929, -1340,  -737, -1441, -2254, -2685, -3358, -3488,\n+      -3635, -3187, -2665, -2142, -1515,  -124,  1727,  2798,  3965,  5065,\n+       6150,  6513,  6089,  5773,  5044,  4471,  4677,  5052,  3938,  4537,\n+       4425,  3652,  3063,  2178,  1267,    84, -1109, -1974, -2905, -3650,\n+      -4264, -4741, -4136, -6324, -5826, -5143, -4851, -4344, -3225, -1386,\n+          5,  1153,  2198,  2833,  2835,  2563,  1337,  1194,   503,  -329,\n+       -289,  -754,  -634],\n+    # 50\n+    [  -578,   -40,   559,   880,   749,   464,     0,  -516, -1140, -1655,\n+      -1818, -1589, -1555, -1337, -1769, -1919, -2372, -2981, -3485, -3976,\n+      -3941, -3565, -2614, -2223, -1253,   802,  2406,  3239,  4434,  5428,\n+       6265,  6394,  6180,  5690,  5855,  5347,  4506,  4685,  4799,  4445,\n+       3972,  3165,  2745,  1601,  1084,    41, -1170, -1701, -1916, -2914,\n+      -3305, -3790, -4435, -4128, -4163, -4535, -4190, -3891, -2951, -1869,\n+       -414,   851,  1494,  2097,  2268,  1939,  2031,  2460,   638,   578,\n+        325,    98,  -578],\n+    # 55\n+    [   -18,   482,   905,  1562,  1739,   983,  1097,   568,    34,  -713,\n+       -695, -1072, -1576, -1879, -2479, -2884, -3275, -3971, -4456, -4654,\n+      -4461, -3688, -2697, -1623,  -823,  1270,  2523,  3883,  4967,  5977,\n+       6049,  6149,  6095,  5776,  5820,  5575,  4642,  4099,  4025,  3462,\n+       2679,  2447,  1951,  1601,  1151,   663,   157,  -603,  -952, -1987,\n+      -2609, -3316, -3600, -3684, -3717, -3836, -4024, -3452, -2950, -1861,\n+       -903,    89,   975,  1499,  1560,  1601,  1922,  2031,  2326,   -58,\n+        506,  -177,   -18],\n+    # 60\n+    [    93,   673,   969,  1168,  1498,  1486,  1439,  1165,  1128,   720,\n+          5,  -689, -1610, -2409, -3094, -3585, -4193, -4772, -4678, -4521,\n+      -4184, -2955, -2252,  -834,   503,  1676,  2882,  4130,  4892,  5611,\n+       6390,  6338,  6069,  5974,  5582,  5461,  4788,  4503,  4080,  2957,\n+       1893,  1773,  1586,  1544,  1136,  1026,   622,    50,  -389, -1484,\n+      -2123, -2625, -3028, -3143, -3366, -3288, -3396, -3069, -2770, -2605,\n+      -1663,  -555,    25,   491,  1168,  1395,  1641,  1597,  1426,  1299,\n+        921,  -160,    93],\n+    # 65\n+    [   419,   424,   443,   723,   884,  1030,  1077,  1191,  1065,   734,\n+        265, -1052, -1591, -2136, -2773, -3435, -3988, -3978, -3698, -3509,\n+      -3370, -2490, -1347,  -263,  1647,  2582,  3291,  4802,  4447,  5609,\n+       5879,  6454,  6709,  6606,  5988,  5365,  5103,  4385,  3996,  3250,\n+       2526,  1766,  1817,  1751,  1275,   857,   636,    29,   -12,  -918,\n+      -1364, -1871, -2023, -2102, -2258, -2441, -2371, -2192, -1908, -1799,\n+      -1720, -1662,  -385,    86,   466,   880,   715,   834,  1010,  1105,\n+        877,   616,   419],\n+    # 70\n+    [   242,    93,    98,    62,   -54,   -25,  -127,  -156,  -253,  -412,\n+       -805, -1106, -1506, -1773, -2464, -2829, -2740, -2579, -2559, -2271,\n+      -1849,  -853,   294,  1055,  2357,  2780,  2907,  3909,  4522,  5272,\n+       5594,  5903,  5966,  5930,  5592,  5188,  4878,  4561,  4190,  3834,\n+       2963,  2451,  1981,  1525,  1064,   694,   253,   -70,  -318,  -781,\n+       -979, -1048, -1274, -1413, -1175, -1313, -1449, -1206,  -850, -1087,\n+       -828,  -933,  -540,  -301,   -35,    53,   279,   267,   345,   371,\n+        334,   289,   242],\n+    # 75\n+    [   128,   228,   376,    46,  -173,  -355,  -417,  -548,  -764,  -925,\n+       -419,  -950, -1185, -1102, -1293, -1355, -1075,  -713,  -365,   167,\n+        516,  1381,  1882,  1826,  1956,  2492,  3192,  3541,  3750,  4123,\n+       4462,  4592,  4472,  4705,  4613,  4559,  4340,  4392,  4144,  3973,\n+       3119,  2582,  2057,  1684,  1199,   834,   477,   325,   295,  -198,\n+       -459,  -670,  -706,  -677,  -766,  -852,  -939,  -905,  -637,  -601,\n+       -531,  -433,  -292,  -158,    88,    85,   118,   121,   147,   179,\n+        173,   149,   128],\n+    # 80\n+    [   342,   293,   244,   159,    38,    20,    15,   -15,  -109,  -119,\n+       -240,  -182,    16,   397,   550,   264,   350,   670,   865,   681,\n+       1188,  1136,   703,  1153,  1930,  2412,  2776,  3118,  3351,  3634,\n+       3653,  3272,  3177,  3161,  3354,  3671,  3615,  3572,  3522,  3274,\n+       2914,  2682,  2426,  2185,  1845,  1584,  1297,  1005,   809,   507,\n+        248,   314,   230,    96,   149,   240,   274,   297,   153,   109,\n+        164,    91,   104,    43,    12,   153,   243,   170,   184,    59,\n+         99,   158,   342],\n+    # 85\n+    [   912,   961,  1013,  1013,   997,  1032,  1026,  1050,  1072,  1132,\n+       1156,  1253,  1310,  1389,  1441,  1493,  1508,  1565,  1621,  1642,\n+       1768,  1888,  2036,  2089,  2117,  2106,  2010,  2120,  2276,  2376,\n+       2426,  2427,  2526,  2582,  2493,  2534,  2628,  2564,  2471,  2509,\n+       2407,  2332,  2214,  2122,  1987,  1855,  1714,  1619,  1517,  1474,\n+       1406,  1351,  1308,  1264,  1181,  1081,  1047,  1084,  1043,   964,\n+        851,   755,   732,   706,   697,   785,   864,   762,   686,   729,\n+        789,   856,   912],\n+    # 90\n+    [  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,\n+       1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,\n+       1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,\n+       1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,\n+       1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,\n+       1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,\n+       1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,  1490,\n+       1490,  1490,  1490]]\n+\n+\n+#  This table is wmm2015.  Values obtained from MagneticField, part of\n+#  geographiclib., by using devtools/get_mag_var_table.py\n+#\n+#  magvar[][] has the magnetic variation (declination), in hundreths of\n+#  a degree, on a 5 degree by 5 * degree grid for the entire planet.\n+#\n+#  This table is duplicated in geoid.c.  Keep them in sync.\n+#\n+\n+magvar_table = [\n+    # -90\n+    [  14920,  14420,  13920,  13420,  12920,  12420,  11920,  11420,  10920,\n+       10420,   9920,   9420,   8920,   8420,   7920,   7420,   6920,   6420,\n+        5920,   5420,   4920,   4420,   3920,   3420,   2920,   2420,   1920,\n+        1420,    920,    420,    -80,   -580,  -1080,  -1580,  -2080,  -2580,\n+       -3080,  -3580,  -4080,  -4580,  -5080,  -5580,  -6080,  -6580,  -7080,\n+       -7580,  -8080,  -8580,  -9080,  -9580, -10080, -10580, -11080, -11580,\n+      -12080, -12580, -13080, -13580, -14080, -14580, -15080, -15580, -16080,\n+      -16580, -17080, -17580,  17920,  17420,  16920,  16420,  15920,  15420,\n+       14920],\n+    # -85\n+    [  14174,  13609,  13052,  12504,  11965,  11435,  10914,  10402,   9898,\n+        9403,   8915,   8434,   7960,   7492,   7029,   6572,   6119,   5671,\n+        5226,   4784,   4346,   3909,   3475,   3042,   2611,   2180,   1750,\n+        1319,    889,    457,     25,   -409,   -845,  -1283,  -1723,  -2166,\n+       -2612,  -3061,  -3514,  -3971,  -4431,  -4895,  -5364,  -5837,  -6315,\n+       -6798,  -7285,  -7778,  -8277,  -8781,  -9291,  -9808, -10331, -10860,\n+      -11397, -11940, -12491, -13048, -13613, -14184, -14761, -15344, -15931,\n+      -16523, -17117, -17714,  17690,  17094,  16500,  15910,  15325,  14746,\n+       14174],\n+    # -80\n+    [  12958,  12331,  11733,  11163,  10619,  10099,   9600,   9121,   8659,\n+        8211,   7776,   7352,   6937,   6529,   6127,   5730,   5338,   4949,\n+        4563,   4180,   3799,   3420,   3043,   2667,   2292,   1918,   1544,\n+        1170,    795,    419,     40,   -342,   -727,  -1117,  -1512,  -1912,\n+       -2318,  -2729,  -3147,  -3571,  -4002,  -4438,  -4880,  -5328,  -5782,\n+       -6241,  -6707,  -7180,  -7659,  -8146,  -8641,  -9146,  -9662, -10190,\n+      -10732, -11290, -11866, -12461, -13077, -13716, -14379, -15064, -15772,\n+      -16500, -17244, -17999,  17241,  16485,  15738,  15007,  14298,  13614,\n+       12958],\n+    # -75\n+    [  11045,  10435,   9882,   9378,   8915,   8485,   8081,   7699,   7335,\n+        6983,   6640,   6304,   5972,   5642,   5313,   4982,   4651,   4317,\n+        3982,   3646,   3309,   2972,   2635,   2300,   1966,   1634,   1304,\n+         975,    646,    316,    -16,   -352,   -694,  -1042,  -1398,  -1764,\n+       -2138,  -2523,  -2916,  -3319,  -3729,  -4146,  -4569,  -4997,  -5430,\n+       -5866,  -6306,  -6750,  -7197,  -7650,  -8109,  -8576,  -9054,  -9545,\n+      -10054, -10585, -11143, -11736, -12371, -13058, -13806, -14624, -15519,\n+      -16489, -17525,  17396,  16307,  15245,  14242,  13317,  12478,  11723,\n+       11045],\n+    # -70\n+    [   8567,   8144,   7771,   7437,   7132,   6851,   6587,   6336,   6095,\n+        5858,   5623,   5386,   5144,   4896,   4639,   4372,   4094,   3807,\n+        3510,   3206,   2895,   2582,   2266,   1952,   1641,   1334,   1033,\n+         736,    444,    155,   -136,   -429,   -729,  -1038,  -1359,  -1693,\n+       -2042,  -2406,  -2782,  -3170,  -3568,  -3973,  -4383,  -4795,  -5208,\n+       -5620,  -6030,  -6437,  -6843,  -7246,  -7650,  -8054,  -8463,  -8880,\n+       -9308,  -9756, -10230, -10744, -11314, -11965, -12737, -13686, -14889,\n+      -16424,  17708,  15701,  13867,  12382,  11234,  10344,   9636,   9056,\n+        8567],\n+    # -65\n+    [   6318,   6126,   5946,   5777,   5617,   5466,   5322,   5183,   5046,\n+        4910,   4770,   4622,   4464,   4291,   4101,   3891,   3661,   3411,\n+        3142,   2857,   2558,   2250,   1938,   1625,   1318,   1019,    730,\n+         454,    189,    -67,   -318,   -570,   -827,  -1096,  -1381,  -1685,\n+       -2009,  -2354,  -2717,  -3095,  -3485,  -3881,  -4279,  -4676,  -5068,\n+       -5453,  -5828,  -6193,  -6546,  -6887,  -7218,  -7538,  -7849,  -8152,\n+       -8450,  -8744,  -9037,  -9335,  -9645,  -9979, -10368, -10886, -11809,\n+      -15193,  10922,   8778,   8037,   7595,   7264,   6988,   6744,   6523,\n+        6318],\n+    # -60\n+    [   4768,   4709,   4640,   4566,   4490,   4416,   4344,   4274,   4208,\n+        4141,   4071,   3993,   3902,   3794,   3663,   3505,   3318,   3100,\n+        2852,   2578,   2280,   1965,   1640,   1313,    991,    682,    392,\n+         123,   -125,   -354,   -569,   -778,   -991,  -1215,  -1458,  -1726,\n+       -2023,  -2347,  -2695,  -3064,  -3445,  -3833,  -4220,  -4600,  -4969,\n+       -5323,  -5658,  -5972,  -6264,  -6532,  -6776,  -6993,  -7181,  -7338,\n+       -7458,  -7532,  -7546,  -7474,  -7273,  -6857,  -6058,  -4557,  -2058,\n+         727,   2633,   3688,   4259,   4572,   4736,   4812,   4830,   4812,\n+        4768],\n+    # -55\n+    [   3771,   3768,   3750,   3720,   3685,   3647,   3611,   3577,   3546,\n+        3519,   3492,   3462,   3421,   3364,   3283,   3171,   3023,   2835,\n+        2607,   2339,   2036,   1705,   1356,   1001,    652,    319,     13,\n+        -261,   -501,   -709,   -891,  -1058,  -1222,  -1394,  -1587,  -1810,\n+       -2069,  -2365,  -2694,  -3048,  -3419,  -3795,  -4168,  -4529,  -4871,\n+       -5189,  -5479,  -5737,  -5961,  -6148,  -6293,  -6394,  -6445,  -6436,\n+       -6355,  -6184,  -5896,  -5455,  -4817,  -3939,  -2818,  -1532,   -251,\n+         866,   1747,   2404,   2878,   3214,   3446,   3600,   3697,   3750,\n+        3771],\n+    # -50\n+    [   3099,   3120,   3123,   3115,   3098,   3077,   3056,   3036,   3021,\n+        3011,   3006,   3001,   2992,   2971,   2929,   2856,   2743,   2584,\n+        2373,   2111,   1800,   1449,   1071,    680,    294,    -71,   -401,\n+        -686,   -925,  -1118,  -1273,  -1401,  -1516,  -1633,  -1767,  -1932,\n+       -2138,  -2392,  -2689,  -3021,  -3374,  -3734,  -4089,  -4425,  -4736,\n+       -5014,  -5253,  -5450,  -5601,  -5700,  -5743,  -5724,  -5634,  -5465,\n+       -5202,  -4832,  -4344,  -3732,  -3008,  -2203,  -1368,   -556,    188,\n+         839,   1387,   1838,   2201,   2485,   2704,   2865,   2979,   3054,\n+        3099],\n+    # -45\n+    [   2611,   2642,   2656,   2658,   2651,   2639,   2625,   2610,   2598,\n+        2592,   2591,   2595,   2601,   2600,   2583,   2539,   2456,   2321,\n+        2128,   1871,   1554,   1183,    774,    346,    -76,   -472,   -823,\n+       -1120,  -1357,  -1540,  -1675,  -1774,  -1850,  -1915,  -1987,  -2083,\n+       -2220,  -2411,  -2657,  -2951,  -3276,  -3612,  -3941,  -4248,  -4522,\n+       -4755,  -4940,  -5073,  -5147,  -5158,  -5102,  -4972,  -4762,  -4467,\n+       -4083,  -3612,  -3068,  -2474,  -1858,  -1251,   -675,   -141,    343,\n+         777,   1161,   1495,   1781,   2020,   2214,   2366,   2479,   2559,\n+        2611],\n+    # -40\n+    [   2236,   2269,   2287,   2295,   2295,   2289,   2279,   2266,   2253,\n+        2243,   2238,   2239,   2245,   2250,   2246,   2220,   2157,   2043,\n+        1864,   1613,   1290,    903,    468,     12,   -438,   -854,  -1216,\n+       -1513,  -1745,  -1916,  -2038,  -2121,  -2172,  -2202,  -2220,  -2243,\n+       -2294,  -2397,  -2566,  -2798,  -3077,  -3376,  -3672,  -3945,  -4180,\n+       -4367,  -4499,  -4570,  -4575,  -4509,  -4370,  -4157,  -3867,  -3504,\n+       -3076,  -2600,  -2099,  -1602,  -1131,   -697,   -301,     62,    396,\n+         706,    992,   1252,   1485,   1688,   1860,   1999,   2106,   2183,\n+        2236],\n+    # -35\n+    [   1934,   1966,   1985,   1996,   2001,   2000,   1994,   1982,   1967,\n+        1952,   1939,   1932,   1930,   1932,   1930,   1911,   1860,   1758,\n+        1589,   1343,   1016,    618,    167,   -305,   -767,  -1187,  -1544,\n+       -1830,  -2047,  -2204,  -2315,  -2387,  -2426,  -2432,  -2406,  -2360,\n+       -2315,  -2308,  -2368,  -2508,  -2716,  -2964,  -3222,  -3462,  -3666,\n+       -3818,  -3909,  -3934,  -3888,  -3771,  -3584,  -3331,  -3015,  -2645,\n+       -2236,  -1809,  -1390,  -1003,   -658,   -355,    -85,    165,    403,\n+         634,    856,   1066,   1261,   1436,   1588,   1714,   1813,   1885,\n+        1934],\n+    # -30\n+    [   1687,   1715,   1732,   1744,   1752,   1755,   1753,   1744,   1728,\n+        1709,   1689,   1672,   1660,   1653,   1647,   1629,   1581,   1485,\n+        1321,   1075,    746,    343,   -113,   -588,  -1045,  -1453,  -1792,\n+       -2056,  -2251,  -2390,  -2484,  -2542,  -2565,  -2544,  -2475,  -2360,\n+       -2220,  -2093,  -2026,  -2049,  -2164,  -2349,  -2568,  -2786,  -2976,\n+       -3116,  -3193,  -3201,  -3138,  -3007,  -2815,  -2567,  -2271,  -1936,\n+       -1577,  -1218,   -883,   -590,   -345,   -140,     41,    212,    385,\n+         563,    742,    918,   1085,   1238,   1374,   1488,   1578,   1643,\n+        1687],\n+    # -25\n+    [   1485,   1507,   1520,   1530,   1539,   1544,   1545,   1539,   1525,\n+        1504,   1479,   1454,   1434,   1420,   1407,   1385,   1336,   1239,\n+        1074,    827,    497,     93,   -359,   -823,  -1262,  -1647,  -1960,\n+       -2198,  -2366,  -2478,  -2546,  -2575,  -2564,  -2502,  -2380,  -2200,\n+       -1978,  -1753,  -1575,  -1486,  -1504,  -1617,  -1794,  -1998,  -2189,\n+       -2341,  -2432,  -2454,  -2406,  -2295,  -2131,  -1920,  -1670,  -1388,\n+       -1089,   -796,   -531,   -312,   -141,     -8,    108,    224,    353,\n+         495,    646,    798,    945,   1082,   1204,   1308,   1389,   1447,\n+        1485],\n+    # -20\n+    [   1322,   1337,   1344,   1350,   1356,   1363,   1366,   1363,   1351,\n+        1330,   1303,   1275,   1249,   1230,   1212,   1186,   1132,   1030,\n+         859,    609,    278,   -121,   -561,  -1006,  -1420,  -1775,  -2058,\n+       -2264,  -2401,  -2478,  -2506,  -2489,  -2425,  -2309,  -2134,  -1903,\n+       -1633,  -1359,  -1122,   -961,   -902,   -945,  -1074,  -1254,  -1445,\n+       -1612,  -1727,  -1778,  -1764,  -1691,  -1570,  -1409,  -1213,   -986,\n+        -743,   -504,   -294,   -131,    -15,     66,    135,    213,    311,\n+         430,    563,    699,    832,    957,   1070,   1165,   1240,   1291,\n+        1322],\n+    # -15\n+    [   1194,   1201,   1202,   1201,   1204,   1209,   1213,   1211,   1202,\n+        1183,   1157,   1128,   1101,   1080,   1059,   1028,    968,    858,\n+         680,    425,     94,   -296,   -718,  -1138,  -1522,  -1846,  -2096,\n+       -2268,  -2367,  -2401,  -2378,  -2304,  -2182,  -2013,  -1798,  -1545,\n+       -1270,   -996,   -752,   -566,   -462,   -451,   -529,   -673,   -849,\n+       -1019,  -1151,  -1228,  -1246,  -1212,  -1135,  -1023,   -876,   -700,\n+        -505,   -311,   -145,    -23,     53,     97,    133,    184,    262,\n+         366,    488,    615,    739,    856,    963,   1054,   1124,   1170,\n+        1194],\n+    # -10\n+    [   1097,   1098,   1090,   1083,   1081,   1083,   1087,   1087,   1080,\n+        1063,   1039,   1011,    986,    965,    943,    906,    838,    718,\n+         531,    271,    -56,   -434,   -835,  -1226,  -1578,  -1869,  -2085,\n+       -2221,  -2279,  -2265,  -2188,  -2059,  -1886,  -1678,  -1446,  -1198,\n+        -946,   -703,   -482,   -302,   -181,   -135,   -170,   -275,   -425,\n+        -583,   -719,   -810,   -851,   -848,   -808,   -736,   -633,   -500,\n+        -346,   -191,    -59,     32,     78,     96,    109,    141,    205,\n+         300,    415,    537,    658,    772,    877,    966,   1035,   1078,\n+        1097],\n+    # -5\n+    [   1026,   1022,   1009,    995,    988,    987,    990,    992,    987,\n+         973,    951,    926,    903,    882,    856,    813,    734,    603,\n+         407,    144,   -179,   -542,   -919,  -1281,  -1601,  -1858,  -2038,\n+       -2136,  -2152,  -2094,  -1971,  -1797,  -1589,  -1363,  -1131,   -903,\n+        -685,   -480,   -293,   -131,    -10,     55,     50,    -22,   -143,\n+        -284,   -412,   -507,   -561,   -578,   -565,   -525,   -457,   -360,\n+        -242,   -121,    -18,     47,     71,     69,     66,     85,    140,\n+         229,    339,    460,    580,    695,    803,    895,    966,   1010,\n+        1026],\n+    # 0\n+    [    975,    971,    953,    935,    924,    922,    926,    930,    928,\n+         917,    898,    875,    852,    828,    796,    743,    651,    506,\n+         300,     34,   -282,   -629,   -981,  -1314,  -1601,  -1823,  -1967,\n+       -2028,  -2006,  -1910,  -1753,  -1553,  -1327,  -1096,   -873,   -669,\n+        -484,   -315,   -160,    -20,     94,    166,    181,    134,     38,\n+         -84,   -201,   -293,   -352,   -380,   -384,   -367,   -327,   -261,\n+        -175,    -84,     -9,     33,     39,     23,      8,     17,     64,\n+         147,    255,    375,    497,    617,    731,    831,    908,    957,\n+         975],\n+    # 5\n+    [    937,    936,    920,    901,    890,    888,    895,    902,    904,\n+         897,    880,    858,    832,    803,    761,    693,    584,    423,\n+         205,    -65,   -374,   -704,  -1031,  -1333,  -1587,  -1773,  -1881,\n+       -1906,  -1853,  -1730,  -1554,  -1341,  -1112,   -885,   -675,   -490,\n+        -331,   -191,    -62,     58,    162,    235,    260,    231,    154,\n+          50,    -56,   -142,   -201,   -234,   -249,   -248,   -228,   -188,\n+        -130,    -67,    -18,      2,     -9,    -38,    -64,    -63,    -24,\n+          52,    157,    276,    402,    529,    652,    763,    852,    911,\n+         937],\n+    # 10\n+    [    901,    911,    902,    889,    881,    883,    895,    908,    916,\n+         913,    899,    876,    845,    806,    749,    662,    532,    352,\n+         120,   -155,   -459,   -773,  -1076,  -1347,  -1565,  -1715,  -1787,\n+       -1781,  -1703,  -1564,  -1380,  -1167,   -942,   -724,   -526,   -357,\n+        -216,    -96,     13,    117,    210,    280,    312,    294,    232,\n+         142,     47,    -33,    -89,   -125,   -145,   -153,   -149,   -129,\n+         -97,    -61,    -38,    -40,    -67,   -110,   -146,   -156,   -127,\n+         -58,     40,    159,    289,    424,    559,    683,    787,    861,\n+         901],\n+    # 15\n+    [    859,    887,    893,    892,    893,    904,    924,    945,    959,\n+         962,    950,    925,    888,    836,    760,    651,    497,    294,\n+          45,   -238,   -540,   -841,  -1120,  -1360,  -1542,  -1655,  -1694,\n+       -1661,  -1565,  -1418,  -1234,  -1028,   -812,   -604,   -416,   -257,\n+        -128,    -21,     75,    165,    249,    315,    349,    340,    290,\n+         212,    127,     53,      0,    -35,    -58,    -73,    -80,    -78,\n+         -68,    -58,    -61,    -85,   -131,   -189,   -238,   -260,   -243,\n+        -185,    -94,     22,    155,    298,    445,    584,    706,    799,\n+         859],\n+    # 20\n+    [    803,    856,    885,    903,    921,    945,    976,   1006,   1029,\n+        1037,   1027,   1000,    955,    888,    793,    658,    478,    250,\n+         -18,   -314,   -617,   -908,  -1167,  -1377,  -1524,  -1602,  -1610,\n+       -1554,  -1446,  -1296,  -1117,   -920,   -715,   -516,   -336,   -184,\n+         -60,     40,    127,    208,    284,    346,    381,    379,    339,\n+         273,    198,    131,     81,     46,     22,      3,    -14,    -27,\n+         -38,    -54,    -83,   -131,   -198,   -273,   -338,   -374,   -371,\n+        -326,   -244,   -131,      3,    153,    310,    464,    605,    720,\n+         803],\n+    # 25\n+    [    731,    813,    871,    916,    957,   1000,   1045,   1086,   1117,\n+        1131,   1123,   1094,   1040,    958,    842,    682,    475,    221,\n+         -70,   -381,   -690,   -976,  -1218,  -1402,  -1518,  -1565,  -1546,\n+       -1472,  -1354,  -1203,  -1030,   -841,   -646,   -455,   -281,   -130,\n+          -8,     90,    173,    249,    318,    377,    413,    418,    389,\n+         336,    273,    214,    167,    134,    107,     83,     57,     29,\n+          -4,    -45,   -101,   -176,   -267,   -361,   -444,   -497,   -509,\n+        -477,   -404,   -296,   -162,     -7,    159,    326,    483,    621,\n+         731],\n+    # 30\n+    [    643,    756,    848,    926,    996,   1062,   1124,   1178,   1217,\n+        1235,   1230,   1197,   1134,   1038,    902,    718,    484,    203,\n+        -113,   -443,   -762,  -1046,  -1276,  -1439,  -1530,  -1551,  -1511,\n+       -1422,  -1297,  -1145,   -975,   -792,   -604,   -418,   -246,    -96,\n+          29,    129,    213,    287,    354,    411,    450,    464,    449,\n+         411,    362,    313,    272,    238,    208,    176,    139,     94,\n+          38,    -31,   -117,   -220,   -337,   -454,   -556,   -626,   -654,\n+        -634,   -569,   -466,   -331,   -174,     -1,    176,    348,    506,\n+         643],\n+    # 35\n+    [    547,    691,    818,    930,   1031,   1123,   1205,   1273,   1321,\n+        1345,   1340,   1304,   1234,   1124,    968,    760,    499,    189,\n+        -153,   -505,   -837,  -1125,  -1347,  -1495,  -1567,  -1570,  -1514,\n+       -1413,  -1281,  -1126,   -956,   -776,   -590,   -406,   -233,    -80,\n+          50,    157,    246,    323,    392,    452,    498,    523,    525,\n+         507,    476,    440,    405,    371,    335,    292,    239,    173,\n+          90,    -10,   -129,   -265,   -411,   -553,   -675,   -761,   -801,\n+        -792,   -734,   -633,   -499,   -338,   -161,     25,    210,    386,\n+         547],\n+    # 40\n+    [    454,    626,    784,    929,   1061,   1180,   1283,   1366,   1424,\n+        1454,   1452,   1413,   1335,   1211,   1036,    804,    513,    173,\n+        -199,   -576,   -925,  -1219,  -1439,  -1577,  -1637,  -1627,  -1560,\n+       -1450,  -1311,  -1152,   -978,   -795,   -607,   -422,   -245,    -85,\n+          54,    172,    271,    358,    435,    504,    562,    603,    627,\n+         632,    622,    602,    575,    541,    497,    439,    365,    271,\n+         156,     19,   -139,   -312,   -489,   -657,   -799,   -900,   -950,\n+        -947,   -892,   -792,   -656,   -492,   -310,   -117,     79,    271,\n+         454],\n+    # 45\n+    [    374,    569,    754,    927,   1087,   1230,   1354,   1454,   1525,\n+        1563,   1565,   1525,   1439,   1301,   1105,    845,    522,    145,\n+        -262,   -668,  -1037,  -1341,  -1562,  -1696,  -1748,  -1729,  -1654,\n+       -1538,  -1392,  -1225,  -1045,   -855,   -661,   -469,   -284,   -114,\n+          39,    172,    289,    392,    486,    572,    648,    712,    761,\n+         793,    808,    807,    790,    756,    702,    627,    526,    397,\n+         241,     59,   -144,   -359,   -573,   -769,   -930,  -1043,  -1099,\n+       -1098,  -1042,   -940,   -799,   -630,   -442,   -241,    -34,    172,\n+         374],\n+    # 50\n+    [    313,    526,    732,    928,   1110,   1276,   1419,   1536,   1622,\n+        1671,   1679,   1641,   1549,   1396,   1175,    882,    518,     96,\n+        -355,   -797,  -1192,  -1508,  -1733,  -1864,  -1910,  -1885,  -1804,\n+       -1680,  -1526,  -1350,  -1159,   -960,   -755,   -552,   -354,   -168,\n+           4,    160,    301,    430,    549,    661,    764,    857,    935,\n+         997,   1040,   1060,   1055,   1023,    960,    864,    731,    561,\n+         355,    119,   -139,   -405,   -660,   -886,  -1066,  -1187,  -1245,\n+       -1241,  -1181,  -1072,   -925,   -749,   -552,   -342,   -125,     94,\n+         313],\n+    # 55\n+    [    270,    499,    721,    935,   1135,   1319,   1480,   1614,   1716,\n+        1779,   1797,   1761,   1664,   1495,   1245,    909,    490,      9,\n+        -500,   -988,  -1413,  -1744,  -1971,  -2099,  -2138,  -2105,  -2015,\n+       -1881,  -1716,  -1528,  -1324,  -1110,   -890,   -670,   -454,   -246,\n+         -48,    137,    311,    475,    629,    776,    914,   1041,   1155,\n+        1250,   1322,   1366,   1377,   1351,   1281,   1164,    997,    779,\n+         514,    211,   -114,   -441,   -744,  -1003,  -1201,  -1329,  -1387,\n+       -1377,  -1308,  -1191,  -1034,   -849,   -642,   -422,   -194,     38,\n+         270],\n+    # 60\n+    [    240,    482,    719,    946,   1162,   1361,   1539,   1690,   1808,\n+        1886,   1914,   1882,   1779,   1590,   1302,    908,    415,   -149,\n+        -734,  -1280,  -1738,  -2081,  -2305,  -2422,  -2447,  -2399,  -2294,\n+       -2145,  -1963,  -1758,  -1536,  -1302,  -1062,   -819,   -577,   -340,\n+        -110,    113,    326,    532,    730,    919,   1099,   1267,   1420,\n+        1551,   1657,   1730,   1764,   1749,   1680,   1548,   1349,   1080,\n+         747,    364,    -44,   -446,   -809,  -1108,  -1328,  -1464,  -1519,\n+       -1502,  -1424,  -1297,  -1130,   -934,   -717,   -487,   -247,     -4,\n+         240],\n+    # 65\n+    [    214,    468,    717,    957,   1186,   1399,   1592,   1759,   1891,\n+        1982,   2018,   1988,   1872,   1652,   1308,    830,    230,   -448,\n+       -1130,  -1738,  -2220,  -2559,  -2763,  -2853,  -2849,  -2773,  -2641,\n+       -2467,  -2260,  -2030,  -1782,  -1523,  -1255,   -983,   -709,   -437,\n+        -168,     96,    355,    608,    853,   1090,   1316,   1529,   1725,\n+        1899,   2044,   2155,   2220,   2232,   2178,   2047,   1828,   1514,\n+        1111,    635,    124,   -376,   -819,  -1174,  -1426,  -1577,  -1635,\n+       -1615,  -1530,  -1394,  -1218,  -1013,   -787,   -546,   -296,    -42,\n+         214],\n+    # 70\n+    [    178,    442,    702,    955,   1195,   1421,   1625,   1802,   1944,\n+        2040,   2074,   2030,   1881,   1600,   1159,    546,   -215,  -1041,\n+       -1821,  -2461,  -2924,  -3215,  -3362,  -3394,  -3337,  -3214,  -3040,\n+       -2827,  -2585,  -2320,  -2040,  -1747,  -1445,  -1138,   -828,   -517,\n+        -207,    101,    406,    706,    999,   1283,   1558,   1819,   2062,\n+        2284,   2479,   2637,   2752,   2811,   2799,   2700,   2496,   2170,\n+        1716,   1147,    511,   -125,   -689,  -1136,  -1450,  -1637,  -1714,\n+       -1702,  -1619,  -1481,  -1302,  -1092,   -859,   -611,   -352,    -88,\n+         178],\n+    # 75\n+    [    114,    385,    652,    911,   1157,   1386,   1592,   1767,   1899,\n+        1975,   1973,   1868,   1622,   1193,    548,   -307,  -1284,  -2226,\n+       -2997,  -3542,  -3876,  -4038,  -4070,  -4006,  -3869,  -3677,  -3445,\n+       -3182,  -2895,  -2589,  -2269,  -1939,  -1600,  -1257,   -909,   -559,\n+        -210,    140,    486,    829,   1167,   1497,   1818,   2128,   2423,\n+        2698,   2951,   3173,   3356,   3491,   3562,   3550,   3431,   3177,\n+        2759,   2165,   1420,    605,   -165,   -797,  -1254,  -1540,  -1684,\n+       -1714,  -1659,  -1539,  -1370,  -1166,   -936,   -687,   -426,   -157,\n+         114],\n+    # 80\n+    [     -8,    249,    501,    742,    965,   1164,   1327,   1442,   1489,\n+        1441,   1262,    902,    306,   -553,  -1618,  -2716,  -3648,  -4317,\n+       -4731,  -4941,  -4998,  -4945,  -4811,  -4617,  -4378,  -4104,  -3804,\n+       -3483,  -3146,  -2796,  -2436,  -2068,  -1693,  -1315,   -933,   -549,\n+        -164,    220,    603,    984,   1361,   1733,   2099,   2457,   2805,\n+        3140,   3459,   3758,   4031,   4273,   4472,   4616,   4686,   4657,\n+        4492,   4146,   3573,   2755,   1755,    731,   -142,   -778,  -1181,\n+       -1395,  -1468,  -1438,  -1333,  -1176,   -980,   -758,   -517,   -265,\n+          -8],\n+    # 85\n+    [   -546,   -534,   -552,   -627,   -794,  -1097,  -1585,  -2300,  -3236,\n+       -4294,  -5297,  -6100,  -6655,  -6988,  -7144,  -7167,  -7093,  -6945,\n+       -6742,  -6496,  -6217,  -5912,  -5585,  -5241,  -4883,  -4513,  -4134,\n+       -3746,  -3352,  -2952,  -2547,  -2138,  -1726,  -1312,   -896,   -478,\n+         -60,    359,    778,   1196,   1612,   2028,   2441,   2851,   3257,\n+        3660,   4057,   4448,   4831,   5205,   5567,   5916,   6248,   6558,\n+        6841,   7088,   7287,   7421,   7465,   7378,   7100,   6543,   5611,\n+        4289,   2795,   1493,    572,      0,   -325,   -488,   -553,   -561,\n+        -546],\n+    # 90\n+    [ -17825, -17325, -16825, -16325, -15825, -15325, -14825, -14325, -13825,\n+      -13325, -12825, -12325, -11825, -11325, -10825, -10325,  -9825,  -9325,\n+       -8825,  -8325,  -7825,  -7325,  -6825,  -6325,  -5825,  -5325,  -4825,\n+       -4325,  -3825,  -3325,  -2825,  -2325,  -1825,  -1325,   -825,   -325,\n+         175,    675,   1175,   1675,   2175,   2675,   3175,   3675,   4175,\n+        4675,   5175,   5675,   6175,   6675,   7175,   7675,   8175,   8675,\n+        9175,   9675,  10175,  10675,  11175,  11675,  12175,  12675,  13175,\n+       13675,  14175,  14675,  15175,  15675,  16175,  16675,  17175,  17675,\n+      -17825]]\n+\n+# \"enum\" for display units\n+unspecified = 0\n+imperial = 1\n+nautical = 2\n+metric = 3\n+\n+# \"enum\" for deg_to_str() conversion type\n+deg_dd = 0\n+deg_ddmm = 1\n+deg_ddmmss = 2\n+\n+\n+def _non_finite(num):\n+    \"\"\"Is this number not finite?\"\"\"\n+    return math.isnan(num) or math.isinf(num)\n+\n+\n+def deg_to_str(fmt, degrees):\n+    \"\"\"String-format a latitude/longitude.\"\"\"\n+    try:\n+        degrees = float(degrees)\n+    except ValueError:\n+        return ''\n+\n+    if _non_finite(degrees):\n+        return ''\n+\n+    if degrees >= 360:\n+        degrees -= 360\n+    if not math.fabs(degrees) <= 360:\n+        return ''\n+\n+    if fmt is deg_dd:\n+        degrees += 1.0e-9\n+        return '%12.8f' % degrees\n+\n+    degrees += 1.0e-8 / 36.0\n+    (fmin, fdeg) = math.modf(degrees)\n+\n+    if fmt is deg_ddmm:\n+        return '%3d %09.6f\\'' % (fdeg, math.fabs(60. * fmin))\n+\n+    (fsec, fmin) = math.modf(60. * fmin)\n+    return '%3d %02d\\' %08.5f\\\"' % (fdeg, math.fabs(fmin),\n+                                    math.fabs(60. * fsec))\n+\n+\n+def gpsd_units():\n+    \"\"\"Deduce a set of units from locale and environment.\"\"\"\n+    unit_lookup = {'imperial': imperial,\n+                   'metric': metric,\n+                   'nautical': nautical}\n+    if 'GPSD_UNITS' in os.environ:\n+        store = os.environ['GPSD_UNITS']\n+        if isinstance(store, (str)) and store in unit_lookup:\n+            return unit_lookup[store]\n+    for inner in ['LC_MEASUREMENT', 'LANG']:\n+        if inner in os.environ:\n+            store = os.environ[inner]\n+            if isinstance(store, (str)):\n+                if store in ['C', 'POSIX'] or store.startswith('en_US'):\n+                    return imperial\n+                return metric\n+    return unspecified\n+\n+\n+# Arguments are in signed decimal latitude and longitude. For example,\n+# the location of Montevideo (GF15vc) is: -34.91, -56.21166\n+# plagarized from https://ham.stackexchange.com/questions/221\n+# by https://ham.stackexchange.com/users/10/walter-underwood-k6wru\n+def maidenhead(dec_lat, dec_lon):\n+    \"\"\"Convert latitude and longitude to Maidenhead grid locators.\"\"\"\n+    try:\n+        dec_lat = float(dec_lat)\n+        dec_lon = float(dec_lon)\n+    except ValueError:\n+        return ''\n+    if _non_finite(dec_lat) or _non_finite(dec_lon):\n+        return ''\n+\n+    if 90 < math.fabs(dec_lat) or 180 < math.fabs(dec_lon):\n+        return ''\n+\n+    if 89.99999 < dec_lat:\n+        # force North Pole to just inside lat_sq 'R'\n+        dec_lat = 89.99999\n+\n+    if 179.99999 < dec_lon:\n+        # force 180 to just inside lon_sq 'R'\n+        dec_lon = 179.99999\n+\n+    adj_lat = dec_lat + 90.0\n+    adj_lon = dec_lon + 180.0\n+\n+    # divide into 18 zones (fields) each 20 degrees lon, 10 degrees lat\n+    grid_lat_sq = chr(int(adj_lat / 10) + 65)\n+    grid_lon_sq = chr(int(adj_lon / 20) + 65)\n+\n+    # divide into 10 zones (squares) each 2 degrees lon, 1 degrees lat\n+    grid_lat_field = str(int(adj_lat % 10))\n+    grid_lon_field = str(int((adj_lon / 2) % 10))\n+\n+    # remainder in minutes\n+    adj_lat_remainder = (adj_lat - int(adj_lat)) * 60\n+    adj_lon_remainder = ((adj_lon) - int(adj_lon / 2) * 2) * 60\n+\n+    # divide into 24 zones (subsquares) each 5 degrees lon, 2.5 degrees lat\n+    grid_lat_subsq = chr(97 + int(adj_lat_remainder / 2.5))\n+    grid_lon_subsq = chr(97 + int(adj_lon_remainder / 5))\n+\n+    # remainder in seconds\n+    adj_lat_remainder = (adj_lat_remainder % 2.5) * 60\n+    adj_lon_remainder = (adj_lon_remainder % 5.0) * 60\n+\n+    # divide into 10 zones (extended squares) each 30 secs lon, 15 secs lat\n+    grid_lat_extsq = chr(48 + int(adj_lat_remainder / 15))\n+    grid_lon_extsq = chr(48 + int(adj_lon_remainder / 30))\n+\n+    return (grid_lon_sq + grid_lat_sq +\n+            grid_lon_field + grid_lat_field +\n+            grid_lon_subsq + grid_lat_subsq +\n+            grid_lon_extsq + grid_lat_extsq)\n+\n+def __bilinear(lat, lon, table):\n+    \"\"\"Return bilinear interpolated data from table\"\"\"\n+\n+    try:\n+        lat = float(lat)\n+        lon = float(lon)\n+    except ValueError:\n+        return ''\n+    if _non_finite(lat) or _non_finite(lon):\n+        return ''\n+\n+    if math.fabs(lat) > 90 or math.fabs(lon) > 180:\n+        return ''\n+\n+    row = int(math.floor((90.0 + lat) / TABLE_SPAN))\n+    column = int(math.floor((180.0 + lon) / TABLE_SPAN))\n+\n+    if row < (TABLE_ROWS - 1):\n+        grid_w = row\n+        grid_e = row + 1\n+    else:\n+        grid_w = row - 1\n+        grid_e = row\n+    if column < (TABLE_COLS - 1):\n+        grid_s = column\n+        grid_n = column + 1\n+    else:\n+        grid_s = column - 1\n+        grid_n = column\n+\n+    south = grid_s * TABLE_SPAN - 180\n+    north = grid_n * TABLE_SPAN - 180\n+    west = grid_w * TABLE_SPAN - 90\n+    east = grid_e * TABLE_SPAN - 90\n+\n+    delta = TABLE_SPAN * TABLE_SPAN * 100\n+    from_west = lat - west\n+    from_south = lon - south\n+    from_east = east - lat\n+    from_north = north - lon\n+\n+    result = table[grid_e][grid_n] * from_west * from_south\n+    result += table[grid_w][grid_n] * from_east * from_south\n+    result += table[grid_e][grid_s] * from_west * from_north\n+    result += table[grid_w][grid_s] * from_east * from_north\n+    return result / delta\n+\n+\n+def mag_var(lat, lon):\n+    \"\"\"Return magnetic variation (declination) in degrees.\n+Given a lat/lon in degrees\"\"\"\n+    return __bilinear(lat, lon, magvar_table)\n+\n+\n+def wgs84_separation(lat, lon):\n+    \"\"\"Return MSL-WGS84 geodetic separation in meters.\n+Given a lat/lon in degrees\"\"\"\n+    return __bilinear(lat, lon, GEOID_DELTA)\n+\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/fake.py b/tools/gps/fake.py\nnew file mode 100644\n--- /dev/null\n+++ b/tools/gps/fake.py\n@@ -0,0 +1,843 @@\n+# This code run compatibly under Python 2 and 3.x for x >= 2.\n+# Preserve this property!\n+#\n+# This file is Copyright 2010 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-Clause\n+\"\"\"\n+gpsfake.py -- classes for creating a controlled test environment around gpsd.\n+\n+The gpsfake(1) regression tester shipped with GPSD is a trivial wrapper\n+around this code.  For a more interesting usage example, see the\n+valgrind-audit script shipped with the GPSD code.\n+\n+To use this code, start by instantiating a TestSession class.  Use the\n+prefix argument if you want to run the daemon under some kind of run-time\n+monitor like valgrind or gdb.  Here are some particularly useful possibilities:\n+\n+valgrind --tool=memcheck --gen-suppressions=yes --leak-check=yes\n+    Run under Valgrind, checking for malloc errors and memory leaks.\n+\n+xterm -e gdb -tui --args\n+    Run under gdb, controlled from a new xterm.\n+\n+You can use the options argument to pass in daemon options; normally you will\n+use this to set the debug-logging level.\n+\n+On initialization, the test object spawns an instance of gpsd with no\n+devices or clients attached, connected to a control socket.\n+\n+TestSession has methods to attach and detch fake GPSes. The\n+TestSession class simulates GPS devices for you with objects composed\n+from a pty and a class instance that cycles sentences into the master side\n+from some specified logfile; gpsd reads the slave side.  A fake GPS is\n+identified by the string naming its slave device.\n+\n+TestSession also has methods to start and end client sessions.  Daemon\n+responses to a client are fed to a hook function which, by default,\n+discards them.  Note that this data is 'bytes' to accommodate possible\n+binary data in Python 3; use polystr() if you need a str.  You can\n+change the hook to misc.get_bytes_stream(sys.stdout).write to dump\n+responses to standard output (this is what the gpsfake executable does)\n+or do something more exotic. A client session is identified by a small\n+integer that counts the number of client session starts.\n+\n+There are a couple of convenience methods.  TestSession.wait() does nothing,\n+allowing a specified number of seconds to elapse.  TestSession.send()\n+ships commands to an open client session.\n+\n+TestSession does not currently capture the daemon's log output.  It is\n+run with -N, so the output will go to stderr (along with, for example,\n+Valgrind notifications).\n+\n+Each FakeGPS instance tries to packetize the data from the logfile it\n+is initialized with. It uses the same packet-getter as the daemon.\n+Exception: if there is a Delay-Cookie line in a header comment, that\n+delimiter is used to split up the test load.\n+\n+The TestSession code maintains a run queue of FakeGPS and gps.gs\n+(client- session) objects. It repeatedly cycles through the run queue.\n+For each client session object in the queue, it tries to read data\n+from gpsd.  For each fake GPS, it sends one line or packet of stored\n+data.  When a fake-GPS's go predicate becomes false, the fake GPS is\n+removed from the run queue.\n+\n+There are two ways to use this code.  The more deterministic is\n+non-threaded mode: set up your client sessions and fake GPS devices,\n+then call the run() method.  The run() method will terminate when\n+there are no more objects in the run queue.  Note, you must have\n+created at least one fake client or fake GPS before calling run(),\n+otherwise it will terminate immediately.\n+\n+To allow for adding and removing clients while the test is running,\n+run in threaded mode by calling the start() method.  This simply calls\n+the run method in a subthread, with locking of critical regions.\n+\"\"\"\n+# This code runs compatibly under Python 2 and 3.x for x >= 2.\n+# Preserve this property!\n+from __future__ import absolute_import, print_function, division\n+\n+import os\n+import pty\n+import select\n+import signal\n+import socket\n+import stat\n+import subprocess\n+import sys\n+import termios  # fcntl, array, struct\n+import threading\n+import time\n+\n+import gps\n+from . import packet as sniffer\n+\n+# The magic number below has to be derived from observation.  If\n+# it's too high you'll slow the tests down a lot.  If it's too low\n+# you'll get regression tests timing out.\n+\n+# WRITE_PAD: Define a per-line delay on writes so we won't spam the\n+# buffers in the pty layer or gpsd itself. Values smaller than the\n+# system timer tick don't make any difference here. Can be set from\n+# WRITE_PAD in the environment.\n+\n+if sys.platform.startswith(\"linux\"):\n+    WRITE_PAD = 0.0\n+elif sys.platform.startswith(\"freebsd\"):\n+    WRITE_PAD = 0.01\n+elif sys.platform.startswith(\"netbsd5\"):\n+    WRITE_PAD = 0.200\n+elif sys.platform.startswith(\"netbsd\"):\n+    WRITE_PAD = 0.01\n+elif sys.platform.startswith(\"darwin\"):\n+    # darwin Darwin-13.4.0-x86_64-i386-64bit\n+    WRITE_PAD = 0.005\n+else:\n+    WRITE_PAD = 0.004\n+\n+# Additional delays in slow mode\n+WRITE_PAD_SLOWDOWN = 0.01\n+\n+# If a test takes longer than this, we deem it to have timed out\n+TEST_TIMEOUT = 60\n+\n+\n+def GetDelay(slow=False):\n+    \"Get appropriate per-line delay.\"\n+    delay = float(os.getenv(\"WRITE_PAD\", WRITE_PAD))\n+    if slow:\n+        delay += WRITE_PAD_SLOWDOWN\n+    return delay\n+\n+\n+class TestError(BaseException):\n+    \"Class TestError\"\n+    def __init__(self, msg):\n+        super(TestError, self).__init__()\n+        self.msg = msg\n+\n+\n+class TestLoadError(TestError):\n+    \"Class TestLoadError, empty\"\n+\n+\n+class TestLoad(object):\n+    \"Digest a logfile into a list of sentences we can cycle through.\"\n+\n+    def __init__(self, logfp, predump=False, slow=False, oneshot=False):\n+        self.sentences = []  # This is the interesting part\n+        if isinstance(logfp, str):\n+            logfp = open(logfp, \"rb\")\n+        self.name = logfp.name\n+        self.logfp = logfp\n+        self.predump = predump\n+        self.type = None\n+        self.sourcetype = \"pty\"\n+        self.serial = None\n+        self.delay = GetDelay(slow)\n+        self.delimiter = None\n+        # Stash away a copy in case we need to resplit\n+        text = logfp.read()\n+        logfp = open(logfp.name, 'rb')\n+        # Grab the packets in the normal way\n+        getter = sniffer.new()\n+        # gps.packet.register_report(reporter)\n+        type_latch = None\n+        commentlen = 0\n+        while True:\n+            # Note that packet data is bytes rather than str\n+            (plen, ptype, packet, _counter) = getter.get(logfp.fileno())\n+            if plen <= 0:\n+                break\n+            elif ptype == sniffer.COMMENT_PACKET:\n+                commentlen += len(packet)\n+                # Some comments are magic\n+                if b\"Serial:\" in packet:\n+                    # Change serial parameters\n+                    packet = packet[1:].strip()\n+                    try:\n+                        (_xx, baud, params) = packet.split()\n+                        baud = int(baud)\n+                        if params[0] in (b'7', b'8'):\n+                            databits = int(params[0])\n+                        else:\n+                            raise ValueError\n+                        if params[1] in (b'N', b'O', b'E'):\n+                            parity = params[1]\n+                        else:\n+                            raise ValueError\n+                        if params[2] in (b'1', b'2'):\n+                            stopbits = int(params[2])\n+                        else:\n+                            raise ValueError\n+                    except (ValueError, IndexError):\n+                        raise TestLoadError(\"bad serial-parameter spec in %s\" %\n+                                            self.name)\n+                    self.serial = (baud, databits, parity, stopbits)\n+                elif b\"Transport: UDP\" in packet:\n+                    self.sourcetype = \"UDP\"\n+                elif b\"Transport: TCP\" in packet:\n+                    self.sourcetype = \"TCP\"\n+                elif b\"Delay-Cookie:\" in packet:\n+                    if packet.startswith(b\"#\"):\n+                        packet = packet[1:]\n+                    try:\n+                        (_dummy, self.delimiter, delay) = \\\n+                            packet.strip().split()\n+                        self.delay = float(delay)\n+                    except ValueError:\n+                        raise TestLoadError(\"bad Delay-Cookie line in %s\" %\n+                                            self.name)\n+                    self.resplit = True\n+            else:\n+                if type_latch is None:\n+                    type_latch = ptype\n+                if self.predump:\n+                    print(repr(packet))\n+                if not packet:\n+                    raise TestLoadError(\"zero-length packet from %s\" %\n+                                        self.name)\n+                self.sentences.append(packet)\n+        # Look at the first packet to grok the GPS type\n+        self.textual = (type_latch == sniffer.NMEA_PACKET)\n+        if self.textual:\n+            self.legend = \"gpsfake: line %d: \"\n+        else:\n+            self.legend = \"gpsfake: packet %d\"\n+        # Maybe this needs to be split on different delimiters?\n+        if self.delimiter is not None:\n+            self.sentences = text[commentlen:].split(self.delimiter)\n+        # Do we want single-shot operation?\n+        if oneshot:\n+            self.sentences.append(b\"# EOF\\n\")\n+\n+\n+class PacketError(TestError):\n+    \"Class PacketError, empty\"\n+\n+\n+class FakeGPS(object):\n+    \"Class FakeGPS\"\n+    def __init__(self, testload, progress=lambda x: None):\n+        self.exhausted = 0\n+        self.go_predicate = lambda: True\n+        self.index = 0\n+        self.progress = progress\n+        self.readers = 0\n+        self.testload = testload\n+        self.progress(\"gpsfake: %s provides %d sentences\\n\"\n+                      % (self.testload.name, len(self.testload.sentences)))\n+\n+    def write(self, line):\n+        \"Throw an error if this superclass is ever instantiated.\"\n+        raise ValueError(line)\n+\n+    def feed(self):\n+        \"Feed a line from the contents of the GPS log to the daemon.\"\n+        line = self.testload.sentences[self.index\n+                                       % len(self.testload.sentences)]\n+        if b\"%Delay:\" in line:\n+            # Delay specified number of seconds\n+            delay = line.split()[1]\n+            time.sleep(int(delay))\n+        # self.write has to be set by the derived class\n+        self.write(line)\n+        time.sleep(self.testload.delay)\n+        self.index += 1\n+\n+\n+class FakePTY(FakeGPS):\n+    \"A FakePTY is a pty with a test log ready to be cycled to it.\"\n+\n+    def __init__(self, testload,\n+                 speed=4800, databits=8, parity='N', stopbits=1,\n+                 progress=lambda x: None):\n+        super(FakePTY, self).__init__(testload, progress)\n+        # Allow Serial: header to be overridden by explicit speed.\n+        if self.testload.serial:\n+            (speed, databits, parity, stopbits) = self.testload.serial\n+        self.speed = speed\n+        baudrates = {\n+            0: termios.B0,\n+            50: termios.B50,\n+            75: termios.B75,\n+            110: termios.B110,\n+            134: termios.B134,\n+            150: termios.B150,\n+            200: termios.B200,\n+            300: termios.B300,\n+            600: termios.B600,\n+            1200: termios.B1200,\n+            1800: termios.B1800,\n+            2400: termios.B2400,\n+            4800: termios.B4800,\n+            9600: termios.B9600,\n+            19200: termios.B19200,\n+            38400: termios.B38400,\n+            57600: termios.B57600,\n+            115200: termios.B115200,\n+            230400: termios.B230400,\n+        }\n+        (self.fd, self.slave_fd) = pty.openpty()\n+        self.byname = os.ttyname(self.slave_fd)\n+        os.chmod(self.byname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP |\n+                 stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH)\n+        (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(\n+            self.slave_fd)\n+        cc[termios.VMIN] = 1\n+        cflag &= ~(termios.PARENB | termios.PARODD | termios.CRTSCTS)\n+        cflag |= termios.CREAD | termios.CLOCAL\n+        iflag = oflag = lflag = 0\n+        iflag &= ~ (termios.PARMRK | termios.INPCK)\n+        cflag &= ~ (termios.CSIZE | termios.CSTOPB | termios.PARENB |\n+                    termios.PARODD)\n+        if databits == 7:\n+            cflag |= termios.CS7\n+        else:\n+            cflag |= termios.CS8\n+        if stopbits == 2:\n+            cflag |= termios.CSTOPB\n+        # Warning: attempting to set parity makes Fedora lose its cookies\n+        if parity == 'E':\n+            iflag |= termios.INPCK\n+            cflag |= termios.PARENB\n+        elif parity == 'O':\n+            iflag |= termios.INPCK\n+            cflag |= termios.PARENB | termios.PARODD\n+        ispeed = ospeed = baudrates[speed]\n+        try:\n+            termios.tcsetattr(self.slave_fd, termios.TCSANOW,\n+                              [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])\n+        except termios.error:\n+            raise TestLoadError(\"error attempting to set serial mode to %s \"\n+                                \" %s%s%s\"\n+                                % (speed, databits, parity, stopbits))\n+\n+    def read(self):\n+        \"Discard control strings written by gpsd.\"\n+        # A tcflush implementation works on Linux but fails on OpenBSD 4.\n+        termios.tcflush(self.fd, termios.TCIFLUSH)\n+        # Alas, the FIONREAD version also works on Linux and fails on OpenBSD.\n+        # try:\n+        #    buf = array.array('i', [0])\n+        #    fcntl.ioctl(self.master_fd, termios.FIONREAD, buf, True)\n+        #    n = struct.unpack('i', buf)[0]\n+        #    os.read(self.master_fd, n)\n+        # except IOError:\n+        #    pass\n+\n+    def write(self, line):\n+        self.progress(\"gpsfake: %s writes %d=%s\\n\"\n+                      % (self.testload.name, len(line), repr(line)))\n+        os.write(self.fd, line)\n+\n+    def drain(self):\n+        \"Wait for the associated device to drain (e.g. before closing).\"\n+        termios.tcdrain(self.fd)\n+\n+\n+def cleansocket(host, port, socktype=socket.SOCK_STREAM):\n+    \"Get a socket that we can re-use cleanly after it's closed.\"\n+    cs = socket.socket(socket.AF_INET, socktype)\n+    # This magic prevents \"Address already in use\" errors after\n+    # we release the socket.\n+    cs.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n+    cs.bind((host, port))\n+    return cs\n+\n+\n+def freeport(socktype=socket.SOCK_STREAM):\n+    \"\"\"Get a free port number for the given connection type.\n+\n+    This lets the OS assign a unique port, and then assumes\n+    that it will become available for reuse once the socket\n+    is closed, and remain so long enough for the real use.\n+    \"\"\"\n+    s = cleansocket(\"127.0.0.1\", 0, socktype)\n+    port = s.getsockname()[1]\n+    s.close()\n+    return port\n+\n+\n+class FakeTCP(FakeGPS):\n+    \"A TCP serverlet with a test log ready to be cycled to it.\"\n+\n+    def __init__(self, testload,\n+                 host, port,\n+                 progress=lambda x: None):\n+        super(FakeTCP, self).__init__(testload, progress)\n+        self.host = host\n+        self.dispatcher = cleansocket(self.host, int(port))\n+        # Get actual assigned port\n+        self.port = self.dispatcher.getsockname()[1]\n+        self.byname = \"tcp://\" + host + \":\" + str(self.port)\n+        self.dispatcher.listen(5)\n+        self.readables = [self.dispatcher]\n+\n+    def read(self):\n+        \"Handle connection requests and data.\"\n+        readable, _writable, _errored = select.select(self.readables, [], [],\n+                                                      0)\n+        for s in readable:\n+            if s == self.dispatcher:  # Connection request\n+                client_socket, _address = s.accept()\n+                self.readables = [client_socket]\n+                # Depending on timing, gpsd may try to reconnect between the\n+                # end of the log data and the remove_device.  With no listener,\n+                # this results in spurious error messages.  Keeping the\n+                # listener around avoids this.  It will eventually be closed\n+                # by the Python object cleanup. self.dispatcher.close()\n+            else:  # Incoming data\n+                data = s.recv(1024)\n+                if not data:\n+                    s.close()\n+                    self.readables.remove(s)\n+\n+    def write(self, line):\n+        \"Send the next log packet to everybody connected.\"\n+        self.progress(\"gpsfake: %s writes %d=%s\\n\"\n+                      % (self.testload.name, len(line), repr(line)))\n+        for s in self.readables:\n+            if s != self.dispatcher:\n+                s.send(line)\n+\n+    def drain(self):\n+        \"Wait for the associated device(s) to drain (e.g. before closing).\"\n+        for s in self.readables:\n+            if s != self.dispatcher:\n+                s.shutdown(socket.SHUT_RDWR)\n+\n+\n+class FakeUDP(FakeGPS):\n+    \"A UDP broadcaster with a test log ready to be cycled to it.\"\n+\n+    def __init__(self, testload,\n+                 ipaddr, port,\n+                 progress=lambda x: None):\n+        super(FakeUDP, self).__init__(testload, progress)\n+        self.byname = \"udp://\" + ipaddr + \":\" + str(port)\n+        self.ipaddr = ipaddr\n+        self.port = port\n+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n+\n+    def read(self):\n+        \"Discard control strings written by gpsd.\"\n+        return\n+\n+    def write(self, line):\n+        self.progress(\"gpsfake: %s writes %d=%s\\n\"\n+                      % (self.testload.name, len(line), repr(line)))\n+        self.sock.sendto(line, (self.ipaddr, int(self.port)))\n+\n+    def drain(self):\n+        \"Wait for the associated device to drain (e.g. before closing).\"\n+        # shutdown() fails on UDP\n+        return  # shutdown() fails on UDP\n+\n+\n+class SubprogramError(TestError):\n+    \"Class SubprogramError\"\n+    def __str__(self):\n+        return repr(self.msg)\n+\n+\n+class SubprogramInstance(object):\n+    \"Class for generic subprogram.\"\n+    ERROR = SubprogramError\n+\n+    def __init__(self):\n+        self.spawncmd = None\n+        self.process = None\n+        self.returncode = None\n+        self.env = None\n+\n+    def spawn_sub(self, program, options, background=False, prefix=\"\",\n+                  env=None):\n+        \"Spawn a subprogram instance.\"\n+        spawncmd = None\n+\n+        # Look for program in GPSD_HOME env variable\n+        if os.environ.get('GPSD_HOME'):\n+            for path in os.environ['GPSD_HOME'].split(':'):\n+                _spawncmd = \"%s/%s\" % (path, program)\n+                if os.path.isfile(_spawncmd) and os.access(_spawncmd, os.X_OK):\n+                    spawncmd = _spawncmd\n+                    break\n+\n+        # if we could not find it yet try PATH env variable for it\n+        if not spawncmd:\n+            if '/usr/sbin' not in os.environ['PATH']:\n+                os.environ['PATH'] = os.environ['PATH'] + \":/usr/sbin\"\n+            for path in os.environ['PATH'].split(':'):\n+                _spawncmd = \"%s/%s\" % (path, program)\n+                if os.path.isfile(_spawncmd) and os.access(_spawncmd, os.X_OK):\n+                    spawncmd = _spawncmd\n+                    break\n+\n+        if not spawncmd:\n+            raise self.ERROR(\"Cannot execute %s: executable not found. \"\n+                             \"Set GPSD_HOME env variable\" % program)\n+        self.spawncmd = [spawncmd] + options.split()\n+        if prefix:\n+            self.spawncmd = prefix.split() + self.spawncmd\n+        if env:\n+            self.env = os.environ.copy()\n+            self.env.update(env)\n+        self.process = subprocess.Popen(self.spawncmd, env=self.env)\n+        if not background:\n+            self.returncode = status = self.process.wait()\n+            if os.WIFSIGNALED(status) or os.WEXITSTATUS(status):\n+                raise self.ERROR(\"%s exited with status %d\"\n+                                 % (program, status))\n+\n+    def is_alive(self):\n+        \"Is the program still alive?\"\n+        if not self.process:\n+            return False\n+        self.returncode = self.process.poll()\n+        if self.returncode is None:\n+            return True\n+        self.process = None\n+        return False\n+\n+    def kill(self):\n+        \"Kill the program instance.\"\n+        while self.is_alive():\n+            try:  # terminate() may fail if already killed\n+                self.process.terminate()\n+            except OSError:\n+                continue\n+            time.sleep(0.01)\n+\n+\n+class DaemonError(SubprogramError):\n+    \"Class DaemonError\"\n+\n+\n+class DaemonInstance(SubprogramInstance):\n+    \"Control a gpsd instance.\"\n+    ERROR = DaemonError\n+\n+    def __init__(self, control_socket=None):\n+        self.sock = None\n+        super(DaemonInstance, self).__init__()\n+        if control_socket:\n+            self.control_socket = control_socket\n+        else:\n+            tmpdir = os.environ.get('TMPDIR', '/tmp')\n+            self.control_socket = \"%s/gpsfake-%d.sock\" % (tmpdir, os.getpid())\n+\n+    def spawn(self, options, port, background=False, prefix=\"\"):\n+        \"Spawn a daemon instance.\"\n+        # The -b option to suppress hanging on probe returns is needed to cope\n+        # with OpenBSD (and possibly other non-Linux systems) that don't\n+        # support anything we can use to implement the FakeGPS.read() method\n+        opts = (\" -b -N -S %s -F %s %s\"\n+                % (port, self.control_socket, options))\n+        # Derive a unique SHM key from the port # to avoid collisions.\n+        # Use 'Gp' as the prefix to avoid colliding with 'GPSD'.\n+        shmkey = '0x4770%.04X' % int(port)\n+        env = {'GPSD_SHM_KEY': shmkey}\n+        self.spawn_sub('gpsd', opts, background, prefix, env)\n+\n+    def wait_ready(self):\n+        \"Wait for the daemon to create the control socket.\"\n+        while self.is_alive():\n+            if os.path.exists(self.control_socket):\n+                return\n+            time.sleep(0.1)\n+\n+    def __get_control_socket(self):\n+        # Now we know it's running, get a connection to the control socket.\n+        if not os.path.exists(self.control_socket):\n+            return None\n+        try:\n+            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0)\n+            self.sock.connect(self.control_socket)\n+        except socket.error:\n+            if self.sock:\n+                self.sock.close()\n+            self.sock = None\n+        return self.sock\n+\n+    def add_device(self, path):\n+        \"Add a device to the daemon's internal search list.\"\n+        if self.__get_control_socket():\n+            self.sock.sendall(gps.polybytes(\"+%s\\r\\n\\x00\" % path))\n+            self.sock.recv(12)\n+            self.sock.close()\n+\n+    def remove_device(self, path):\n+        \"Remove a device from the daemon's internal search list.\"\n+        if self.__get_control_socket():\n+            self.sock.sendall(gps.polybytes(\"-%s\\r\\n\\x00\" % path))\n+            self.sock.recv(12)\n+            self.sock.close()\n+\n+\n+class TestSessionError(TestError):\n+    \"class TestSessionError\"\n+    # why does testSessionError() do nothing? \"\n+\n+\n+class TestSession(object):\n+    \"Manage a session including a daemon with fake GPSes and clients.\"\n+\n+    def __init__(self, prefix=None, port=None, options=None, verbose=0,\n+                 predump=False, udp=False, tcp=False, slow=False,\n+                 timeout=None):\n+        \"Initialize the test session by launching the daemon.\"\n+        self.prefix = prefix\n+        self.options = options\n+        self.verbose = verbose\n+        self.predump = predump\n+        self.udp = udp\n+        self.tcp = tcp\n+        self.slow = slow\n+        self.daemon = DaemonInstance()\n+        self.fakegpslist = {}\n+        self.client_id = 0\n+        self.readers = 0\n+        self.writers = 0\n+        self.runqueue = []\n+        self.index = 0\n+        if port:\n+            self.port = port\n+        else:\n+            self.port = freeport()\n+        self.progress = lambda x: None\n+        # for debugging\n+        # self.progress = lambda x: sys.stderr.write(\"# Hi \" + x)\n+        self.reporter = lambda x: None\n+        self.default_predicate = None\n+        self.fd_set = []\n+        self.threadlock = None\n+        self.timeout = TEST_TIMEOUT if timeout is None else timeout\n+\n+    def spawn(self):\n+        \"Spawn daemon\"\n+        for sig in (signal.SIGQUIT, signal.SIGINT, signal.SIGTERM):\n+            signal.signal(sig, lambda unused, dummy: self.cleanup())\n+        self.daemon.spawn(background=True, prefix=self.prefix, port=self.port,\n+                          options=self.options)\n+        self.daemon.wait_ready()\n+\n+    def set_predicate(self, pred):\n+        \"Set a default go predicate for the session.\"\n+        self.default_predicate = pred\n+\n+    def gps_add(self, logfile, speed=19200, pred=None, oneshot=False):\n+        \"Add a simulated GPS being fed by the specified logfile.\"\n+        self.progress(\"gpsfake: gps_add(%s, %d)\\n\" % (logfile, speed))\n+        if logfile not in self.fakegpslist:\n+            testload = TestLoad(logfile, predump=self.predump, slow=self.slow,\n+                                oneshot=oneshot)\n+            if testload.sourcetype == \"UDP\" or self.udp:\n+                newgps = FakeUDP(testload, ipaddr=\"127.0.0.1\",\n+                                 port=freeport(socket.SOCK_DGRAM),\n+                                 progress=self.progress)\n+            elif testload.sourcetype == \"TCP\" or self.tcp:\n+                # Let OS assign the port\n+                newgps = FakeTCP(testload, host=\"127.0.0.1\", port=0,\n+                                 progress=self.progress)\n+            else:\n+                newgps = FakePTY(testload, speed=speed,\n+                                 progress=self.progress)\n+            if pred:\n+                newgps.go_predicate = pred\n+            elif self.default_predicate:\n+                newgps.go_predicate = self.default_predicate\n+            self.fakegpslist[newgps.byname] = newgps\n+            self.append(newgps)\n+            newgps.exhausted = 0\n+        self.daemon.add_device(newgps.byname)\n+        return newgps.byname\n+\n+    def gps_remove(self, name):\n+        \"Remove a simulated GPS from the daemon's search list.\"\n+        self.progress(\"gpsfake: gps_remove(%s)\\n\" % name)\n+        self.fakegpslist[name].drain()\n+        self.remove(self.fakegpslist[name])\n+        self.daemon.remove_device(name)\n+        del self.fakegpslist[name]\n+\n+    def client_add(self, commands):\n+        \"Initiate a client session and force connection to a fake GPS.\"\n+        self.progress(\"gpsfake: client_add()\\n\")\n+        try:\n+            newclient = gps.gps(port=self.port, verbose=self.verbose)\n+        except socket.error:\n+            if not self.daemon.is_alive():\n+                raise TestSessionError(\"daemon died\")\n+            raise\n+        self.append(newclient)\n+        newclient.id = self.client_id + 1\n+        self.client_id += 1\n+        self.progress(\"gpsfake: client %d has %s\\n\"\n+                      % (self.client_id, newclient.device))\n+        if commands:\n+            self.initialize(newclient, commands)\n+        return self.client_id\n+\n+    def client_remove(self, cid):\n+        \"Terminate a client session.\"\n+        self.progress(\"gpsfake: client_remove(%d)\\n\" % cid)\n+        for obj in self.runqueue:\n+            if isinstance(obj, gps.gps) and obj.id == cid:\n+                self.remove(obj)\n+                return True\n+        return False\n+\n+    def wait(self, seconds):\n+        \"Wait, doing nothing.\"\n+        self.progress(\"gpsfake: wait(%d)\\n\" % seconds)\n+        time.sleep(seconds)\n+\n+    def gather(self, seconds):\n+        \"Wait, doing nothing but watching for sentences.\"\n+        self.progress(\"gpsfake: gather(%d)\\n\" % seconds)\n+        time.sleep(seconds)\n+\n+    def cleanup(self):\n+        \"We're done, kill the daemon.\"\n+        self.progress(\"gpsfake: cleanup()\\n\")\n+        if self.daemon:\n+            self.daemon.kill()\n+            self.daemon = None\n+\n+    def run(self):\n+        \"Run the tests.\"\n+        try:\n+            self.progress(\"gpsfake: test loop begins\\n\")\n+            while self.daemon:\n+                if not self.daemon.is_alive():\n+                    raise TestSessionError(\"daemon died\")\n+                # We have to read anything that gpsd might have tried\n+                # to send to the GPS here -- under OpenBSD the\n+                # TIOCDRAIN will hang, otherwise.\n+                for device in self.runqueue:\n+                    if isinstance(device, FakeGPS):\n+                        device.read()\n+                had_output = False\n+                chosen = self.choose()\n+                if isinstance(chosen, FakeGPS):\n+                    if (((chosen.exhausted and self.timeout and\n+                         (time.time() - chosen.exhausted > self.timeout) and\n+                         chosen.byname in self.fakegpslist))):\n+                        sys.stderr.write(\n+                            \"Test timed out: maybe increase WRITE_PAD (= %s)\\n\"\n+                            % GetDelay(self.slow))\n+                        raise SystemExit(1)\n+\n+                    if not chosen.go_predicate(chosen.index, chosen):\n+                        if chosen.exhausted == 0:\n+                            chosen.exhausted = time.time()\n+                            self.progress(\"gpsfake: GPS %s ran out of input\\n\"\n+                                          % chosen.byname)\n+                    else:\n+                        chosen.feed()\n+                elif isinstance(chosen, gps.gps):\n+                    if chosen.enqueued:\n+                        chosen.send(chosen.enqueued)\n+                        chosen.enqueued = \"\"\n+                    while chosen.waiting():\n+                        if not self.daemon or not self.daemon.is_alive():\n+                            raise TestSessionError(\"daemon died\")\n+                        ret = chosen.read()\n+                        if 0 > ret:\n+                            raise TestSessionError(\"daemon output stopped\")\n+                        # FIXME: test for 0 == ret.\n+                        had_output = True\n+                        if not chosen.valid & gps.PACKET_SET:\n+                            continue\n+                        self.reporter(chosen.bresponse)\n+                        if ((chosen.data[\"class\"] == \"DEVICE\" and\n+                             chosen.data[\"activated\"] == 0 and\n+                             chosen.data[\"path\"] in self.fakegpslist)):\n+                            self.gps_remove(chosen.data[\"path\"])\n+                            self.progress(\n+                                \"gpsfake: GPS %s removed (notification)\\n\"\n+                                % chosen.data[\"path\"])\n+                else:\n+                    raise TestSessionError(\"test object of unknown type\")\n+                if not self.writers and not had_output:\n+                    self.progress(\"gpsfake: no writers and no output\\n\")\n+                    break\n+            self.progress(\"gpsfake: test loop ends\\n\")\n+        finally:\n+            self.cleanup()\n+\n+    # All knowledge about locks and threading is below this line,\n+    # except for the bare fact that self.threadlock is set to None\n+    # in the class init method.\n+\n+    def append(self, obj):\n+        \"Add a producer or consumer to the object list.\"\n+        if self.threadlock:\n+            self.threadlock.acquire()\n+        self.runqueue.append(obj)\n+        if isinstance(obj, FakeGPS):\n+            self.writers += 1\n+        elif isinstance(obj, gps.gps):\n+            self.readers += 1\n+        if self.threadlock:\n+            self.threadlock.release()\n+\n+    def remove(self, obj):\n+        \"Remove a producer or consumer from the object list.\"\n+        if self.threadlock:\n+            self.threadlock.acquire()\n+        self.runqueue.remove(obj)\n+        if isinstance(obj, FakeGPS):\n+            self.writers -= 1\n+        elif isinstance(obj, gps.gps):\n+            self.readers -= 1\n+        self.index = min(len(self.runqueue) - 1, self.index)\n+        if self.threadlock:\n+            self.threadlock.release()\n+\n+    def choose(self):\n+        \"Atomically get the next object scheduled to do something.\"\n+        if self.threadlock:\n+            self.threadlock.acquire()\n+        chosen = self.index\n+        self.index += 1\n+        self.index %= len(self.runqueue)\n+        if self.threadlock:\n+            self.threadlock.release()\n+        return self.runqueue[chosen]\n+\n+    def initialize(self, client, commands):\n+        \"Arrange for client to ship specified commands when it goes active.\"\n+        client.enqueued = \"\"\n+        if not self.threadlock:\n+            client.send(commands)\n+        else:\n+            client.enqueued = commands\n+\n+    def start(self):\n+        \"Start thread\"\n+        self.threadlock = threading.Lock()\n+        threading.Thread(target=self.run)\n+\n+# End\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/gps.py b/tools/gps/gps.py\nnew file mode 100755\n--- /dev/null\n+++ b/tools/gps/gps.py\n@@ -0,0 +1,385 @@\n+#!/usr/bin/env python\n+# -*- coding: utf-8 -*-\n+# This code is generated by scons.  Do not hand-hack it!\n+'''gps.py -- Python interface to GPSD.\n+\n+This interface has a lot of historical cruft in it related to old\n+protocol, and was modeled on the C interface. It won't be thrown\n+away, but it's likely to be deprecated in favor of something more\n+Pythonic.\n+\n+The JSON parts of this (which will be reused by any new interface)\n+now live in a different module.\n+'''\n+\n+#\n+# This file is Copyright 2010 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-Clause\n+#\n+\n+# This code runs compatibly under Python 2 and 3.x for x >= 2.\n+# Preserve this property!\n+from __future__ import absolute_import, print_function, division\n+\n+from .client import *\n+from .watch_options import *\n+\n+\n+NaN = float('nan')\n+\n+\n+def isfinite(f):\n+    \"Check if f is finite\"\n+    # Python 2 does not think +Inf or -Inf are NaN\n+    # Python 2 has no easier way to test for Inf\n+    return float('-inf') < float(f) < float('inf')\n+\n+\n+# Don't hand-hack this list, it's generated.\n+ONLINE_SET = (1 << 1)\n+TIME_SET = (1 << 2)\n+TIMERR_SET = (1 << 3)\n+LATLON_SET = (1 << 4)\n+ALTITUDE_SET = (1 << 5)\n+SPEED_SET = (1 << 6)\n+TRACK_SET = (1 << 7)\n+CLIMB_SET = (1 << 8)\n+STATUS_SET = (1 << 9)\n+MODE_SET = (1 << 10)\n+DOP_SET = (1 << 11)\n+HERR_SET = (1 << 12)\n+VERR_SET = (1 << 13)\n+ATTITUDE_SET = (1 << 14)\n+SATELLITE_SET = (1 << 15)\n+SPEEDERR_SET = (1 << 16)\n+TRACKERR_SET = (1 << 17)\n+CLIMBERR_SET = (1 << 18)\n+DEVICE_SET = (1 << 19)\n+DEVICELIST_SET = (1 << 20)\n+DEVICEID_SET = (1 << 21)\n+RTCM2_SET = (1 << 22)\n+RTCM3_SET = (1 << 23)\n+AIS_SET = (1 << 24)\n+PACKET_SET = (1 << 25)\n+SUBFRAME_SET = (1 << 26)\n+GST_SET = (1 << 27)\n+VERSION_SET = (1 << 28)\n+POLICY_SET = (1 << 29)\n+LOGMESSAGE_SET = (1 << 30)\n+ERROR_SET = (1 << 31)\n+TIMEDRIFT_SET = (1 << 32)\n+EOF_SET = (1 << 33)\n+SET_HIGH_BIT = 34\n+UNION_SET = (RTCM2_SET | RTCM3_SET | SUBFRAME_SET | AIS_SET | VERSION_SET |\n+             DEVICELIST_SET | ERROR_SET | GST_SET)\n+STATUS_NO_FIX = 0\n+STATUS_FIX = 1\n+STATUS_DGPS_FIX = 2\n+STATUS_RTK_FIX = 3\n+STATUS_RTK_FLT = 4\n+STATUS_DR = 5\n+STATUS_GNSSDR = 6\n+STATUS_TIME = 7\n+STATUS_SIM = 8\n+STATUS_PPS_FIX = 9\n+MODE_NO_FIX = 1\n+MODE_2D = 2\n+MODE_3D = 3\n+MAXCHANNELS = 72  # Copied from gps.h, but not required to match\n+SIGNAL_STRENGTH_UNKNOWN = NaN\n+\n+\n+class gpsfix(object):\n+    \"Class to hold one GPS fix\"\n+\n+    def __init__(self):\n+        \"Init class gpsfix\"\n+\n+        self.altitude = NaN         # Meters DEPRECATED\n+        self.altHAE = NaN           # Meters\n+        self.altMSL = NaN           # Meters\n+        self.climb = NaN            # Meters per second\n+        self.datum = \"\"\n+        self.dgpsAge = -1\n+        self.dgpsSta = \"\"\n+        self.depth = NaN\n+        self.device = \"\"\n+        self.ecefx = NaN\n+        self.ecefy = NaN\n+        self.ecefz = NaN\n+        self.ecefvx = NaN\n+        self.ecefvy = NaN\n+        self.ecefvz = NaN\n+        self.ecefpAcc = NaN\n+        self.ecefvAcc = NaN\n+        self.epc = NaN\n+        self.epd = NaN\n+        self.eph = NaN\n+        self.eps = NaN\n+        self.ept = NaN\n+        self.epv = NaN\n+        self.epx = NaN\n+        self.epy = NaN\n+        self.geoidSep = NaN        # Meters\n+        self.latitude = self.longitude = 0.0\n+        self.magtrack = NaN\n+        self.magvar = NaN\n+        self.mode = MODE_NO_FIX\n+        self.relN = NaN\n+        self.relE = NaN\n+        self.relD = NaN\n+        self.sep = NaN              # a.k.a. epe\n+        self.speed = NaN            # Knots\n+        self.status = STATUS_NO_FIX\n+        self.time = NaN\n+        self.track = NaN            # Degrees from true north\n+        self.velN = NaN\n+        self.velE = NaN\n+        self.velD = NaN\n+\n+\n+class gpsdata(object):\n+    \"Position, track, velocity and status information returned by a GPS.\"\n+\n+    class satellite(object):\n+        \"Class to hold satellite data\"\n+        def __init__(self, PRN, elevation, azimuth, ss, used=None):\n+            self.PRN = PRN\n+            self.elevation = elevation\n+            self.azimuth = azimuth\n+            self.ss = ss\n+            self.used = used\n+\n+        def __repr__(self):\n+            return \"PRN: %3d  E: %3d  Az: %3d  Ss: %3d  Used: %s\" % (\n+                self.PRN, self.elevation, self.azimuth, self.ss,\n+                \"ny\"[self.used])\n+\n+    def __init__(self):\n+        # Initialize all data members\n+        self.online = 0                 # NZ if GPS on, zero if not\n+\n+        self.valid = 0\n+        self.fix = gpsfix()\n+\n+        self.status = STATUS_NO_FIX\n+        self.utc = \"\"\n+\n+        self.satellites_used = 0        # Satellites used in last fix\n+        self.xdop = self.ydop = self.vdop = self.tdop = 0\n+        self.pdop = self.hdop = self.gdop = 0.0\n+\n+        self.epe = 0.0\n+\n+        self.satellites = []            # satellite objects in view\n+\n+        self.gps_id = None\n+        self.driver_mode = 0\n+        self.baudrate = 0\n+        self.stopbits = 0\n+        self.cycle = 0\n+        self.mincycle = 0\n+        self.device = None\n+        self.devices = []\n+\n+        self.version = None\n+\n+    def __repr__(self):\n+        st = \"Time:     %s (%s)\\n\" % (self.utc, self.fix.time)\n+        st += \"Lat/Lon:  %f %f\\n\" % (self.fix.latitude, self.fix.longitude)\n+        if not isfinite(self.fix.altHAE):\n+            st += \"Altitude HAE: ?\\n\"\n+        else:\n+            st += \"Altitude HAE: %f\\n\" % (self.fix.altHAE)\n+        if not isfinite(self.fix.speed):\n+            st += \"Speed:    ?\\n\"\n+        else:\n+            st += \"Speed:    %f\\n\" % (self.fix.speed)\n+        if not isfinite(self.fix.track):\n+            st += \"Track:    ?\\n\"\n+        else:\n+            st += \"Track:    %f\\n\" % (self.fix.track)\n+        st += \"Status:   STATUS_%s\\n\" \\\n+              % (\"NO_FIX\", \"FIX\", \"DGPS_FIX\")[self.status]\n+        st += \"Mode:     MODE_%s\\n\" \\\n+              % (\"ZERO\", \"NO_FIX\", \"2D\", \"3D\")[self.fix.mode]\n+        st += \"Quality:  %d p=%2.2f h=%2.2f v=%2.2f t=%2.2f g=%2.2f\\n\" % \\\n+              (self.satellites_used, self.pdop, self.hdop, self.vdop,\n+               self.tdop, self.gdop)\n+        st += \"Y: %s satellites in view:\\n\" % len(self.satellites)\n+        for sat in self.satellites:\n+            st += \"    %r\\n\" % sat\n+        return st\n+\n+\n+class gps(gpscommon, gpsdata, gpsjson):\n+    \"Client interface to a running gpsd instance.\"\n+\n+    # module version, would be nice to automate the version\n+    __version__ = \"3.20.1~dev\"\n+\n+    def __init__(self, host=\"127.0.0.1\", port=GPSD_PORT, verbose=0, mode=0,\n+                 reconnect=False):\n+        self.activated = None\n+        self.clock_sec = NaN\n+        self.clock_nsec = NaN\n+        self.path = ''\n+        self.precision = 0\n+        self.real_sec = NaN\n+        self.real_nsec = NaN\n+        self.serialmode = \"8N1\"\n+        gpscommon.__init__(self, host, port, verbose, reconnect)\n+        gpsdata.__init__(self)\n+        gpsjson.__init__(self)\n+        if mode:\n+            self.stream(mode)\n+\n+    def _oldstyle_shim(self):\n+        # The rest is backwards compatibility for the old interface\n+        def default(k, dflt, vbit=0):\n+            \"Return default for key\"\n+            if k not in self.data.keys():\n+                return dflt\n+\n+            self.valid |= vbit\n+            return self.data[k]\n+\n+        if self.data.get(\"class\") == \"VERSION\":\n+            self.version = self.data\n+        elif self.data.get(\"class\") == \"DEVICE\":\n+            self.valid = ONLINE_SET | DEVICE_SET\n+            self.path = self.data[\"path\"]\n+            self.activated = default(\"activated\", None)\n+            driver = default(\"driver\", None, DEVICEID_SET)\n+            subtype = default(\"subtype\", None, DEVICEID_SET)\n+            self.gps_id = driver\n+            if subtype:\n+                self.gps_id += \" \" + subtype\n+            self.baudrate = default(\"bps\", 0)\n+            self.cycle = default(\"cycle\", NaN)\n+            self.driver_mode = default(\"native\", 0)\n+            self.mincycle = default(\"mincycle\", NaN)\n+            self.serialmode = default(\"serialmode\", \"8N1\")\n+        elif self.data.get(\"class\") == \"TPV\":\n+            self.device = default(\"device\", \"missing\")\n+            self.utc = default(\"time\", None, TIME_SET)\n+            self.valid = ONLINE_SET\n+            if self.utc is not None:\n+                # self.utc is always iso 8601 string\n+                # just copy to fix.time\n+                self.fix.time = self.utc\n+            self.fix.altitude = default(\"alt\", NaN, ALTITUDE_SET)  # DEPRECATED\n+            self.fix.altHAE = default(\"altHAE\", NaN, ALTITUDE_SET)\n+            self.fix.altMSL = default(\"altMSL\", NaN, ALTITUDE_SET)\n+            self.fix.climb = default(\"climb\", NaN, CLIMB_SET)\n+            self.fix.epc = default(\"epc\", NaN, CLIMBERR_SET)\n+            self.fix.epd = default(\"epd\", NaN)\n+            self.fix.eps = default(\"eps\", NaN, SPEEDERR_SET)\n+            self.fix.ept = default(\"ept\", NaN, TIMERR_SET)\n+            self.fix.epv = default(\"epv\", NaN, VERR_SET)\n+            self.fix.epx = default(\"epx\", NaN, HERR_SET)\n+            self.fix.epy = default(\"epy\", NaN, HERR_SET)\n+            self.fix.latitude = default(\"lat\", NaN, LATLON_SET)\n+            self.fix.longitude = default(\"lon\", NaN)\n+            self.fix.mode = default(\"mode\", 0, MODE_SET)\n+            self.fix.speed = default(\"speed\", NaN, SPEED_SET)\n+            self.fix.status = default(\"status\", 1)\n+            self.fix.track = default(\"track\", NaN, TRACK_SET)\n+        elif self.data.get(\"class\") == \"SKY\":\n+            self.device = default(\"device\", \"missing\")\n+            for attrp in (\"g\", \"h\", \"p\", \"t\", \"v\", \"x\", \"y\"):\n+                n = attrp + \"dop\"\n+                setattr(self, n, default(n, NaN, DOP_SET))\n+            if \"satellites\" in self.data.keys():\n+                self.satellites = []\n+                for sat in self.data['satellites']:\n+                    if 'el' not in sat:\n+                        sat['el'] = -999\n+                    if 'az' not in sat:\n+                        sat['az'] = -999\n+                    if 'ss' not in sat:\n+                        sat['ss'] = -999\n+                    self.satellites.append(gps.satellite(PRN=sat['PRN'],\n+                                           elevation=sat['el'],\n+                                           azimuth=sat['az'], ss=sat['ss'],\n+                                           used=sat['used']))\n+            self.satellites_used = 0\n+            for sat in self.satellites:\n+                if sat.used:\n+                    self.satellites_used += 1\n+            self.valid = ONLINE_SET | SATELLITE_SET\n+        elif self.data.get(\"class\") == \"PPS\":\n+            self.device = default(\"device\", \"missing\")\n+            self.real_sec = default(\"real_sec\", NaN)\n+            self.real_nsec = default(\"real_nsec\", NaN)\n+            self.clock_sec = default(\"clock_sec\", NaN)\n+            self.clock_nsec = default(\"clock_nsec\", NaN)\n+            self.precision = default(\"precision\", 0)\n+        # elif self.data.get(\"class\") == \"DEVICES\":\n+        # TODO: handle class DEVICES    # pylint: disable=fixme\n+\n+    def read(self):\n+        \"Read and interpret data from the daemon.\"\n+        status = gpscommon.read(self)\n+        if status <= 0:\n+            return status\n+        if self.response.startswith(\"{\") and self.response.endswith(\"}\\r\\n\"):\n+            self.unpack(self.response)\n+            self._oldstyle_shim()\n+            self.valid |= PACKET_SET\n+        return 0\n+\n+    def __next__(self):\n+        \"Python 3 version of next().\"\n+        if self.read() == -1:\n+            raise StopIteration\n+        if hasattr(self, \"data\"):\n+            return self.data\n+\n+        return self.response\n+\n+    def next(self):\n+        \"Python 2 backward compatibility.\"\n+        return self.__next__()\n+\n+    def stream(self, flags=0, devpath=None):\n+        \"Ask gpsd to stream reports at your client.\"\n+\n+        gpsjson.stream(self, flags, devpath)\n+\n+\n+def is_sbas(prn):\n+    \"Is this the NMEA ID of an SBAS satellite?\"\n+    return 120 <= prn <= 158\n+\n+\n+if __name__ == '__main__':\n+    import getopt\n+    import sys\n+    (options, arguments) = getopt.getopt(sys.argv[1:], \"v\")\n+    streaming = False\n+    verbose = False\n+    for (switch, val) in options:\n+        if switch == '-v':\n+            verbose = True\n+    if len(arguments) > 2:\n+        print('Usage: gps.py [-v] [host [port]]')\n+        sys.exit(1)\n+\n+    opts = {\"verbose\": verbose}\n+    if arguments:\n+        opts[\"host\"] = arguments[0]\n+    if arguments:\n+        opts[\"port\"] = arguments[1]\n+\n+    session = gps(**opts)\n+    session.stream(WATCH_ENABLE)\n+    try:\n+        for report in session:\n+            print(report)\n+    except KeyboardInterrupt:\n+        # Avoid garble on ^C\n+        print(\"\")\n+\n+# gps.py ends here\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/misc.py b/tools/gps/misc.py\nnew file mode 100644\n--- /dev/null\n+++ b/tools/gps/misc.py\n@@ -0,0 +1,294 @@\n+# misc.py - miscellaneous geodesy and time functions\n+\"miscellaneous geodesy and time functions\"\n+#\n+# This file is Copyright 2010 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-Clause\n+\n+# This code runs compatibly under Python 2 and 3.x for x >= 2.\n+# Preserve this property!\n+from __future__ import absolute_import, print_function, division\n+\n+import calendar\n+import io\n+import math\n+import time\n+\n+\n+def monotonic():\n+    \"\"\"return monotonic seconds, of unknown epoch.\n+    Python 2 to 3.7 has time.clock(), deprecates in 3.3+, removed in 3.8\n+    Python 3.5+ has time.monotonic()\n+    This always works\n+    \"\"\"\n+\n+    if hasattr(time, 'monotonic'):\n+        return time.monotonic()\n+    # else\n+    return time.clock()\n+\n+\n+# Determine a single class for testing \"stringness\"\n+try:\n+    STR_CLASS = basestring  # Base class for 'str' and 'unicode' in Python 2\n+except NameError:\n+    STR_CLASS = str         # In Python 3, 'str' is the base class\n+\n+# We need to be able to handle data which may be a mixture of text and binary\n+# data.  The text in this context is known to be limited to US-ASCII, so\n+# there aren't any issues regarding character sets, but we need to ensure\n+# that binary data is preserved.  In Python 2, this happens naturally with\n+# \"strings\" and the 'str' and 'bytes' types are synonyms.  But in Python 3,\n+# these are distinct types (with 'str' being based on Unicode), and conversions\n+# are encoding-sensitive.  The most straightforward encoding to use in this\n+# context is 'latin-1' (a.k.a.'iso-8859-1'), which directly maps all 256\n+# 8-bit character values to Unicode page 0.  Thus, if we can enforce the use\n+# of 'latin-1' encoding, we can preserve arbitrary binary data while correctly\n+# mapping any actual text to the proper characters.\n+\n+BINARY_ENCODING = 'latin-1'\n+\n+if bytes is str:  # In Python 2 these functions can be null transformations\n+\n+    polystr = str\n+    polybytes = bytes\n+\n+    def make_std_wrapper(stream):\n+        \"Dummy stdio wrapper function.\"\n+        return stream\n+\n+    def get_bytes_stream(stream):\n+        \"Dummy stdio bytes buffer function.\"\n+        return stream\n+\n+else:  # Otherwise we do something real\n+\n+    def polystr(o):\n+        \"Convert bytes or str to str with proper encoding.\"\n+        if isinstance(o, str):\n+            return o\n+        if isinstance(o, bytes) or isinstance(o, bytearray):\n+            return str(o, encoding=BINARY_ENCODING)\n+        if isinstance(o, int):\n+            return str(o)\n+        raise ValueError\n+\n+    def polybytes(o):\n+        \"Convert bytes or str to bytes with proper encoding.\"\n+        if isinstance(o, bytes):\n+            return o\n+        if isinstance(o, str):\n+            return bytes(o, encoding=BINARY_ENCODING)\n+        raise ValueError\n+\n+    def make_std_wrapper(stream):\n+        \"Standard input/output wrapper factory function\"\n+        # This ensures that the encoding of standard output and standard\n+        # error on Python 3 matches the binary encoding we use to turn\n+        # bytes to Unicode in polystr above.\n+        #\n+        # newline=\"\\n\" ensures that Python 3 won't mangle line breaks\n+        # line_buffering=True ensures that interactive command sessions\n+        # work as expected\n+        return io.TextIOWrapper(stream.buffer, encoding=BINARY_ENCODING,\n+                                newline=\"\\n\", line_buffering=True)\n+\n+    def get_bytes_stream(stream):\n+        \"Standard input/output bytes buffer function\"\n+        return stream.buffer\n+\n+\n+# some multipliers for interpreting GPS output\n+# Note: A Texas Foot is ( meters * 3937/1200)\n+#       (Texas Natural Resources Code, Subchapter D, Sec 21.071 - 79)\n+#       not the same as an international fooot.\n+FEET_TO_METERS = 0.3048                   # U.S./British feet to meters, exact\n+METERS_TO_FEET = (1 / FEET_TO_METERS)     # Meters to U.S./British feet, exact\n+MILES_TO_METERS = 1.609344                # Miles to meters, exact\n+METERS_TO_MILES = (1 / MILES_TO_METERS)   # Meters to miles, exact\n+FATHOMS_TO_METERS = 1.8288                # Fathoms to meters, exact\n+METERS_TO_FATHOMS = (1 / FATHOMS_TO_METERS)  # Meters to fathoms, exact\n+KNOTS_TO_MPH = (1852 / 1609.344)          # Knots to miles per hour, exact\n+KNOTS_TO_KPH = 1.852                      # Knots to kilometers per hour, exact\n+MPS_TO_KPH = 3.6                        # Meters per second to klicks/hr, exact\n+KNOTS_TO_MPS = (KNOTS_TO_KPH / MPS_TO_KPH)  # Knots to meters per second, exact\n+MPS_TO_MPH = (1 / 0.44704)             # Meters/second to miles per hour, exact\n+MPS_TO_KNOTS = (3600.0 / 1852.0)            # Meters per second to knots, exact\n+\n+\n+def Deg2Rad(x):\n+    \"Degrees to radians.\"\n+    return x * (math.pi / 180)\n+\n+\n+def Rad2Deg(x):\n+    \"Radians to degrees.\"\n+    return x * (180 / math.pi)\n+\n+\n+def CalcRad(lat):\n+    \"Radius of curvature in meters at specified latitude WGS-84.\"\n+    # the radius of curvature of an ellipsoidal Earth in the plane of a\n+    # meridian of latitude is given by\n+    #\n+    # R' = a * (1 - e^2) / (1 - e^2 * (sin(lat))^2)^(3/2)\n+    #\n+    # where\n+    #   a   is the equatorial radius (surface to center distance),\n+    #   b   is the polar radius (surface to center distance),\n+    #   e   is the first  eccentricity of the ellipsoid\n+    #   e2  is e^2  = (a^2 - b^2) / a^2\n+    #   es  is the second eccentricity of the ellipsoid (UNUSED)\n+    #   es2 is es^2 = (a^2 - b^2) / b^2\n+    #\n+    # for WGS-84:\n+    # a   = 6378.137 km (3963 mi)\n+    # b   = 6356.752314245 km (3950 mi)\n+    # e2  = 0.00669437999014132\n+    # es2 = 0.00673949674227643\n+    a = 6378.137\n+    e2 = 0.00669437999014132\n+    sc = math.sin(math.radians(lat))\n+    x = a * (1.0 - e2)\n+    z = 1.0 - e2 * pow(sc, 2)\n+    y = pow(z, 1.5)\n+    r = x / y\n+\n+    r = r * 1000.0      # Convert to meters\n+    return r\n+\n+\n+def EarthDistance(c1, c2):\n+    \"\"\"\n+    Vincenty's formula (inverse method) to calculate the distance (in\n+    kilometers or miles) between two points on the surface of a spheroid\n+    WGS 84 accurate to 1mm!\n+    \"\"\"\n+\n+    (lat1, lon1) = c1\n+    (lat2, lon2) = c2\n+\n+    # WGS 84\n+    a = 6378137  # meters\n+    f = 1 / 298.257223563\n+    b = 6356752.314245  # meters; b = (1 - f)a\n+\n+    # MILES_PER_KILOMETER = 1000.0 / (.3048 * 5280.0)\n+\n+    MAX_ITERATIONS = 200\n+    CONVERGENCE_THRESHOLD = 1e-12  # .000,000,000,001\n+\n+    # short-circuit coincident points\n+    if lat1 == lat2 and lon1 == lon2:\n+        return 0.0\n+\n+    U1 = math.atan((1 - f) * math.tan(math.radians(lat1)))\n+    U2 = math.atan((1 - f) * math.tan(math.radians(lat2)))\n+    L = math.radians(lon1 - lon2)\n+    Lambda = L\n+\n+    sinU1 = math.sin(U1)\n+    cosU1 = math.cos(U1)\n+    sinU2 = math.sin(U2)\n+    cosU2 = math.cos(U2)\n+\n+    for _ in range(MAX_ITERATIONS):\n+        sinLambda = math.sin(Lambda)\n+        cosLambda = math.cos(Lambda)\n+        sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 +\n+                             (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2)\n+        if sinSigma == 0:\n+            return 0.0  # coincident points\n+        cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda\n+        sigma = math.atan2(sinSigma, cosSigma)\n+        sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma\n+        cosSqAlpha = 1 - sinAlpha ** 2\n+        try:\n+            cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSqAlpha\n+        except ZeroDivisionError:\n+            cos2SigmaM = 0\n+        C = f / 16 * cosSqAlpha * (4 + f * (4 - 3 * cosSqAlpha))\n+        LambdaPrev = Lambda\n+        Lambda = L + (1 - C) * f * sinAlpha * (sigma + C * sinSigma *\n+                                               (cos2SigmaM + C * cosSigma *\n+                                                (-1 + 2 * cos2SigmaM ** 2)))\n+        if abs(Lambda - LambdaPrev) < CONVERGENCE_THRESHOLD:\n+            break  # successful convergence\n+    else:\n+        # failure to converge\n+        # fall back top EarthDistanceSmall\n+        return EarthDistanceSmall(c1, c2)\n+\n+    uSq = cosSqAlpha * (a ** 2 - b ** 2) / (b ** 2)\n+    A = 1 + uSq / 16384 * (4096 + uSq * (-768 + uSq * (320 - 175 * uSq)))\n+    B = uSq / 1024 * (256 + uSq * (-128 + uSq * (74 - 47 * uSq)))\n+    deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (\n+        cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM *\n+        (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)))\n+    s = b * A * (sigma - deltaSigma)\n+\n+    # return meters to 6 decimal places\n+    return round(s, 6)\n+\n+\n+def EarthDistanceSmall(c1, c2):\n+    \"Distance in meters between two close points specified in degrees.\"\n+    # This calculation is known as an Equirectangular Projection\n+    # fewer numeric issues for small angles that other methods\n+    # the main use here is for when Vincenty's fails to converge.\n+    (lat1, lon1) = c1\n+    (lat2, lon2) = c2\n+    avglat = (lat1 + lat2) / 2\n+    phi = math.radians(avglat)    # radians of avg latitude\n+    # meters per degree at this latitude, corrected for WGS84 ellipsoid\n+    # Note the wikipedia numbers are NOT ellipsoid corrected:\n+    # https://en.wikipedia.org/wiki/Decimal_degrees#Precision\n+    m_per_d = (111132.954 - 559.822 * math.cos(2 * phi) +\n+               1.175 * math.cos(4 * phi))\n+    dlat = (lat1 - lat2) * m_per_d\n+    dlon = (lon1 - lon2) * m_per_d * math.cos(phi)\n+\n+    dist = math.sqrt(math.pow(dlat, 2) + math.pow(dlon, 2))\n+    return dist\n+\n+\n+def MeterOffset(c1, c2):\n+    \"Return offset in meters of second arg from first.\"\n+    (lat1, lon1) = c1\n+    (lat2, lon2) = c2\n+    dx = EarthDistance((lat1, lon1), (lat1, lon2))\n+    dy = EarthDistance((lat1, lon1), (lat2, lon1))\n+    if lat1 < lat2:\n+        dy = -dy\n+    if lon1 < lon2:\n+        dx = -dx\n+    return (dx, dy)\n+\n+\n+def isotime(s):\n+    \"Convert timestamps in ISO8661 format to and from Unix time.\"\n+    if isinstance(s, int):\n+        return time.strftime(\"%Y-%m-%dT%H:%M:%S\", time.gmtime(s))\n+\n+    if isinstance(s, float):\n+        date = int(s)\n+        msec = s - date\n+        date = time.strftime(\"%Y-%m-%dT%H:%M:%S\", time.gmtime(s))\n+        return date + \".\" + repr(msec)[3:]\n+\n+    if isinstance(s, STR_CLASS):\n+        if s[-1] == \"Z\":\n+            s = s[:-1]\n+        if \".\" in s:\n+            (date, msec) = s.split(\".\")\n+        else:\n+            date = s\n+            msec = \"0\"\n+        # Note: no leap-second correction!\n+        return calendar.timegm(\n+            time.strptime(date, \"%Y-%m-%dT%H:%M:%S\")) + float(\"0.\" + msec)\n+\n+    # else:\n+    raise TypeError\n+\n+# End\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/packet.py b/tools/gps/packet.py\nnew file mode 100755\n--- /dev/null\n+++ b/tools/gps/packet.py\n@@ -0,0 +1,166 @@\n+#!/usr/bin/env python\n+#\n+# This code is generated by scons.  Do not hand-hack it!\n+#\n+# -*- coding: utf-8 -*-\n+#\n+# packet.py - helper functions for various bit\n+#\n+# theoretically comprised of reusable bits, in practice probably not so much.\n+#\n+# This code run compatibly under Python 2 and 3.x for x >= 2.\n+# Preserve this property!\n+#\n+# This file is Copyright 2019 by the GPSD project\n+# SPDX-License-Identifier: BSD-2-Clause\n+\"\"\"Python binding of the libgpsd module for recognizing GPS packets.\n+\n+The new() function returns a new packet-lexer instance.  Lexer instances\n+have two methods:\n+    get() takes a file descriptor argument and returns a tuple consisting of\n+the integer packet type and string packet value.  On end of file it returns\n+(-1, '').\n+    reset() resets the packet-lexer to its initial state.\n+    The module also has a register_report() function that accepts a callback\n+for debug message reporting.  The callback will get two arguments, the error\n+level of the message and the message itself.\n+\"\"\"\n+from __future__ import absolute_import, print_function\n+import ctypes\n+import gps.misc\n+import os\n+\n+\n+# Packet types and  Logging levels extracted from gpsd.h\n+MAX_PACKET_LENGTH = 9216\n+COMMENT_PACKET = 0\n+NMEA_PACKET = 1\n+AIVDM_PACKET = 2\n+GARMINTXT_PACKET = 3\n+SIRF_PACKET = 4\n+ZODIAC_PACKET = 5\n+TSIP_PACKET = 6\n+EVERMORE_PACKET = 7\n+ITALK_PACKET = 8\n+GARMIN_PACKET = 9\n+NAVCOM_PACKET = 10\n+UBX_PACKET = 11\n+SUPERSTAR2_PACKET = 12\n+ONCORE_PACKET = 13\n+GEOSTAR_PACKET = 14\n+NMEA2000_PACKET = 15\n+GREIS_PACKET = 16\n+MAX_GPSPACKET_TYPE = 16\n+RTCM2_PACKET = 17\n+RTCM3_PACKET = 18\n+JSON_PACKET = 19\n+PACKET_TYPES = 20\n+SKY_PACKET = 21\n+LOG_SHOUT = 0\n+LOG_WARN = 1\n+LOG_CLIENT = 2\n+LOG_INF = 3\n+LOG_PROG = 4\n+LOG_IO = 5\n+LOG_DATA = 6\n+LOG_SPIN = 7\n+LOG_RAW = 8\n+ISGPS_ERRLEVEL_BASE = LOG_RAW\n+\n+\n+class GpsdErrOutT(ctypes.Structure):\n+    pass\n+\n+\n+# Add raw interface to the packet FFI stub.\n+_packet = None\n+_cwd = os.getcwd()\n+if 'gpsd' in _cwd.split('/'):\n+    _path = _cwd + '/'\n+else:\n+    _path = '/usr/local/lib'\n+    if _path[-1] != '/':\n+        _path += '/'\n+\n+try:\n+    _packet = ctypes.CDLL('%slibgpsdpacket.so.27.0.0' % _path)\n+except OSError:\n+    print('Failed to load the library:\\t%slibgpsdpacket.so.27.0.0' % _path)\n+    exit(1)\n+\n+_lexer_size = ctypes.c_size_t.in_dll(_packet, \"fvi_size_lexer\")\n+LEXER_SIZE = _lexer_size.value\n+_buffer_size = ctypes.c_size_t.in_dll(_packet, \"fvi_size_buffer\").value\n+\n+REPORTER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_char_p)\n+GpsdErrOutT._fields_ = [('debug', ctypes.c_int),\n+                        ('report', REPORTER),\n+                        ('label', ctypes.c_char_p)]\n+\n+\n+class lexer_t(ctypes.Structure):\n+    _fields_ = [\n+        ('packet_type', ctypes.c_int),\n+        ('state', ctypes.c_uint),\n+        ('length', ctypes.c_size_t),\n+        ('inbuffer', ctypes.c_ubyte * _buffer_size),\n+        ('inbuflen', ctypes.c_size_t),\n+        ('inbufptr', ctypes.c_char_p),\n+        ('outbuffer', ctypes.c_ubyte * _buffer_size),\n+        ('outbuflen', ctypes.c_size_t),\n+        ('char_counter', ctypes.c_ulong),\n+        ('retry_counter', ctypes.c_ulong),\n+        ('counter', ctypes.c_uint),\n+        ('errout', GpsdErrOutT),\n+        ]\n+\n+\n+def new():\n+    \"\"\"new() -> new packet-self object\"\"\"\n+    return Lexer()\n+\n+\n+def register_report(reporter):\n+    \"\"\"register_report(callback)\n+\n+    callback must be a callable object expecting a string as parameter.\"\"\"\n+    global _loaded\n+    if callable(reporter):\n+        _loaded.errout.report = REPORTER(reporter)\n+\n+\n+class Lexer():\n+    \"\"\"GPS packet lexer object\n+\n+Fetch a single packet from file descriptor\n+\"\"\"\n+    pointer = None\n+\n+    def __init__(self):\n+        global _loaded\n+        _packet.ffi_Lexer_init.restype = ctypes.POINTER(lexer_t)\n+        self.pointer = _packet.ffi_Lexer_init()\n+        _loaded = self.pointer.contents\n+\n+    def get(self, file_handle):\n+        \"\"\"Get a packet from a file descriptor.\"\"\"\n+        global _loaded\n+        _packet.packet_get.restype = ctypes.c_int\n+        _packet.packet_get.argtypes = [ctypes.c_int, ctypes.POINTER(lexer_t)]\n+        length = _packet.packet_get(file_handle, self.pointer)\n+        _loaded = self.pointer.contents\n+        packet = ''\n+        for octet in range(_loaded.outbuflen):\n+            packet += chr(_loaded.outbuffer[octet])\n+        return [length,\n+                _loaded.packet_type,\n+                gps.misc.polybytes(packet),\n+                _loaded.char_counter]\n+\n+    def reset(self):\n+        \"\"\"Reset the packet self to ground state.\"\"\"\n+        _packet.ffi_Lexer_init.restype = None\n+        _packet.ffi_Lexer_init.argtypes = [ctypes.POINTER(lexer_t)]\n+        _packet.ffi_Lexer_init(self.pointer)\n+\n+# vim: set expandtab shiftwidth=4\ndiff --git a/tools/gps/watch_options.py b/tools/gps/watch_options.py\nnew file mode 100644\n--- /dev/null\n+++ b/tools/gps/watch_options.py\n@@ -0,0 +1,16 @@\n+\"WATCH options - controls what data is streamed, and how it's converted\"\n+WATCH_ENABLE = 0x000001        # enable streaming\n+WATCH_DISABLE = 0x000002       # disable watching\n+WATCH_JSON = 0x000010          # JSON output\n+WATCH_NMEA = 0x000020          # output in NMEA\n+WATCH_RARE = 0x000040          # output of packets in hex\n+WATCH_RAW = 0x000080           # output of raw packets\n+\n+WATCH_SCALED = 0x000100        # scale output to floats\n+WATCH_TIMING = 0x000200        # timing information\n+WATCH_DEVICE = 0x000800        # watch specific device\n+WATCH_SPLIT24 = 0x001000       # split AIS Type 24s\n+WATCH_PPS = 0x002000           # enable PPS JSON\n+\n+WATCH_NEWSTYLE = 0x010000      # force JSON streaming\n+WATCH_OLDSTYLE = 0x020000      # force old-style streaming\ndiff --git a/web_app/ConfigManager.py b/web_app/ConfigManager.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/ConfigManager.py\n@@ -0,0 +1,368 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+from reach_tools import reach_tools\n+\n+import os\n+from glob import glob\n+from shutil import copy, Error\n+\n+# This module aims to make working with RTKLIB configs easier\n+# It allows to parse RTKLIB .conf files to python dictionaries and backwards\n+# Note that on startup it reads on of the default configs\n+# and keeps the order of settings, stored there\n+\n+class Config:\n+\n+    def __init__(self, file_name = None, items = None):\n+        # we keep all the options for current config file and their order here\n+\n+        # config file name\n+        self.current_file_name = None\n+\n+        # due to how sending stuff over socket.io works, we need to keep config data\n+        # as a dictionary. to maintain order later in the browser we keep it in the\n+        # following form:\n+        # items = {\n+        #     \"0\": item0,\n+        #     \"1\": item1\n+        # }\n+        # where item is also a dictionary:\n+        # item0 = {\n+        #     \"parameter\": \"p\",\n+        #     \"value\": \"v\",\n+        #     \"comment\": \"c\",\n+        #     \"description\": \"d\"\n+        # }\n+\n+        if items is None:\n+            self.items = {}\n+        else:\n+            self.items = items\n+\n+        # if we pass the file to the constructor, then read the values\n+        if file_name is not None:\n+            self.readFromFile(file_name)\n+\n+    def formStringFromItem(self, item):\n+        # form a line to put into a RTKLIB config file:\n+\n+        # item must be a dict, described in the __init__ method comments!\n+\n+        # we want to write values aligned for easier reading\n+        # hence need to add a number of spaces after the parameter\n+        if item:\n+            parameter_with_trailing_spaces = item[\"parameter\"] + \" \" * (18 - len(item[\"parameter\"]))\n+\n+            s = [parameter_with_trailing_spaces, \"=\" + item[\"value\"]]\n+\n+            if \"comment\" in item:\n+                s.append(\"#\")\n+                s.append(item[\"comment\"])\n+\n+            if \"description\" in item:\n+                s.append(\"##\")\n+                s.append(item[\"description\"])\n+\n+            return \" \".join(s)\n+        else:\n+            return \"\"\n+\n+    def extractItemFromString(self, string):\n+        # extract information from a config file line\n+        # return true, if the line\n+\n+        # create an empty item\n+        item = {}\n+\n+        # cut the line into pieces by spaces\n+        separated_lines = string.split()\n+        length = len(separated_lines)\n+\n+        # first, check if this line is empty\n+        if length > 0:\n+\n+            # second, check if it's fully commented\n+            if separated_lines[0][0] != \"#\":\n+\n+                # extract the parameter and value\n+                item[\"parameter\"] = separated_lines[0]\n+                item[\"value\"] = separated_lines[1][1:]\n+\n+                # check if we have more info, possibly useful comment\n+                if length > 3 and separated_lines[2] == \"#\":\n+                    item[\"comment\"] = separated_lines[3]\n+\n+                    # check if we have more info, possibly description\n+                    if length > 5 and separated_lines[4] == \"##\":\n+                        # in order to have a description with spaces, we take all what's left\n+                        # after the \"##\" symbols and create a single line out of it:\n+                        description = separated_lines[5:]\n+                        description = \" \".join(description)\n+                        item[\"description\"] = description\n+\n+                # check if we have only a description, rather than a comment and description\n+                if length >3 and separated_lines[2] == \"##\":\n+                    description = separated_lines[3:]\n+                    description = \" \".join(description)\n+                    item[\"description\"] = description\n+\n+                # add information about available serial connections to input and output paths\n+                if \"path\" in item[\"parameter\"]:\n+                    item[\"comment\"] = self.formSelectCommentFromList(reach_tools.getAvailableSerialPorts())\n+\n+        # we return the item we managed to extract form from string. if it's empty,\n+        # then we could not parse the string, hence it's empty, commented, or invalid\n+        return item\n+\n+    def formSelectCommentFromList(self, items_list):\n+        comment = \"\"\n+\n+        if items_list:\n+            comment = \"(\"\n+\n+            for index, item in enumerate(items_list):\n+                comment += str(index) + \":\" + str(item) + \",\"\n+\n+            comment = comment[:-1] + \")\"\n+\n+        return comment\n+\n+    def parseBluetoothEntries(self, config_dict):\n+        # check if anything is set as a tcpsvr with path :8143\n+        # and change it to bluetooth\n+        entries_with_bt_port = {k: v for (k, v) in config_dict.items() if v[\"value\"] == \"localhost:8143\"}\n+\n+        for entry in entries_with_bt_port.keys():\n+            # can be log or out or in\n+            io_field = entries_with_bt_port[entry][\"parameter\"].split(\"-\")[0]\n+\n+            # find the corresponding io type\n+            io_type_entries = {k: v for (k, v) in config_dict.items() if io_field + \"-type\" in v[\"parameter\"]}\n+\n+            for key in io_type_entries.keys():\n+                config_dict[key][\"value\"] = \"bluetooth\"\n+\n+        return config_dict\n+\n+    def processConfig(self, config_dict):\n+        # sometimes, when reading we need to handle special situations, like bluetooth connection\n+\n+        config_dict = self.parseBluetoothEntries(config_dict)\n+\n+        return config_dict\n+\n+    def readFromFile(self, from_file):\n+\n+        # save file name as current\n+        self.current_file_name = from_file\n+\n+        # clear previous data\n+        self.items = {}\n+        items_dict = {}\n+\n+        # current item container\n+        item = {}\n+\n+        with open(from_file, \"r\") as f:\n+            i = 0\n+            for line in f:\n+                # we mine for info in every line of the file\n+                # if the info is valid, we add this item to the items dict\n+                item = self.extractItemFromString(line)\n+\n+                if item:\n+                    # save the info as {\"0\": item0, ...}\n+                    items_dict[str(i)] = item\n+\n+                    i += 1\n+\n+        self.items = self.processConfig(items_dict)\n+\n+    def writeToFile(self, to_file = None):\n+\n+        # by default, we write to the current file\n+        if to_file == None:\n+            to_file = self.current_file_name\n+\n+        # we keep the config as a dict, which is unordered\n+        # now is a time to convert it to a list, so that we could\n+        # write it to a file maintaining the order\n+\n+        # create an empty list of the same length as we have items\n+        items_list = [\"\"] * len(self.items)\n+\n+        # turn our dict with current items into a list in the correct order:\n+        for item_number in self.items:\n+            # some of the fields are not numbers and need to be treated separately\n+            try:\n+                int_item_number = int(item_number)\n+            except ValueError:\n+                pass\n+            else:\n+                items_list[int_item_number] = self.items[item_number]\n+\n+        with open(to_file, \"w\") as f:\n+            line = \"# rtkrcv options for rtk (v.2.4.2)\"\n+            f.write(line + \"\\n\\n\")\n+\n+            for item in items_list:\n+                f.write(self.formStringFromItem(item) + \"\\n\")\n+\n+class ConfigManager:\n+\n+    def __init__(self, rtklib_path, config_path):\n+\n+        self.config_path = config_path\n+\n+        self.default_rover_config = \"rtkbase_single_default.conf\"\n+        self.default_base_config = \"rtkbase_base_default.conf\"\n+\n+        self.available_configs = []\n+        self.updateAvailableConfigs()\n+\n+        # create a buffer for keeping config data\n+        # read default one into buffer\n+\n+        self.buffered_config = Config(os.path.join(self.config_path, self.default_rover_config))\n+\n+    def updateAvailableConfigs(self):\n+\n+        self.available_configs = []\n+\n+        # get a list of available .conf files in the config directory\n+        configs = glob(self.config_path + \"*.conf\")\n+        self.available_configs = [os.path.basename(config) for config in configs]\n+\n+\n+        # we do not show the base config\n+        try:\n+            self.available_configs.remove(self.default_base_config)\n+        except:\n+            pass\n+\n+    def readConfig(self, from_file):\n+\n+        if from_file is None:\n+            from_file = self.default_rover_config\n+\n+        # check if this is a full path or just a name\n+        # if it's a name, then we use the default location\n+        if \"/\" in from_file:\n+            config_file_path = from_file\n+        else:\n+            config_file_path = self.config_path + from_file\n+\n+        self.buffered_config.readFromFile(config_file_path)\n+\n+    def writeConfig(self, to_file = None, config_values = None):\n+\n+        if to_file is None:\n+            to_file = self.default_rover_config\n+\n+        # check if this is a full path or just a name\n+        # if it's a name, then we use the default location\n+        if \"/\" not in to_file:\n+            to_file = self.config_path + to_file\n+\n+        # do the actual writing\n+\n+        # if we receive config_values to write, then we create another config instance\n+        # and use write on it\n+        if config_values is None:\n+            self.buffered_config.writeToFile(to_file)\n+        else:\n+            conf = Config(items = config_values)\n+            conf.writeToFile(to_file)\n+\n+    def resetConfigToDefault(self, config_name):\n+        # try to copy default config to the working configs directory\n+        if \"/\" not in config_name:\n+            default_config_value = self.config_path + config_name\n+        else:\n+            default_config_value = config_name\n+\n+        try:\n+            copy(default_config_value, self.config_path)\n+        except IOError as e:\n+            print(\"Error resetting config \" + config_name + \" to default. Error: \" + e.filename + \" - \" + e.strerror)\n+        except OSError as e:\n+            print('Error: %s' % e)\n+\n+    def deleteConfig(self, config_name):\n+        # try to delete config if it exists\n+        if \"/\" not in config_name:\n+            config_name = self.config_path + config_name\n+\n+        try:\n+            os.remove(config_name)\n+        except OSError as e:\n+            print (\"Error: \" + e.filename + \" - \" + e.strerror)\n+\n+    def readItemFromConfig(self, property, from_file):\n+        # read a complete item from config, found by \"parameter part\"\n+\n+        conf = Config(self.config_path + from_file)\n+\n+        # cycle through this config to find the needed option\n+        for item_number in conf.items:\n+            if conf.items[item_number][\"parameter\"] == property:\n+                # in case we found it\n+                return conf.items[item_number]\n+\n+        # in case we didn't\n+        return None\n+\n+    def writeItemToConfig(self, item, to_file):\n+        # write a complete item to the file\n+\n+        # first we read the whole file\n+        conf = Config(self.config_path + to_file)\n+\n+        # then we substitute the one property\n+        # cycle through this config to find the needed option\n+        for item_number in conf.items:\n+            if conf.items[item_number][\"parameter\"] == item[\"parameter\"]:\n+                # in case we found it\n+                conf.items[item_number] = item\n+\n+                # rewrite the file again:\n+                self.writeConfig(to_file, conf.items)\n+                return 1\n+                break\n+\n+        # in case we didn't find it\n+        return None\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\ndiff --git a/web_app/LogManager.py b/web_app/LogManager.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/LogManager.py\n@@ -0,0 +1,179 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import os\n+import math\n+from glob import glob\n+\n+from log_converter import convbin\n+\n+class LogManager():\n+\n+    supported_solution_formats = [\"llh\", \"xyz\", \"enu\", \"nmea\", \"erb\", \"zip\", \"bz2\", \"tar\", \"tag\"]\n+\n+    def __init__(self, rtklib_path, log_path):\n+\n+        self.log_path = log_path\n+        self.convbin = convbin.Convbin(rtklib_path)\n+\n+        self.log_being_converted = \"\"\n+\n+        self.available_logs = []\n+        self.updateAvailableLogs()\n+\n+    def updateAvailableLogs(self):\n+\n+        self.available_logs = []\n+\n+        print(\"Getting a list of available logs\")\n+        for log in glob(self.log_path + \"/*\"):\n+        \n+            log_name = os.path.basename(log)\n+            # get size in bytes and convert to MB\n+            log_size = self.getLogSize(log)\n+\n+            potential_zip_path = os.path.splitext(log)[0] + \".zip\"\n+\n+            log_format = self.getLogFormat(log)\n+            is_being_converted = True if log == self.log_being_converted else False\n+\n+            self.available_logs.append({\n+                \"name\": log_name,\n+                \"size\": log_size,\n+                \"format\": log_format,\n+                \"is_being_converted\": is_being_converted\n+            })\n+\n+            self.available_logs.sort(key = lambda date: date['name'], reverse = True)\n+        \n+        #Adding an id to each log\n+        id = 0\n+        for log in self.available_logs:\n+            log['id'] = id\n+            id += 1\n+\n+        \n+    \"\"\"\n+    def getLogCompareString(self, log_name):\n+        name_without_extension = os.path.splitext(log_name)[0]\n+        print(\"log name: \", log_name)\n+        log_type, log_date = name_without_extension.split(\"_\")\n+        return log_date + log_type[0:2]\n+    \"\"\"\n+    def getLogSize(self, log_path):\n+        size = os.path.getsize(log_path) / (1024 * 1024.0)\n+        return \"{0:.2f}\".format(size)\n+\n+    def getLogFormat(self, log_path):\n+        file_path, extension = os.path.splitext(log_path)\n+        extension = extension[1:]\n+\n+        \"\"\"\n+        # removed because a zip file is not necesseraly RINEX\n+        potential_zip_path = file_path + \".zip\"\n+        if os.path.isfile(potential_zip_path):\n+            return \"RINEX\"\n+        \"\"\"\n+        if (extension in self.supported_solution_formats or\n+                    extension in self.convbin.supported_log_formats):\n+            return extension.upper()\n+        else:\n+            return \"\"\n+\n+    def formTimeString(self, seconds):\n+        # form a x minutes y seconds string from seconds\n+        m, s = divmod(seconds, 60)\n+\n+        s = math.ceil(s)\n+\n+        format_string = \"{0:.0f} minutes \" if m > 0 else \"\"\n+        format_string += \"{1:.0f} seconds\"\n+\n+        return format_string.format(m, s)\n+\n+    def calculateConversionTime(self, log_path):\n+        # calculate time to convert based on log size and format\n+        log_size = os.path.getsize(log_path) / (1024*1024.0)\n+        conversion_time = 0\n+\n+        if log_path.endswith(\"rtcm3\"):\n+            conversion_time = 42.0 * log_size\n+        elif log_path.endswith(\"ubx\"):\n+            conversion_time = 1.8 * log_size\n+\n+        return \"{:.0f}\".format(conversion_time)\n+\n+    def cleanLogFiles(self, log_path):\n+        # delete all files except for the raw log\n+        full_path_logs = glob(self.log_path + \"/*.rtcm3\") + glob(self.log_path + \"/*.ubx\")\n+        extensions_not_to_delete = [\".zip\", \".ubx\", \".rtcm3\"]\n+\n+        log_without_extension = os.path.splitext(log_path)[0]\n+        log_files = glob(log_without_extension + \"*\")\n+\n+        for log_file in log_files:\n+            if not any(log_file.endswith(ext) for ext in extensions_not_to_delete):\n+                try:\n+                    os.remove(log_file)\n+                except OSError as e:\n+                    print (\"Error: \" + e.filename + \" - \" + e.strerror)\n+\n+    def deleteLog(self, log_filename):\n+        # try to delete log if it exists\n+\n+        #log_name, extension = os.path.splitext(log_filename)\n+\n+        # try to delete raw log\n+        print(\"Deleting log \" + log_filename)\n+        try:\n+            os.remove(os.path.join(self.log_path, log_filename))\n+        except OSError as e:\n+            print (\"Error: \" + e.log_filename + \" - \" + e.strerror)\n+\n+        \"\"\"\n+        print(\"Deleting log \" + log_name + \".zip\")\n+        try:\n+            os.remove(self.log_path + \"/\" + log_name + \".zip\")\n+        except OSError as e:\n+            print (\"Error: \" + e.filename + \" - \" + e.strerror)\n+        \"\"\"\n+\n+    def getRINEXVersion(self):\n+        # read RINEX version from system file\n+        print(\"Getting RINEX version from system settings\")\n+        version = \"3.01\"\n+        try:\n+            with open(os.path.join(os.path.expanduser(\"~\"), \".reach/rinex_version\"), \"r\") as f:\n+                version = f.readline().rstrip(\"\\n\")\n+        except (IOError, OSError):\n+            print(\"No such file detected, defaulting to 3.01\")\n+\n+        return version\n+\n+    def setRINEXVersion(self, version):\n+        # write RINEX version to system file\n+        print(\"Writing new RINEX version to system file\")\n+\n+        with open(os.path.join(os.path.expanduser(\"~\"), \".reach/rinex_version\"), \"w\") as f:\n+            f.write(version)\n+\ndiff --git a/web_app/RTKBaseConfigManager.py b/web_app/RTKBaseConfigManager.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/RTKBaseConfigManager.py\n@@ -0,0 +1,179 @@\n+import os\n+from configparser import ConfigParser\n+from secrets import token_urlsafe\n+\n+class RTKBaseConfigManager:\n+    \"\"\" A class to easily access the settings from RTKBase settings.conf \"\"\"\n+\n+    NON_QUOTED_KEYS = (\"basedir\", \"web_authentification\", \"new_web_password\", \"web_password_hash\",\n+                     \"flask_secret_key\", \"archive_name\", \"user\")\n+\n+    def __init__(self, default_settings_path, user_settings_path):\n+        \"\"\" \n+            :param default_settings_path: path to the default settings file \n+            :param user_settings_path: path to the user settings file \n+        \"\"\"\n+        self.user_settings_path = user_settings_path\n+        self.config = self.merge_default_and_user(default_settings_path, user_settings_path)\n+        self.expand_path()\n+        self.write_file(self.config)\n+\n+    def merge_default_and_user(self, default, user):\n+        \"\"\"\n+            After a software update if there is some new entries in the default settings file,\n+            we need to add them to the user settings file. This function will do this: It loads\n+            the default settings then overwrite the existing values from the user settings. Then\n+            the function write these settings on disk.\n+\n+            :param default: path to the default settings file \n+            :param user: path to the user settings file\n+            :return: the new config object\n+        \"\"\"\n+        config = ConfigParser(interpolation=None)\n+        config.read(default)\n+        #if there is no existing user settings file, config.read return\n+        #an empty object.\n+        config.read(user)\n+        return config\n+\n+\n+    def parseconfig(self, settings_path):\n+        \"\"\"\n+            Parse the config file with interpolation=None or it will break run_cast.sh\n+        \"\"\"\n+        config = ConfigParser(interpolation=None)\n+        config.read(settings_path)\n+        return config\n+\n+    def expand_path(self):\n+        \"\"\"\n+            get the paths and convert $BASEDIR to the real path\n+        \"\"\"\n+        datadir = self.config.get(\"local_storage\", \"datadir\")\n+        if \"$BASEDIR\" in datadir:\n+            exp_datadir = os.path.abspath(os.path.join(os.path.dirname(__file__), \"../\", datadir.strip(\"$BASEDIR/\")))\n+            self.update_setting(\"local_storage\", \"datadir\", exp_datadir)\n+        \n+        logdir = self.config.get(\"log\", \"logdir\")\n+        if \"$BASEDIR\" in logdir:\n+            exp_logdir = os.path.abspath(os.path.join(os.path.dirname(__file__), \"../\", logdir.strip(\"$BASEDIR/\")))\n+            self.update_setting(\"log\", \"logdir\", exp_logdir)\n+\n+\n+\n+    def listvalues(self):\n+        \"\"\"\n+            print all keys/values from all sections in the settings\n+        \"\"\"\n+        for section in self.config.sections():\n+            print(\"SECTION: {}\".format(section))\n+            for key in self.config[section]:\n+                print(\"{} : {} \".format(key, self.config[section].get(key)))\n+\n+    def get_main_settings(self):\n+        \"\"\"\n+            Get a subset of the settings from the main section in an ordered object\n+            and remove the single quotes.      \n+        \"\"\"\n+        ordered_main = [{\"source_section\" : \"main\"}]\n+        for key in (\"position\", \"com_port\", \"com_port_settings\", \"receiver\", \"receiver_format\", \"tcp_port\"):\n+            ordered_main.append({key : self.config.get('main', key).strip(\"'\")})\n+        return ordered_main\n+\n+    def get_ntrip_settings(self):\n+        \"\"\"\n+            Get a subset of the settings from the ntrip section in an ordered object\n+            and remove the single quotes.    \n+        \"\"\"\n+        ordered_ntrip = [{\"source_section\" : \"ntrip\"}]\n+        for key in (\"svr_addr\", \"svr_port\", \"svr_pwd\", \"mnt_name\", \"rtcm_msg\"):\n+            ordered_ntrip.append({key : self.config.get('ntrip', key).strip(\"'\")})\n+        return ordered_ntrip\n+    \n+    def get_file_settings(self):\n+        \"\"\"\n+            Get a subset of the settings from the file section in an ordered object\n+            and remove the single quotes.       \n+        \"\"\"\n+        ordered_file = [{\"source_section\" : \"local_storage\"}]\n+        for key in (\"datadir\", \"file_name\", \"file_rotate_time\", \"file_overlap_time\", \"archive_rotate\"):\n+            ordered_file.append({key : self.config.get('local_storage', key).strip(\"'\")})\n+        return ordered_file\n+\n+    def get_rtcm_svr_settings(self):\n+        \"\"\"\n+            Get a subset of the settings from the file section in an ordered object\n+            and remove the single quotes.       \n+        \"\"\"\n+        ordered_rtcm_svr = [{\"source_section\" : \"rtcm_svr\"}]\n+        for key in (\"rtcm_svr_port\", \"rtcm_svr_msg\"):\n+            ordered_rtcm_svr.append({key : self.config.get('rtcm_svr', key).strip(\"'\")})\n+        return ordered_rtcm_svr\n+\n+    def get_ordered_settings(self):\n+        \"\"\"\n+            Get a subset of the main, ntrip and file sections from the settings file\n+            Return a dict where values are a list (to keeps the settings ordered)\n+        \"\"\"\n+        ordered_settings = {}\n+        ordered_settings['main'] = self.get_main_settings()\n+        ordered_settings['ntrip'] = self.get_ntrip_settings()\n+        ordered_settings['file'] = self.get_file_settings()\n+        ordered_settings['rtcm_svr'] = self.get_rtcm_svr_settings()\n+        return ordered_settings\n+\n+    def get_web_authentification(self):\n+        \"\"\"\n+            a simple method to convert the web_authentification value\n+            to a boolean\n+            :return boolean\n+        \"\"\"\n+        return self.config.getboolean(\"general\", \"web_authentification\")\n+\n+    def get_secret_key(self):\n+        \"\"\"\n+            Return the flask secret key, or generate a new one if it doesn't exists\n+        \"\"\"\n+        SECRET_KEY = self.config.get(\"general\", \"flask_secret_key\", fallback='None')\n+        if SECRET_KEY is 'None' or SECRET_KEY == '':\n+            SECRET_KEY = token_urlsafe(48)\n+            self.update_setting(\"general\", \"flask_secret_key\", SECRET_KEY)\n+        \n+        return SECRET_KEY\n+\n+    def get(self, *args, **kwargs):\n+        \"\"\"\n+            a wrapper around configparser.get()\n+        \"\"\"\n+        return self.config.get(*args, **kwargs)\n+\n+    def update_setting(self, section, setting, value, write_file=True):\n+        \"\"\"\n+            Update a setting in the config file and write the file (default)\n+            If the setting is not in the NON_QUOTED_KEYS list, the method will\n+            add single quotes\n+            :param section: the section in the config file\n+            :param setting: the setting (like a key in a dict)\n+            :param value: the new value for the setting\n+            :param write_file: write the file or not\n+        \"\"\"\n+        #Add single quotes around the value\n+        if setting not in self.NON_QUOTED_KEYS:\n+            value = \"'\" + value + \"'\"\n+        try:\n+            self.config[section][setting] = value\n+            if write_file:\n+                self.write_file()\n+        except Exception as e:\n+            print(e)\n+            return False\n+\n+    def write_file(self, settings=None):\n+        \"\"\"\n+            write on disk the settings to the config file\n+        \"\"\"\n+        if settings is None:\n+            settings = self.config\n+\n+        with open(self.user_settings_path, \"w\") as configfile:\n+            settings.write(configfile, space_around_delimiters=False)\ndiff --git a/web_app/RTKLIB.py b/web_app/RTKLIB.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/RTKLIB.py\n@@ -0,0 +1,704 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+from RtkController import RtkController\n+from ConfigManager import ConfigManager\n+from Str2StrController import Str2StrController\n+from LogManager import LogManager\n+#from ReachLED import ReachLED\n+from reach_tools import reach_tools, gps_time\n+\n+import json\n+import time\n+import os\n+import signal\n+import zipfile\n+\n+from subprocess import check_output, Popen, PIPE\n+from threading import Semaphore, Thread\n+\n+# master class for working with all RTKLIB programmes\n+# prevents them from stacking up and handles errors\n+# also handles all data broadcast through websockets\n+\n+class RTKLIB:\n+\n+    # we will save RTKLIB state here for later loading\n+    state_file = os.path.join(os.path.expanduser(\"~\"), \".reach/rtk_state\")\n+    # if the state file is not available, these settings are loaded\n+    default_state = {\n+        \"started\": \"no\",\n+        \"state\": \"base\"\n+    }\n+\n+    def __init__(self, socketio, rtklib_path = None, config_path=None, enable_led = True, log_path = None):\n+\n+        print(\"RTKLIB 1\")\n+        print(rtklib_path)\n+        print(log_path)\n+\n+        if rtklib_path is None:\n+            rtklib_path = os.path.join(os.path.expanduser(\"~\"), \"RTKLIB\")\n+        \n+        if config_path is None:\n+            self.config_path = os.path.join(os.path.dirname(__file__), \"rtklib_configs\")\n+        else:\n+            self.config_path = config_path\n+\n+        if log_path is None:\n+            #TODO find a better default location\n+            self.log_path = \"../data\"\n+        else:\n+            self.log_path = log_path\n+\n+        # This value should stay below the timeout value or the Satellite/Coordinate broadcast\n+        # thread will stop\n+        self.sleep_count = 0\n+        \n+        # default state for RTKLIB is \"rover single\"\n+        self.state = \"base\"\n+\n+        # we need this to broadcast stuff\n+        self.socketio = socketio\n+\n+        # these are necessary to handle rover mode\n+        self.rtkc = RtkController(rtklib_path, self.config_path)\n+        self.conm = ConfigManager(rtklib_path, self.config_path)\n+\n+        # this one handles base settings\n+        self.s2sc = Str2StrController(rtklib_path)\n+\n+        # take care of serving logs\n+        self.logm = LogManager(rtklib_path, self.log_path)\n+\n+        # basic synchronisation to prevent errors\n+        self.semaphore = Semaphore()\n+\n+        # we need this to send led signals\n+#        self.enable_led = enable_led\n+\n+#        if self.enable_led:\n+#            self.led = ReachLED()\n+\n+        # broadcast satellite levels and status with these\n+        self.server_not_interrupted = True\n+        self.satellite_thread = None\n+        self.coordinate_thread = None\n+        self.conversion_thread = None\n+\n+        self.system_time_correct = False\n+#        self.system_time_correct = True\n+\n+        self.time_thread = Thread(target = self.setCorrectTime)\n+        self.time_thread.start()\n+\n+        # we try to restore previous state\n+        # in case we can't, we start as rover in single mode\n+        # self.loadState()\n+\n+    def setCorrectTime(self):\n+        # determine if we have ntp service ready or we need gps time\n+\n+        print(\"RTKLIB 2 GPS time sync\")\n+\n+##        if not gps_time.time_synchronised_by_ntp():\n+            # wait for gps time\n+##            print(\"Time is not synced by NTP\")\n+#            self.updateLED(\"orange,off\")\n+#        gps_time.set_gps_time(\"/dev/ttyACM0\", 115200)\n+\n+        print(\"Time is synced by GPS!\")\n+\n+        self.system_time_correct = True\n+        self.socketio.emit(\"system time corrected\", {}, namespace=\"/test\")\n+\n+        self.loadState()\n+        self.socketio.emit(\"system state reloaded\", {}, namespace=\"/test\")\n+\n+    \n+    \n+    def launchBase(self):\n+        # due to the way str2str works, we can't really separate launch and start\n+        # all the configuration goes to startBase() function\n+        # this launchBase() function exists to change the state of RTKLIB instance\n+        # and to make the process for base and rover modes similar\n+\n+        self.semaphore.acquire()\n+\n+        self.state = \"base\"\n+\n+        #self.saveState()\n+\n+#        if self.enable_led:\n+#            self.updateLED()\n+\n+        print(\"RTKLIB 7 Base mode launched\")\n+\n+\n+        self.semaphore.release()\n+\n+    def shutdownBase(self):\n+        # due to the way str2str works, we can't really separate launch and start\n+        # all the configuration goes to startBase() function\n+        # this shutdownBase() function exists to change the state of RTKLIB instance\n+        # and to make the process for base and rover modes similar\n+\n+        self.stopBase()\n+\n+        self.semaphore.acquire()\n+\n+        self.state = \"inactive\"\n+\n+        \n+        print(\"RTKLIB 8 Base mode shutdown\")\n+\n+        self.semaphore.release()\n+\n+    def startBase(self, rtcm3_messages = None, base_position = None, gps_cmd_file = None):\n+\n+        self.semaphore.acquire()\n+        \"\"\"\n+        print(\"RTKLIB 9 Attempting to start str2str...\")\n+\n+        \n+        res = self.s2sc.start(rtcm3_messages, base_position, gps_cmd_file)\n+        if res < 0:\n+            print(\"str2str start failed\")\n+        elif res == 1:\n+            print(\"str2str start successful\")\n+        elif res == 2:\n+            print(\"str2str already started\")\n+        \n+        self.saveState()\n+        \"\"\"\n+        #TODO need refactoring\n+        #maybe a new method to launch/start rtkrcv outside\n+        #startBase and startRover\n+        #TODO launchRover and startRover send a config_name to rtkc\n+        #I don't do this here :-/\n+        print(\"RTKLIB 9a Attempting to launch rtkrcv...\")\n+\n+        res2 = self.rtkc.launch()\n+        \n+        if res2 < 0:\n+            print(\"rtkrcv launch failed\")\n+        elif res2 == 1:\n+            print(\"rtkrcv launch successful\")\n+        elif res2 == 2:\n+            print(\"rtkrcv already launched\")\n+        \n+        #TODO need refactoring\n+        #maybe a new method to launch/start rtkrcv outside\n+        #startBase and startRover\n+        print(\"RTKLIB 9b Attempting to start rtkrcv...\")\n+        res3 = self.rtkc.start()\n+\n+        if res3 == -1:\n+            print(\"rtkrcv start failed\")\n+        elif res3 == 1:\n+            print(\"rtkrcv start successful\")\n+            print(\"Starting coordinate and satellite broadcast\")\n+        elif res3 == 2:\n+            print(\"rtkrcv already started\")\n+\n+        # start fresh data broadcast\n+        #TODO the satellite and coordinate broadcast start\n+        #when rtkrcv start failed\n+\n+        self.server_not_interrupted = True\n+\n+        if self.satellite_thread is None:\n+            self.satellite_thread = Thread(target = self.broadcastSatellites)\n+            self.satellite_thread.start()\n+\n+        if self.coordinate_thread is None:\n+            self.coordinate_thread = Thread(target = self.broadcastCoordinates)\n+            self.coordinate_thread.start()\n+\n+        self.semaphore.release()\n+\n+        return res3\n+\n+    def stopBase(self):\n+\n+        self.semaphore.acquire()\n+        \n+\n+        print(\"RTKLIB 10a Attempting to stop rtkrcv...\")\n+\n+        res2 = self.rtkc.stop()\n+        if res2 == -1:\n+            print(\"rtkrcv stop failed\")\n+        elif res2 == 1:\n+            print(\"rtkrcv stop successful\")\n+        elif res2 == 2:\n+            print(\"rtkrcv already stopped\")\n+\n+        print(\"RTKLIB 10b Attempting to stop satellite broadcasting...\")\n+\n+        self.server_not_interrupted = False\n+\n+        if self.satellite_thread is not None:\n+            self.satellite_thread.join()\n+            self.satellite_thread = None\n+\n+        if self.coordinate_thread is not None:\n+            self.coordinate_thread.join()\n+            self.coordinate_thread = None\n+\n+        print(\"RTKLIB 10c Attempting rtkrcv shutdown\")\n+\n+        res = self.rtkc.shutdown()\n+\n+        if res < 0:\n+            print(\"rtkrcv shutdown failed\")\n+        elif res == 1:\n+            print(\"rtkrcv shutdown successful\")\n+            self.state = \"inactive\"\n+        elif res == 2:\n+            print(\"rtkrcv already shutdown\")\n+            self.state = \"inactive\"\n+        self.semaphore.release()\n+\n+        return res\n+\n+    def readConfigBase(self):\n+\n+        self.semaphore.acquire()\n+\n+        print(\"RTKLIB 11 Got signal to read base config\")\n+\n+        self.socketio.emit(\"current config base\", self.s2sc.readConfig(), namespace = \"/test\")\n+\n+        self.semaphore.release()\n+\n+    def writeConfigBase(self, config):\n+\n+        self.semaphore.acquire()\n+\n+        print(\"RTKLIB 12 Got signal to write base config\")\n+\n+        self.s2sc.writeConfig(config)\n+\n+        print(\"Restarting str2str...\")\n+\n+        res = self.s2sc.stop() + self.s2sc.start()\n+\n+        if res > 1:\n+            print(\"Restart successful\")\n+        else:\n+            print(\"Restart failed\")\n+\n+        self.saveState()\n+\n+#        if self.enable_led:\n+#            self.updateLED()\n+\n+        self.semaphore.release()\n+\n+        return res\n+\n+    def shutdown(self):\n+        # shutdown whatever mode we are in. stop broadcast threads\n+\n+        print(\"RTKLIB 17 Shutting down\")\n+\n+        # clean up broadcast and blink threads\n+        self.server_not_interrupted = False\n+#        self.led.blinker_not_interrupted = False\n+\n+        if self.coordinate_thread is not None:\n+            self.coordinate_thread.join()\n+\n+        if self.satellite_thread is not None:\n+            self.satellite_thread.join()\n+\n+#        if self.led.blinker_thread is not None:\n+#            self.led.blinker_thread.join()\n+\n+        # shutdown base\n+\n+        elif self.state == \"base\":\n+            return self.shutdownBase()\n+\n+        # otherwise, we are inactive\n+        return 1\n+\n+    def deleteConfig(self, config_name):\n+        # pass deleteConfig to conm\n+\n+        print(\"RTKLIB 18 Got signal to delete config \" + config_name)\n+\n+        self.conm.deleteConfig(config_name)\n+\n+        self.conm.updateAvailableConfigs()\n+\n+        # send available configs to the browser\n+        self.socketio.emit(\"available configs\", {\"available_configs\": self.conm.available_configs}, namespace=\"/test\")\n+\n+        print(self.conm.available_configs)\n+\n+    def cancelLogConversion(self, raw_log_path):\n+        if self.logm.log_being_converted:\n+            print(\"Canceling log conversion for \" + raw_log_path)\n+\n+            self.logm.convbin.child.kill(signal.SIGINT)\n+\n+            self.conversion_thread.join()\n+            self.logm.convbin.child.close(force = True)\n+\n+            print(\"Thread killed\")\n+            self.logm.cleanLogFiles(raw_log_path)\n+            self.logm.log_being_converted = \"\"\n+\n+            print(\"Canceled msg sent\")\n+\n+    def processLogPackage(self, raw_log_path):\n+\n+        currently_converting = False\n+\n+        try:\n+            print(\"conversion thread is alive \" + str(self.conversion_thread.isAlive()))\n+            currently_converting = self.conversion_thread.isAlive()\n+        except AttributeError:\n+            pass\n+\n+        log_filename = os.path.basename(raw_log_path)\n+        potential_zip_path = os.path.splitext(raw_log_path)[0] + \".zip\"\n+\n+        can_send_file = True\n+\n+        # can't send if there is no converted package and we are busy\n+        if (not os.path.isfile(potential_zip_path)) and (currently_converting):\n+            can_send_file = False\n+\n+        if can_send_file:\n+            print(\"Starting a new bg conversion thread for log \" + raw_log_path)\n+            self.logm.log_being_converted = raw_log_path\n+            self.conversion_thread = Thread(target = self.getRINEXPackage, args = (raw_log_path, ))\n+            self.conversion_thread.start()\n+        else:\n+            error_msg = {\n+                \"name\": os.path.basename(raw_log_path),\n+                \"conversion_status\": \"A log is being converted at the moment. Please wait\",\n+                \"messages_parsed\": \"\"\n+            }\n+            self.socketio.emit(\"log conversion failed\", error_msg, namespace=\"/test\")\n+\n+    def conversionIsRequired(self, raw_log_path):\n+        log_filename = os.path.basename(raw_log_path)\n+        potential_zip_path = os.path.splitext(raw_log_path)[0] + \".zip\"\n+\n+        print(\"Comparing \" + raw_log_path + \" and \" + potential_zip_path  + \" for conversion\")\n+\n+        if os.path.isfile(potential_zip_path):\n+            print(\"Raw logs differ \" + str(self.rawLogsDiffer(raw_log_path, potential_zip_path)))\n+            return self.rawLogsDiffer(raw_log_path, potential_zip_path)\n+        else:\n+            print(\"No zip file!!! Conversion required\")\n+            return True\n+\n+    def rawLogsDiffer(self, raw_log_path, zip_package_path):\n+        # check if the raw log is the same size in the zip and in filesystem\n+        log_name = os.path.basename(raw_log_path)\n+        raw_log_size = os.path.getsize(raw_log_path)\n+\n+        zip_package = zipfile.ZipFile(zip_package_path)\n+        raw_file_inside_info = zip_package.getinfo(\"Raw/\" + log_name)\n+        raw_file_inside_size = raw_file_inside_info.file_size\n+\n+        print(\"Sizes:\")\n+        print(\"Inside: \" + str(raw_file_inside_size))\n+        print(\"Raw:    \" + str(raw_log_size))\n+\n+        if raw_log_size == raw_file_inside_size:\n+            return False\n+        else:\n+            return True\n+\n+    def getRINEXPackage(self, raw_log_path):\n+        # if this is a solution log, return the file right away\n+        if \"sol\" in raw_log_path:\n+            log_url_tail = \"/logs/download/\" + os.path.basename(raw_log_path)\n+            self.socketio.emit(\"log download path\", {\"log_url_tail\": log_url_tail}, namespace=\"/test\")\n+            return raw_log_path\n+\n+        # return RINEX package if it already exists\n+        # create one if not\n+        log_filename = os.path.basename(raw_log_path)\n+        potential_zip_path = os.path.splitext(raw_log_path)[0] + \".zip\"\n+        result_path = \"\"\n+\n+        if self.conversionIsRequired(raw_log_path):\n+            print(\"Conversion is Required!\")\n+            result_path = self.createRINEXPackage(raw_log_path)\n+            # handle canceled conversion\n+            if result_path is None:\n+                log_url_tail = \"/logs/download/\" + os.path.basename(raw_log_path)\n+                self.socketio.emit(\"log download path\", {\"log_url_tail\": log_url_tail}, namespace=\"/test\")\n+                return None\n+        else:\n+            result_path = potential_zip_path\n+            print(\"Conversion is not Required!\")\n+            already_converted_package = {\n+                \"name\": log_filename,\n+                \"conversion_status\": \"Log already converted. Details inside the package\",\n+                \"messages_parsed\": \"\"\n+            }\n+            self.socketio.emit(\"log conversion results\", already_converted_package, namespace=\"/test\")\n+\n+        log_url_tail = \"/logs/download/\" + os.path.basename(result_path)\n+        self.socketio.emit(\"log download path\", {\"log_url_tail\": log_url_tail}, namespace=\"/test\")\n+\n+        self.cleanBusyMessages()\n+        self.logm.log_being_converted = \"\"\n+\n+        return result_path\n+\n+    def cleanBusyMessages(self):\n+        # if user tried to convert other logs during conversion, he got an error message\n+        # this function clears them to show it's ok to convert again\n+        self.socketio.emit(\"clean busy messages\", {}, namespace=\"/test\")\n+\n+    def createRINEXPackage(self, raw_log_path):\n+        # create a RINEX package before download\n+        # in case we fail to convert, return the raw log path back\n+        result = raw_log_path\n+        log_filename = os.path.basename(raw_log_path)\n+\n+        conversion_time_string = self.logm.calculateConversionTime(raw_log_path)\n+\n+        start_package = {\n+            \"name\": log_filename,\n+            \"conversion_time\": conversion_time_string\n+        }\n+\n+        conversion_result_package = {\n+            \"name\": log_filename,\n+            \"conversion_status\": \"\",\n+            \"messages_parsed\": \"\",\n+            \"log_url_tail\": \"\"\n+        }\n+\n+        self.socketio.emit(\"log conversion start\", start_package, namespace=\"/test\")\n+        try:\n+            log = self.logm.convbin.convertRTKLIBLogToRINEX(raw_log_path, self.logm.getRINEXVersion())\n+        except (ValueError, IndexError):\n+            print(\"Conversion canceled\")\n+            conversion_result_package[\"conversion_status\"] = \"Conversion canceled, downloading raw log\"\n+            self.socketio.emit(\"log conversion results\", conversion_result_package, namespace=\"/test\")\n+            return None\n+\n+        print(\"Log conversion done!\")\n+\n+        if log is not None:\n+            result = log.createLogPackage()\n+            if log.isValid():\n+                conversion_result_package[\"conversion_status\"] = \"Log converted to RINEX\"\n+                conversion_result_package[\"messages_parsed\"] = log.log_metadata.formValidMessagesString()\n+            else:\n+                conversion_result_package[\"conversion_status\"] = \"Conversion successful, but log does not contain any useful data. Downloading raw log\"\n+        else:\n+            print(\"Could not convert log. Is the extension wrong?\")\n+            conversion_result_package[\"conversion_status\"] = \"Log conversion failed. Downloading raw log\"\n+\n+        self.socketio.emit(\"log conversion results\", conversion_result_package, namespace=\"/test\")\n+\n+        print(\"Log conversion results:\")\n+        print(str(log))\n+\n+        return result\n+\n+    def saveState(self):\n+        # save current state for future resurrection:\n+        # state is a list of parameters:\n+        # rover state example: [\"rover\", \"started\", \"reach_single_default.conf\"]\n+        # base state example: [\"base\", \"stopped\", \"input_stream\", \"output_stream\"]\n+\n+        state = {}\n+\n+        # save \"rover\", \"base\" or \"inactive\" state\n+        state[\"state\"] = self.state\n+\n+        if self.state == \"rover\":\n+            started = self.rtkc.started\n+        elif self.state == \"base\":\n+            started = self.s2sc.started\n+        elif self.state == \"inactive\":\n+            started = False\n+\n+        state[\"started\"] = \"yes\" if started else \"no\"\n+\n+        # dump rover state\n+        state[\"rover\"] = {\"current_config\": self.rtkc.current_config}\n+\n+        # dump rover state\n+        state[\"base\"] = {\n+            \"input_stream\": self.s2sc.input_stream,\n+            \"output_stream\": self.s2sc.output_stream,\n+            \"rtcm3_messages\": self.s2sc.rtcm3_messages,\n+            \"base_position\": self.s2sc.base_position,\n+            \"gps_cmd_file\": self.s2sc.gps_cmd_file\n+        }\n+\n+        print(\"RTKLIB 20 DEBUG saving state\")\n+        print(str(state))\n+\n+        with open(self.state_file, \"w\") as f:\n+            json.dump(state, f, sort_keys = True, indent = 4)\n+\n+        reach_tools.run_command_safely([\"sync\"])\n+\n+        return state\n+\n+    def byteify(self, input):\n+        # thanks to Mark Amery from StackOverflow for this awesome function\n+        if isinstance(input, dict):\n+            return {self.byteify(key): self.byteify(value) for key, value in input.items()}\n+        elif isinstance(input, list):\n+            return [self.byteify(element) for element in input]\n+        elif isinstance(input, str):\n+            #no need to convert to utf-8 anymore with Python v3.x\n+            #return input.encode('utf-8')\n+            return input\n+        else:\n+            return input\n+\n+    def getState(self):\n+        # get the state, currently saved in the state file\n+        print(\"RTKLIB 21 Trying to read previously saved state...\")\n+\n+        try:\n+            f = open(self.state_file, \"r\")\n+        except IOError:\n+            # can't find the file, let's create a new one with default state\n+            print(\"Could not find existing state, Launching default mode...\")\n+\n+            return self.default_state\n+        else:\n+\n+            print(\"Found existing state...trying to decode...\")\n+\n+            try:\n+                json_state = json.load(f)\n+            except ValueError:\n+                # could not properly decode current state\n+                print(\"Could not decode json state. Launching default mode...\")\n+                f.close()\n+\n+                return self.default_state\n+            else:\n+                print(\"Decoding succesful\")\n+\n+                f.close()\n+\n+                # convert unicode strings to normal\n+                json_state = self.byteify(json_state)\n+\n+                print(\"That's what we found:\")\n+                print(str(json_state))\n+\n+                return json_state\n+\n+    def loadState(self):\n+\n+        # get current state\n+        json_state = self.getState()\n+\n+        print(\"RTKLIB 22 Now loading the state printed above... \")\n+        #print(str(json_state))\n+        # first, we restore the base state, because no matter what we end up doing,\n+        # we need to restore base state\n+\n+        if json_state[\"state\"] == \"base\":\n+            self.launchBase()\n+\n+            if json_state[\"started\"] == \"yes\":\n+                self.startBase()\n+\n+        print(str(json_state[\"state\"]) + \" state loaded\")\n+\n+    def sendState(self):\n+        # send current state to every connecting browser\n+\n+        state = self.getState()\n+        print(\"RTKLIB 22a\")\n+        #print(str(state))\n+        self.conm.updateAvailableConfigs()\n+        state[\"available_configs\"] = self.conm.available_configs\n+\n+        state[\"system_time_correct\"] = self.system_time_correct\n+        state[\"log_path\"] = str(self.log_path)\n+\n+        print(\"Available configs to send: \")\n+        print(str(state[\"available_configs\"]))\n+\n+        print(\"Full state: \")\n+        for key in state:\n+            print(\"{} : {}\".format(key, state[key]))\n+\n+        self.socketio.emit(\"current state\", state, namespace = \"/test\")\n+\n+\n+    # this function reads satellite levels from an existing rtkrcv instance\n+    # and emits them to the connected browser as messages\n+    def broadcastSatellites(self):\n+        count = 0\n+\n+        while self.server_not_interrupted:\n+\n+            # update satellite levels\n+            self.rtkc.getObs()\n+\n+#            if count % 10 == 0:\n+            #print(\"Sending sat rover levels:\\n\" + str(self.rtkc.obs_rover))\n+            #print(\"Sending sat base levels:\\n\" + str(self.rtkc.obs_base))\n+\n+            self.socketio.emit(\"satellite broadcast rover\", self.rtkc.obs_rover, namespace = \"/test\")\n+            #self.socketio.emit(\"satellite broadcast base\", self.rtkc.obs_base, namespace = \"/test\")\n+            count += 1\n+            self.sleep_count +=1\n+            time.sleep(1)\n+        #print(\"exiting satellite broadcast\")\n+\n+    # this function reads current rtklib status, coordinates and obs count\n+    def broadcastCoordinates(self):\n+        count = 0\n+\n+        while self.server_not_interrupted:\n+\n+            # update RTKLIB status\n+            self.rtkc.getStatus()\n+\n+#            if count % 10 == 0:\n+#                print(\"Sending RTKLIB status select information:\")\n+#                print(self.rtkc.status)\n+\n+            self.socketio.emit(\"coordinate broadcast\", self.rtkc.status, namespace = \"/test\")\n+\n+#            if self.enable_led:\n+#                self.updateLED()\n+\n+            count += 1\n+            time.sleep(1)\n+        #print(\"exiting coordinate broadcast\")\ndiff --git a/web_app/RtkController.py b/web_app/RtkController.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/RtkController.py\n@@ -0,0 +1,301 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import os\n+import time\n+import signal\n+import pexpect\n+from threading import Semaphore, Thread\n+\n+# This module automates working with RTKRCV directly\n+# You can get sat levels, current status, start and restart the software\n+\n+class RtkController:\n+\n+    def __init__(self, rtklib_path, config_path):\n+\n+        self.bin_path = rtklib_path\n+        self.config_path = config_path\n+\n+        self.child = 0\n+\n+        self.status = {}\n+        self.obs_rover = {}\n+        self.obs_base = {}\n+        self.info = {}\n+        self.semaphore = Semaphore()\n+\n+        self.started = False\n+        self.launched = False\n+        self.current_config = \"\"\n+\n+    def expectAnswer(self, last_command = \"\"):\n+        a = self.child.expect([\"rtkrcv>\", pexpect.EOF, \"error\"])\n+        # check rtklib output for any errors\n+        if a == 1:\n+            print(\"got EOF while waiting for rtkrcv> . Shutting down\")\n+            print(\"This means something went wrong and rtkrcv just stopped\")\n+            print(\"output before exception: \" + str(self.child))\n+            return -1\n+\n+        if a == 2:\n+            print(\"Could not \" + last_command + \". Please check path to binary or config name\")\n+            print(\"You may also check serial port for availability\")\n+            return -2\n+\n+        return 1\n+\n+    def launch(self, config_name = None):\n+        # run an rtkrcv instance with the specified config:\n+        # if there is a slash in the name we consider it a full location\n+        # otherwise, it's supposed to be in the upper directory(rtkrcv inside app)\n+\n+        if config_name is None:\n+            config_name = \"rtkbase_single_default.conf\"\n+\n+        if not self.launched:\n+\n+            self.semaphore.acquire()\n+\n+            if \"/\" in config_name:\n+                spawn_command = self.bin_path + \"/rtkrcv -o \" + config_name\n+            else:\n+                spawn_command = self.bin_path + \"/rtkrcv -o \" + os.path.join(self.config_path, config_name)\n+\n+            self.child = pexpect.spawn(spawn_command, cwd = self.bin_path, echo = False)\n+\n+            print('Launching rtklib with: \"' + spawn_command + '\"')\n+\n+            if self.expectAnswer(\"spawn\") < 0:\n+                self.semaphore.release()\n+                return -1\n+\n+            self.semaphore.release()\n+            self.launched = True\n+            self.current_config = config_name\n+\n+            # launch success\n+            return 1\n+\n+        # already launched\n+        return 2\n+\n+    def shutdown(self):\n+\n+        if self.launched:\n+            self.semaphore.acquire()\n+\n+            self.child.kill(signal.SIGUSR2)\n+\n+            # wait for rtkrcv to shutdown\n+            try:\n+                self.child.wait()\n+            except pexpect.ExceptionPexpect:\n+                print(\"Already dead!!\")\n+\n+            if self.child.isalive():\n+                r = -1\n+            else:\n+                r = 1\n+\n+            self.semaphore.release()\n+            self.launched = False\n+\n+            return r\n+\n+        # already shut down\n+        return 2\n+\n+\n+    def start(self):\n+\n+        if not self.started:\n+            self.semaphore.acquire()\n+\n+            self.child.send(\"start\\r\\n\")\n+\n+            if self.expectAnswer(\"start\") < 0:\n+                self.semaphore.release()\n+                return -1\n+\n+            self.semaphore.release()\n+            self.started = True\n+\n+            self.restart()\n+            print(\"Restart\")\n+            return 1\n+\n+        # already started\n+        return 2\n+\n+    def stop(self):\n+\n+        if self.started:\n+            self.semaphore.acquire()\n+\n+            self.child.send(\"stop\\r\\n\")\n+\n+            if self.expectAnswer(\"stop\") < 0:\n+                self.semaphore.release()\n+                return -1\n+\n+            self.semaphore.release()\n+\n+            self.started = False\n+\n+            return 1\n+\n+        # already stopped\n+        return 2\n+\n+    def restart(self):\n+\n+        if self.started:\n+            self.semaphore.acquire()\n+\n+            self.child.send(\"restart\\r\\n\")\n+\n+            if self.expectAnswer(\"restart\") < 0:\n+                self.semaphore.release()\n+                return -1\n+\n+            self.semaphore.release()\n+\n+            return 3\n+        else:\n+            # if we are not started yet, just start\n+            return self.start()\n+\n+    def loadConfig(self, config_name = \"rtk.conf\"):\n+\n+        self.semaphore.acquire()\n+\n+        if \"/\" not in config_name:\n+            # we assume this is not the full path\n+            # so it must be in the upper dir\n+            self.child.send(\"load \" + \"../\" + config_name + \"\\r\\n\")\n+        else:\n+            self.child.send(\"load \" + config_name + \"\\r\\n\")\n+\n+        if self.expectAnswer(\"load config\") < 0:\n+            self.semaphore.release()\n+            return -1\n+\n+        self.semaphore.release()\n+\n+        self.current_config = config_name\n+\n+        return 1\n+\n+    def getStatus(self):\n+\n+        self.semaphore.acquire()\n+\n+        self.child.send(\"status\\r\\n\")\n+\n+        if self.expectAnswer(\"get status\") < 0:\n+            self.semaphore.release()\n+            return -1\n+\n+        status = self.child.before.decode().split(\"\\r\\n\")\n+\n+        if status != {}:\n+            for line in status:\n+                spl = line.split(\":\", 1)\n+\n+                if len(spl) > 1:\n+\n+                    param = spl[0].strip()\n+                    value = spl[1].strip()\n+\n+                    self.status[param] = value\n+\n+        self.semaphore.release()\n+\n+        return 1\n+\n+    def getObs(self):\n+\n+        self.semaphore.acquire()\n+\n+        self.obs_rover = {}\n+        self.obs_base = {}\n+\n+        self.child.send(\"obs\\r\\n\")\n+\n+        if self.expectAnswer(\"get obs\") < 0:\n+            self.semaphore.release()\n+            return -1\n+\n+        obs = self.child.before.decode().split(\"\\r\\n\")\n+        obs = [_f for _f in obs if _f]\n+\n+        matching_strings = [s for s in obs if \"SAT\" in s]\n+\n+        if matching_strings != []:\n+            # find the header of the OBS table\n+            header_index = obs.index(matching_strings[0])\n+\n+            # split the header string into columns\n+            header = obs[header_index].split()\n+\n+            if \"S1\" in header:\n+                # find the indexes of the needed columns\n+                sat_name_index = header.index(\"SAT\")\n+                sat_level_index = header.index(\"S1\")\n+                sat_input_source_index = header.index(\"R\")\n+\n+                if len(obs) > (header_index + 1):\n+                    # we have some info about the actual satellites:\n+\n+                    self.obs_rover = {}\n+                    self.obs_base = {}\n+\n+                    for line in obs[header_index+1:]:\n+                        spl = line.split()\n+\n+                        if len(spl) > sat_level_index:\n+                            name = spl[sat_name_index]\n+                            level = spl[sat_level_index]\n+\n+                            # R parameter corresponds to the input source number\n+                            if spl[sat_input_source_index] == \"1\":\n+                                # we consider 1 to be rover,\n+                                self.obs_rover[name] = level\n+                            elif spl[sat_input_source_index] == \"2\":\n+                                # 2 to be base\n+                                self.obs_base[name] = level\n+\n+                else:\n+                    self.obs_base = {}\n+                    self.obs_rover = {}\n+\n+        self.semaphore.release()\n+\n+        return 1\n+\n+\n+\n+\n+\n+\ndiff --git a/web_app/ServiceController.py b/web_app/ServiceController.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/ServiceController.py\n@@ -0,0 +1,42 @@\n+import os\n+from pystemd.systemd1 import Unit\n+from pystemd.systemd1 import Manager\n+\n+class ServiceController(object):\n+    \"\"\"\n+        A simple wrapper around pystemd to manage systemd services\n+    \"\"\"\n+    \n+    manager = Manager(_autoload=True)\n+\n+    def __init__(self, unit):\n+        \"\"\"\n+            param: unit: a systemd unit name (ie str2str_tcp.service...)\n+        \"\"\"\n+        self.unit = Unit(bytes(unit, 'utf-8'), _autoload=True)\n+        \n+    def isActive(self):\n+        if self.unit.Unit.ActiveState == b'active':\n+            return True\n+        elif self.unit.Unit.ActiveState == b'activating':\n+            #TODO manage this transitionnal state differently\n+            return True\n+        else:\n+            return False\n+\n+    def getUser(self):\n+        return self.unit.Service.User.decode()\n+    \n+    def status(self):\n+        return (self.unit.Unit.SubState).decode()\n+\n+    def start(self):\n+        self.manager.Manager.EnableUnitFiles(self.unit.Unit.Names, False, True)\n+        return self.unit.Unit.Start(b'replace')\n+        \n+    def stop(self):\n+        self.manager.Manager.DisableUnitFiles(self.unit.Unit.Names, False)\n+        return self.unit.Unit.Stop(b'replace')\n+        \n+    def restart(self):\n+        return self.unit.Unit.Restart(b'replace')\n\\ No newline at end of file\ndiff --git a/web_app/Str2StrController.py b/web_app/Str2StrController.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/Str2StrController.py\n@@ -0,0 +1,300 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import os\n+import signal\n+import pexpect\n+from glob import glob\n+\n+from reach_tools import reach_tools\n+\n+# This module automates working with STR2STR software\n+\n+class Str2StrController:\n+\n+    def __init__(self, rtklib_path):\n+\n+        self.bin_path = rtklib_path\n+\n+        self.gps_cmd_file_path = rtklib_path + \"/app/rtkrcv\"\n+        self.gps_cmd_file = \"GPS_10Hz.cmd\"\n+\n+        self.child = 0\n+        self.started = False\n+\n+        # port settings are kept as class properties:\n+        self.input_stream = \"\"\n+        self.output_stream = \"\"\n+\n+        # Reach defaults for base position and rtcm3 messages:\n+        self.rtcm3_messages = [\"1002\", \"1006\", \"1008\", \"1010\", \"1019\", \"1020\"]\n+        self.base_position = [] # lat, lon, height\n+\n+        self.setSerialStream() # input ublox serial\n+        #self.setTCPClientStream()\n+        self.setTCPServerStream(input = False) # output tcp server on port 9000\n+\n+    def getAvailableReceiverCommandFiles(self):\n+        # returns a list of available cmd files in the working rtkrcv directory\n+        available_cmd_files = glob(self.gps_cmd_file_path + \"/\" +\"*.cmd\")\n+        available_cmd_files = [os.path.basename(cmd_file) for cmd_file in available_cmd_files]\n+\n+        return available_cmd_files\n+\n+    def formCommentString(self, options_list):\n+\n+        comment = \"(\"\n+\n+        for index, option in enumerate(options_list):\n+            comment += str(index) + \":\" + str(option)\n+\n+            if index < len(options_list) - 1:\n+                comment += \",\"\n+\n+        comment += \")\"\n+\n+        return comment\n+\n+    def readConfig(self):\n+        parameters_to_send = {}\n+\n+        parameters_to_send[\"0\"] = {\n+            \"parameter\": \"outstr-path\",\n+            \"value\": self.output_stream,\n+            \"comment\": self.formCommentString(reach_tools.getAvailableSerialPorts()),\n+            \"description\": \"Output path for corrections\"\n+        }\n+\n+        parameters_to_send[\"1\"] = {\"parameter\": \"rtcm3_out_messages\", \"value\": \",\".join(self.rtcm3_messages), \"description\": \"RTCM3 messages for output\"}\n+\n+        # if we don't have a set base position we want to send empty strings\n+        if not self.base_position:\n+            base_pos = [\"\", \"\", \"\"]\n+        else:\n+            base_pos = self.base_position\n+\n+        parameters_to_send[\"2\"] = {\"parameter\": \"base_pos_lat\", \"value\": base_pos[0], \"description\": \"Base latitude\"}\n+        parameters_to_send[\"3\"] = {\"parameter\": \"base_pos_lon\", \"value\": base_pos[1], \"description\": \"Base longitude\"}\n+        parameters_to_send[\"4\"] = {\"parameter\": \"base_pos_height\", \"value\": base_pos[2], \"description\": \"Base height\"}\n+\n+        parameters_to_send[\"5\"] = {\n+                \"parameter\": \"gps_cmd_file\",\n+                \"value\": self.gps_cmd_file,\n+                \"description\": \"Receiver configuration file\",\n+                \"comment\": self.formCommentString(self.getAvailableReceiverCommandFiles())\n+        }\n+\n+        print(\"DEBUG read\")\n+        print(parameters_to_send)\n+\n+        return parameters_to_send\n+\n+    def writeConfig(self, parameters_received):\n+\n+        print(\"DEBUG write\")\n+\n+        print(parameters_received)\n+\n+        coordinate_filled_flag = 3\n+        base_pos = []\n+\n+        self.output_stream = parameters_received[\"0\"][\"value\"]\n+\n+        # llh\n+        self.base_position = []\n+        self.base_position.append(parameters_received[\"2\"][\"value\"])\n+        self.base_position.append(parameters_received[\"3\"][\"value\"])\n+        self.base_position.append(parameters_received[\"4\"][\"value\"])\n+\n+        self.rtcm3_messages = parameters_received[\"1\"][\"value\"].split(\",\")\n+\n+        self.gps_cmd_file = parameters_received[\"5\"][\"value\"]\n+\n+    def setPort(self, port, input = True, format = \"ubx\"):\n+        if input:\n+            self.input_stream = port + \"#\" + format\n+        else:\n+            # str2str only supports rtcm3 for output\n+            self.output_stream = port + \"#\" + \"rtcm3\"\n+\n+    def setSerialStream(self, serial_parameters = None, input = True, format = \"ubx\"):\n+        # easier way to specify serial port for str2str\n+        # serial_parameters is a list of options for our serial device:\n+        # 1. serial port\n+        # 2. baudrate\n+        # 3. byte size\n+        # 4. parity bit\n+        # 5. stop bit\n+        # 6. fctr\n+        # default parameters here are Reach standards\n+\n+        def_parameters = [\n+            \"ttyACM0\",\n+            \"230400\",\n+            \"8\",\n+            \"n\",\n+            \"1\",\n+            \"off\"\n+        ]\n+\n+        if serial_parameters is None:\n+            serial_parameters = def_parameters\n+\n+        port = \"serial://\" + \":\".join(serial_parameters)\n+\n+        self.setPort(port, input, format)\n+\n+    def setTCPClientStream(self, tcp_client_parameters = None, input = True, format = \"ubx\"):\n+        # easier way to specify tcp connection parameters for str2str\n+        # tcp client parameters include:\n+        # 1. ip address\n+        # 2. port number\n+\n+        def_parameters = [\n+            \"localhost\",\n+            \"5015\"\n+        ]\n+\n+        if tcp_client_parameters is None:\n+            tcp_client_parameters = def_parameters\n+\n+        port = \"tcpcli://\" + \":\".join(tcp_server_parameters)\n+\n+        self.setPort(port, input, format)\n+\n+    def setTCPServerStream(self, tcp_server_parameters = None, input = True, format = \"ubx\"):\n+        # tcp server parameters only include the port number:\n+        # 1. port number\n+\n+        def_parameters = [\n+            \"9000\"\n+        ]\n+\n+        if tcp_server_parameters is None:\n+            tcp_server_parameters = def_parameters\n+\n+        port = \"tcpsvr://:\" + def_parameters[0]\n+\n+        self.setPort(port, input, format)\n+\n+    def setNTRIPClientStream(self, ntrip_client_parameters = None, input = True, format = \"ubx\"):\n+        # ntrip client parameters:\n+        # 1. user\n+        # 2. password\n+        # 3. address\n+        # 4. port\n+        # 5. mount point\n+\n+        port = \"ntrip://\" + ntrip_client_parameters[0] + \":\"\n+        port += ntrip_client_parameters[1] + \"@\" + ntrip_client_parameters[2] + \":\"\n+        port += ntrip_client_parameters[3] + \"/\" + ntrip_client_parameters[4]\n+\n+        self.setPort(port, input, format)\n+\n+    def setNTRIPServerStream(self, ntrip_server_parameters = None, input = True, format = \"ubx\"):\n+        # ntrip client parameters:\n+        # 1. password\n+        # 2. address\n+        # 3. port\n+        # 4. mount point\n+        # 5. str ???\n+\n+        port = \"ntrips://:\" + ntrip_client_parameters[0] + \"@\" + ntrip_client_parameters[1]\n+        port += \":\" + ntrip_client_parameters[2] + \"/\" + ntrip_client_parameters[3] + \":\"\n+        port += ntrip_client_parameters[4]\n+\n+        self.setPort(port, input, format)\n+\n+    def start(self, rtcm3_messages = None, base_position = None, gps_cmd_file = None):\n+        # when we start str2str we also have 3 important optional parameters\n+        # 1. rtcm3 message types. We have standard 1002, 1006, 1013, 1019 by default\n+        # 2. base position in llh. By default we don't pass any values, however it is best to use this feature\n+        # 3. gps cmd file will take care of msg frequency and msg types\n+        # To pass parameters to this function use string lists, like [\"1002\", \"1006\"] or [\"60\", \"30\", \"100\"]\n+\n+        print(self.bin_path)\n+\n+        if not self.started:\n+            if rtcm3_messages is None:\n+                rtcm3_messages = self.rtcm3_messages\n+\n+            if base_position is None:\n+                base_position = self.base_position\n+\n+            if gps_cmd_file is None:\n+                gps_cmd_file = self.gps_cmd_file\n+\n+            cmd = \"/str2str -in \" + self.input_stream + \" -out \" + self.output_stream + \" -msg \" + \",\".join(rtcm3_messages)\n+\n+            if \"\" in base_position:\n+                base_position = []\n+\n+            if base_position:\n+                cmd += \" -p \" + \" \".join(base_position)\n+\n+            if gps_cmd_file:\n+                cmd += \" -c \" + self.gps_cmd_file_path + \"/\" + gps_cmd_file\n+\n+            cmd = self.bin_path + cmd\n+            print(\"Starting str2str with\")\n+            print(cmd)\n+\n+            self.child = pexpect.spawn(cmd, cwd = self.bin_path, echo = False)\n+\n+            a = self.child.expect([\"stream server start\", pexpect.EOF, \"error\"])\n+            # check if we encountered any errors launching str2str\n+            if a == 1:\n+                print(\"got EOF while waiting for stream start. Shutting down\")\n+                print(\"This means something went wrong and str2str just stopped\")\n+                print(\"output before exception: \" + str(self.child))\n+                return -1\n+\n+            if a == 2:\n+                print(\"Could not start str2str. Please check path to binary or parameters, like serial port\")\n+                print(\"You may also check serial, tcp, ntrip ports for availability\")\n+                return -2\n+\n+            # if we are here, everything is good\n+            self.started = True\n+            return 1\n+\n+        # str2str already started\n+        return 2\n+\n+    def stop(self):\n+        # terminate the stream\n+\n+        if self.started:\n+            self.child.kill(signal.SIGUSR2)\n+            try:\n+                self.child.wait()\n+            except pexpect.ExceptionPexpect:\n+                print(\"Str2str already down\")\n+\n+            self.started = False\n+            return 1\n+\n+        # str2str already stopped\n+        return 2\n+\n+\ndiff --git a/web_app/log_converter/convbin.py b/web_app/log_converter/convbin.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/log_converter/convbin.py\n@@ -0,0 +1,118 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import pexpect\n+from .logs import Log, LogMetadata\n+\n+class Convbin:\n+\n+    supported_log_formats = [\"rtcm2\", \"rtcm3\", \"nov\", \"oem3\", \"ubx\", \"ss2\", \"hemis\", \"stq\", \"javad\", \"nvs\", \"binex\", \"rinex\"]\n+\n+    def __init__(self, rtklib_path):\n+        self.bin_path = rtklib_path\n+        self.child = 0\n+\n+    def convertRTKLIBLogToRINEX(self, log_path, rinex_version=\"3.01\"):\n+\n+        print(\"Converting log \" + log_path + \"...\")\n+\n+        result = None\n+\n+        # check if extension format is in the list of the supported ones\n+        log_format = [f for f in self.supported_log_formats if log_path.endswith(f)]\n+\n+        if log_format:\n+            try:\n+                log_metadata = self.convertLogToRINEX(log_path, log_format[0], rinex_version)\n+            except ValueError:\n+                return None\n+\n+            if log_metadata:\n+                result = Log(log_path, log_metadata)\n+\n+        return result\n+\n+    def convertLogToRINEX(self, log_path, format, rinex_version):\n+\n+        spawn_command = \" \".join([\n+            self.bin_path + \"/convbin\",\n+            \"-r\",\n+            format,\n+            \"-v\",\n+            rinex_version,\n+            \"-od\",\n+            \"-os\",\n+            \"-oi\",\n+            \"-ot\",\n+            \"-ol\",\n+            log_path\n+        ])\n+\n+        print(\"Specified format is \" + format)\n+\n+        print(\"Spawning convbin with \" + spawn_command)\n+        self.child = pexpect.spawn(spawn_command, echo = False)\n+        print(\"Process spawned!\")\n+        self.child.expect(pexpect.EOF, timeout = None)\n+\n+        if self.child.exitstatus != 0 and self.child.signalstatus == None:\n+            print(\"Convbin killed by external signal\")\n+            raise ValueError\n+\n+        print(\"Conversion process finished correctly\")\n+        return self.parseConvbinOutput(self.child.before)\n+\n+    def parseConvbinOutput(self, output):\n+\n+        result_string = self.extractResultingString(output)\n+\n+        if self.resultStringIsValid(result_string):\n+            return LogMetadata(result_string)\n+        else:\n+            return None\n+\n+    def resultStringIsValid(self, result_string):\n+        return True if len(result_string) > 21 else False\n+\n+    def extractResultingString(self, output):\n+        # get the last line of the convbin output\n+\n+        last_line_end = output.rfind(\"\\r\\r\\n\")\n+        last_line_start = output.rfind(\"\\r\", 0, last_line_end) + 1\n+\n+        return output[last_line_start:last_line_end]\n+\n+\n+if __name__ == \"__main__\":\n+    cb = Convbin(\"/home/reach/RTKLIB\")\n+    rlog = cb.convertRTKLIBLogToRINEX(\"/home/reach/logs/rov_201601210734.ubx\")\n+    print(rlog)\n+    # print(\"base\")\n+    # blog = cb.convertRTKLIBLogToRINEX(\"/home/egor/RTK/convbin_test/ref_201601080935.rtcm3\")\n+    # print(blog)\n+    # print(\"Kinelog\")\n+    # kinelog = KinematicLog(rlog, blog)\n+    # print(kinelog)\n+    # kinelog.createKinematicLogPackage(\"lol.zip\")\n+    # rlog.createLogPackage(\"lol1.zip\")\n+\ndiff --git a/web_app/log_converter/logs.py b/web_app/log_converter/logs.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/log_converter/logs.py\n@@ -0,0 +1,239 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import glob\n+import zipfile\n+import os\n+\n+class LogMetadata:\n+\n+    message_names = {\n+        \"OBS\": \"Obs\",\n+        \"NAV\": \"GPS nav\",\n+        \"GNAV\": \"GLONASS nav\",\n+        \"HNAV\": \"GEO nav\",\n+        \"QNAV\": \"QZSS nav\",\n+        \"LNAV\": \"Galileo nav\",\n+        \"SBAS\": \"SBAS log\",\n+        \"Errors\": \"Errors\",\n+        \"TM\": \"Time marks\"\n+    }\n+\n+    def __init__(self, convbin_output):\n+\n+        self.start_timestamp = 0\n+        self.stop_timestamp = 0\n+        self.navigation_messages = {msg_type: 0 for msg_type in list(self.message_names.keys())}\n+\n+        self.extractDataFromString(convbin_output)\n+\n+    def __str__(self):\n+        to_print = \"Log start time: \" + self.formatTimestamp(self.start_timestamp) + \"\\n\"\n+        to_print += \"Log stop time:  \" + self.formatTimestamp(self.stop_timestamp) + \"\\n\"\n+        to_print += \"Navigation messages parsed:\\n\"\n+        to_print += self.formValidMessagesString()\n+\n+        return to_print\n+\n+    def formatTimestamp(self, timestamp):\n+        # 19800106000000\n+        timestamp = str(timestamp)\n+\n+        # date\n+        human_readable_timestamp = timestamp[:4] + \"-\" + timestamp[4:6] + \"-\" + timestamp[6:8]\n+        # time\n+        human_readable_timestamp += \" \" + timestamp[8:10]\n+        human_readable_timestamp += \":\" + timestamp[10:12]\n+        human_readable_timestamp += \":\" + timestamp[12:14]\n+\n+        return human_readable_timestamp\n+\n+    def countValidMessages(self):\n+\n+        valid_messages = 0\n+\n+        for msg_type, msg_count in list(self.navigation_messages.items()):\n+            if msg_type is not \"Errors\":\n+                valid_messages += int(msg_count)\n+\n+        return valid_messages\n+\n+    def formValidMessagesString(self):\n+\n+        correct_order = list(self.message_names.keys())\n+\n+        to_print = \"Messages inside: \"\n+\n+        for msg in correct_order:\n+            msg_type = msg\n+            msg_count = self.navigation_messages[msg_type]\n+            if int(msg_count) > 0:\n+                to_print += self.message_names[msg_type] + \": \" + msg_count + \", \"\n+\n+        return to_print[:-2]\n+\n+    def extractDataFromString(self, data_string):\n+        # example string:\n+        # 2016/01/08 09:35:02-01/08 11:24:58: O=32977 N=31 G=41 E=2\n+\n+        data_list = data_string.split(\" \")\n+        data_list = [_f for _f in data_list if _f]\n+\n+        # first 3 parts mark the time properties\n+        # the next elemets show message counts\n+\n+        self.extractTimeDataFromString(data_list[:3])\n+        self.extractMessageCountFromString(data_list[3:])\n+\n+    def extractTimeDataFromString(self, data_list):\n+        # example string(split into a list by spaces)\n+        # 2016/01/08 09:35:02-01/08 11:24:58:\n+\n+        # remove all the extra punctuation\n+        raw_data = \"\".join(data_list)\n+        raw_data = raw_data.translate(None, \"/:\\r\")\n+\n+        print(\"Raw data is \" + raw_data)\n+        start_timestamp, stop_timestamp = raw_data.split(\"-\")\n+        stop_year = self.calculateStopYear(start_timestamp, stop_timestamp)\n+\n+        self.start_timestamp = start_timestamp\n+        self.stop_timestamp = stop_year + stop_timestamp\n+\n+    def calculateStopYear(self, start_timestamp, stop_timestamp):\n+        # calc stop year for the stop timestamp\n+\n+        start_year = int(start_timestamp[:4])\n+        start_month = int(start_timestamp[4:6])\n+\n+        stop_month = int(stop_timestamp[0:2])\n+\n+        # we assume logs can't last longer than a year\n+        stop_year = start_year if start_month <= stop_month else start_year + 1\n+\n+        return str(stop_year)\n+\n+    def extractMessageCountFromString(self, data_list):\n+        # example string(split into a list by spaces)\n+        # O=32977 N=31 G=41 E=2\n+\n+        msg_dictionary = {msg_type[0]: msg_type for msg_type in list(self.message_names.keys())}\n+\n+        for entry in data_list:\n+            split_entry = entry.split(\"=\")\n+            msg_type = msg_dictionary[split_entry[0]]\n+            msg_count = split_entry[1]\n+\n+            # append the resulting data\n+            self.navigation_messages[msg_type] = msg_count\n+\n+\n+class Log:\n+\n+    rinex_file_extensions = [\".obs\", \".nav\", \".gnav\", \".hnav\", \".qnav\", \".lnav\", \".sbs\"]\n+\n+    def __init__(self, log_path, log_metadata):\n+\n+        self.log_path = log_path\n+        self.log_name = os.path.splitext(os.path.basename(self.log_path))[0]\n+\n+        self.log_metadata = log_metadata\n+\n+        self.RINEX_files = self.findRINEXFiles(os.path.dirname(self.log_path))\n+\n+        self.log_package_path = \"\"\n+\n+    def __str__(self):\n+\n+        to_print = \"Printing log info:\\n\"\n+        to_print += \"Full path to log == \" + self.log_path + \"\\n\"\n+        to_print += \"Available RINEX files: \"\n+        to_print += str(self.RINEX_files) + \"\\n\"\n+        to_print += str(self.log_metadata) + \"\\n\"\n+        to_print += \"ZIP file layout:\\n\"\n+        to_print += str(self.prepareLogPackage())\n+\n+        return to_print\n+\n+    def isValid(self):\n+        # determine whether the log has valuable RINEX info\n+        return True if self.log_metadata.countValidMessages() else False\n+\n+    def findRINEXFiles(self, log_directory):\n+\n+        files_in_dir = glob.glob(log_directory + \"/*\")\n+        rinex_files_in_dir = []\n+\n+        for f in files_in_dir:\n+            filename = os.path.basename(f)\n+            name, extension = os.path.splitext(filename)\n+\n+            if extension in self.rinex_file_extensions:\n+                if name == self.log_name:\n+                    rinex_files_in_dir.append(f)\n+\n+        return rinex_files_in_dir\n+\n+    def prepareLogPackage(self):\n+        # return a list of tuples [(\"abspath\", \"wanted_path_inside_zip\"), ... ]\n+        # with raw and RINEX logs:w\n+\n+        files_list = []\n+        # add raw log\n+        files_list.append((self.log_path, \"Raw/\" + os.path.basename(self.log_path)))\n+\n+        for rinex_file in self.RINEX_files:\n+            rinex_files_paths = (rinex_file, \"RINEX/\" + os.path.basename(rinex_file))\n+            files_list.append(rinex_files_paths)\n+\n+        return files_list\n+\n+    def createLogPackage(self, package_destination=None):\n+        # files_list is a list of tuples [(\"abspath\", \"wanted_path_inside_zip\"), ... ]\n+\n+        if package_destination is None:\n+            package_destination = os.path.dirname(self.log_path) + \"/\" + self.log_name + \".zip\"\n+        file_tree = self.prepareLogPackage()\n+\n+        with zipfile.ZipFile(package_destination, \"w\") as newzip:\n+            for f in file_tree:\n+                newzip.write(f[0], f[1])\n+\n+            newzip.writestr(\"readme.txt\", str(self.log_metadata))\n+\n+        # delete unzipped files\n+        self.deleteLogFiles()\n+\n+        return package_destination\n+\n+    def deleteLogFiles(self):\n+\n+        all_log_files = self.RINEX_files\n+        # all_log_files.append(self.log_path)\n+\n+        for log in all_log_files:\n+            try:\n+                os.remove(log)\n+            except OSError:\n+                pass\n+\ndiff --git a/web_app/port.py b/web_app/port.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/port.py\n@@ -0,0 +1,48 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+from os import system\n+\n+def sh(script):\n+    system(\"bash -c '%s'\" % script)\n+\n+# change baudrate to 230400\n+def br230400():\n+    cmd = [\"echo\", \"-en\", '\"\\\\xb5\\\\x62\\\\x06\\\\x00\\\\x01\\\\x00\\\\x01\\\\x08\\\\x22\\\\xb5\\\\x62\\\\x06\\\\x00\\\\x14\\\\x00\\\\x01\\\\x00\\\\x00\\\\x00\\\\xd0\\\\x08\\\\x00\\\\x00\\\\x00\\\\x84\\\\x03\\\\x00\\\\x07\\\\x00\\\\x03\\\\x00\\\\x00\\\\x00\\\\x00\\\\x00\\\\x84\\\\xe8\\\\xb5\\\\x62\\\\x06\\\\x00\\\\x01\\\\x00\\\\x01\\\\x08\\\\x22\"', \">\", \"/dev/ttyMFD1\"]\n+    cmd = \" \".join(cmd)\n+    sh(cmd)\n+\n+# change baudrate to 230400 from any previous baudrates\n+def changeBaudrateTo115200():\n+    # typical baudrate values\n+#    br = [\"4800\", \"9600\", \"19200\", \"38400\", \"57600\", \"115200\", \"230400\"]\n+    br = [\"4800\", \"9600\", \"19200\", \"38400\", \"57600\", \"115200\"]\n+    cmd = [\"stty\", \"-F\", \"/dev/ttyACM0\"]\n+\n+    for rate in br:\n+        cmd.append(str(rate))\n+        cmd_line = \" \".join(cmd)\n+        sh(cmd_line)\n+\n+#        br230400()\n+#        cmd.pop()\ndiff --git a/web_app/reach_tools/__init__.py b/web_app/reach_tools/__init__.py\nnew file mode 100644\ndiff --git a/web_app/reach_tools/gps_time.py b/web_app/reach_tools/gps_time.py\nnew file mode 100755\n--- /dev/null\n+++ b/web_app/reach_tools/gps_time.py\n@@ -0,0 +1,186 @@\n+#!/usr/bin/python\n+\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import serial\n+import binascii\n+import ctypes\n+import subprocess\n+from . import reach_tools\n+\n+def hexify(char_list):\n+    \"\"\" transform a char list into a list of int values\"\"\"\n+    return [ord(c) for c in char_list]\n+\n+def enable_nav_timeutc(port):\n+    poll_time_utc = [\"b5\", \"62\", \"06\", \"01\", \"03\", \"00\", \"01\", \"21\", \"01\", \"2d\", \"85\"]\n+    msg = binascii.unhexlify(\"\".join(poll_time_utc))\n+    port.write(msg)\n+\n+def time_synchronised_by_ntp():\n+    out = subprocess.check_output(\"timedatectl\")\n+\n+    if \"NTP synchronized: yes\" in out:\n+        return True\n+    else:\n+        return False\n+\n+def update_system_time(date, time):\n+    # requires a date list and a time list\n+    # [\"YYYY\", \"MM\", \"DD\"], [\"hh\", \"mm\", \"ss\"]\n+    print(\"##### UPDATING SYSTEM TIME #####\")\n+    print(date)\n+    print(time)\n+    # busybox date cmd can use a following format\n+    # YYYY.MM.DD-hh:mm:ss\n+    # real date -s needs YYYY-MM-DD hh:mm:ss\n+    printable_date = \"-\".join(str(x) for x in date)\n+    printable_time = \":\".join(str(x) for x in time)\n+\n+    datetime_string = printable_date + \" \" + printable_time\n+    cmd = [\"date\", \"-s\", datetime_string]\n+    out = subprocess.check_output(cmd)\n+\n+def get_gps_time(port):\n+\n+    try:\n+        multiple_bytes = port.read(1024)\n+    except OSError:\n+        print(\"Could not open serial device\")\n+    else:\n+        ubx_log = hexify(multiple_bytes)\n+        time_data = MSG_NAV_TIMEUTC(ubx_log)\n+        print(time_data)\n+\n+        if time_data.time_valid:\n+            return time_data.date, time_data.time\n+\n+    return None, None\n+\n+def set_gps_time(serial_device, baud_rate):\n+\n+    port = serial.Serial(serial_device, baud_rate, timeout = 1.5)\n+    enable_nav_timeutc(port)\n+\n+    print(\"Restarting ntp service for faster sync...\")\n+    reach_tools.run_command_safely([\"timedatectl\", \"set-ntp\", \"false\"])\n+    reach_tools.run_command_safely([\"timedatectl\", \"set-ntp\", \"true\"])\n+\n+    print(\"TIMEUTC enabled\")\n+    time = None\n+    ntp_not_synced = True\n+\n+    while time is None and ntp_not_synced:\n+        date, time = get_gps_time(port)\n+        ntp_not_synced = not time_synchronised_by_ntp()\n+\n+    if ntp_not_synced:\n+        update_system_time(date, time)\n+\n+class MSG_NAV_TIMEUTC:\n+\n+    msg_start = [0xb5, 0x62, 0x01, 0x21, 0x14, 0x00]\n+    msg_length = 28\n+\n+    def __init__(self, ubx_hex_log):\n+        self.time_valid = False\n+        self.date = None\n+        self.time = None\n+\n+        extracted_messages = self.scan_log(ubx_hex_log)\n+\n+        if extracted_messages:\n+            for msg in extracted_messages:\n+                if self.is_valid(msg):\n+                    if self.time_is_valid(msg):\n+                        self.time_valid = True\n+                        self.date, self.time= self.unpack(msg)\n+\n+    def __str__(self):\n+        to_print = \"ubx NAV-TIMEUTC message\\n\"\n+\n+        if self.time_valid:\n+            to_print += \"Time data is valid\\n\"\n+            to_print += \".\".join(str(x) for x in self.date)\n+            to_print += \" \"\n+            to_print += \":\".join(str(x) for x in self.time)\n+        else:\n+            to_print += \"Time data is invalid!\"\n+\n+        return to_print\n+\n+    def scan_log(self, ubx_hex_log):\n+        \"\"\"Search the provided log for a required msg header\"\"\"\n+        matches = []\n+        pattern = self.msg_start\n+        msg_length = self.msg_length\n+\n+        for i in range(0, len(ubx_hex_log)):\n+            if ubx_hex_log[i] == pattern[0] and ubx_hex_log[i:i + len(pattern)] == pattern:\n+                matches.append(ubx_hex_log[i:i + msg_length])\n+\n+        return matches\n+\n+    def is_valid(self, msg):\n+        \"\"\"Count and verify the checksum of a ubx message. msg is a list of hex values\"\"\"\n+\n+        to_check = msg[2:-2]\n+\n+        ck_a = ctypes.c_uint8(0)\n+        ck_b = ctypes.c_uint8(0)\n+\n+        for num in to_check:\n+            byte = ctypes.c_uint8(num)\n+            ck_a.value = ck_a.value + byte.value\n+            ck_b.value = ck_b.value + ck_a.value\n+\n+        if (ck_a.value, ck_b.value) == (ctypes.c_uint8(msg[-2]).value, ctypes.c_uint8(msg[-1]).value):\n+            return True\n+        else:\n+            return False\n+\n+    def time_is_valid(self, msg):\n+        \"\"\"Check the flags confirming utc time in the message is valid\"\"\"\n+        flag_byte = ctypes.c_uint8(msg[-3])\n+        return True if flag_byte.value & 4 == 4 else False\n+\n+    def unpack(self, msg):\n+        \"\"\"Extract the actual time from the message\"\"\"\n+        datetime = []\n+\n+        # unpack year\n+        byte1 = ctypes.c_uint8(msg[18])\n+        byte2 = ctypes.c_uint8(msg[19])\n+\n+        year = ctypes.c_uint16(byte2.value << 8 | byte1.value).value\n+        datetime.append(year)\n+        # unpack month, day, hour, minute, second\n+        for i in range(20, 25):\n+            datetime.append(msg[i])\n+\n+        date = datetime[:3]\n+        time = datetime[3:]\n+\n+        return date, time\n+\ndiff --git a/web_app/reach_tools/provisioner.py b/web_app/reach_tools/provisioner.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/reach_tools/provisioner.py\n@@ -0,0 +1,148 @@\n+#!/usr/bin/python\n+\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import pip\n+import subprocess\n+import os\n+from . import reach_tools\n+import imp\n+import shutil\n+\n+def install_pip_packages():\n+\n+    packages = [\n+        (\"pybluez\", \"bluetooth\")\n+    ]\n+\n+    for p in packages:\n+        try:\n+            imp.find_module(p[1])\n+        except ImportError:\n+            print(\"No module \" + p[0] + \" found...\")\n+            pip.main([\"install\", p[0]])\n+\n+def check_opkg_packages(packages):\n+\n+    packages_to_check = packages\n+\n+    try:\n+        out = subprocess.check_output([\"opkg\", \"list-installed\"])\n+    except subprocess.CalledProcessError:\n+        print(\"Error getting installed opkg packages\")\n+        return None\n+    else:\n+        for p in out.split(\"\\n\"):\n+            if p:\n+                installed_package_name = p.split()[0]\n+                if installed_package_name in packages_to_check:\n+                    packages_to_check.remove(installed_package_name)\n+\n+        return packages_to_check\n+\n+def install_opkg_packages(packages):\n+\n+    packages = check_opkg_packages(packages)\n+\n+    if packages:\n+        print(\"Installing missing packages:\")\n+        print(packages)\n+        try:\n+            subprocess.check_output([\"opkg\", \"update\"])\n+        except subprocess.CalledProcessError:\n+            print(\"No internet connection, so no package installs!\")\n+            pass\n+        else:\n+            for p in packages:\n+                subprocess.check_output([\"opkg\", \"install\", p])\n+\n+def restart_bt_daemon():\n+    reach_tools.run_command_safely([\"rfkill\", \"unblock\", \"bluetooth\"])\n+    reach_tools.run_command_safely([\"systemctl\", \"daemon-reload\"])\n+    reach_tools.run_command_safely([\"systemctl\", \"restart\", \"bluetooth.service\"])\n+    reach_tools.run_command_safely([\"systemctl\", \"restart\", \"bluetooth.service\"])\n+    reach_tools.run_command_safely([\"hciconfig\", \"hci0\", \"reset\"])\n+\n+def enable_bt_compatibility(file_path):\n+\n+    with open(file_path, \"r\") as f:\n+        data_read = f.readlines()\n+\n+    need_to_update = True\n+    required_line = 0\n+\n+    for line in data_read:\n+        if \"ExecStart=/usr/lib/bluez5/bluetooth/bluetoothd -C\" in line:\n+            need_to_update = False\n+\n+    if need_to_update:\n+        data_to_write = []\n+\n+        for line in data_read:\n+            if \"ExecStart=/usr/lib/bluez5/bluetooth/bluetoothd\" in line:\n+                to_append = \"ExecStart=/usr/lib/bluez5/bluetooth/bluetoothd -C\\n\"\n+            else:\n+                to_append = line\n+\n+            data_to_write.append(to_append)\n+\n+        with open(file_path, \"w\") as f:\n+            f.writelines(data_to_write)\n+\n+        reach_tools.run_command_safely([\"sync\"])\n+\n+        restart_bt_daemon()\n+\n+def update_bluetooth_service():\n+    first = \"/lib/systemd/system/bluetooth.service\"\n+    second = \"/etc/systemd/system/bluetooth.target.wants/bluetooth.service\"\n+    enable_bt_compatibility(first)\n+    enable_bt_compatibility(second)\n+    restart_bt_daemon()\n+\n+def check_RTKLIB_integrity():\n+    RTKLIB_path = \"/home/reach/RTKLIB/\"\n+    reachview_binaries_path = \"/home/reach/rtklib_configs/\"\n+\n+    RTKLIB_binaries = [\n+        (RTKLIB_path + \"app/rtkrcv/gcc/rtkrcv\", reachview_binaries_path + \"rtkrcv\"),\n+        (RTKLIB_path + \"app/convbin/gcc/convbin\", reachview_binaries_path + \"convbin\"),\n+        (RTKLIB_path + \"app/str2str/gcc/str2str\", reachview_binaries_path + \"str2str\")\n+    ]\n+\n+    for b in RTKLIB_binaries:\n+        if not os.path.isfile(b[0]):\n+            print(\"Could not find \" + b[0] + \"! Copying from ReachView backup...\")\n+            shutil.copy(b[1], b[0])\n+\n+def provision_reach():\n+    install_pip_packages()\n+    packages = [\"kernel-module-ftdi-sio\"]\n+    install_opkg_packages(packages)\n+    update_bluetooth_service()\n+    check_RTKLIB_integrity()\n+\n+if __name__ == \"__main__\":\n+    provision_reach()\n+\ndiff --git a/web_app/reach_tools/reach_tools.py b/web_app/reach_tools/reach_tools.py\nnew file mode 100644\n--- /dev/null\n+++ b/web_app/reach_tools/reach_tools.py\n@@ -0,0 +1,171 @@\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+import os\n+import subprocess\n+\n+def getImageVersion():\n+\n+    image_version_file = \"/home/pi/.reach/image_version\"\n+\n+    try:\n+        with open(image_version_file, \"r\") as f:\n+            image_version = f.readline().rstrip(\"\\n\")\n+    except (IOError, OSError):\n+        print(\"Could not find version file inside system\")\n+        print(\"This is image v1.0\")\n+        image_version = \"v1.0\"\n+\n+    return image_version\n+\n+def getNetworkStatus():\n+\n+    # get Wi-Fi mode, Master or Managed\n+#    cmd = [\"configure_edison\", \"--showWiFiMode\"]\n+#    cmd = \" \".join(cmd)\n+#    mode = subprocess.check_output(cmd, shell = True).strip()\n+\n+#    mode = \"Managed\"\n+#    ssid = \"empty\"\n+#    ip_address = \"empty\"\n+\n+    mode = \"Master\"\n+    ssid = \"RTK_test\"\n+    ip_address = \"192.168.43.1\"\n+\n+#    if mode == \"Managed\":\n+        # we are in managed, client mode\n+        # we can extract all information from \"wpa_cli status\"\n+\n+#        cmd = [\"wpa_cli\", \"status\"]\n+#        cmd = \" \".join(cmd)\n+#        out = subprocess.check_output(cmd, shell = True)\n+\n+#        out = out.split(\"\\n\")\n+\n+#        for line in out:\n+#            if \"ssid=\" in line:\n+#                ssid = line[5:]\n+#            if \"ip_address=\" in line:\n+#                ip_address = line[11:]\n+\n+#    if mode == \"Master\":\n+        # we are in master, AP mode\n+        # we can extract all info from \"configure_edison\"\n+        # with differnet parameters\n+\n+        # example of the output {\"hostname\": \"reach\", \"ssid\": \"reach:ec:e8\", \"default_ssid\": \"edison_ap\"}\n+#        cmd = [\"configure_edison\", \"--showNames\"]\n+#        cmd = \" \".join(cmd)\n+#        out = subprocess.check_output(cmd, shell = True)\n+\n+#        anchor = '\"ssid\": \"'\n+\n+#        ssid_start_position = out.find(anchor) + len(anchor)\n+#        ssid_stop_position = out.find('\"', ssid_start_position)\n+\n+#        ssid = out[ssid_start_position:ssid_stop_position]\n+\n+#        cmd = [\"configure_edison\", \"--showWiFiIP\"]\n+#        cmd = \" \".join(cmd)\n+#        ip_address = subprocess.check_output(cmd, shell = True).strip()\n+\n+    return {\"mode\": mode, \"ssid\": ssid, \"ip_address\": ip_address}\n+\n+def getAppVersion():\n+    # Extract git tag as software version\n+#    git_tag_cmd = \"git describe --tags\"\n+#    app_version = subprocess.check_output([git_tag_cmd], shell = True, cwd = \"/home/reach\")\n+    app_version = 1.0\n+\n+    return app_version\n+\n+def getSystemStatus():\n+\n+    system_status = {\n+        \"network_status\": getNetworkStatus(),\n+        \"image_version\": getImageVersion(),\n+        \"app_version\": getAppVersion(),\n+    }\n+\n+    return system_status\n+\n+def getAvailableSerialPorts():\n+\n+    possible_ports_ports_to_use = [\"ttyACM0\", \"ttyUSB0\"]\n+    serial_ports_to_use = [port for port in possible_ports_ports_to_use if os.path.exists(\"/dev/\" + port)]\n+\n+    return serial_ports_to_use\n+\n+def getLogsSize(logs_path):\n+    #logs_path = \"/home/pi/logs/\"\n+    size_in_bytes = sum(os.path.getsize(logs_path + f) for f in os.listdir(logs_path) if os.path.isfile(logs_path + f))\n+    return size_in_bytes/(1024*1024)\n+\n+def getFreeSpace(logs_path):\n+    space = os.statvfs(os.path.expanduser(\"~\"))\n+    free = space.f_bavail * space.f_frsize / 1024000\n+    total = space.f_blocks * space.f_frsize / 1024000\n+\n+    used_by_logs = getLogsSize(logs_path)\n+    total_for_logs = free + used_by_logs\n+    percentage = (float(used_by_logs)/float(total_for_logs)) * 100\n+    total_for_logs_gb = float(total_for_logs) / 1024.0\n+\n+    result = {\n+        \"used\": \"{0:.0f}\".format(used_by_logs),\n+        \"total\": \"{0:.1f}\".format(total_for_logs_gb),\n+        \"percentage\": \"{0:.0f}\".format(percentage)\n+    }\n+\n+    print(\"Returning sizes!\")\n+    print(result)\n+\n+    return result\n+\n+def run_command_safely(cmd):\n+    try:\n+        out = subprocess.check_output(cmd)\n+    except subprocess.CalledProcessError:\n+        out = None\n+\n+    return out\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\n+\ndiff --git a/web_app/server.py b/web_app/server.py\nnew file mode 100755\n--- /dev/null\n+++ b/web_app/server.py\n@@ -0,0 +1,565 @@\n+#!/usr/bin/python\n+\n+# This Flask app is a heavily modified version of Reachview\n+# modified to be used as a front end for GNSS base\n+# author: St\u00e9phane P\u00e9neau\n+# source: https://github.com/Stefal/rtkbase\n+\n+# ReachView code is placed under the GPL license.\n+# Written by Egor Fedorov (egor.fedorov@emlid.com)\n+# Copyright (c) 2015, Emlid Limited\n+# All rights reserved.\n+\n+# If you are interested in using ReachView code as a part of a\n+# closed source project, please contact Emlid Limited (info@emlid.com).\n+\n+# This file is part of ReachView.\n+\n+# ReachView is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+\n+# ReachView is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+\n+# You should have received a copy of the GNU General Public License\n+# along with ReachView.  If not, see <http://www.gnu.org/licenses/>.\n+\n+#from gevent import monkey\n+#monkey.patch_all()\n+import eventlet\n+eventlet.monkey_patch()\n+\n+import time\n+import json\n+import os\n+import signal\n+import sys\n+import requests\n+\n+from threading import Thread\n+from RTKLIB import RTKLIB\n+from port import changeBaudrateTo115200\n+from reach_tools import reach_tools, provisioner\n+from ServiceController import ServiceController\n+from RTKBaseConfigManager import RTKBaseConfigManager\n+\n+#print(\"Installing all required packages\")\n+#provisioner.provision_reach()\n+\n+#import reach_bluetooth.bluetoothctl\n+#import reach_bluetooth.tcp_bridge\n+\n+from threading import Thread\n+from flask_bootstrap import Bootstrap\n+from flask import Flask, render_template, session, request, flash, url_for\n+from flask import send_file, send_from_directory, safe_join, redirect, abort\n+from flask import g\n+from flask_wtf import FlaskForm\n+from wtforms import PasswordField, BooleanField, SubmitField\n+from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin\n+from wtforms.validators import ValidationError, DataRequired, EqualTo\n+from flask_socketio import SocketIO, emit, disconnect\n+from subprocess import check_output\n+\n+from werkzeug.security import generate_password_hash\n+from werkzeug.security import check_password_hash\n+from werkzeug.urls import url_parse\n+\n+app = Flask(__name__)\n+app.debug = False\n+app.config[\"SECRET_KEY\"] = \"secret!\"\n+#app.config[\"UPLOAD_FOLDER\"] = os.path.join(os.path.dirname(__file__), \"../logs\")\n+app.config[\"DOWNLOAD_FOLDER\"] = os.path.join(os.path.dirname(__file__), \"../data\")\n+app.config[\"LOGIN_DISABLED\"] = False\n+\n+path_to_rtklib = \"/usr/local/bin\"\n+\n+login=LoginManager(app)\n+login.login_view = 'login_page'\n+socketio = SocketIO(app)\n+bootstrap = Bootstrap(app)\n+\n+rtk = RTKLIB(socketio, rtklib_path=path_to_rtklib, log_path=app.config[\"DOWNLOAD_FOLDER\"])\n+services_list = [{\"service_unit\" : \"str2str_tcp.service\", \"name\" : \"main\"},\n+                 {\"service_unit\" : \"str2str_ntrip.service\", \"name\" : \"ntrip\"},\n+                 {\"service_unit\" : \"str2str_rtcm_svr.service\", \"name\" : \"rtcm_svr\"},\n+                 {\"service_unit\" : \"str2str_file.service\", \"name\" : \"file\"},\n+                 ]\n+\n+\n+#Delay before rtkrcv will stop if no user is on status.html page\n+rtkcv_standby_delay = 600\n+\n+#Get settings from settings.conf.default and settings.conf\n+rtkbaseconfig = RTKBaseConfigManager(os.path.join(os.path.dirname(__file__), \"../settings.conf.default\"), os.path.join(os.path.dirname(__file__), \"../settings.conf\"))\n+\n+class User(UserMixin):\n+    \"\"\" Class for user authentification \"\"\"\n+    def __init__(self, username):\n+        self.id=username\n+        self.password_hash = rtkbaseconfig.get(\"general\", \"web_password_hash\")\n+\n+    def check_password(self, password):\n+        return check_password_hash(self.password_hash, password)\n+\n+class LoginForm(FlaskForm):\n+    \"\"\" Class for the loginform\"\"\"\n+    #username = StringField('Username', validators=[DataRequired()])\n+    password = PasswordField('Please enter the password:', validators=[DataRequired()])\n+    remember_me = BooleanField('Remember Me')\n+    submit = SubmitField('Sign In')\n+\n+def update_password(config_object):\n+    \"\"\"\n+        Check in settings.conf if web_password entry contains a value\n+        If yes, this function will generate a new hash for it and\n+        remove the web_password value\n+        :param config_object: a RTKBaseConfigManager instance\n+    \"\"\"\n+    new_password = config_object.get(\"general\", \"new_web_password\")\n+    if new_password != \"\":\n+        config_object.update_setting(\"general\", \"web_password_hash\", generate_password_hash(new_password))\n+        config_object.update_setting(\"general\", \"new_web_password\", \"\")\n+        \n+def manager():\n+    \"\"\" This manager runs inside a thread\n+        It checks how long rtkrcv is running since the last user leaves the\n+        status web page, and stop rtkrcv when sleep_count reaches rtkrcv_standby delay\n+    \"\"\"\n+    while True:\n+        if rtk.sleep_count > rtkcv_standby_delay and rtk.state != \"inactive\":\n+            rtk.stopBase()\n+            rtk.sleep_count = 0\n+        elif rtk.sleep_count > rtkcv_standby_delay:\n+            print(\"I'd like to stop rtkrcv (sleep_count = {}), but rtk.state is: {}\".format(rtk.sleep_count, rtk.state))\n+        time.sleep(1)\n+\n+@socketio.on(\"check update\", namespace=\"/test\")\n+def check_update(source_url = None, current_release = None, prerelease=True, emit = True):\n+    \"\"\"\n+        Check if a RTKBase update exists\n+        :param source_url: the url where we will try to find an update. It uses the github api.\n+        :param current_release: The current RTKBase release\n+        :param prerelease: True/False Get prerelease or not\n+        :param emit: send the result to the web front end with socketio\n+        :return The new release version inside a dict (release version and url for this release)\n+    \"\"\"\n+    new_release = {}\n+    source_url = source_url if source_url is not None else \"https://api.github.com/repos/stefal/rtkbase/releases\"\n+    current_release = current_release if current_release is not None else rtkbaseconfig.get(\"general\", \"version\").strip(\"v\")\n+    \n+    try:    \n+        response = requests.get(source_url)\n+        response = response.json()\n+        for release in response:\n+            if release.get(\"prerelease\") & prerelease or release.get(\"prerelease\") == False:\n+                latest_release = release[\"tag_name\"].strip(\"v\")\n+                if latest_release > current_release and latest_release <= rtkbaseconfig.get(\"general\", \"checkpoint_version\"):\n+                    new_release = {\"new_release\" : latest_release, \"url\" : release.get(\"tarball_url\")}\n+                    break\n+             \n+    except Exception as e:\n+        print(\"Check update error: \", e)\n+        \n+    if emit:\n+        socketio.emit(\"new release\", json.dumps(new_release), namespace=\"/test\")\n+    print\n+    return new_release\n+\n+@socketio.on(\"update rtkbase\", namespace=\"/test\")       \n+def update_rtkbase():\n+    \"\"\"\n+        Check if a RTKBase exists, download it and update rtkbase\n+    \"\"\"\n+    #Check if an update is available\n+    update_url = check_update(emit=False).get(\"url\")\n+    if update_url is None:\n+        return\n+\n+    import tarfile\n+    #Download update\n+    update_archive = \"/var/tmp/rtkbase_update.tar.gz\"\n+    try:\n+        response = requests.get(update_url)\n+        with open(update_archive, \"wb\") as f:\n+            f.write(response.content)\n+    except Exception as e:\n+        print(\"Error: Can't download update - \", e)\n+\n+    #Get the \"root\" folder in the archive\n+    tar = tarfile.open(update_archive)\n+    for tarinfo in tar:\n+        if tarinfo.isdir():\n+            primary_folder = tarinfo.name\n+            break\n+    \n+    #Extract archive\n+    tar.extractall(\"/var/tmp\")\n+\n+    #launch update script\n+    rtk.shutdownBase()\n+    rtkbase_path = os.path.abspath(os.path.join(os.path.dirname(__file__), \"../\"))\n+    source_path = os.path.join(\"/var/tmp/\", primary_folder)\n+    script_path = os.path.join(source_path, \"rtkbase_update.sh\")\n+    current_release = rtkbaseconfig.get(\"general\", \"version\").strip(\"v\")\n+    standard_user = rtkbaseconfig.get(\"general\", \"user\")\n+    os.execl(script_path, \"unused arg0\", source_path, rtkbase_path, app.config[\"DOWNLOAD_FOLDER\"].split(\"/\")[-1], current_release, standard_user)\n+\n+@app.before_request\n+def inject_release():\n+    \"\"\"\n+        Insert the RTKBase release number as a global variable for Flask/Jinja\n+    \"\"\"\n+    g.version = rtkbaseconfig.get(\"general\", \"version\")\n+\n+@login.user_loader\n+def load_user(id):\n+    return User(id)\n+\n+@app.route('/')\n+@app.route('/index')\n+@app.route('/status')\n+@login_required\n+def status_page():\n+    \"\"\"\n+        The status web page with the gnss satellites levels and a map\n+    \"\"\"\n+    return render_template(\"status.html\")\n+\n+@app.route('/settings')\n+@login_required\n+def settings_page():\n+    \"\"\"\n+        The settings page where you can manage the various services, the parameters, update, power...\n+    \"\"\"\n+    main_settings = rtkbaseconfig.get_main_settings()\n+    ntrip_settings = rtkbaseconfig.get_ntrip_settings()\n+    file_settings = rtkbaseconfig.get_file_settings()\n+    rtcm_svr_settings = rtkbaseconfig.get_rtcm_svr_settings()\n+\n+    return render_template(\"settings.html\", main_settings = main_settings,\n+                                            ntrip_settings = ntrip_settings,\n+                                            file_settings = file_settings,\n+                                            rtcm_svr_settings = rtcm_svr_settings)\n+\n+@app.route('/logs')\n+@login_required\n+def logs_page():\n+    \"\"\"\n+        The data web pages where you can download/delete the raw gnss data\n+    \"\"\"\n+    return render_template(\"logs.html\")\n+\n+@app.route(\"/logs/download/<path:log_name>\")\n+@login_required\n+def downloadLog(log_name):\n+    \"\"\" Route for downloading raw gnss data\"\"\"\n+    try:\n+        full_log_path = rtk.logm.log_path + \"/\" + log_name\n+        return send_file(full_log_path, as_attachment = True)\n+    except FileNotFoundError:\n+        abort(404)\n+\n+@app.route('/login', methods=['GET', 'POST'])\n+def login_page():\n+    if current_user.is_authenticated:\n+        return redirect(url_for('status_page'))\n+    loginform = LoginForm()\n+    if loginform.validate_on_submit():\n+        user = User('admin')\n+        password = loginform.password.data\n+        if not user.check_password(password):\n+            return abort(401)\n+        \n+        login_user(user, remember=loginform.remember_me.data)\n+        next_page = request.args.get('next')\n+        if not next_page or url_parse(next_page).netloc != '':\n+            next_page = url_for('status_page')\n+\n+        return redirect(next_page)\n+        \n+    return render_template('login.html', title='Sign In', form=loginform)\n+\n+@app.route('/logout')\n+def logout():\n+    logout_user()\n+    return redirect(url_for('login_page'))\n+\n+#### Handle connect/disconnect events ####\n+\n+@socketio.on(\"connect\", namespace=\"/test\")\n+def testConnect():\n+    print(\"Browser client connected\")\n+    rtk.sendState()\n+\n+@socketio.on(\"disconnect\", namespace=\"/test\")\n+def testDisconnect():\n+    print(\"Browser client disconnected\")\n+\n+#### Log list handling ###\n+\n+@socketio.on(\"get logs list\", namespace=\"/test\")\n+def getAvailableLogs():\n+    print(\"DEBUG updating logs\")\n+    rtk.logm.updateAvailableLogs()\n+    print(\"Updated logs list is \" + str(rtk.logm.available_logs))\n+    rtk.socketio.emit(\"available logs\", rtk.logm.available_logs, namespace=\"/test\")\n+\n+#### str2str launch/shutdown handling ####\n+\n+@socketio.on(\"launch base\", namespace=\"/test\")\n+def launchBase():\n+    rtk.launchBase()\n+\n+@socketio.on(\"shutdown base\", namespace=\"/test\")\n+def shutdownBase():\n+    rtk.shutdownBase()\n+\n+#### str2str start/stop handling ####\n+\n+@socketio.on(\"start base\", namespace=\"/test\")\n+def startBase():\n+    rtk.startBase()\n+\n+@socketio.on(\"stop base\", namespace=\"/test\")\n+def stopBase():\n+    rtk.stopBase()\n+\n+@socketio.on(\"on graph\", namespace=\"/test\")\n+def continueBase():\n+    rtk.sleep_count = 0\n+#### Free space handler\n+\n+@socketio.on(\"get available space\", namespace=\"/test\")\n+def getAvailableSpace():\n+    rtk.socketio.emit(\"available space\", reach_tools.getFreeSpace(path_to_gnss_log), namespace=\"/test\")\n+\n+#### Delete log button handler ####\n+\n+@socketio.on(\"delete log\", namespace=\"/test\")\n+def deleteLog(json):\n+    rtk.logm.deleteLog(json.get(\"name\"))\n+    # Sending the the new available logs\n+    getAvailableLogs()\n+\n+#### Download and convert log handlers ####\n+\n+@socketio.on(\"process log\", namespace=\"/test\")\n+def processLog(json):\n+    log_name = json.get(\"name\")\n+\n+    print(\"Got signal to process a log, name = \" + str(log_name))\n+    print(\"Path to log == \" + rtk.logm.log_path + \"/\" + str(log_name))\n+\n+    raw_log_path = rtk.logm.log_path + \"/\" + log_name\n+    rtk.processLogPackage(raw_log_path)\n+\n+@socketio.on(\"cancel log conversion\", namespace=\"/test\")\n+def cancelLogConversion(json):\n+    log_name = json.get(\"name\")\n+    raw_log_path = rtk.logm.log_path + \"/\" + log_name\n+    rtk.cancelLogConversion(raw_log_path)\n+\n+#### RINEX versioning ####\n+\n+@socketio.on(\"read RINEX version\", namespace=\"/test\")\n+def readRINEXVersion():\n+    rinex_version = rtk.logm.getRINEXVersion()\n+    rtk.socketio.emit(\"current RINEX version\", {\"version\": rinex_version}, namespace=\"/test\")\n+\n+@socketio.on(\"write RINEX version\", namespace=\"/test\")\n+def writeRINEXVersion(json):\n+    rinex_version = json.get(\"version\")\n+    rtk.logm.setRINEXVersion(rinex_version)\n+\n+#### Device hardware functions ####\n+\n+@socketio.on(\"reboot device\", namespace=\"/test\")\n+def rebootRtkbase():\n+    print(\"Rebooting...\")\n+    rtk.shutdown()\n+    #socketio.stop() hang. I disabled it\n+    #socketio.stop()\n+    check_output(\"reboot\")\n+\n+@socketio.on(\"shutdown device\", namespace=\"/test\")\n+def shutdownRtkbase():\n+    print(\"Shutdown...\")\n+    rtk.shutdown()\n+    #socketio.stop() hang. I disabled it\n+    #socketio.stop()\n+    check_output([\"shutdown\", \"now\"])\n+\n+@socketio.on(\"turn off wi-fi\", namespace=\"/test\")\n+def turnOffWiFi():\n+    print(\"Turning off wi-fi\")\n+#    check_output(\"rfkill block wlan\", shell = True)\n+\n+#### Systemd Services functions ####\n+\n+def load_units(services):\n+    \"\"\"\n+        load unit service before getting status\n+        :param services: A list of systemd services (dict) containing a service_unit key:value\n+        :return The dict list updated with the pystemd ServiceController object\n+\n+        example: \n+            services = [{\"service_unit\" : \"str2str_tcp.service\"}]\n+            return will be [{\"service_unit\" : \"str2str_tcp.service\", \"unit\" : a pystemd object}]\n+        \n+    \"\"\"\n+    for service in services:\n+        service[\"unit\"] = ServiceController(service[\"service_unit\"])\n+    return services\n+\n+def update_std_user(services):\n+    \"\"\"\n+        check which user run str2str_file service and update settings.conf\n+        :param services: A list of systemd services (dict) containing a service_unit key:value\n+    \"\"\"\n+    service = next(x for x in services_list if x[\"name\"] == \"file\")\n+    user = service[\"unit\"].getUser()\n+    rtkbaseconfig.update_setting(\"general\", \"user\", user)\n+\n+def restartServices(restart_services_list):\n+    \"\"\"\n+        Restart already running services\n+        This function will refresh all services status, then compare the global services_list and \n+        the restart_services_list to find the services we need to restart.\n+        #TODO I don't really like this global services_list use.\n+    \"\"\"\n+    #Update services status\n+    for service in services_list:\n+        service[\"active\"] = service[\"unit\"].isActive()\n+\n+    #Restart running services\n+    for restart_service in restart_services_list:\n+        for service in services_list:\n+            if service[\"name\"] == restart_service and service[\"active\"] is True:\n+                print(\"Restarting service: \", service[\"name\"])\n+                service[\"unit\"].restart()\n+    \n+    #refresh service status\n+    getServicesStatus()\n+\n+@socketio.on(\"get services status\", namespace=\"/test\")\n+def getServicesStatus():\n+    \"\"\"\n+        Get the status of services listed in services_list\n+        (services_list is global)\n+    \"\"\"\n+\n+    print(\"Getting services status\")\n+    \n+    for service in services_list:\n+        service[\"active\"] = service[\"unit\"].isActive()\n+\n+    services_status = []\n+    for service in services_list: \n+        services_status.append({key:service[key] for key in service if key != 'unit'})\n+    \n+    print(services_status)\n+    socketio.emit(\"services status\", json.dumps(services_status), namespace=\"/test\")\n+    return services_status\n+\n+@socketio.on(\"services switch\", namespace=\"/test\")\n+def switchService(json):\n+    \"\"\"\n+        Start or stop some systemd services\n+        As a service could need some time to start or stop, there is a 5 seconds sleep\n+        before refreshing the status.\n+        param: json: A json var from the web front end containing one or more service\n+        name with their new status.\n+    \"\"\"\n+    print(\"Received service to switch\", json)\n+    try:\n+        for service in services_list:\n+            if json[\"name\"] == service[\"name\"] and json[\"active\"] == True:\n+                print(\"Trying to start service {}\".format(service[\"name\"]))\n+                service[\"unit\"].start()\n+            elif json[\"name\"] == service[\"name\"] and json[\"active\"] == False:\n+                print(\"Trying to stop service {}\".format(service[\"name\"]))\n+                service[\"unit\"].stop()\n+\n+    except Exception as e:\n+        print(e)\n+    finally:\n+        time.sleep(5)\n+        getServicesStatus()\n+\n+@socketio.on(\"form data\", namespace=\"/test\")\n+def update_settings(json):\n+    \"\"\"\n+        Get the form data from the web front end, and save theses values to settings.conf\n+        Then restart the services which have a dependency with these parameters.\n+        param json: A json variable containing the source fom and the new paramaters\n+    \"\"\"\n+    print(\"received settings form\", json)\n+    source_section = json.pop().get(\"source_form\")\n+    print(\"section: \", source_section)\n+    if source_section == \"change_password\":\n+        if json[0].get(\"value\") == json[1].get(\"value\"):\n+            rtkbaseconfig.update_setting(\"general\", \"new_web_password\", json[0].get(\"value\"))\n+            update_password(rtkbaseconfig)\n+            socketio.emit(\"password updated\", namespace=\"/test\")\n+\n+        else:\n+            print(\"ERREUR, MAUVAIS PASS\")\n+    else:\n+        for form_input in json:\n+            print(\"name: \", form_input.get(\"name\"))\n+            print(\"value: \", form_input.get(\"value\"))\n+            rtkbaseconfig.update_setting(source_section, form_input.get(\"name\"), form_input.get(\"value\"), write_file=False)\n+        rtkbaseconfig.write_file()\n+\n+        #Restart service if needed\n+        if source_section == \"main\":\n+            restartServices((\"main\", \"ntrip\", \"rtcm_svr\", \"file\"))\n+        elif source_section == \"ntrip\":\n+            restartServices((\"ntrip\",))\n+        elif source_section == \"rtcm_svr\":\n+            restartServices((\"rtcm_svr\",))\n+        elif source_section == \"local_storage\":\n+            restartServices((\"file\",))\n+\n+if __name__ == \"__main__\":\n+    try:\n+        #check if a new password is defined in settings.conf\n+        update_password(rtkbaseconfig)\n+        #check if authentification is required\n+        if not rtkbaseconfig.get_web_authentification():\n+            app.config[\"LOGIN_DISABLED\"] = True\n+        #get data path\n+        app.config[\"DOWNLOAD_FOLDER\"] = rtkbaseconfig.get(\"local_storage\", \"datadir\")\n+        #load services status managed with systemd\n+        services_list = load_units(services_list)\n+        #Update standard user in settings.conf\n+        update_std_user(services_list)\n+        #Start a \"manager\" thread\n+        manager_thread = Thread(target=manager, daemon=True)\n+        manager_thread.start()\n+\n+        app.secret_key = rtkbaseconfig.get_secret_key()\n+        socketio.run(app, host = \"0.0.0.0\", port = 80)\n+\n+    except KeyboardInterrupt:\n+        print(\"Server interrupted by user!!\")\n+\n+        # clean up broadcast and blink threads\n+        rtk.server_not_interrupted = False\n+#        rtk.led.blinker_not_interrupted = False\n+        rtk.waiting_for_single = False\n+\n+        if rtk.coordinate_thread is not None:\n+            rtk.coordinate_thread.join()\n+\n+        if rtk.satellite_thread is not None:\n+            rtk.satellite_thread.join()\n+\n+#        if rtk.led.blinker_thread is not None:\n+#            rtk.led.blinker_thread.join()\n+\n", "test_patch": "diff --git a/receiver_cfg/ubxconfig.sh b/receiver_cfg/test_ubx.sh\nsimilarity index 98%\nrename from receiver_cfg/ubxconfig.sh\nrename to receiver_cfg/test_ubx.sh\n--- a/receiver_cfg/ubxconfig.sh\n+++ b/receiver_cfg/test_ubx.sh\n@@ -74,6 +74,8 @@ ver_check ()\n {\n dev_mon_ver=`dev_mon_ver`\n file_mon_ver=`file_mon_ver $1`\n+echo \"Device version: \" ${dev_mon_ver}\n+echo \"File version: \" ${file_mon_ver}\n \n if [[ ${dev_mon_ver} =~ .*${file_mon_ver}.* ]]\n then\n@@ -105,6 +107,7 @@ then\n else\n return 1\n fi\n+return 1\n }\n \n send_ubx ()\ndiff --git a/unit/test.service b/web_app/log_converter/__init__.py\nsimilarity index 100%\nrename from unit/test.service\nrename to web_app/log_converter/__init__.py\n", "problem_statement": "", "hints_text": "", "created_at": "2020-06-07T08:01:41Z"}2