#!/usr/bin/env python3 # coding=utf-8 """ agps3.py is a Python 2.7-3.5 GPSD interface (http://www.catb.org/gpsd) Defaults host='127.0.0.1', port=2947, gpsd_protocol='json' in two classes. 1) 'GPSDSocket' creates a GPSD socket connection & request/retrieve GPSD output. 2) 'DataStream' unpacks the streamed gpsd data into object attribute values. Import from gps3 import agps3 Instantiate gpsd_socket = agps3.GPSDSocket() data_stream = agps3.DataStream() Run gpsd_socket.connect() gpsd_socket.watch() Iterate for new_data in gpsd_socket if new_data: data_stream.unpack(new_data) Use print('Lat/Lon = ',data_stream.lat,' ', data_stream.lon) print('Altitude = ',data_stream.alt) Consult Lines 136-ff for Attribute/Key possibilities. """ from __future__ import print_function import json import select import socket import sys __author__ = 'Moe' __copyright__ = 'Copyright 2015-2017 Moe' __license__ = 'MIT' __version__ = '0.35' HOST = '127.0.0.1' # gpsd GPSD_PORT = 2947 # defaults PROTOCOL = 'json' # " class GPSDSocket(object): """Establish a socket with gpsd, by which to send commands and receive data.""" def __init__(self): self.streamSock = None self.response = None def connect(self, host=HOST, port=GPSD_PORT): """Connect to a host on a given port. Arguments: host: default host='127.0.0.1' port: default port=2947 """ for alotta_stuff in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): family, socktype, proto, _canonname, host_port = alotta_stuff try: self.streamSock = socket.socket(family, socktype, proto) self.streamSock.connect(host_port) self.streamSock.setblocking(False) except (OSError, IOError) as error: sys.stderr.write(f'\r\nGPSDSocket.connect exception is--> {error}') sys.stderr.write(f'\r\nAGPS3 connection to gpsd at \'{host}\' on port \'{port}\' failed\r\n') def watch(self, enable=True, gpsd_protocol=PROTOCOL, devicepath=None): """watch gpsd in various gpsd_protocols or devices. Arguments: enable: (bool) stream data to socket gpsd_protocol: (str) 'json' | 'nmea' | 'rare' | 'raw' | 'scaled' | 'split24' | 'pps' devicepath: (str) device path - '/dev/ttyUSBn' for some number n or '/dev/whatever_works' Returns: command: (str) e.g., '?WATCH={"enable":true,"json":true};' """ # N.B.: 'timing' requires special attention, as it is undocumented and lives with dragons. command = '?WATCH={{"enable":true,"{0}":true}}'.format(gpsd_protocol) if gpsd_protocol == 'rare': # 1 for a channel, gpsd reports the unprocessed NMEA or AIVDM data stream command = command.replace('"rare":true', '"raw":1') if gpsd_protocol == 'raw': # 2 channel that processes binary data, received data verbatim without hex-dumping. command = command.replace('"raw":true', '"raw",2') if not enable: command = command.replace('true', 'false') # sets -all- command values false . if devicepath: command = command.replace('}', ',"device":"') + devicepath + '"}' return self.send(command) def send(self, commands): """Ship commands to the daemon Arguments: commands: e.g., '?WATCH={{'enable':true,'json':true}}'|'?VERSION;'|'?DEVICES;'|'?DEVICE;'|'?POLL;' """ try: self.streamSock.send(bytes(commands, encoding='utf-8')) except TypeError: self.streamSock.send(commands) # 2.7 chokes on 'bytes' and 'encoding=' except (OSError, IOError) as error: # HEY MOE, LEAVE THIS ALONE FOR NOW! sys.stderr.write(f'\nAGPS3 send command fail with {error}\n') # [Errno 107] Transport endpoint is not connected def __iter__(self): """banana""" # <--- for scale return self def next(self, timeout=0): """Return empty unless new data is ready for the client. Arguments: timeout: Default timeout=0 range zero to float specifies a time-out as a floating point number in seconds. Will sit and wait for timeout seconds. When the timeout argument is omitted the function blocks until at least one file descriptor is ready. A time-out value of zero specifies a poll and never blocks. """ try: waitin, _waitout, _waiterror = select.select((self.streamSock,), (), (), timeout) if not waitin: return else: gpsd_response = self.streamSock.makefile() # '.makefile(buffering=4096)' In strictly Python3 self.response = gpsd_response.readline() return self.response except StopIteration as error: sys.stderr.write('The readline exception in GPSDSocket.next is--> {}'.format(error)) __next__ = next # Workaround for changes in iterating between Python 2.7 and 3 def close(self): """turn off stream and close socket""" if self.streamSock: self.watch(enable=False) self.streamSock.close() self.streamSock = None class DataStream(object): """Retrieve JSON Object(s) from GPSDSocket and unpack it into respective object attributes, e.g., self.lat yielding hours of fun and entertainment. """ packages = { 'VERSION': {'release', 'proto_major', 'proto_minor', 'remote', 'rev'}, 'TPV': {'alt', 'climb', 'device', 'epc', 'epd', 'eps', 'ept', 'epv', 'epx', 'epy', 'lat', 'lon', 'mode', 'speed', 'tag', 'time', 'track'}, 'SKY': {'satellites', 'gdop', 'hdop', 'pdop', 'tdop', 'vdop', 'xdop', 'ydop'}, # Subset of SKY: \\\'satellites': {'PRN', 'ss', 'el', 'az', 'used'}/// # is always present. 'GST': {'alt', 'device', 'lat', 'lon', 'major', 'minor', 'orient', 'rms', 'time'}, # In 'GST', 'lat' and 'lon' present a name collision and are amended to 'sdlat', 'sdlon', # because they are standard deviations of of 'TPV' 'lat' and 'lon' 'ATT': {'acc_len', 'acc_x', 'acc_y', 'acc_z', 'depth', 'device', 'dip', 'gyro_x', 'gyro_y', 'heading', 'mag_len', 'mag_st', 'mag_x', 'mag_y', 'mag_z', 'pitch', 'pitch_st', 'roll', 'roll_st', 'temperature', 'time', 'yaw', 'yaw_st'}, # 'POLL': {'active', 'tpv', 'sky', 'time'}, 'PPS': {'device', 'clock_sec', 'clock_nsec', 'real_sec', 'real_nsec', 'precision'}, 'TOFF': {'device', 'clock_sec', 'clock_nsec', 'real_sec', 'real_nsec'}, 'DEVICES': {'devices', 'remote'}, 'DEVICE': {'activated', 'bps', 'cycle', 'mincycle', 'driver', 'flags', 'native', 'parity', 'path', 'stopbits', 'subtype'}, # 'AIS': {} # see: http://catb.org/gpsd/AIVDM.html 'ERROR': {'message'}} # TODO: Full suite of possible GPSD output def __init__(self): """Potential data packages from gpsd for a generator of class attributes""" for laundry_list in self.packages.values(): for item in laundry_list: # Fudge around the namespace collision with GST data package lat/lon being standard deviations if laundry_list == 'GST' and item == 'lat' or 'lon': setattr(self, 'sd' + item, 'n/a') setattr(self, item, 'n/a') def unpack(self, gpsd_socket_response): """Sets new socket data as DataStream attributes in those initialised dictionaries Arguments: gpsd_socket_response (json object): Provides: self attributes, e.g., self.lat, self.gdop Raises: AttributeError: 'str' object has no attribute 'keys' when the device falls out of the system ValueError, KeyError: most likely extra, or mangled JSON data, should not happen, but that applies to a lot of things. """ try: fresh_data = json.loads(gpsd_socket_response) # 'class' is popped for iterator lead class_name = fresh_data.pop('class') for key in self.packages[class_name]: # Fudge around the namespace collision with GST data package lat/lon being standard deviations if class_name == 'GST' and key == 'lat' or 'lon': setattr(self, 'sd' + key, fresh_data.get(key, 'n/a')) setattr(self, key, fresh_data.get(key, 'n/a')) # Updates and restores 'n/a' if attribute is absent in the data except AttributeError: # 'str' object has no attribute 'keys' sys.stderr.write('There is an unexpected exception unpacking JSON object') return except (ValueError, KeyError) as error: sys.stderr.write(str(error)) # Extra data or aberrant data in stream. return if __name__ == '__main__': print(__doc__) # # Someday a cleaner Python interface will live here # # End