gpsplot.py.in 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. #!@PYSHEBANG@
  2. # -*- coding: utf-8 -*-
  3. # @GENERATED@
  4. # This file is Copyright 2020 by the GPSD project
  5. # SPDX-License-Identifier: BSD-2-clause
  6. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  7. # Preserve this property!
  8. #
  9. # Codacy D203 and D211 conflict, I choose D203
  10. # Codacy D212 and D213 conflict, I choose D212
  11. """gpsplot -- plot gpsd data in real time with matplotlib."""
  12. from __future__ import print_function
  13. import argparse
  14. import datetime
  15. import os
  16. import socket
  17. import sys
  18. import time # for time.time()
  19. if 'DISPLAY' not in os.environ or not os.environ['DISPLAY']:
  20. have_display = False
  21. else:
  22. have_display = True
  23. try:
  24. # Don't fail until after check for -h or -V.
  25. from matplotlib import pyplot as plt
  26. have_matplotlib = True
  27. except (ImportError, RuntimeError):
  28. have_matplotlib = False
  29. # pylint wants local modules last
  30. try:
  31. import gps
  32. import gps.clienthelpers
  33. except ImportError as e:
  34. sys.stderr.write(
  35. "%s: can't load Python gps libraries -- check PYTHONPATH.\n" %
  36. (sys.argv[0]))
  37. sys.stderr.write("%s\n" % e)
  38. sys.exit(1)
  39. gps_version = '@VERSION@'
  40. if gps.__version__ != gps_version:
  41. sys.stderr.write("%s: ERROR: need gps module version %s, got %s\n" %
  42. (sys.argv[0], gps_version, gps.__version__))
  43. sys.exit(1)
  44. def _do_one_line(data):
  45. """Dump one report line."""
  46. if options.stripchart:
  47. if 'time' not in data:
  48. plt.pause(0.05)
  49. return
  50. timestr = datetime.datetime.strptime(data['time'],
  51. '%Y-%m-%dT%H:%M:%S.%fZ')
  52. if fields[0] in data and fields[2] in data:
  53. axs[0].scatter(timestr, data[fields[0]])
  54. axs[1].scatter(timestr, data[fields[2]])
  55. if fields[4] in data:
  56. axs[2].scatter(timestr, data[fields[4]] * conversion.altfactor)
  57. else:
  58. # must be scatterplot
  59. if fields[0] in data and fields[2] in data:
  60. axs[0].scatter(data[fields[2]], data[fields[0]])
  61. if fields[4] in data:
  62. axs[1].scatter(0.5, data[fields[4]] * conversion.altfactor)
  63. plt.pause(0.05)
  64. try:
  65. # try to tighten up the plot. let it fail silently
  66. plt.tight_layout(h_pad=.2)
  67. except ValueError:
  68. pass
  69. # get default units from the environment
  70. # GPSD_UNITS, LC_MEASUREMENT and LANG
  71. default_units = gps.clienthelpers.unit_adjustments()
  72. description = 'Create dynamic plots from gpsd with matplotlib.'
  73. usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
  74. epilog = ('BSD terms apply: see the file COPYING in the distribution root'
  75. ' for details.')
  76. parser = argparse.ArgumentParser(
  77. description=description,
  78. epilog=epilog,
  79. formatter_class=argparse.RawDescriptionHelpFormatter,
  80. usage=usage)
  81. parser.add_argument(
  82. '-?',
  83. action="help",
  84. help='show this help message and exit'
  85. )
  86. parser.add_argument(
  87. '-D',
  88. '--debug',
  89. default=0,
  90. dest='debug',
  91. help='Set level of debug. Must be integer. [Default %(default)s)]',
  92. type=int,
  93. )
  94. parser.add_argument(
  95. '--device',
  96. default='',
  97. dest='device',
  98. help='The device to connect. [Default %(default)s)]',
  99. )
  100. parser.add_argument(
  101. '--fields',
  102. choices=['llh', 'llm'],
  103. default='llh',
  104. dest='fields',
  105. help='Fields to plot. [Default %(default)s)]',
  106. )
  107. parser.add_argument(
  108. '--host',
  109. default='localhost',
  110. dest='host',
  111. help='The host to connect. [Default %(default)s)]',
  112. )
  113. parser.add_argument(
  114. '-n',
  115. '--count',
  116. default=0,
  117. dest='count',
  118. help='Count of messages to parse. 0 to disable. [Default %(default)s)]',
  119. type=int,
  120. )
  121. parser.add_argument(
  122. '--port',
  123. default=gps.GPSD_PORT,
  124. dest='port',
  125. help='The port to connect. [Default %(default)s)]',
  126. )
  127. parser.add_argument(
  128. '--plottype',
  129. choices=['scatterplot', 'stripchart'],
  130. default='scatterplot',
  131. dest='plottype',
  132. help='Plot type. [Default %(default)s)]',
  133. )
  134. parser.add_argument(
  135. '-u', '--units',
  136. choices=['i', 'imperial', 'n', 'nautical', 'm', 'metric'],
  137. default=default_units.name,
  138. dest='units',
  139. help='Units [Default %(default)s)]',
  140. )
  141. parser.add_argument(
  142. '-V', '--version',
  143. action='version',
  144. help='Output version to stderr, then exit',
  145. version="%(prog)s: Version " + gps_version + "\n",
  146. )
  147. parser.add_argument(
  148. '-x',
  149. '--seconds',
  150. default=0,
  151. dest='seconds',
  152. help='Seconds of messages to parse. 0 to disable. [Default %(default)s)]',
  153. type=int,
  154. )
  155. parser.add_argument(
  156. 'target',
  157. help='[host[:port[:device]]]',
  158. nargs='?',
  159. )
  160. options = parser.parse_args()
  161. # allow -V and -h, above, before exiting on matplotlib, or DISPLAY, not found
  162. if not have_display:
  163. # matplotlib will not import w/o DISPLAY
  164. sys.stderr.write("gpsplot: ERROR: $DISPLAY not set\n")
  165. sys.exit(1)
  166. if not have_matplotlib:
  167. sys.stderr.write("gpsplot: ERROR: required Python module "
  168. "matplotlib not found\n")
  169. sys.exit(1)
  170. # get conversion factors
  171. conversion = gps.clienthelpers.unit_adjustments(units=options.units)
  172. flds = {'llh': ('lat', 'Latitude', 'lon', 'Longitude', 'altHAE', 'altHAE'),
  173. 'llm': ('lat', 'Latitude', 'lon', 'Longitude', 'altMSL', 'altMSL'),
  174. }
  175. if options.fields not in flds:
  176. sys.stderr.write("gpsplot: Invalid --fields argument %s\n" %
  177. options.fields)
  178. sys.exit(1)
  179. fields = flds[options.fields]
  180. # the options host, port, device are set by the defaults
  181. if options.target:
  182. # override host, port and device with target
  183. arg = options.target.split(':')
  184. len_arg = len(arg)
  185. if len_arg == 1:
  186. (options.host,) = arg
  187. elif len_arg == 2:
  188. (options.host, options.port) = arg
  189. elif len_arg == 3:
  190. (options.host, options.port, options.device) = arg
  191. else:
  192. parser.print_help()
  193. sys.exit(0)
  194. if not options.port:
  195. options.port = gps.GPSD_PORT
  196. options.scatterplot = False
  197. options.stripchart = False
  198. if 'scatterplot' == options.plottype.lower():
  199. # scatterplot
  200. options.scatterplot = True
  201. else:
  202. # stripchart
  203. options.stripchart = True
  204. options.mclass = 'TPV'
  205. # Fields to parse
  206. # autodetect, read one message, use those fields
  207. options.json_fields = None
  208. options.frames = None
  209. options.subclass = None
  210. try:
  211. session = gps.gps(host=options.host, port=options.port,
  212. verbose=options.debug)
  213. except socket.error:
  214. sys.stderr.write("gpsplot: Could not connect to gpsd daemon\n")
  215. sys.exit(1)
  216. session.stream(gps.WATCH_ENABLE | gps.WATCH_SCALED, devpath=options.device)
  217. plt.ion()
  218. x = []
  219. y = []
  220. if options.scatterplot:
  221. fig = plt.figure(figsize=(7, 7))
  222. # x/y
  223. ax = fig.add_axes([0.18, 0.17, 0.65, 0.65])
  224. plt.xticks(rotation=30, ha='right')
  225. # z
  226. ax1 = fig.add_axes([0.85, 0.17, 0.02, 0.65])
  227. axs = [ax, ax1]
  228. for ax in axs:
  229. ax.ticklabel_format(useOffset=False)
  230. axs[0].tick_params(direction='in', top=True, right=True)
  231. axs[0].set_xlabel(fields[3])
  232. axs[0].set_ylabel(fields[1])
  233. axs[1].set_title("%s (%s)" % (fields[5], conversion.altunits))
  234. axs[1].tick_params(bottom=False, left=False, right=True,
  235. labeltop=True, labelbottom=False)
  236. axs[1].xaxis.set_visible(False)
  237. axs[1].yaxis.tick_right()
  238. elif options.stripchart:
  239. fig, axs = plt.subplots(3, sharex=True, figsize=(7, 7))
  240. for ax in axs:
  241. ax.ticklabel_format(useOffset=False)
  242. ax.tick_params(direction='in', top=True, right=True)
  243. axs[0].set_title(fields[1])
  244. axs[1].set_title(fields[3])
  245. axs[2].set_title("%s (%s)" % (fields[5], conversion.altunits))
  246. # plt.xticks(rotation=30, ha='right')
  247. fig.autofmt_xdate(rotation=30, ha='right')
  248. plt.subplots_adjust(left=0.16, bottom=0.10)
  249. else:
  250. sys.stderr.write("Error: Unknown plot type\n")
  251. sys.exit(1)
  252. count = 0
  253. if 0 < options.seconds:
  254. end_seconds = time.time() + options.seconds
  255. else:
  256. end_seconds = 0
  257. try:
  258. while True:
  259. report = session.next()
  260. if report['class'] != options.mclass:
  261. continue
  262. _do_one_line(report)
  263. if 0 < options.count:
  264. count += 1
  265. if count >= options.count:
  266. break
  267. if 0 < options.seconds:
  268. if time.time() > end_seconds:
  269. break
  270. except KeyboardInterrupt:
  271. # caught control-C
  272. # FIXME: plot is in different process, does not come here...
  273. print()
  274. sys.exit(1)
  275. # make the plot persist until window closed.
  276. plt.show(block=True)