summaryrefslogtreecommitdiff
path: root/gps/client.py
blob: d288bec7f2dd4d1a581c5639a1e20ba15731f6ed (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
"gpsd client functions"
# This file is Copyright (c) 2010 by the GPSD project
# BSD terms apply: see the file COPYING in the distribution root for details.
#
# This code run compatibly under Python 2 and 3.x for x >= 2.
# Preserve this property!
from __future__ import absolute_import, print_function, division

import json
import select
import socket
import sys
import time

from .misc import polystr, polybytes
from .watch_options import *

GPSD_PORT = "2947"


class gpscommon(object):
    "Isolate socket handling and buffering from the protocol interpretation."

    host = "127.0.0.1"
    port = GPSD_PORT

    def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0,
                 should_reconnect=False):
        self.stream_command = b''
        self.linebuffer = b''
        self.received = time.time()
        self.reconnect = should_reconnect
        self.verbose = verbose
        self.sock = None        # in case we blow up in connect
        # Provide the response in both 'str' and 'bytes' form
        self.bresponse = b''
        self.response = polystr(self.bresponse)

        if host is not None:
            self.host = host
        if port is not None:
            self.port = port
        self.connect(self.host, self.port)

    def connect(self, host, port):
        """Connect to a host on a given port.

        If the hostname ends with a colon (`:') followed by a number, and
        there is no port specified, that suffix will be stripped off and the
        number interpreted as the port number to use.
        """
        if not port and (host.find(':') == host.rfind(':')):
            i = host.rfind(':')
            if i >= 0:
                host, port = host[:i], host[i + 1:]
            try:
                port = int(port)
            except ValueError:
                raise socket.error("nonnumeric port")
        # if self.verbose > 0:
        #    print 'connect:', (host, port)
        msg = "getaddrinfo returns an empty list"
        self.sock = None
        for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
            af, socktype, proto, _canonname, sa = res
            try:
                self.sock = socket.socket(af, socktype, proto)
                # if self.debuglevel > 0: print 'connect:', (host, port)
                self.sock.connect(sa)
                if self.verbose > 0:
                    print('connected to tcp://{}:{}'.format(host, port))
                break
            # do not use except ConnectionRefusedError
            # # Python 2.7 doc does have this exception
            except socket.error as e:
                if self.verbose > 1:
                    msg = str(e) + ' (to {}:{})'.format(host, port)
                    sys.stderr.write("error: {}\n".format(msg.strip()))
                self.close()
                raise  # propogate error to caller

    def close(self):
        "Close the gpsd socket"
        if self.sock:
            self.sock.close()
        self.sock = None

    def __del__(self):
        "Close the gpsd socket"
        self.close()

    def waiting(self, timeout=0):
        "Return True if data is ready for the client."
        if self.linebuffer:
            return True
        if self.sock is None:
            return False

        (winput, _woutput, _wexceptions) = select.select(
            (self.sock,), (), (), timeout)
        return winput != []

    def read(self):
        "Wait for and read data being streamed from the daemon."

        if None is self.sock:
            self.connect(self.host, self.port)
            if None is self.sock:
                return -1
            self.stream()

        eol = self.linebuffer.find(b'\n')
        if eol == -1:
            # RTCM3 JSON can be over 4.4k long, so go big
            frag = self.sock.recv(8192)

            self.linebuffer += frag
            if not self.linebuffer:
                if self.verbose > 1:
                    sys.stderr.write(
                        "poll: no available data: returning -1.\n")
                # Read failed
                return -1

            eol = self.linebuffer.find(b'\n')
            if eol == -1:
                if self.verbose > 1:
                    sys.stderr.write("poll: partial message: returning 0.\n")
                # Read succeeded, but only got a fragment
                self.response = ''  # Don't duplicate last response
                return 0
        else:
            if self.verbose > 1:
                sys.stderr.write("poll: fetching from buffer.\n")

        # We got a line
        eol += 1
        # Provide the response in both 'str' and 'bytes' form
        self.bresponse = self.linebuffer[:eol]
        self.response = polystr(self.bresponse)
        self.linebuffer = self.linebuffer[eol:]

        # Can happen if daemon terminates while we're reading.
        if not self.response:
            return -1
        if 1 < self.verbose:
            sys.stderr.write("poll: data is %s\n" % repr(self.response))
        self.received = time.time()
        # We got a \n-terminated line
        return len(self.response)

    # Note that the 'data' method is sometimes shadowed by a name
    # collision, rendering it unusable.  The documentation recommends
    # accessing 'response' directly.  Consequently, no accessor method
    # for 'bresponse' is currently provided.

    def data(self):
        "Return the client data buffer."
        return self.response

    def send(self, commands):
        "Ship commands to the daemon."
        lineend = "\n"
        if isinstance(commands, bytes):
            lineend = polybytes("\n")
        if not commands.endswith(lineend):
            commands += lineend

        if self.sock is None:
            self.stream_command = commands
        else:
            self.sock.send(polybytes(commands))


