gpscsv.py.in 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. #!@PYSHEBANG@
  2. # @GENERATED@
  3. # -*- coding: utf-8 -*-
  4. # This file is Copyright 2010 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. """gpscsv -- convert gpsd JSON strams into csv files."""
  10. from __future__ import print_function
  11. import argparse
  12. import socket
  13. import sys
  14. import time # for time.time()
  15. def _do_one_line(data):
  16. """dump one report line."""
  17. global options
  18. if options.json_fields is None:
  19. # no fields specified, use the 1st ones found
  20. fields = data.keys()
  21. options.json_fields = []
  22. for f in fields:
  23. if isinstance(data[f], (dict, list)):
  24. # skip dictionay fields
  25. continue
  26. options.json_fields.append(f)
  27. if 0 < options.header:
  28. f = options.separator.join(options.json_fields)
  29. if 2 == options.header:
  30. f = '# ' + f
  31. print(f)
  32. out = []
  33. for fld in options.json_fields:
  34. if 'time' == fld and options.cvtisotime:
  35. # convert 2020-08-17T23:58:01.000Z to 1597708682.0
  36. data[fld] = gps.isotime(data[fld])
  37. if fld not in data or 'null' == data[fld]:
  38. out.append('')
  39. else:
  40. out.append(str(data[fld]))
  41. print(options.separator.join(out))
  42. # pylint wants local modules last
  43. try:
  44. import gps
  45. except ImportError as e:
  46. sys.stderr.write(
  47. "%s: can't load Python gps libraries -- check PYTHONPATH.\n" %
  48. (sys.argv[0]))
  49. sys.stderr.write("%s\n" % e)
  50. sys.exit(1)
  51. gps_version = '@VERSION@'
  52. if gps.__version__ != gps_version:
  53. sys.stderr.write("%s: ERROR: need gps module version %s, got %s\n" %
  54. (sys.argv[0], gps_version, gps.__version__))
  55. sys.exit(1)
  56. description = 'Convert one gpsd JSON message class to csv format.'
  57. usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
  58. epilog = (' -c ALMANAC to dump the Almanacs from Subframes 4/5.\n'
  59. ' -c HEALTH to dump health data from Subframe 4, page 25\n'
  60. ' -c HEALTH2 to dump health data from Subframe 5, page 25\n'
  61. ' -c IONO to dump the Iono/UTC data from Subframe 4, page 18\n'
  62. ' -c NMCT to dump the ERD data from Subframe 4, page 13\n'
  63. ' -c SAT to dump the satellite records from the SKY messages.\n'
  64. ' -c SUBFRAME1 to dump Ephemeris1 from Subframe 1\n'
  65. ' -c SUBFRAME2 to dump Ephemeris2 from Subframe 2\n'
  66. ' -c SUBFRAME3 to dump Ephermeris3 from Subframe 3\n\n'
  67. 'BSD terms apply: see the file COPYING in the distribution root'
  68. ' for details.')
  69. parser = argparse.ArgumentParser(
  70. description=description,
  71. epilog=epilog,
  72. formatter_class=argparse.RawDescriptionHelpFormatter,
  73. usage=usage)
  74. parser.add_argument(
  75. '-c',
  76. '--class',
  77. dest='mclass', # class is a reserved word
  78. default='TPV',
  79. help='Message class to process. [Default %(default)s)]'
  80. )
  81. parser.add_argument(
  82. '--cvt-isotime',
  83. dest='cvtisotime',
  84. default=False,
  85. action="store_true",
  86. help='Convert ISO time to UNIX time'
  87. )
  88. parser.add_argument(
  89. '-D',
  90. '--debug',
  91. dest='debug',
  92. default=0,
  93. type=int,
  94. help='Set level of debug. Must be integer. [Default %(default)s)]'
  95. )
  96. parser.add_argument(
  97. '--device',
  98. dest='device',
  99. default='',
  100. help='The device to connect. [Default %(default)s)]'
  101. )
  102. parser.add_argument(
  103. '-f',
  104. '--fields',
  105. dest='fields',
  106. default=None,
  107. help='Fields to process. '' for all. Default varies by class.'
  108. )
  109. parser.add_argument(
  110. '--header',
  111. dest='header',
  112. default=1,
  113. type=int,
  114. help='0: No header, 1: fields, 2: comment. [Default %(default)s)]'
  115. )
  116. parser.add_argument(
  117. '--host',
  118. dest='host',
  119. default='localhost',
  120. help='The host to connect. [Default %(default)s)]'
  121. )
  122. parser.add_argument(
  123. '-n',
  124. '--count',
  125. dest='count',
  126. default=0,
  127. type=int,
  128. help='Count of messages to parse. 0 to disable. [Default %(default)s)]'
  129. )
  130. parser.add_argument(
  131. '--port',
  132. dest='port',
  133. default=gps.GPSD_PORT,
  134. help='The port to connect. [Default %(default)s)]'
  135. )
  136. parser.add_argument(
  137. '--separator',
  138. dest='separator',
  139. default=',',
  140. type=str,
  141. help='CSV field separator character. [Default %(default)s)]'
  142. )
  143. parser.add_argument(
  144. '-V', '--version',
  145. action='version',
  146. version="%(prog)s: Version " + gps_version + "\n",
  147. help='Output version to stderr, then exit'
  148. )
  149. parser.add_argument(
  150. '-x',
  151. '--seconds',
  152. dest='seconds',
  153. default=0,
  154. type=int,
  155. help='Seconds of messages to parse. 0 to disable. [Default %(default)s)]'
  156. )
  157. parser.add_argument(
  158. 'target',
  159. nargs='?',
  160. help='[host[:port[:device]]]'
  161. )
  162. options = parser.parse_args()
  163. # the options host, port, device are set by the defaults
  164. if options.target:
  165. # override host, port and device with target
  166. arg = options.target.split(':')
  167. len_arg = len(arg)
  168. if len_arg == 1:
  169. (options.host,) = arg
  170. elif len_arg == 2:
  171. (options.host, options.port) = arg
  172. elif len_arg == 3:
  173. (options.host, options.port, options.device) = arg
  174. else:
  175. parser.print_help()
  176. sys.exit(0)
  177. # Fields to parse
  178. # Python dicts are unorderd, so try to clean things up a little
  179. options.json_fields = None
  180. if options.fields is None:
  181. default_fields = {'ALMANAC': ('TOW17', 'tSV', 'ID', 'Health', 'e',
  182. 'toa', 'deltai', 'Omegad', 'sqrtA',
  183. 'Omega0', 'omega', 'M0', 'af0', 'af1'),
  184. 'HEALTH': ('TOW17', 'tSV',
  185. 'SV1', 'SV2', 'SV3', 'SV4', 'SV5',
  186. 'SV6', 'SV7', 'SV8', 'SV9', 'SV10',
  187. 'SV11', 'SV12', 'SV13', 'SV14', 'SV15',
  188. 'SV16', 'SV17', 'SV18', 'SV19', 'SV20',
  189. 'SV21', 'SV22', 'SV23', 'SV24', 'SV25',
  190. 'SV26', 'SV27', 'SV28', 'SV29', 'SV30',
  191. 'SV31', 'SV32',
  192. 'SVH25', 'SVH26', 'SVH27', 'SVH28', 'SVH29',
  193. 'SVH30', 'SVH31', 'SVH32'),
  194. 'HEALTH2': ('TOW17', 'tSV', 'toa', 'WNa',
  195. 'SVH1', 'SVH2', 'SVH3', 'SVH4', 'SVH5',
  196. 'SVH6', 'SVH7', 'SVH8', 'SVH9', 'SVH10',
  197. 'SVH11', 'SVH12', 'SVH13', 'SVH14', 'SVH15',
  198. 'SVH16', 'SVH17', 'SVH18', 'SVH19', 'SVH20',
  199. 'SVH21', 'SVH22', 'SVH23', 'SVH24'),
  200. 'IONO': ('TOW17', 'tSV', 'WNt', 'tot',
  201. 'lsf', 'ls', 'WNlsf', 'DN',
  202. 'A0', 'A1', 'a0', 'a1', 'a2', 'a3',
  203. 'b0', 'b1', 'b2', 'b3'),
  204. 'NMCT': ('TOW17', 'tSV', 'ai',
  205. 'ERD1', 'ERD2', 'ERD3', 'ERD4', 'ERD5',
  206. 'ERD6', 'ERD7', 'ERD8', 'ERD9', 'ERD10',
  207. 'ERD11', 'ERD12', 'ERD13', 'ERD14', 'ERD15',
  208. 'ERD16', 'ERD17', 'ERD18', 'ERD19', 'ERD20',
  209. 'ERD21', 'ERD22', 'ERD23', 'ERD24', 'ERD25',
  210. 'ERD26', 'ERD27', 'ERD28', 'ERD29', 'ERD30',
  211. 'ERD31'),
  212. 'SAT': ('time', 'gnssid', 'svid', 'PRN', 'az', 'el',
  213. 'ss', 'used', 'health'),
  214. 'SKY': ('time', 'xdop', 'ydop', 'vdop', 'tdop',
  215. 'hdop', 'gdop', 'pdop'),
  216. 'SUBFRAME1': ('TOW17', 'tSV', 'IODC', 'WN', 'ura',
  217. 'hlth', 'L2', 'L2P', 'Tgd',
  218. 'toc', 'af0', 'af1', 'af2'),
  219. 'SUBFRAME2': ('TOW17', 'tSV', 'IODE', 'M0',
  220. 'deltan', 'e', 'sqrtA', 'FIT', 'AODO',
  221. 'Crs', 'Cuc', 'Cus', 'toe'),
  222. 'SUBFRAME3': ('TOW17', 'tSV', 'IODE',
  223. 'Crc', 'Cic', 'Cis', 'Omega0', 'i0',
  224. 'omega', 'Omegad', 'IDOT'),
  225. 'TPV': ('time', 'lat', 'lon', 'altHAE'),
  226. }
  227. # None specified, use defaults, if they exist
  228. if options.mclass in default_fields:
  229. options.json_fields = default_fields[options.mclass]
  230. else:
  231. # autodetect, read one message, use those fields
  232. options.json_fields = None
  233. elif '' == options.fields:
  234. # autodetect, read one message, use those fields
  235. options.json_fields = None
  236. else:
  237. options.json_fields = options.fields.split(',')
  238. options.frames = None
  239. if 'ALMANAC' == options.mclass:
  240. options.mclass = 'SUBFRAME'
  241. options.subclass = 'ALMANAC'
  242. options.frames = [4, 5]
  243. elif 'HEALTH' == options.mclass:
  244. options.mclass = 'SUBFRAME'
  245. options.subclass = 'HEALTH'
  246. options.frames = [4]
  247. elif 'HEALTH2' == options.mclass:
  248. options.mclass = 'SUBFRAME'
  249. options.subclass = 'HEALTH2'
  250. options.frames = [5]
  251. elif 'IONO' == options.mclass:
  252. options.mclass = 'SUBFRAME'
  253. options.subclass = 'IONO'
  254. options.frames = [4]
  255. elif 'NMCT' == options.mclass:
  256. # Note: ai is probably 01, which means the NMCT data is encypted.
  257. options.mclass = 'SUBFRAME'
  258. options.subclass = 'NMCT'
  259. options.frames = [4]
  260. elif 'SAT' == options.mclass:
  261. options.mclass = 'SKY'
  262. options.subclass = 'SAT'
  263. elif 'SUBFRAME1' == options.mclass:
  264. options.mclass = 'SUBFRAME'
  265. options.subclass = 'EPHEM1'
  266. options.frames = [1]
  267. elif 'SUBFRAME2' == options.mclass:
  268. options.mclass = 'SUBFRAME'
  269. options.subclass = 'EPHEM2'
  270. options.frames = [2]
  271. elif 'SUBFRAME3' == options.mclass:
  272. options.mclass = 'SUBFRAME'
  273. options.subclass = 'EPHEM3'
  274. options.frames = [3]
  275. else:
  276. options.subclass = None
  277. try:
  278. session = gps.gps(host=options.host, port=options.port,
  279. verbose=options.debug)
  280. except socket.error:
  281. sys.stderr.write("gpscsv: Could not connect to gpsd daemon\n")
  282. sys.exit(1)
  283. session.stream(gps.WATCH_ENABLE | gps.WATCH_SCALED, devpath=options.device)
  284. # top line is headings
  285. if options.json_fields is not None and 0 < options.header:
  286. f = options.separator.join(options.json_fields)
  287. if 2 == options.header:
  288. f = '# ' + f
  289. print(f)
  290. count = 0
  291. if 0 < options.seconds:
  292. end_seconds = time.time() + options.seconds
  293. else:
  294. end_seconds = 0
  295. try:
  296. while True:
  297. report = session.next()
  298. if report['class'] != options.mclass:
  299. continue
  300. if 'SAT' == options.subclass:
  301. # grab the sats, one at a time
  302. for sat in report['satellites']:
  303. subreport = {'time': report['time']}
  304. subreport.update(sat)
  305. _do_one_line(subreport)
  306. elif 'SUBFRAME' == options.mclass:
  307. if report['frame'] not in options.frames:
  308. continue
  309. if options.subclass not in report:
  310. # Not all subframe 4/5 have ALMANAC
  311. continue
  312. subreport = {'TOW17': report['TOW17'], 'tSV': report['tSV']}
  313. subreport.update(report[options.subclass])
  314. _do_one_line(subreport)
  315. else:
  316. _do_one_line(report)
  317. if 0 < options.count:
  318. count += 1
  319. if count >= options.count:
  320. break
  321. if 0 < options.seconds:
  322. if time.time() > end_seconds:
  323. break
  324. except KeyboardInterrupt:
  325. # caught control-C
  326. print()
  327. sys.exit(1)