From c64ba24072c1d33fcbcf47159d3e6e53a0bdac3b Mon Sep 17 00:00:00 2001 From: Daniel_M_Williams Date: Fri, 9 Mar 2018 11:07:39 -0500 Subject: [PATCH] [feature] Client-side libraries may automatically reconnect - moved watch options to their own file to rationalize definition - defaults behavior is to exit-on-failure - activated by calling to `gps.gps(..., reconnect=True)` - refactors stream(...) function to deduplicate processing - added dictionary methods to 'class dictwrapper' --- gps/client.py | 138 ++++++++++++++++++++++++++++++++++++++++++--------------- gps/gps.py | 94 ++++----------------------------------- gps/options.py | 65 +++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 122 deletions(-) create mode 100644 gps/options.py diff --git a/gps/client.py b/gps/client.py index c660d92..38469db 100644 --- a/gps/client.py +++ b/gps/client.py @@ -13,18 +13,26 @@ import time from .misc import polystr, polybytes -GPSD_PORT = "2947" +from .options import * +GPSD_PORT = "2947" class gpscommon(object): "Isolate socket handling and buffering from the protocol interpretation." - def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0): + def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0, should_reconnect=False): self.sock = None # in case we blow up in connect self.linebuffer = b'' self.verbose = verbose + self.stream_command = '' + self.reconnect = should_reconnect + self.last_read = time.time() # time of last successful non-zero read + self.timeout = 10.0 # after this many seconds, assume connection is dead + if host is not None: - self.connect(host, port) + self.host = host + self.port = port + self.connect(self.host, self.port) def connect(self, host, port): """Connect to a host on a given port. @@ -41,8 +49,8 @@ class gpscommon(object): 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): @@ -51,14 +59,19 @@ class gpscommon(object): 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.... {}:{}... streaming '.format(host, port)) + self.stream() + break + except socket.error as e: - msg = str(e) - # if self.debuglevel > 0: print 'connect fail:', (host, port) + msg = str(e) + ' (to {}:{})'.format(host, port) + if self.verbose > 1: + sys.stderr.write("error: {}\n".format(msg.strip())) self.close() - continue - break - if not self.sock: - raise socket.error(msg) + + if not self.reconnect: + raise def close(self): if self.sock: @@ -78,24 +91,41 @@ class gpscommon(object): def read(self): "Wait for and read data being streamed from the daemon." - if self.verbose > 1: - sys.stderr.write("poll: reading from daemon...\n") + + age = time.time() - self.last_read + if age > self.timeout and self.sock: + if self.verbose > 1: + sys.stderr.write("disconnect: connection expired...\n") + self.close() + + 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 + if self.verbose > 1: + sys.stderr.write("poll: reading from daemon...\n") + frag = self.sock.recv(8192) + self.linebuffer += frag - if self.verbose > 1: - sys.stderr.write("poll: read complete.\n") - if not self.linebuffer: + if 0 == len(self.linebuffer): if self.verbose > 1: - sys.stderr.write("poll: returning -1.\n") + sys.stderr.write("poll: no available data: returning -1.\n") # Read failed return -1 + + self.last_read = time.time() + eol = self.linebuffer.find(b'\n') if eol == -1: if self.verbose > 1: - sys.stderr.write("poll: returning 0.\n") + 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 @@ -132,20 +162,8 @@ class gpscommon(object): "Ship commands to the daemon." if not commands.endswith("\n"): commands += "\n" - self.sock.send(polybytes(commands)) - -WATCH_ENABLE = 0x000001 # enable streaming -WATCH_DISABLE = 0x000002 # disable watching -WATCH_JSON = 0x000010 # JSON output -WATCH_NMEA = 0x000020 # output in NMEA -WATCH_RARE = 0x000040 # output of packets in hex -WATCH_RAW = 0x000080 # output of raw packets -WATCH_SCALED = 0x000100 # scale output to floats -WATCH_TIMING = 0x000200 # timing information -WATCH_SPLIT24 = 0x001000 # split AIS Type 24s -WATCH_PPS = 0x002000 # enable PPS in raw/NMEA -WATCH_DEVICE = 0x000800 # watch specific device - + if None is not self.sock: + self.sock.send(polybytes(commands)) class json_error(BaseException): def __init__(self, data, explanation): @@ -171,8 +189,42 @@ class gpsjson(object): self.data.satellites = [dictwrapper(x) for x in self.data.satellites] - def stream(self, flags=0, devpath=None): - "Control streaming reports from the daemon," + def set_flags(self, flags=0, devpath=None): + self.stream_command = self.generate_stream_command( flags, devpath ) + self.stream() + + def generate_stream_command(self, flags=0, devpath=None): + + if flags & WATCH_OLDSTYLE: + cmd = self.generate_stream_command_old_style(flags) + else: + cmd = self.generate_stream_command_new_style(flags, devpath) + + if self.verbose > 1: + sys.stderr.write("set: flags (0x{}) => {}\n".format(hex(flags), cmd)) + + return cmd + + @staticmethod + def generate_stream_command_old_style(flags=0): + if flags & WATCH_DISABLE: + arg = "w-" + if flags & WATCH_NMEA: + arg += 'r-' + return arg + 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): + + 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: @@ -191,7 +243,8 @@ class gpsjson(object): arg += ',"split24":false' if flags & WATCH_PPS: arg += ',"pps":false' - else: # flags & WATCH_ENABLE: + return arg + "}\n" + elif flags & WATCH_ENABLE: arg = '?WATCH={"enable":true' if flags & WATCH_JSON: arg += ',"json":true' @@ -211,8 +264,18 @@ class gpsjson(object): arg += ',"pps":true' if flags & WATCH_DEVICE: arg += ',"device":"%s"' % devpath - return self.send(arg + "}") + return arg + "}\n" + else: + return "" + def stream(self, flags=0, devpath=None): + "Control streaming reports from the daemon," + + if 0 < flags: + self.set_flags(flags, devpath) + + if self.stream_command: + self.send(self.stream_command) class dictwrapper(object): "Wrapper that yields both class and dictionary behavior," @@ -241,6 +304,9 @@ class dictwrapper(object): return "" __repr__ = __str__ + def __len__(self): + return len(self.__dict__) + # # Someday a cleaner Python interface using this machinery will live here # diff --git a/gps/gps.py b/gps/gps.py index bc35f99..f3b7b26 100755 --- a/gps/gps.py +++ b/gps/gps.py @@ -19,75 +19,14 @@ # Preserve this property! from __future__ import absolute_import, print_function, division +# since Python 2.6 +from math import isnan + from .client import * +from .options import * NaN = float('nan') - -def isnan(x): - return str(x) == 'nan' - -# Don't hand-hack this list, it's generated. -ONLINE_SET = (1 << 1) -TIME_SET = (1 << 2) -TIMERR_SET = (1 << 3) -LATLON_SET = (1 << 4) -ALTITUDE_SET = (1 << 5) -SPEED_SET = (1 << 6) -TRACK_SET = (1 << 7) -CLIMB_SET = (1 << 8) -STATUS_SET = (1 << 9) -MODE_SET = (1 << 10) -DOP_SET = (1 << 11) -HERR_SET = (1 << 12) -VERR_SET = (1 << 13) -ATTITUDE_SET = (1 << 14) -SATELLITE_SET = (1 << 15) -SPEEDERR_SET = (1 << 16) -TRACKERR_SET = (1 << 17) -CLIMBERR_SET = (1 << 18) -DEVICE_SET = (1 << 19) -DEVICELIST_SET = (1 << 20) -DEVICEID_SET = (1 << 21) -RTCM2_SET = (1 << 22) -RTCM3_SET = (1 << 23) -AIS_SET = (1 << 24) -PACKET_SET = (1 << 25) -SUBFRAME_SET = (1 << 26) -GST_SET = (1 << 27) -VERSION_SET = (1 << 28) -POLICY_SET = (1 << 29) -LOGMESSAGE_SET = (1 << 30) -ERROR_SET = (1 << 31) -TIMEDRIFT_SET = (1 << 32) -EOF_SET = (1 << 33) -SET_HIGH_BIT = 34 -UNION_SET = (RTCM2_SET | RTCM3_SET | SUBFRAME_SET | AIS_SET | VERSION_SET - | DEVICELIST_SET | ERROR_SET | GST_SET) -STATUS_NO_FIX = 0 -STATUS_FIX = 1 -STATUS_DGPS_FIX = 2 -MODE_NO_FIX = 1 -MODE_2D = 2 -MODE_3D = 3 -MAXCHANNELS = 72 # Copied from gps.h, but not required to match -SIGNAL_STRENGTH_UNKNOWN = NaN - -WATCH_ENABLE = 0x000001 # enable streaming -WATCH_DISABLE = 0x000002 # disable watching -WATCH_JSON = 0x000010 # JSON output -WATCH_NMEA = 0x000020 # output in NMEA -WATCH_RARE = 0x000040 # output of packets in hex -WATCH_RAW = 0x000080 # output of raw packets -WATCH_SCALED = 0x000100 # scale output to floats -WATCH_TIMING = 0x000200 # timing information -WATCH_DEVICE = 0x000800 # watch specific device -WATCH_SPLIT24 = 0x001000 # split AIS Type 24s -WATCH_PPS = 0x002000 # enable PPS JSON -WATCH_NEWSTYLE = 0x010000 # force JSON streaming -WATCH_OLDSTYLE = 0x020000 # force old-style streaming - - class gpsfix(object): def __init__(self): self.mode = MODE_NO_FIX @@ -182,8 +121,8 @@ class gpsdata(object): class gps(gpscommon, gpsdata, gpsjson): "Client interface to a running gpsd instance." - def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0, mode=0): - gpscommon.__init__(self, host, port, verbose) + def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0, mode=0, reconnect=False): + gpscommon.__init__(self, host, port, verbose, reconnect) gpsdata.__init__(self) if mode: self.stream(mode) @@ -288,25 +227,8 @@ class gps(gpscommon, gpsdata, gpsjson): def stream(self, flags=0, devpath=None): "Ask gpsd to stream reports at your client." - if (flags & (WATCH_JSON | WATCH_OLDSTYLE | WATCH_NMEA - | WATCH_RAW)) == 0: - flags |= WATCH_JSON - if flags & WATCH_DISABLE: - if flags & WATCH_OLDSTYLE: - arg = "w-" - if flags & WATCH_NMEA: - arg += 'r-' - return self.send(arg) - else: - gpsjson.stream(self, flags, devpath) - else: # flags & WATCH_ENABLE: - if flags & WATCH_OLDSTYLE: - arg = 'w+' - if flags & WATCH_NMEA: - arg += 'r+' - return self.send(arg) - else: - gpsjson.stream(self, flags, devpath) + + gpsjson.stream(self, flags, devpath) def is_sbas(prn): diff --git a/gps/options.py b/gps/options.py new file mode 100644 index 0000000..cf3994f --- /dev/null +++ b/gps/options.py @@ -0,0 +1,65 @@ + +NaN = float('nan') + +# Don't hand-hack this list, it's generated. +ONLINE_SET = (1 << 1) +TIME_SET = (1 << 2) +TIMERR_SET = (1 << 3) +LATLON_SET = (1 << 4) +ALTITUDE_SET = (1 << 5) +SPEED_SET = (1 << 6) +TRACK_SET = (1 << 7) +CLIMB_SET = (1 << 8) +STATUS_SET = (1 << 9) +MODE_SET = (1 << 10) +DOP_SET = (1 << 11) +HERR_SET = (1 << 12) +VERR_SET = (1 << 13) +ATTITUDE_SET = (1 << 14) +SATELLITE_SET = (1 << 15) +SPEEDERR_SET = (1 << 16) +TRACKERR_SET = (1 << 17) +CLIMBERR_SET = (1 << 18) +DEVICE_SET = (1 << 19) +DEVICELIST_SET = (1 << 20) +DEVICEID_SET = (1 << 21) +RTCM2_SET = (1 << 22) +RTCM3_SET = (1 << 23) +AIS_SET = (1 << 24) +PACKET_SET = (1 << 25) +SUBFRAME_SET = (1 << 26) +GST_SET = (1 << 27) +VERSION_SET = (1 << 28) +POLICY_SET = (1 << 29) +LOGMESSAGE_SET = (1 << 30) +ERROR_SET = (1 << 31) +TIMEDRIFT_SET = (1 << 32) +EOF_SET = (1 << 33) +SET_HIGH_BIT = 34 +UNION_SET = (RTCM2_SET | RTCM3_SET | SUBFRAME_SET | AIS_SET | VERSION_SET + | DEVICELIST_SET | ERROR_SET | GST_SET) +STATUS_NO_FIX = 0 +STATUS_FIX = 1 +STATUS_DGPS_FIX = 2 +MODE_NO_FIX = 1 +MODE_2D = 2 +MODE_3D = 3 +MAXCHANNELS = 72 # Copied from gps.h, but not required to match +SIGNAL_STRENGTH_UNKNOWN = NaN + +# WATCH options - controls what data is streamed, and how it's converted +WATCH_ENABLE = 0x000001 # enable streaming +WATCH_DISABLE = 0x000002 # disable watching +WATCH_JSON = 0x000010 # JSON output +WATCH_NMEA = 0x000020 # output in NMEA +WATCH_RARE = 0x000040 # output of packets in hex +WATCH_RAW = 0x000080 # output of raw packets + +WATCH_SCALED = 0x000100 # scale output to floats +WATCH_TIMING = 0x000200 # timing information +WATCH_DEVICE = 0x000800 # watch specific device +WATCH_SPLIT24 = 0x001000 # split AIS Type 24s +WATCH_PPS = 0x002000 # enable PPS JSON + +WATCH_NEWSTYLE = 0x010000 # force JSON streaming +WATCH_OLDSTYLE = 0x020000 # force old-style streaming -- 2.7.4