class json_error(BaseException):
    "Class for JSON errors"

    def __init__(self, data, explanation):
        BaseException.__init__(self)
        self.data = data
        self.explanation = explanation


class gpsjson(object):
    "Basic JSON decoding."

    def __init__(self):
        self.data = None
        self.stream_command = None
        self.verbose = -1

    def __iter__(self):
        "Broken __iter__"
        return self

    def unpack(self, buf):
        "Unpack a JSON string"
        try:
            self.data = dictwrapper(json.loads(buf.strip(), encoding="ascii"))
        except ValueError as e:
            raise json_error(buf, e.args[0])
        # Should be done for any other array-valued subobjects, too.
        # This particular logic can fire on SKY or RTCM2 objects.
        if hasattr(self.data, "satellites"):
            self.data.satellites = [dictwrapper(x)
                                    for x in self.data.satellites]

    def stream(self, flags=0, devpath=None):
        "Control streaming reports from the daemon,"

        if 0 < flags:
            self.stream_command = self.generate_stream_command(flags, devpath)
        else:
            self.stream_command = self.enqueued

        if self.stream_command:
            if self.verbose > 1:
                sys.stderr.write("send: stream as:"
                                 " {}\n".format(self.stream_command))
            self.send(self.stream_command)
        else:
            raise TypeError("Invalid streaming command!! : " + str(flags))

    def generate_stream_command(self, flags=0, devpath=None):
        "Generate stream command"
        if flags & WATCH_OLDSTYLE:
            return self.generate_stream_command_old_style(flags)

        return self.generate_stream_command_new_style(flags, devpath)

    @staticmethod
    def generate_stream_command_old_style(flags=0):
        "Generate stream command, old style"
        if flags & WATCH_DISABLE:
            arg = "w-"
            if flags & WATCH_NMEA:
                arg += 'r-'

        elif flags & WATCH_ENABLE:
            arg = 'w+'
            if flags & WATCH_NMEA:
                arg += 'r+'

        return arg

    @staticmethod
    def generate_stream_command_new_style(flags=0, devpath=None):
        "Generate stream command, new style"

        if (flags & (WATCH_JSON | WATCH_OLDSTYLE | WATCH_NMEA |
                     WATCH_RAW)) == 0:
            flags |= WATCH_JSON

        if flags & WATCH_DISABLE:
            arg = '?WATCH={"enable":false'
            if flags & WATCH_JSON:
                arg += ',"json":false'
            if flags & WATCH_NMEA:
                arg += ',"nmea":false'
            if flags & WATCH_RARE:
                arg += ',"raw":1'
            if flags & WATCH_RAW:
                arg += ',"raw":2'
            if flags & WATCH_SCALED:
                arg += ',"scaled":false'
            if flags & WATCH_TIMING:
                arg += ',"timing":false'
            if flags & WATCH_SPLIT24:
                arg += ',"split24":false'
            if flags & WATCH_PPS:
                arg += ',"pps":false'
        else:  # flags & WATCH_ENABLE:
            arg = '?WATCH={"enable":true'
            if flags & WATCH_JSON:
                arg += ',"json":true'
            if flags & WATCH_NMEA:
                arg += ',"nmea":true'
            if flags & WATCH_RARE:
                arg += ',"raw":1'
            if flags & WATCH_RAW:
                arg += ',"raw":2'
            if flags & WATCH_SCALED:
                arg += ',"scaled":true'
            if flags & WATCH_TIMING:
                arg += ',"timing":true'
            if flags & WATCH_SPLIT24:
                arg += ',"split24":true'
            if flags & WATCH_PPS:
                arg += ',"pps":true'
            if flags & WATCH_DEVICE:
                arg += ',"device":"%s"' % devpath
        arg += "}"
        return arg


class dictwrapper(object):
    "Wrapper that yields both class and dictionary behavior,"

    def __init__(self, ddict):
        "Init class dictwrapper"
        self.__dict__ = ddict

    def get(self, k, d=None):
        "Get dictwrapper"
        return self.__dict__.get(k, d)

    def keys(self):
        "Keys dictwrapper"
        return self.__dict__.keys()

    def __getitem__(self, key):
        "Emulate dictionary, for new-style interface."
        return self.__dict__[key]

    def __iter__(self):
        "Iterate dictwrapper"
        return self.__dict__.__iter__()

    def __setitem__(self, key, val):
        "Emulate dictionary, for new-style interface."
        self.__dict__[key] = val

    def __contains__(self, key):
        "Find key in dictwrapper"
        return key in self.__dict__

    def __str__(self):
        "dictwrapper to string"
        return "<dictwrapper: " + str(self.__dict__) + ">"
    __repr__ = __str__

    def __len__(self):
        "length of dictwrapper"
        return len(self.__dict__)

#
# Someday a cleaner Python interface using this machinery will live here
#

# End