client.py 12 KB

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