gpsprof.py.in 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316
  1. #!@PYSHEBANG@
  2. #
  3. # @GENERATED@
  4. # This file is Copyright 2010 by the GPSD project
  5. # SPDX-License-Identifier: BSD-2-clause
  6. #
  7. # Updated to conform with RCC-219-00, RCC/IRIG Standard 261-00
  8. # "STANDARD REPORT FORMAT FOR GLOBAL POSITIONING SYSTEM (GPS) RECEIVERS AND
  9. # SYSTEMS ACCURACY TESTS AND EVALUATIONS"
  10. #
  11. # TODO: put date from data on plot, not time of replot.
  12. # TODO: add lat/lon to polar plots
  13. #
  14. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  15. # Preserve this property!
  16. # Codacy D203 and D211 conflict, I choose D203
  17. # Codacy D212 and D213 conflict, I choose D212
  18. """Collect and plot latency-profiling data from a running gpsd.
  19. Requires gnuplot, but gnuplot can be on another host.
  20. """
  21. from __future__ import absolute_import, print_function, division
  22. import argparse
  23. import copy
  24. import math
  25. import os
  26. import signal
  27. import socket
  28. import sys
  29. import time
  30. # pylint wants local modules last
  31. try:
  32. import gps
  33. import gps.clienthelpers
  34. except ImportError as e:
  35. sys.stderr.write(
  36. "gpsprof: can't load Python gps libraries -- check PYTHONPATH.\n")
  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("gpsprof: ERROR: need gps module version %s, got %s\n" %
  42. (gps_version, gps.__version__))
  43. sys.exit(1)
  44. def dist_2d(a, b):
  45. """Calculate distance between a[x,y] and b[x,y]."""
  46. # x and y are orthogonal, probably lat/lon in meters
  47. # ignore altitude change.
  48. return math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2)
  49. def dist_3d(a, b):
  50. """Calculate distance between a[x,y,z] and b[x,y,z]."""
  51. # x, y, and z are othogonal, probably ECEF, probably in meters
  52. return math.sqrt((a[0] - b[0]) ** 2 +
  53. (a[1] - b[1]) ** 2 +
  54. (a[2] - b[2]) ** 2)
  55. def wgs84_to_ecef(wgs84):
  56. """Convert wgs84 coordinates to ECEF ones."""
  57. # unpack args
  58. (lat, lon, alt) = wgs84
  59. # convert lat/lon/altitude in degrees and altitude in meters
  60. # to ecef x, y, z in meters
  61. # see
  62. # http://www.mathworks.de/help/toolbox/aeroblks/llatoecefposition.html
  63. lat = math.radians(lat)
  64. lon = math.radians(lon)
  65. rad = 6378137.0 # Radius of the Earth (in meters)
  66. f = 1.0 / 298.257223563 # Flattening factor WGS84 Model
  67. cosLat = math.cos(lat)
  68. sinLat = math.sin(lat)
  69. FF = (1.0 - f) ** 2
  70. C = 1 / math.sqrt((cosLat ** 2) + (FF * sinLat ** 2))
  71. S = C * FF
  72. x = (rad * C + alt) * cosLat * math.cos(lon)
  73. y = (rad * C + alt) * cosLat * math.sin(lon)
  74. z = (rad * S + alt) * sinLat
  75. return (x, y, z)
  76. class Baton(object):
  77. """Ship progress indication to stderr."""
  78. def __init__(self, prompt, endmsg=None):
  79. """Init class baton."""
  80. self.stream = sys.stderr
  81. self.stream.write(prompt + "...")
  82. if os.isatty(self.stream.fileno()):
  83. self.stream.write(" \b")
  84. self.stream.flush()
  85. self.count = 0
  86. self.endmsg = endmsg
  87. self.time = time.time()
  88. def twirl(self, ch=None):
  89. """Twirl the baton."""
  90. if self.stream is None:
  91. return
  92. if ch:
  93. self.stream.write(ch)
  94. elif os.isatty(self.stream.fileno()):
  95. self.stream.write("-/|\\"[self.count % 4])
  96. self.stream.write("\b")
  97. self.count = self.count + 1
  98. self.stream.flush()
  99. return
  100. def end(self, msg=None):
  101. """Write the end message."""
  102. if msg is None:
  103. msg = self.endmsg
  104. if self.stream:
  105. self.stream.write("...(%2.2f sec) %s.\n"
  106. % (time.time() - self.time, msg))
  107. class stats(object):
  108. """Class for 1D stats: min, max, mean, sigma, skewness, kurtosis."""
  109. def __init__(self):
  110. """Init class stats."""
  111. self.min = 0.0
  112. self.max = 0.0
  113. self.mean = 0.0
  114. self.median = 0.0
  115. self.sigma = 0.0
  116. self.skewness = 0.0
  117. self.kurtosis = 0.0
  118. def __str__(self):
  119. """Return a nice string, for debug."""
  120. return ("min %f, max %f, mean %f, median %f, sigma %f, skewedness %f, "
  121. "kurtosis %f" %
  122. (self.min, self.max, self.mean, self.median,
  123. self.sigma, self.skewness, self.kurtosis))
  124. def min_max_mean(self, fixes, index):
  125. """Find min, max, and mean of fixes[index]."""
  126. if not fixes:
  127. return
  128. # might be fast to go through list once?
  129. if isinstance(fixes[0], tuple):
  130. self.mean = sum([x[index] for x in fixes]) / len(fixes)
  131. self.min = min([x[index] for x in fixes])
  132. self.max = max([x[index] for x in fixes])
  133. else:
  134. # must be float
  135. self.mean = sum(fixes) / len(fixes)
  136. self.min = min(fixes)
  137. self.max = max(fixes)
  138. return
  139. def moments(self, fixes, index):
  140. """Find and set the (sigma, skewness, kurtosis) of fixes[index]."""
  141. # The skewness of a random variable X is the third standardized
  142. # moment and is a dimension-less ratio. ntpviz uses the Pearson's
  143. # moment coefficient of skewness. Wikipedia describes it
  144. # best: "The qualitative interpretation of the skew is complicated
  145. # and unintuitive." A normal distribution has a skewness of zero.
  146. self.skewness = float('nan')
  147. # The kurtosis of a random variable X is the fourth standardized
  148. # moment and is a dimension-less ratio. Here we use the Pearson's
  149. # moment coefficient of kurtosis. A normal distribution has a
  150. # kurtosis of three. NIST describes a kurtosis over three as
  151. # "heavy tailed" and one under three as "light tailed".
  152. self.kurtosis = float('nan')
  153. if not fixes:
  154. return
  155. m3 = 0.0
  156. m4 = 0.0
  157. if isinstance(fixes[0], tuple):
  158. sum_squares = [(x[index] - self.mean) ** 2 for x in fixes]
  159. sigma = math.sqrt(sum(sum_squares) / (len(fixes) - 1))
  160. for fix in fixes:
  161. m3 += pow(fix[index] - sigma, 3)
  162. m4 += pow(fix[index] - sigma, 4)
  163. else:
  164. # must be float
  165. sum_squares = [(x - self.mean) ** 2 for x in fixes]
  166. sigma = math.sqrt(sum(sum_squares) / (len(fixes) - 1))
  167. for fix in fixes:
  168. m3 += pow(fix - sigma, 3)
  169. m4 += pow(fix - sigma, 4)
  170. self.sigma = sigma
  171. if sigma > 0.0001:
  172. self.skewness = m3 / (len(fixes) * pow(sigma, 3))
  173. self.kurtosis = m4 / (len(fixes) * pow(sigma, 4))
  174. return
  175. class plotter(object):
  176. """Generic class for gathering and plotting sensor statistics."""
  177. requires_time = False # Default
  178. def __init__(self):
  179. """Init class plotter."""
  180. self.device = []
  181. self.fixes = []
  182. self.in_replot = False
  183. self.session = None
  184. self.start_time = int(time.time())
  185. self.watch = set(['TPV'])
  186. def whatami(self):
  187. """How do we identify this plotting run?"""
  188. desc = "%s" % gps.misc.isotime(self.start_time)
  189. if 'driver' in self.device:
  190. desc += ", %s" % self.device['driver']
  191. if 'bps' in self.device:
  192. desc += ", %d %dN%d, cycle %.3gs" % \
  193. (self.device['bps'], 9 - self.device['stopbits'],
  194. self.device['stopbits'], self.device['cycle'])
  195. if 'path' in self.device:
  196. desc += ", %s" % self.device['path']
  197. if 'subtype' in self.device:
  198. desc += "\\n%s" % self.device['subtype']
  199. return desc
  200. def collect(self, verb, log_fp=None):
  201. """Collect data from the GPS."""
  202. try:
  203. self.session = gps.gps(host=options.host, port=options.port,
  204. verbose=verb)
  205. except socket.error:
  206. sys.stderr.write("gpsprof: gpsd unreachable.\n")
  207. sys.exit(1)
  208. # Initialize
  209. self.session.read()
  210. if self.session.version is None:
  211. sys.stderr.write("gpsprof: requires gpsd to speak new protocol.\n")
  212. sys.exit(1)
  213. # Set parameters
  214. flags = gps.WATCH_ENABLE | gps.WATCH_JSON
  215. if self.requires_time:
  216. flags |= gps.WATCH_TIMING
  217. if options.device:
  218. flags |= gps.WATCH_DEVICE
  219. try:
  220. signal.signal(signal.SIGUSR1,
  221. lambda empty, unused: sys.stderr.write(
  222. "%d of %d (%d%%)..."
  223. % (options.wait - countdown, options.wait,
  224. ((options.wait - countdown) * 100.0 /
  225. options.wait))))
  226. signal.siginterrupt(signal.SIGUSR1, False)
  227. self.session.stream(flags, options.device)
  228. baton = Baton("gpsprof: %d looking for fix" % os.getpid(), "done")
  229. countdown = options.wait
  230. basetime = time.time()
  231. while countdown > 0:
  232. if self.session.read() == -1:
  233. sys.stderr.write("gpsprof: gpsd has vanished.\n")
  234. sys.exit(1)
  235. baton.twirl()
  236. if self.session.data["class"] == "ERROR":
  237. sys.stderr.write(" ERROR: %s.\n"
  238. % self.session.data["message"])
  239. sys.exit(1)
  240. if self.session.data["class"] == "DEVICES":
  241. if ((len(self.session.data["devices"]) != 1 and
  242. not options.device)):
  243. sys.stderr.write("ERROR: multiple devices connected, "
  244. "you must explicitly specify the "
  245. "device.\n")
  246. sys.exit(1)
  247. for i in range(len(self.session.data["devices"])):
  248. self.device = copy.copy(
  249. self.session.data["devices"][i])
  250. if self.device['path'] == options.device:
  251. break
  252. if self.session.data["class"] == "WATCH":
  253. if ((self.requires_time and
  254. not self.session.data.get("timing"))):
  255. sys.stderr.write("timing is not enabled.\n")
  256. sys.exit(1)
  257. # Log before filtering - might be good for post-analysis.
  258. if log_fp:
  259. log_fp.write(self.session.response)
  260. # Ignore everything but what we're told to
  261. if self.session.data["class"] not in self.watch:
  262. continue
  263. # We can get some funky artifacts at start of self.session
  264. # apparently due to RS232 buffering effects. Ignore
  265. # them.
  266. if ((options.threshold and
  267. (time.time() - basetime <
  268. self.session.cycle * options.threshold))):
  269. continue
  270. if self.session.fix.mode <= gps.MODE_NO_FIX:
  271. continue
  272. if self.sample():
  273. if countdown == options.wait:
  274. sys.stderr.write("first fix in %.2fsec, gathering %d "
  275. "samples..."
  276. % (time.time() - basetime,
  277. options.wait))
  278. countdown -= 1
  279. baton.end()
  280. finally:
  281. self.session.stream(gps.WATCH_DISABLE | gps.WATCH_TIMING)
  282. signal.signal(signal.SIGUSR1, signal.SIG_DFL)
  283. def replot(self, infp):
  284. """Replot from a JSON log file."""
  285. self.in_replot = True
  286. baton = Baton("gpsprof: replotting", "done")
  287. self.session = gps.gps(host=None)
  288. for line in infp:
  289. baton.twirl()
  290. self.session.unpack(line)
  291. if self.session.data["class"] == "DEVICES":
  292. self.device = copy.copy(self.session.data["devices"][0])
  293. elif self.session.data["class"] not in self.watch:
  294. continue
  295. self.sample()
  296. baton.end()
  297. def dump(self):
  298. """Dump the raw data for post-analysis."""
  299. return self.header() + self.data()
  300. class spaceplot(plotter):
  301. """Spatial scattergram of fixes."""
  302. name = "space"
  303. requires_time = False
  304. def __init__(self):
  305. """Initialize class spaceplot."""
  306. plotter.__init__(self)
  307. self.centroid = None
  308. self.centroid_ecef = None
  309. self.recentered = []
  310. def sample(self):
  311. """Grab samples."""
  312. # Watch out for the NaN value from gps.py.
  313. if (((self.in_replot or self.session.valid) and
  314. self.session.data["class"] == "TPV" and
  315. # Skip TPV without an actual fix.
  316. self.session.data["mode"] >= gps.MODE_2D)):
  317. # get sat used count
  318. sats_used = 0
  319. for sat in self.session.satellites:
  320. if sat.used:
  321. sats_used += 1
  322. if 'altHAE' not in self.session.data:
  323. self.session.data['altHAE'] = gps.NaN
  324. self.fixes.append((self.session.data['lat'],
  325. self.session.data['lon'],
  326. self.session.data['altHAE'], sats_used))
  327. return True
  328. def header(self):
  329. """Return header."""
  330. return "\n# Position uncertainty, %s\n" % self.whatami()
  331. def postprocess(self):
  332. """Postprocess the sample data."""
  333. return
  334. def data(self):
  335. """Format data for dump."""
  336. res = ""
  337. for i in range(len(self.recentered)):
  338. (lat, lon) = self.recentered[i][:2]
  339. (raw1, raw2, alt) = self.fixes[i]
  340. res += "%.9f\t%.9f\t%.9f\t%.9f\t%.9f\n" \
  341. % (lat, lon, raw1, raw2, alt)
  342. return res
  343. def plot(self):
  344. """Plot the data."""
  345. stat_lat = stats()
  346. stat_lon = stats()
  347. stat_alt = stats()
  348. stat_used = stats()
  349. # recentered stats
  350. stat_lat_r = stats()
  351. stat_lon_r = stats()
  352. stat_alt_r = stats()
  353. if not self.fixes:
  354. print("plot(): fixes array is empty\n")
  355. raise Exception('When plotting, there are no fixes.')
  356. sats_used = []
  357. for x in self.fixes:
  358. # skip missing sats, if any, often missing at start
  359. if x[3] != 0:
  360. sats_used.append(x[3])
  361. # calc sats used data: mean, min, max, sigma
  362. stat_used.min_max_mean(sats_used, 0)
  363. stat_used.moments(sats_used, 0)
  364. # find min, max and mean of lat/lon
  365. stat_lat.min_max_mean(self.fixes, 0)
  366. stat_lon.min_max_mean(self.fixes, 1)
  367. # centroid is just arithmetic avg of lat,lon
  368. self.centroid = (stat_lat.mean, stat_lon.mean)
  369. # Sort fixes by distance from centroid
  370. # sorted to make getting CEP() easy
  371. self.fixes.sort(key=lambda p: dist_2d(self.centroid, p[:2]))
  372. # compute min/max as meters, ignoring altitude
  373. # EarthDistance always returns a positive value
  374. lat_min_o = -gps.EarthDistance((stat_lat.min, self.centroid[1]),
  375. self.centroid[:2])
  376. lat_max_o = gps.EarthDistance((stat_lat.max, self.centroid[1]),
  377. self.centroid[:2])
  378. lon_min_o = -gps.EarthDistance((self.centroid[0], stat_lon.min),
  379. self.centroid[:2])
  380. lon_max_o = gps.EarthDistance((self.centroid[0], stat_lon.max),
  381. self.centroid[:2])
  382. # Convert fixes to offsets from centroid in meters
  383. self.recentered = [
  384. gps.MeterOffset(fix[:2], self.centroid) for fix in self.fixes]
  385. stat_lat_r.min_max_mean(self.recentered, 0)
  386. stat_lon_r.min_max_mean(self.recentered, 1)
  387. # compute sigma, skewness and kurtosis of lat/lon
  388. stat_lat_r.moments(self.recentered, 0)
  389. stat_lon_r.moments(self.recentered, 1)
  390. # CEP(50) calculated per RCC 261-00, Section 3.1.1
  391. # Note that this is not the distance such that 50% of the
  392. # points are within that distance; it is a distance calculated
  393. # from the standard deviation as specified by a standard, and
  394. # is likely a true CEP(50) if the distribution meets
  395. # expectations, such as normality. However, it seems in
  396. # general to be close to the actual population CEP(50).
  397. calc_cep = 0.5887 * (stat_lat_r.sigma + stat_lon_r.sigma)
  398. # 2DRMS calculated per RCC 261-00, Section 3.1.4
  399. calc_2drms = 2 * math.sqrt(stat_lat_r.sigma ** 2 +
  400. stat_lon_r.sigma ** 2)
  401. # Note that the fencepost error situation in the following is
  402. # not entirely clear. Issues include CEP(50) = x being
  403. # defined as 50% of the points being < x distance vs <= x
  404. # distance, and the interaction of truncation of len*0.5 vs
  405. # zero-based arrays. (If you care about this because you are
  406. # concerned about your results, then you almost certainly are
  407. # not using enough samples.)
  408. # Compute actual CEP(50%) for our input set. This makes no
  409. # assumptions about the distribution being normal, etc. -- it
  410. # is literally finding the distance such that 50% of our
  411. # samples are within that distance. We simply find the
  412. # distance of the point whose index is half the number of
  413. # points, after having sorted the points by distance.
  414. cep_meters = gps.misc.EarthDistance(
  415. self.centroid[:2], self.fixes[int(len(self.fixes) * 0.50)][:2])
  416. # Compute actual CEP(95%) for our input set. As above, but
  417. # index at 0.95 in the sorted points, so that 95% of points
  418. # are closer, 5% farther.
  419. cep95_meters = gps.misc.EarthDistance(
  420. self.centroid[:2], self.fixes[int(len(self.fixes) * 0.95)][:2])
  421. # Compute actual CEP(99%) for our input set. As above, but
  422. # index at 0.99 in the sorted points, so that 99% of points
  423. # are closer, 1% farther.
  424. cep99_meters = gps.misc.EarthDistance(
  425. self.centroid[:2], self.fixes[int(len(self.fixes) * 0.99)][:2])
  426. # Compute actual CEP(100%) for our input set. This is the
  427. # maximum distance from the centroid for any point in the
  428. # input set, and hence the distance of last point in sorted order.
  429. cep100_meters = gps.misc.EarthDistance(
  430. self.centroid[:2], self.fixes[len(self.fixes) - 1][:2])
  431. # init altitude data
  432. alt_ep = gps.NaN
  433. alt_ep95 = gps.NaN
  434. alt_ep99 = gps.NaN
  435. dist_3d_max = 0.0
  436. alt_fixes = []
  437. alt_fixes_r = []
  438. latlon_data = ""
  439. alt_data = ""
  440. # init calculated hep, sep and mrse
  441. calc_hep = gps.NaN
  442. calc_sep = gps.NaN
  443. calc_mrse = gps.NaN
  444. # grab and format the fixes as gnuplot will use them
  445. for i in range(len(self.recentered)):
  446. # grab valid lat/lon data, recentered and raw
  447. (lat, lon) = self.recentered[i][:2]
  448. alt = self.fixes[i][2]
  449. latlon_data += "%.9f\t%.9f\n" % (lat, lon)
  450. if not math.isnan(alt):
  451. # only keep good fixes
  452. alt_fixes.append(alt)
  453. # micro meters should be good enough
  454. alt_data += "%.6f\n" % (alt)
  455. if alt_fixes:
  456. # got altitude data
  457. # Convert fixes to offsets from avg in meters
  458. alt_data_centered = ""
  459. # find min, max and mean of altitude
  460. stat_alt.min_max_mean(alt_fixes, 0)
  461. for alt in alt_fixes:
  462. alt_fixes_r.append(alt - stat_alt.mean)
  463. alt_data_centered += "%.6f\n" % (alt - stat_alt.mean)
  464. stat_alt_r.min_max_mean(alt_fixes_r, 0)
  465. stat_alt_r.moments(alt_fixes_r, 0)
  466. # centroid in ECEF
  467. self.centroid_ecef = wgs84_to_ecef([stat_lat.mean,
  468. stat_lon.mean,
  469. stat_alt.mean])
  470. # once more through the data, looking for 3D max
  471. for fix_lla in self.fixes:
  472. if not math.isnan(fix_lla[2]):
  473. fix_ecef = wgs84_to_ecef(fix_lla[:3])
  474. dist3d = dist_3d(self.centroid_ecef, fix_ecef)
  475. if dist_3d_max < dist3d:
  476. dist_3d_max = dist3d
  477. # Sort fixes by distance from average altitude
  478. alt_fixes_r.sort(key=abs)
  479. # so we can rank fixes for EPs
  480. alt_ep = abs(alt_fixes_r[int(len(alt_fixes_r) * 0.50)])
  481. alt_ep95 = abs(alt_fixes_r[int(len(alt_fixes_r) * 0.95)])
  482. alt_ep99 = abs(alt_fixes_r[int(len(alt_fixes_r) * 0.99)])
  483. # HEP(50) calculated per RCC 261-00, Section 3.1.2
  484. calc_hep = 0.6745 * stat_alt_r.sigma
  485. # SEP(50) calculated per RCC 261-00, Section 3.1.3 (3)
  486. calc_sep = 0.51 * (stat_lat_r.sigma +
  487. stat_lon_r.sigma +
  488. stat_alt_r.sigma)
  489. # MRSE calculated per RCC 261-00, Section 3.1.5
  490. calc_mrse = math.sqrt(stat_lat_r.sigma ** 2 +
  491. stat_lon_r.sigma ** 2 +
  492. stat_alt_r.sigma ** 2)
  493. fmt_lab11a = ('hep = %.3f meters\\n'
  494. 'sep = %.3f meters\\n'
  495. 'mrse = %.3f meters\\n'
  496. ) % (calc_hep, calc_sep, calc_mrse)
  497. if self.centroid[0] < 0.0:
  498. latstring = "%.9fS" % -self.centroid[0]
  499. elif stat_lat.mean > 0.0:
  500. latstring = "%.9fN" % self.centroid[0]
  501. else:
  502. latstring = "0.0"
  503. if self.centroid[1] < 0.0:
  504. lonstring = "%.9fW" % -self.centroid[1]
  505. elif stat_lon.mean > 0.0:
  506. lonstring = "%.9fE" % self.centroid[1]
  507. else:
  508. lonstring = "0.0"
  509. # oh, this is fun, mixing gnuplot and python string formatting
  510. # Grrr, python implements %s max width or precision incorrectly...
  511. # and the old and new styles also disagree...
  512. fmt = ('set xlabel "Meters east from %s"\n'
  513. 'set ylabel "Meters north from %s"\n'
  514. 'cep=%.9f\n'
  515. 'cep95=%.9f\n'
  516. 'cep99=%.9f\n'
  517. ) % (lonstring, latstring,
  518. cep_meters, cep95_meters, cep99_meters)
  519. fmt += ('set autoscale\n'
  520. 'set multiplot\n'
  521. # plot to use 95% of width
  522. # set x and y scales to same distance
  523. 'set size ratio -1 0.95,0.7\n'
  524. # leave room at bottom for computed variables
  525. 'set origin 0.025,0.30\n'
  526. 'set format x "%.3f"\n'
  527. 'set format y "%.3f"\n'
  528. 'set key left at screen 0.6,0.30 vertical\n'
  529. 'set key noautotitle\n'
  530. 'set style line 2 pt 1\n'
  531. 'set style line 3 pt 2\n'
  532. 'set style line 5 pt 7 ps 1\n'
  533. 'set xtic rotate by -45\n'
  534. 'set border 15\n'
  535. # now the CEP stuff
  536. 'set parametric\n'
  537. 'set trange [0:2*pi]\n'
  538. 'cx(t, r) = sin(t)*r\n'
  539. 'cy(t, r) = cos(t)*r\n'
  540. 'chlen = cep/20\n'
  541. # what do the next two lines do??
  542. 'set arrow from -chlen,0 to chlen,0 nohead\n'
  543. 'set arrow from 0,-chlen to 0,chlen nohead\n')
  544. fmt += ('set label 11 at screen 0.01, screen 0.30 '
  545. '"RCC 261-00\\n'
  546. 'cep = %.3f meters\\n'
  547. '2drms = %.3f meters\\n%s'
  548. '2d max = %.3f meters\\n'
  549. '3d max = %.3f meters"\n'
  550. ) % (calc_cep, calc_2drms, fmt_lab11a, cep100_meters,
  551. dist_3d_max)
  552. # row labels
  553. fmt += ('set label 12 at screen 0.01, screen 0.12 '
  554. '"RCC 261-00\\n'
  555. '\\n'
  556. 'Lat\\n'
  557. 'Lon\\n'
  558. 'AltHAE\\n'
  559. 'Used"\n')
  560. # mean
  561. fmt += ('set label 13 at screen 0.06, screen 0.12 '
  562. '"\\n'
  563. ' mean\\n'
  564. '%s\\n'
  565. '%s\\n'
  566. '%s\\n'
  567. '%s"\n'
  568. ) % ('{0:>15}'.format(latstring),
  569. '{0:>15}'.format(lonstring),
  570. '{0:>15.3f}'.format(stat_alt.mean),
  571. '{0:>15.1f}'.format(stat_used.mean))
  572. fmt += ('set label 14 at screen 0.23, screen 0.12 '
  573. '"\\n'
  574. ' min max sigma '
  575. 'skewness kurtosis\\n'
  576. '%s %s %s meters %s %s\\n'
  577. '%s %s %s meters %s %s\\n'
  578. '%s %s %s meters %s %s\\n'
  579. '%12d %12d %s sats"\n'
  580. ) % ('{0:>10.3f}'.format(lat_min_o),
  581. '{0:>10.3f}'.format(lat_max_o),
  582. '{0:>10.3f}'.format(stat_lat_r.sigma),
  583. '{0:>10.1f}'.format(stat_lat_r.skewness),
  584. '{0:>10.1f}'.format(stat_lat_r.kurtosis),
  585. '{0:>10.3f}'.format(lon_min_o),
  586. '{0:>10.3f}'.format(lon_max_o),
  587. '{0:>10.3f}'.format(stat_lon_r.sigma),
  588. '{0:>10.1f}'.format(stat_lon_r.skewness),
  589. '{0:>10.1f}'.format(stat_lon_r.kurtosis),
  590. '{0:>10.3f}'.format(stat_alt_r.min),
  591. '{0:>10.3f}'.format(stat_alt_r.max),
  592. '{0:>10.3f}'.format(stat_alt_r.sigma),
  593. '{0:>10.1f}'.format(stat_alt_r.skewness),
  594. '{0:>10.1f}'.format(stat_alt_r.kurtosis),
  595. stat_used.min,
  596. stat_used.max,
  597. '{0:>10.1f}'.format(stat_used.sigma))
  598. if 1 < options.debug:
  599. fmt += ('set label 15 at screen 0.6, screen 0.12 '
  600. '"\\n'
  601. ' min\\n'
  602. '%s\\n'
  603. '%s\\n'
  604. '%s"\n'
  605. ) % ('{0:>15.9f}'.format(stat_lat_r.min),
  606. '{0:>15.9f}'.format(stat_lon_r.min),
  607. '{0:>15.3f}'.format(stat_alt.min))
  608. fmt += ('set label 16 at screen 0.75, screen 0.12 '
  609. '"\\n'
  610. ' max\\n'
  611. '%s\\n'
  612. '%s\\n'
  613. '%s"\n'
  614. ) % ('{0:>15.9f}'.format(stat_lat_r.max),
  615. '{0:>15.9f}'.format(stat_lon_r.max),
  616. '{0:>15.3f}'.format(stat_alt.max))
  617. if len(self.fixes) > 1000:
  618. plot_style = 'dots'
  619. else:
  620. plot_style = 'points'
  621. # got altitude data?
  622. if not math.isnan(stat_alt.mean):
  623. fmt += ('set ytics nomirror\n'
  624. 'set y2tics\n'
  625. 'set format y2 "%.3f"\n')
  626. fmt += (('set y2label "AltHAE from %.3f meters"\n') %
  627. (stat_alt.mean))
  628. # add ep(50)s
  629. altitude_x = cep100_meters * 1.2
  630. fmt += ('$EPData << EOD\n'
  631. '%.3f %.3f\n'
  632. '%.3f %.3f\n'
  633. 'EOD\n'
  634. ) % (altitude_x, alt_ep,
  635. altitude_x, -alt_ep)
  636. fmt += ('$EP95Data << EOD\n'
  637. '%.3f %.3f\n'
  638. '%.3f %.3f\n'
  639. 'EOD\n'
  640. ) % (altitude_x, alt_ep95,
  641. altitude_x, -alt_ep95)
  642. fmt += ('$EP99Data << EOD\n'
  643. '%.3f %.3f\n'
  644. '%.3f %.3f\n'
  645. 'EOD\n'
  646. ) % (altitude_x, alt_ep99,
  647. altitude_x, -alt_ep99)
  648. # setup now done, plot it!
  649. fmt += ('plot "-" using 1:2 with %s ls 3 title "%d GPS fixes" '
  650. ', cx(t,cep),cy(t,cep) ls 1 title "CEP (50%%) = %.3f meters"'
  651. ', cx(t,cep95),cy(t,cep95) title "CEP (95%%) = %.3f meters"'
  652. ', cx(t,cep99),cy(t,cep99) title "CEP (99%%) = %.3f meters"'
  653. ) % (plot_style, len(self.fixes),
  654. cep_meters, cep95_meters, cep99_meters)
  655. if not math.isnan(stat_alt.mean):
  656. # add plot of altitude
  657. fmt += (', "-" using ( %.3f ):( $1 - %.3f ) '
  658. 'axes x1y2 with points ls 2 lc "green"'
  659. ' title " %d AltHAE fixes"'
  660. ) % (cep100_meters * 1.1, stat_alt.mean, len(alt_fixes))
  661. # altitude EPs
  662. fmt += (', $EPData using 1:2 '
  663. 'axes x1y2 with points ls 5 lc "dark-green"'
  664. ' title " EP(50%%) = %.3f meters"'
  665. ) % (alt_ep)
  666. fmt += (', $EP95Data using 1:2 '
  667. 'axes x1y2 with points ls 5 lc "blue"'
  668. ' title " EP(95%%) = %.3f meters"'
  669. ) % (alt_ep95)
  670. fmt += (', $EP99Data using 1:2 '
  671. 'axes x1y2 with points ls 5 lc "red"'
  672. ' title " EP(99%%) = %.3f meters"'
  673. ) % (alt_ep99)
  674. fmt += self.header() + latlon_data
  675. if not math.isnan(stat_alt.mean):
  676. # add altitude samples
  677. fmt += 'e\n' + alt_data
  678. return fmt
  679. class polarplot(plotter):
  680. """Polar plot of signal strength."""
  681. name = "polar"
  682. requires_time = False
  683. seen_used = [] # count of seen and used in each SKY
  684. def __init__(self):
  685. """Init class polarplot."""
  686. plotter.__init__(self)
  687. self.watch = set(['SKY'])
  688. def sample(self):
  689. """Grab samples."""
  690. if self.session.data["class"] == "SKY":
  691. sats = self.session.data['satellites']
  692. seen = 0
  693. used = 0
  694. for sat in sats:
  695. seen += 1
  696. # u'ss': 42, u'el': 15, u'PRN': 18, u'az': 80, u'used': True
  697. if ((('az' not in sat) or
  698. ('el' not in sat))):
  699. continue
  700. if sat['used'] is True:
  701. used += 1
  702. if 'polarunused' == self.name:
  703. continue
  704. if (('polarused' == self.name) and (sat['used'] is False)):
  705. continue
  706. self.fixes.append((sat['PRN'], sat['ss'], sat['az'],
  707. sat['el'], sat['used']))
  708. self.seen_used.append((seen, used))
  709. return True
  710. def header(self):
  711. """Return header."""
  712. return "# Polar plot of signal strengths, %s\n" % self.whatami()
  713. def postprocess(self):
  714. """Postprocess the sample data."""
  715. return
  716. def data(self):
  717. """Format data for dump."""
  718. res = ""
  719. for (prn, ss, az, el, used) in self.fixes:
  720. res += "%d\t%d\t%d\t%d\t%s\n" % (prn, ss, az, el, used)
  721. return res
  722. def plot(self):
  723. """Format data for dump."""
  724. # calc SNR: mean, min, max, sigma
  725. stat_ss = stats()
  726. stat_ss.min_max_mean(self.fixes, 1)
  727. stat_ss.moments(self.fixes, 1)
  728. # calc sats seen data: mean, min, max, sigma
  729. stat_seen = stats()
  730. stat_seen.min_max_mean(self.seen_used, 0)
  731. stat_seen.moments(self.seen_used, 0)
  732. # calc sats used data: mean, min, max, sigma
  733. stat_used = stats()
  734. stat_used.min_max_mean(self.seen_used, 1)
  735. stat_used.moments(self.seen_used, 1)
  736. fmt = '''\
  737. unset border
  738. set polar
  739. set angles degrees # set gnuplot on degrees instead of radians
  740. set style line 10 lt 1 lc 0 lw 0.3 #redefine a new line style for the grid
  741. set grid polar 45 #set the grid to be displayed every 45 degrees
  742. set grid ls 10
  743. # x is angle, go from 0 to 360 degrees
  744. # y is radius, go from 90 at middle to 0 at edge
  745. set xrange [0:360]
  746. set rrange [90:0] # 90 at center
  747. set yrange [-90:90]
  748. # set xtics axis #display the xtics on the axis instead of on the border
  749. # set ytics axis
  750. set xtics axis nomirror; set ytics axis nomirror
  751. # "remove" the tics so that only the y tics are displayed
  752. set xtics scale 0
  753. # set the xtics only go from 0 to 90 with increment of 30
  754. # but do not display anything. This has to be done otherwise the grid
  755. # will not be displayed correctly.
  756. set xtics ("" 90, "" 60, "" 30,)
  757. # make the ytics go from the center (0) to 360 with increment of 90
  758. # set ytics 0, 45, 360
  759. set ytics scale 0
  760. # set the ytics only go from 0 to 90 with increment of 30
  761. # but do not display anything. This has to be done otherwise the grid
  762. # will not be displayed correctly.
  763. set ytics ("" 90, "" 60, "" 30,)
  764. set size square
  765. set key lmargin
  766. # this places a compass label on the outside
  767. set_label(x, text) = sprintf( \
  768. "set label '%s' at (93*cos(%f)), (93*sin(%f)) center", text, x, x)
  769. # here all labels are created
  770. # we compute North (0) at top, East (90) at right
  771. # bug gnuplot puts 0 at right, 90 at top
  772. eval set_label(0, "E")
  773. eval set_label(90, "N")
  774. eval set_label(180, "W")
  775. eval set_label(270, "S")
  776. set style line 11 pt 2 ps 2 #set the line style for the plot
  777. set style fill transparent solid 0.8 noborder
  778. # set rmargin then put colorbox in the margin.
  779. set lmargin at screen .08
  780. set rmargin at screen .85
  781. set cbrange [10:60]
  782. set palette defined (100 "blue", 200 "green", 300 "red")
  783. set colorbox user origin .92, .15 size .03, .6
  784. set label 10 at screen 0.89, screen 0.13 "SNR dBHz"
  785. '''
  786. count = len(self.fixes)
  787. fmt += '''\
  788. set label 11 at screen 0.01, screen 0.15 "%s plot, samples %d"
  789. set label 12 at screen 0.01, screen 0.10 "\\nSS\\nSeen\\nUsed"
  790. ''' % (self.name, count)
  791. fmt += '''\
  792. set label 13 at screen 0.11, screen 0.10 "min\\n%d\\n%d\\n%d" right
  793. ''' % (stat_ss.min, stat_seen.min, stat_used.min)
  794. fmt += '''\
  795. set label 14 at screen 0.21, screen 0.10 "max\\n%d\\n%d\\n%d" right
  796. ''' % (stat_ss.max, stat_seen.max, stat_used.max)
  797. fmt += '''\
  798. set label 15 at screen 0.31, screen 0.10 "mean\\n%.1f\\n%.1f\\n%.1f" right
  799. ''' % (stat_ss.mean, stat_seen.mean, stat_used.mean)
  800. fmt += '''\
  801. set label 16 at screen 0.41, screen 0.10 "sigma\\n%.1f\\n%.1f\\n%.1f" right
  802. ''' % (stat_ss.sigma, stat_seen.sigma, stat_used.sigma)
  803. fmt += '''\
  804. # and finally the plot
  805. # flip azimuth to plot north up, east right
  806. # plot "-" u (90 - $3):4 t "Sat" with points ls 11
  807. plot "-" u (90 - $3):4:(1):($2) t "Sat" w circles lc palette
  808. '''
  809. # return fmt + self.header() + self.data()
  810. return self.header() + fmt + self.data()
  811. class polarplotunused(polarplot):
  812. """Polar plot of unused sats signal strength."""
  813. name = "polarunused"
  814. class polarplotused(polarplot):
  815. """Polar plot of used sats signal strength."""
  816. name = "polarused"
  817. class timeplot(plotter):
  818. """Time drift against PPS."""
  819. name = "time"
  820. requires_time = True
  821. def __init__(self):
  822. """Init class timeplot."""
  823. plotter.__init__(self)
  824. self.watch = set(['PPS'])
  825. def sample(self):
  826. """Grab samples."""
  827. if self.session.data["class"] == "PPS":
  828. self.fixes.append((self.session.data['real_sec'],
  829. self.session.data['real_nsec'],
  830. self.session.data['clock_sec'],
  831. self.session.data['clock_nsec']))
  832. return True
  833. def header(self):
  834. """Return header."""
  835. return "# Time drift against PPS, %s\n" % self.whatami()
  836. def postprocess(self):
  837. """Postprocess the sample data."""
  838. return
  839. def data(self):
  840. """Format data for dump."""
  841. res = ""
  842. for (real_sec, real_nsec, clock_sec, clock_nsec) in self.fixes:
  843. res += "%d\t%d\t%d\t%d\n" % (real_sec, real_nsec, clock_sec,
  844. clock_nsec)
  845. return res
  846. def plot(self):
  847. """Format data for dump."""
  848. fmt = '''\
  849. set autoscale
  850. set key below
  851. set ylabel "System clock delta from GPS time (nsec)"
  852. plot "-" using 0:((column(1)-column(3))*1e9 + (column(2)-column(4))) \
  853. title "Delta" with impulses
  854. '''
  855. return fmt + self.header() + self.data()
  856. class uninstrumented(plotter):
  857. """Total times without instrumentation."""
  858. name = "uninstrumented"
  859. requires_time = False
  860. def __init__(self):
  861. """Init class uninstrumentd."""
  862. plotter.__init__(self)
  863. def sample(self):
  864. """Grab samples."""
  865. if self.session.fix.time:
  866. seconds = time.time() - gps.misc.isotime(self.session.data.time)
  867. self.fixes.append(seconds)
  868. return True
  869. return False
  870. def header(self):
  871. """Return header."""
  872. return "# Uninstrumented total latency, " + self.whatami() + "\n"
  873. def postprocess(self):
  874. """Postprocess the sample data."""
  875. return
  876. def data(self):
  877. """Format data for dump."""
  878. res = ""
  879. for seconds in self.fixes:
  880. res += "%2.6lf\n" % seconds
  881. return res
  882. def plot(self):
  883. """Plot the data."""
  884. fmt = '''\
  885. set autoscale
  886. set key below
  887. set key title "Uninstrumented total latency"
  888. plot "-" using 0:1 title "Total time" with impulses
  889. '''
  890. return fmt + self.header() + self.data()
  891. class instrumented(plotter):
  892. """Latency as analyzed by instrumentation."""
  893. name = "instrumented"
  894. requires_time = True
  895. def __init__(self):
  896. """Initialize class instrumented()."""
  897. plotter.__init__(self)
  898. def sample(self):
  899. """Grab the samples."""
  900. if 'rtime' in self.session.data:
  901. self.fixes.append((gps.misc.isotime(self.session.data['time']),
  902. self.session.data["chars"],
  903. self.session.data['sats'],
  904. self.session.data['sor'],
  905. self.session.data['rtime'],
  906. time.time()))
  907. return True
  908. return False
  909. def header(self):
  910. """Return the header."""
  911. res = "# Analyzed latency, " + self.whatami() + "\n"
  912. res += "#-- Fix time -- - Chars - -- Latency - RS232- " \
  913. "Analysis - Recv -\n"
  914. return res
  915. def postprocess(self):
  916. """Postprocess the sample data."""
  917. return
  918. def data(self):
  919. """Format data for dump."""
  920. res = ""
  921. for (fix_time, chars, sats, start, xmit, recv) in self.fixes:
  922. rs232_time = (chars * 10.0) / self.device['bps']
  923. res += "%.3f %9u %2u %.6f %.6f %.6f %.6f\n" \
  924. % (fix_time, chars, sats, start - fix_time,
  925. (start - fix_time) + rs232_time, xmit - fix_time,
  926. recv - fix_time)
  927. return res
  928. def plot(self):
  929. """Do the plot."""
  930. legends = (
  931. "Reception delta",
  932. "Analysis time",
  933. "RS232 time",
  934. "Fix latency",
  935. )
  936. fmt = '''\
  937. set autoscale
  938. set key title "Analyzed latency"
  939. set key below
  940. plot \\\n'''
  941. for (i, legend) in enumerate(legends):
  942. j = len(legends) - i + 4
  943. fmt += ' "-" using 0:%d title "%s" with impulses, \\\n' \
  944. % (j, legend)
  945. fmt = fmt[:-4] + "\n"
  946. return fmt + self.header() + (self.data() + "e\n") * len(legends)
  947. if __name__ == '__main__':
  948. usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
  949. epilog = ('BSD terms apply: see the file COPYING in the distribution '
  950. 'root for details.')
  951. # get default units from the environment
  952. # GPSD_UNITS, LC_MEASUREMENT and LANG
  953. # FIXME: use default_units
  954. default_units = gps.clienthelpers.unit_adjustments()
  955. parser = argparse.ArgumentParser(usage=usage, epilog=epilog)
  956. parser.add_argument(
  957. '-?',
  958. action="help",
  959. help='show this help message and exit'
  960. )
  961. parser.add_argument(
  962. '-D',
  963. '--debug',
  964. dest='debug',
  965. default=0,
  966. type=int,
  967. help='Set level of debug. Must be integer. [Default %(default)s]',
  968. )
  969. parser.add_argument(
  970. '-d', '--dumpfile',
  971. dest='dumpfile',
  972. default=None,
  973. help='Set dumpfile. [Default %(default)s]',
  974. )
  975. parser.add_argument(
  976. '--device',
  977. dest='device',
  978. default='',
  979. help='The device to connect. [Default %(default)s]',
  980. )
  981. parser.add_argument(
  982. '-f', '--formatter',
  983. choices=('instrumented',
  984. 'polar',
  985. 'polarunused',
  986. 'polarused',
  987. 'space',
  988. 'time',
  989. 'uninstrumented',
  990. ),
  991. dest='plotmode',
  992. default='space',
  993. help='Formatter to use. [Default %(default)s]',
  994. )
  995. parser.add_argument(
  996. '--host',
  997. dest='host',
  998. default='localhost',
  999. help='The host to connect. [Default %(default)s]',
  1000. )
  1001. parser.add_argument(
  1002. '-l',
  1003. '--logfile',
  1004. dest='logfile',
  1005. default=None,
  1006. help='Select logfile for JSON. [Default %(default)s]',
  1007. )
  1008. parser.add_argument(
  1009. '-m', '--threshold',
  1010. dest='threshold',
  1011. default=None,
  1012. help='Select logfile. [Default %(default)s]',
  1013. type=int,
  1014. )
  1015. parser.add_argument(
  1016. '-n', '--wait',
  1017. dest='wait',
  1018. default='100',
  1019. help=('Select wait time in seconds before plotting.\n'
  1020. '[Default %(default)s]'),
  1021. type=str,
  1022. )
  1023. parser.add_argument(
  1024. '--port',
  1025. dest='port',
  1026. default=gps.GPSD_PORT,
  1027. help='The port to connect. [Default %(default)s]',
  1028. )
  1029. parser.add_argument(
  1030. '-r', '--redo',
  1031. action='store_true',
  1032. dest='redo',
  1033. default=False,
  1034. help=('Redo from a previous JSON logfile on stdin.\n'
  1035. '[Default %(default)s]'),
  1036. )
  1037. parser.add_argument(
  1038. '-S', '--subtitle',
  1039. dest='subtitle',
  1040. default=None,
  1041. help='Plot subtitle. [Default %(default)s]',
  1042. )
  1043. parser.add_argument(
  1044. '-t', '--title',
  1045. dest='title',
  1046. default=None,
  1047. help='Plot title. [Default %(default)s]',
  1048. )
  1049. parser.add_argument(
  1050. '-T', '--terminal',
  1051. dest='terminal',
  1052. default='x11',
  1053. help='gnuplot terminal type. [Default %(default)s]',
  1054. )
  1055. parser.add_argument(
  1056. '-V', '--version',
  1057. action='version',
  1058. version="%(prog)s: Version " + gps_version + "\n",
  1059. help='Output version to stderr, then exit',
  1060. )
  1061. parser.add_argument(
  1062. 'target',
  1063. nargs='?',
  1064. help='[host[:port[:device]]]',
  1065. )
  1066. options = parser.parse_args()
  1067. if options.logfile:
  1068. options.logfp = open(options.logfile, "w")
  1069. else:
  1070. options.logfp = None
  1071. # the options host, port, device are set by the defaults
  1072. if options.target:
  1073. # override with target
  1074. arg = options.target.split(':')
  1075. len_arg = len(arg)
  1076. if len_arg == 1:
  1077. (options.host,) = arg
  1078. elif len_arg == 2:
  1079. (options.host, options.port) = arg
  1080. elif len_arg == 3:
  1081. (options.host, options.port, options.device) = arg
  1082. else:
  1083. parser.print_help()
  1084. sys.exit(0)
  1085. if not options.port:
  1086. options.port = gps.GPSD_PORT
  1087. formatters = (polarplot, polarplotunused, polarplotused,
  1088. spaceplot, timeplot, uninstrumented, instrumented)
  1089. if options.plotmode:
  1090. for formatter in formatters:
  1091. if formatter.name == options.plotmode:
  1092. plot = formatter()
  1093. break
  1094. else:
  1095. sys.stderr.write("gpsprof: no such formatter.\n")
  1096. sys.exit(1)
  1097. if options.wait[-1] == 'h':
  1098. options.wait = int(options.wait[:-1]) * 360
  1099. else:
  1100. options.wait = int(options.wait)
  1101. try:
  1102. # Get fix data from the GPS
  1103. if options.redo:
  1104. plot.replot(sys.stdin)
  1105. else:
  1106. plot.collect(options.debug, options.logfp)
  1107. plot.postprocess()
  1108. # Save the timing data (only) for post-analysis if required.
  1109. if options.dumpfile:
  1110. with open(options.dumpfile, "w") as fp:
  1111. fp.write(plot.dump())
  1112. if options.logfp:
  1113. options.logfp.close()
  1114. # Ship the plot to standard output
  1115. if not options.title:
  1116. options.title = plot.whatami()
  1117. # escape " for gnuplot
  1118. options.title = options.title.replace('"', '\\"')
  1119. if options.subtitle:
  1120. options.title += '\\n' + options.subtitle
  1121. term_opts = ""
  1122. truecolor_terms = ['png', 'sixelgd', 'wxt']
  1123. if options.terminal in truecolor_terms:
  1124. term_opts = 'truecolor'
  1125. sys.stdout.write("set terminal %s size 800,950 %s\n"
  1126. "set termoption enhanced\n"
  1127. % (options.terminal, term_opts))
  1128. # double quotes on title so \n is parsed by gnuplot
  1129. sys.stdout.write('set title noenhanced "%s\\n\\n"\n' % options.title)
  1130. sys.stdout.write(plot.plot())
  1131. except KeyboardInterrupt:
  1132. pass
  1133. # The following sets edit modes for GNU EMACS
  1134. # Local Variables:
  1135. # mode:python
  1136. # End: