client.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. "gpsd client functions"
  2. # This file is Copyright 2019 by the GPSD project
  3. # SPDX-License-Identifier: BSD-2-Clause
  4. #
  5. # This code run compatibly under Python 2 and 3.x for x >= 2.
  6. # Preserve this property!
  7. from __future__ import absolute_import, print_function, division
  8. import json
  9. import select
  10. import socket
  11. import sys
  12. import time
  13. from .misc import polystr, polybytes
  14. from .watch_options import *
  15. GPSD_PORT = "2947"
  16. class gpscommon(object):
  17. "Isolate socket handling and buffering from the protocol interpretation."
  18. host = "127.0.0.1"
  19. port = GPSD_PORT
  20. def __init__(self, host="127.0.0.1", port=GPSD_PORT, verbose=0,
  21. should_reconnect=False):
  22. self.stream_command = b''
  23. self.linebuffer = b''
  24. self.received = time.time()
  25. self.reconnect = should_reconnect
  26. self.verbose = verbose
  27. self.sock = None # in case we blow up in connect
  28. # Provide the response in both 'str' and 'bytes' form
  29. self.bresponse = b''
  30. self.response = polystr(self.bresponse)
  31. if host is not None and port is not None:
  32. self.host = host
  33. self.port = port
  34. self.connect(self.host, self.port)
  35. def connect(self, host, port):
  36. """Connect to a host on a given port.
  37. If the hostname ends with a colon (`:') followed by a number, and
  38. there is no port specified, that suffix will be stripped off and the
  39. number interpreted as the port number to use.
  40. """
  41. if not port and (host.find(':') == host.rfind(':')):
  42. i = host.rfind(':')
  43. if i >= 0:
  44. host, port = host[:i], host[i + 1:]
  45. try:
  46. port = int(port)
  47. except ValueError:
  48. raise socket.error("nonnumeric port")
  49. # if self.verbose > 0:
  50. # print 'connect:', (host, port)
  51. msg = "getaddrinfo returns an empty list"
  52. self.sock = None
  53. for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
  54. af, socktype, proto, _canonname, sa = res
  55. try:
  56. self.sock = socket.socket(af, socktype, proto)
  57. # if self.debuglevel > 0: print 'connect:', (host, port)
  58. self.sock.connect(sa)
  59. if self.verbose > 0:
  60. print('connected to tcp://{}:{}'.format(host, port))
  61. break
  62. # do not use except ConnectionRefusedError
  63. # # Python 2.7 doc does have this exception
  64. except socket.error as e:
  65. if self.verbose > 1:
  66. msg = str(e) + ' (to {}:{})'.format(host, port)
  67. sys.stderr.write("error: {}\n".format(msg.strip()))
  68. self.close()
  69. raise # propagate error to caller
  70. def close(self):
  71. "Close the gpsd socket"
  72. if self.sock:
  73. self.sock.close()
  74. self.sock = None
  75. def __del__(self):
  76. "Close the gpsd socket"
  77. self.close()
  78. def waiting(self, timeout=0):
  79. "Return True if data is ready for the client."
  80. if self.linebuffer:
  81. return True
  82. if self.sock is None:
  83. return False
  84. (winput, _woutput, _wexceptions) = select.select(
  85. (self.sock,), (), (), timeout)
  86. return winput != []
  87. def read(self):
  88. "Wait for and read data being streamed from the daemon."
  89. if None is self.sock:
  90. self.connect(self.host, self.port)
  91. if None is self.sock:
  92. return -1
  93. self.stream()
  94. eol = self.linebuffer.find(b'\n')
  95. if eol == -1:
  96. # RTCM3 JSON can be over 4.4k long, so go big
  97. frag = self.sock.recv(8192)
  98. self.linebuffer += frag
  99. if not self.linebuffer:
  100. if self.verbose > 1:
  101. sys.stderr.write(
  102. "poll: no available data: returning -1.\n")
  103. # Read failed
  104. return -1
  105. eol = self.linebuffer.find(b'\n')
  106. if eol == -1:
  107. if self.verbose > 1:
  108. sys.stderr.write("poll: partial message: returning 0.\n")
  109. # Read succeeded, but only got a fragment
  110. self.response = '' # Don't duplicate last response
  111. self.bresponse = '' # Don't duplicate last response
  112. return 0
  113. else:
  114. if self.verbose > 1:
  115. sys.stderr.write("poll: fetching from buffer.\n")
  116. # We got a line
  117. eol += 1
  118. # Provide the response in both 'str' and 'bytes' form
  119. self.bresponse = self.linebuffer[:eol]
  120. self.response = polystr(self.bresponse)
  121. self.linebuffer = self.linebuffer[eol:]
  122. # Can happen if daemon terminates while we're reading.
  123. if not self.response:
  124. return -1
  125. if 1 < self.verbose:
  126. sys.stderr.write("poll: data is %s\n" % repr(self.response))
  127. self.received = time.time()
  128. # We got a \n-terminated line
  129. return len(self.response)
  130. # Note that the 'data' method is sometimes shadowed by a name
  131. # collision, rendering it unusable. The documentation recommends
  132. # accessing 'response' directly. Consequently, no accessor method
  133. # for 'bresponse' is currently provided.
  134. def data(self):
  135. "Return the client data buffer."
  136. return self.response
  137. def send(self, commands):
  138. "Ship commands to the daemon."
  139. lineend = "\n"
  140. if isinstance(commands, bytes):
  141. lineend = polybytes("\n")
  142. if not commands.endswith(lineend):
  143. commands += lineend
  144. if self.sock is None:
  145. self.stream_command = commands
  146. else:
  147. self.sock.send(polybytes(commands))
  148. class json_error(BaseException):
  149. "Class for JSON errors"
  150. def __init__(self, data, explanation):
  151. BaseException.__init__(self)
  152. self.data = data
  153. self.explanation = explanation
  154. class gpsjson(object):
  155. "Basic JSON decoding."
  156. def __init__(self):
  157. self.data = None
  158. self.stream_command = None
  159. self.enqueued = None
  160. self.verbose = -1
  161. def __iter__(self):
  162. "Broken __iter__"
  163. return self
  164. def unpack(self, buf):
  165. "Unpack a JSON string"
  166. try:
  167. # json.loads(,encoding=) deprecated Python 3.1. Gone in 3.9
  168. # like it or not, data is now UTF-8
  169. self.data = dictwrapper(json.loads(buf.strip()))
  170. except ValueError as e:
  171. raise json_error(buf, e.args[0])
  172. # Should be done for any other array-valued subobjects, too.
  173. # This particular logic can fire on SKY or RTCM2 objects.
  174. if hasattr(self.data, "satellites"):
  175. self.data.satellites = [dictwrapper(x)
  176. for x in self.data.satellites]
  177. def stream(self, flags=0, devpath=None):
  178. "Control streaming reports from the daemon,"
  179. if 0 < flags:
  180. self.stream_command = self.generate_stream_command(flags, devpath)
  181. else:
  182. self.stream_command = self.enqueued
  183. if self.stream_command:
  184. if self.verbose > 1:
  185. sys.stderr.write("send: stream as:"
  186. " {}\n".format(self.stream_command))
  187. self.send(self.stream_command)
  188. else:
  189. raise TypeError("Invalid streaming command!! : " + str(flags))
  190. def generate_stream_command(self, flags=0, devpath=None):
  191. "Generate stream command"
  192. if flags & WATCH_OLDSTYLE:
  193. return self.generate_stream_command_old_style(flags)
  194. return self.generate_stream_command_new_style(flags, devpath)
  195. @staticmethod
  196. def generate_stream_command_old_style(flags=0):
  197. "Generate stream command, old style"
  198. if flags & WATCH_DISABLE:
  199. arg = "w-"
  200. if flags & WATCH_NMEA:
  201. arg += 'r-'
  202. elif flags & WATCH_ENABLE:
  203. arg = 'w+'
  204. if flags & WATCH_NMEA:
  205. arg += 'r+'
  206. return arg
  207. @staticmethod
  208. def generate_stream_command_new_style(flags=0, devpath=None):
  209. "Generate stream command, new style"
  210. if (flags & (WATCH_JSON | WATCH_OLDSTYLE | WATCH_NMEA |
  211. WATCH_RAW)) == 0:
  212. flags |= WATCH_JSON
  213. if flags & WATCH_DISABLE:
  214. arg = '?WATCH={"enable":false'
  215. if flags & WATCH_JSON:
  216. arg += ',"json":false'
  217. if flags & WATCH_NMEA:
  218. arg += ',"nmea":false'
  219. if flags & WATCH_RARE:
  220. arg += ',"raw":1'
  221. if flags & WATCH_RAW:
  222. arg += ',"raw":2'
  223. if flags & WATCH_SCALED:
  224. arg += ',"scaled":false'
  225. if flags & WATCH_TIMING:
  226. arg += ',"timing":false'
  227. if flags & WATCH_SPLIT24:
  228. arg += ',"split24":false'
  229. if flags & WATCH_PPS:
  230. arg += ',"pps":false'
  231. else: # flags & WATCH_ENABLE:
  232. arg = '?WATCH={"enable":true'
  233. if flags & WATCH_JSON:
  234. arg += ',"json":true'
  235. if flags & WATCH_NMEA:
  236. arg += ',"nmea":true'
  237. if flags & WATCH_RARE:
  238. arg += ',"raw":1'
  239. if flags & WATCH_RAW:
  240. arg += ',"raw":2'
  241. if flags & WATCH_SCALED:
  242. arg += ',"scaled":true'
  243. if flags & WATCH_TIMING:
  244. arg += ',"timing":true'
  245. if flags & WATCH_SPLIT24:
  246. arg += ',"split24":true'
  247. if flags & WATCH_PPS:
  248. arg += ',"pps":true'
  249. if flags & WATCH_DEVICE:
  250. arg += ',"device":"%s"' % devpath
  251. arg += "}"
  252. return arg
  253. class dictwrapper(object):
  254. "Wrapper that yields both class and dictionary behavior,"
  255. def __init__(self, ddict):
  256. "Init class dictwrapper"
  257. self.__dict__ = ddict
  258. def get(self, k, d=None):
  259. "Get dictwrapper"
  260. return self.__dict__.get(k, d)
  261. def keys(self):
  262. "Keys dictwrapper"
  263. return self.__dict__.keys()
  264. def __getitem__(self, key):
  265. "Emulate dictionary, for new-style interface."
  266. return self.__dict__[key]
  267. def __iter__(self):
  268. "Iterate dictwrapper"
  269. return self.__dict__.__iter__()
  270. def __setitem__(self, key, val):
  271. "Emulate dictionary, for new-style interface."
  272. self.__dict__[key] = val
  273. def __contains__(self, key):
  274. "Find key in dictwrapper"
  275. return key in self.__dict__
  276. def __str__(self):
  277. "dictwrapper to string"
  278. return "<dictwrapper: " + str(self.__dict__) + ">"
  279. __repr__ = __str__
  280. def __len__(self):
  281. "length of dictwrapper"
  282. return len(self.__dict__)
  283. #
  284. # Someday a cleaner Python interface using this machinery will live here
  285. #
  286. # End
  287. # vim: set expandtab shiftwidth=4