gpsplot.py.in 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  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. # This code has been profiled and speed tested. Some code choices
  10. # that appear suboptimal are done for speed. Check all changes for
  11. # speed.
  12. #
  13. # Codacy D203 and D211 conflict, I choose D203
  14. # Codacy D212 and D213 conflict, I choose D212
  15. """gpsplot -- plot gpsd data in real time with matplotlib."""
  16. from __future__ import print_function
  17. import argparse
  18. import datetime
  19. import os
  20. import socket
  21. import sys
  22. import time # for time.time()
  23. if 'DISPLAY' not in os.environ or not os.environ['DISPLAY']:
  24. have_display = False
  25. else:
  26. have_display = True
  27. try:
  28. # Don't fail until after check for -h or -V.
  29. import matplotlib
  30. import matplotlib.pyplot
  31. have_matplotlib = True
  32. except (ImportError, RuntimeError):
  33. have_matplotlib = False
  34. # pylint wants local modules last
  35. try:
  36. import gps
  37. import gps.clienthelpers
  38. except ImportError as e:
  39. sys.stderr.write(
  40. "%s: can't load Python gps libraries -- check PYTHONPATH.\n" %
  41. (sys.argv[0]))
  42. sys.stderr.write("%s\n" % e)
  43. sys.exit(1)
  44. gps_version = '@VERSION@'
  45. if gps.__version__ != gps_version:
  46. sys.stderr.write("%s: ERROR: need gps module version %s, got %s\n" %
  47. (sys.argv[0], gps_version, gps.__version__))
  48. sys.exit(1)
  49. def _do_one_line(data):
  50. """Dump one report line."""
  51. # multicolor dots looked cool, but are 3x to 6x slower..
  52. if options.stripchart:
  53. if 'time' not in data:
  54. # pause so we do not block
  55. matplotlib.pyplot.pause(0.001)
  56. return
  57. timestr = datetime.datetime.strptime(data['time'],
  58. '%Y-%m-%dT%H:%M:%S.%fZ')
  59. if fields[0] in data and fields[2] in data:
  60. axs[0].plot(timestr, data[fields[0]], 'r.')
  61. axs[1].plot(timestr, data[fields[2]], 'g.')
  62. if fields[4] in data:
  63. axs[2].plot(timestr, data[fields[4]] * conversion.altfactor, 'b.')
  64. else:
  65. # must be scatterplot
  66. if fields[0] in data and fields[2] in data:
  67. axs[0].plot(data[fields[2]], data[fields[0]], 'r.', alpha=0.5)
  68. if fields[4] in data:
  69. axs[1].plot(0.5, data[fields[4]] * conversion.altfactor,
  70. 'g.', alpha=0.5)
  71. # pause so we do not block, pause() also draws
  72. matplotlib.pyplot.pause(0.001)
  73. if options.stripchart:
  74. try:
  75. # try to tighten up the plot. let it fail silently
  76. matplotlib.pyplot.tight_layout(h_pad=.2)
  77. except ValueError:
  78. pass
  79. # get default units from the environment
  80. # GPSD_UNITS, LC_MEASUREMENT and LANG
  81. default_units = gps.clienthelpers.unit_adjustments()
  82. description = 'Create dynamic plots from gpsd with matplotlib.'
  83. usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
  84. epilog = ('''
  85. The desired Matplotlib backend can be placed in the MPLBACKEND environment
  86. variable.
  87. BSD terms apply: see the file COPYING in the distribution root for details.
  88. '''
  89. )
  90. parser = argparse.ArgumentParser(
  91. description=description,
  92. epilog=epilog,
  93. formatter_class=argparse.RawDescriptionHelpFormatter,
  94. usage=usage)
  95. parser.add_argument(
  96. '-?',
  97. action="help",
  98. help='show this help message and exit'
  99. )
  100. parser.add_argument(
  101. '-b', '--backend',
  102. default='',
  103. dest='backend',
  104. metavar='BACKEND',
  105. help='Set the Matplotlib interactive backend to BACKEND.',
  106. )
  107. parser.add_argument(
  108. '-B', '--backends',
  109. action="store_true",
  110. dest='backends',
  111. help='Print available Matplotlib interactive backends, then exit',
  112. )
  113. parser.add_argument(
  114. '-D',
  115. '--debug',
  116. default=0,
  117. dest='debug',
  118. help='Set level of debug. Must be integer. [Default %(default)s]',
  119. type=int,
  120. )
  121. parser.add_argument(
  122. '--device',
  123. default='',
  124. dest='device',
  125. help='The device to connect. [Default %(default)s]',
  126. )
  127. parser.add_argument(
  128. '--exit',
  129. dest='exit',
  130. default=False,
  131. action="store_true",
  132. help='Exit after --count, --file, or --file completes'
  133. )
  134. parser.add_argument(
  135. '--fields',
  136. choices=['llh', 'llm'],
  137. default='llh',
  138. dest='fields',
  139. help='Fields to plot. [Default %(default)s]',
  140. )
  141. parser.add_argument(
  142. '-f', '--file',
  143. dest='input_file_name',
  144. default=None,
  145. metavar='FILE',
  146. help='Read gpsd JSON from FILE instead of a gpsd instance.',
  147. )
  148. parser.add_argument(
  149. '--host',
  150. default='localhost',
  151. dest='host',
  152. help='The host to connect. [Default %(default)s]',
  153. )
  154. parser.add_argument(
  155. '--image',
  156. dest='image_name',
  157. default=None,
  158. help=('Save plot as IMAGE.EXT. EXT determines image type '
  159. '(.jpg, .png, etc.)'),
  160. metavar='IMAGE.EXT',
  161. )
  162. parser.add_argument(
  163. '-n',
  164. '--count',
  165. default=0,
  166. dest='count',
  167. help=('Stop after COUNT messages to parse. 0 to disable. '
  168. ' [Default %(default)s]'),
  169. metavar='COUNT',
  170. type=int,
  171. )
  172. parser.add_argument(
  173. '--plottype',
  174. choices=['scatterplot', 'stripchart'],
  175. default='scatterplot',
  176. dest='plottype',
  177. help='Plot type. [Default %(default)s]',
  178. )
  179. parser.add_argument(
  180. '--port',
  181. default=gps.GPSD_PORT,
  182. dest='port',
  183. help='The port to connect. [Default %(default)s]',
  184. )
  185. parser.add_argument(
  186. '-u', '--units',
  187. choices=['i', 'imperial', 'n', 'nautical', 'm', 'metric'],
  188. default=default_units.name,
  189. dest='units',
  190. help='Units [Default %(default)s]',
  191. )
  192. parser.add_argument(
  193. '-V', '--version',
  194. action='version',
  195. help='Output version to stderr, then exit',
  196. version="%(prog)s: Version " + gps_version + "\n",
  197. )
  198. parser.add_argument(
  199. '-x',
  200. '--seconds',
  201. default=0,
  202. dest='seconds',
  203. help='Stop after SECONDS. 0 to disable. [Default %(default)s]',
  204. metavar='SECONDS',
  205. type=int,
  206. )
  207. parser.add_argument(
  208. 'target',
  209. help='[host[:port[:device]]]',
  210. nargs='?',
  211. )
  212. options = parser.parse_args()
  213. # allow -V and -h, above, before exiting on matplotlib, or DISPLAY, not found
  214. if not have_display:
  215. # matplotlib will not import w/o DISPLAY
  216. sys.stderr.write("gpsplot: ERROR: $DISPLAY not set\n")
  217. sys.exit(1)
  218. if not have_matplotlib:
  219. sys.stderr.write("gpsplot: ERROR: required Python module "
  220. "matplotlib not found\n")
  221. sys.exit(1)
  222. if options.backends:
  223. print("Available Matplotlib interactive backends:")
  224. for b in matplotlib.rcsetup.interactive_bk:
  225. # matplotlib.rcsetup.interactive_bk is all possible backends.
  226. # not guaranteed to work. Validate it works.
  227. try:
  228. matplotlib.pyplot.switch_backend(b)
  229. print(" ", b)
  230. except (ImportError, RuntimeError):
  231. # ImportError to shut up codacy
  232. continue
  233. sys.exit(0)
  234. # get conversion factors
  235. conversion = gps.clienthelpers.unit_adjustments(units=options.units)
  236. flds = {'llh': ('lat', 'Latitude', 'lon', 'Longitude', 'altHAE', 'altHAE'),
  237. 'llm': ('lat', 'Latitude', 'lon', 'Longitude', 'altMSL', 'altMSL'),
  238. }
  239. if options.fields not in flds:
  240. sys.stderr.write("gpsplot: Invalid --fields argument %s\n" %
  241. options.fields)
  242. sys.exit(1)
  243. fields = flds[options.fields]
  244. # the options host, port, device are set by the defaults
  245. if options.target:
  246. # override host, port and device with target
  247. arg = options.target.split(':')
  248. len_arg = len(arg)
  249. if len_arg == 1:
  250. (options.host,) = arg
  251. elif len_arg == 2:
  252. (options.host, options.port) = arg
  253. elif len_arg == 3:
  254. (options.host, options.port, options.device) = arg
  255. else:
  256. parser.print_help()
  257. sys.exit(0)
  258. if not options.port:
  259. options.port = gps.GPSD_PORT
  260. options.scatterplot = False
  261. options.stripchart = False
  262. if 'scatterplot' == options.plottype.lower():
  263. # scatterplot
  264. options.scatterplot = True
  265. else:
  266. # stripchart
  267. options.stripchart = True
  268. if ((options.exit and
  269. not options.count and
  270. not options.input_file_name and
  271. not options.seconds)):
  272. sys.stderr.write("gpsplot: --exit requires one of: "
  273. " --count, --file or --seconds")
  274. sys.exit(1)
  275. options.mclass = 'TPV'
  276. # Fields to parse
  277. # autodetect, read one message, use those fields
  278. options.json_fields = None
  279. options.frames = None
  280. options.subclass = None
  281. try:
  282. session = gps.gps(host=options.host, port=options.port,
  283. input_file_name=options.input_file_name,
  284. verbose=options.debug)
  285. except socket.error:
  286. sys.stderr.write("gpsplot: Could not connect to gpsd daemon\n")
  287. sys.exit(1)
  288. session.stream(gps.WATCH_ENABLE | gps.WATCH_SCALED, devpath=options.device)
  289. if options.backend:
  290. matplotlib.use(options.backend)
  291. # matplotlib.pyplot.ion() and matplotlib.pyplot.ioff()
  292. # seem to have no speed effect
  293. matplotlib.pyplot.ion()
  294. x = []
  295. y = []
  296. if options.scatterplot:
  297. fig = matplotlib.pyplot.figure(figsize=(7, 7))
  298. # x/y
  299. ax = fig.add_axes([0.18, 0.17, 0.65, 0.65])
  300. matplotlib.pyplot.xticks(rotation=30, ha='right')
  301. # z
  302. ax1 = fig.add_axes([0.85, 0.17, 0.02, 0.65])
  303. axs = [ax, ax1]
  304. for ax in axs:
  305. ax.ticklabel_format(useOffset=False)
  306. axs[0].tick_params(direction='in', top=True, right=True)
  307. axs[0].set_xlabel(fields[3])
  308. axs[0].set_ylabel(fields[1])
  309. axs[1].set_title("%s (%s)" % (fields[5], conversion.altunits))
  310. axs[1].tick_params(bottom=False, left=False, right=True,
  311. labeltop=True, labelbottom=False)
  312. axs[1].xaxis.set_visible(False)
  313. axs[1].yaxis.tick_right()
  314. elif options.stripchart:
  315. fig, axs = matplotlib.pyplot.subplots(3, sharex=True, figsize=(7, 7))
  316. for ax in axs:
  317. ax.ticklabel_format(useOffset=False)
  318. ax.tick_params(direction='in', top=True, right=True)
  319. axs[0].set_title(fields[1])
  320. axs[1].set_title(fields[3])
  321. axs[2].set_title("%s (%s)" % (fields[5], conversion.altunits))
  322. # matplotlib.pyplot.xticks(rotation=30, ha='right')
  323. fig.autofmt_xdate(rotation=30, ha='right')
  324. matplotlib.pyplot.subplots_adjust(left=0.16, bottom=0.10)
  325. else:
  326. sys.stderr.write("Error: Unknown plot type\n")
  327. sys.exit(1)
  328. count = 0
  329. if 0 < options.seconds:
  330. end_seconds = time.time() + options.seconds
  331. else:
  332. end_seconds = 0
  333. try:
  334. while True:
  335. try:
  336. report = session.next()
  337. except StopIteration:
  338. # end of data
  339. break
  340. if report['class'] != options.mclass:
  341. continue
  342. _do_one_line(report)
  343. if 0 < options.count:
  344. count += 1
  345. if count >= options.count:
  346. break
  347. if 0 < options.seconds:
  348. if time.time() > end_seconds:
  349. break
  350. except KeyboardInterrupt:
  351. # caught control-C
  352. # FIXME: plot is in different process, does not come here...
  353. print()
  354. sys.exit(1)
  355. if options.image_name:
  356. fig.savefig(options.image_name, dpi=200)
  357. if options.exit:
  358. sys.exit(0)
  359. # make the plot persist until window closed.
  360. matplotlib.pyplot.show(block=True)