diff options
author | Bernd Zeimetz <bernd@bzed.de> | 2010-04-19 21:51:46 +0200 |
---|---|---|
committer | Bernd Zeimetz <bernd@bzed.de> | 2010-04-20 02:45:04 +0200 |
commit | 342eb9e4bc9aa7124d65da93b814a54258934b07 (patch) | |
tree | ec4d01fbe50bd935be44e8a9bff2a45bc2a435dd /gpsprof | |
parent | 86076034c4d485723a95cf2a311fbfb248357a9a (diff) | |
download | gpsd-342eb9e4bc9aa7124d65da93b814a54258934b07.tar.gz |
Use setup.py to handle the Python shebangs.
Diffstat (limited to 'gpsprof')
-rwxr-xr-x | gpsprof | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/gpsprof b/gpsprof new file mode 100755 index 00000000..d35732fa --- /dev/null +++ b/gpsprof @@ -0,0 +1,487 @@ +#!/usr/bin/env python +# +# This file is Copyright (c) 2010 by the GPSD project +# BSD terms apply: see the file COPYING in the distribution root for details. +# +# Collect and plot latency-profiling data from a running gpsd. +# Requires gnuplot. +# +import sys, os, time, getopt, tempfile, time, socket, math, copy +import gps + +class Baton: + "Ship progress indication to stderr." + def __init__(self, prompt, endmsg=None): + self.stream = sys.stderr + self.stream.write(prompt + "...") + if os.isatty(self.stream.fileno()): + self.stream.write(" \010") + self.stream.flush() + self.count = 0 + self.endmsg = endmsg + self.time = time.time() + return + + def twirl(self, ch=None): + if self.stream is None: + return + if ch: + self.stream.write(ch) + elif os.isatty(self.stream.fileno()): + self.stream.write("-/|\\"[self.count % 4]) + self.stream.write("\010") + self.count = self.count + 1 + self.stream.flush() + return + + def end(self, msg=None): + if msg == None: + msg = self.endmsg + if self.stream: + self.stream.write("...(%2.2f sec) %s.\n" % (time.time() - self.time, msg)) + return + +class spaceplot: + "Total times without instrumentation." + name = "space" + def __init__(self): + self.fixes = [] + def d(self, a, b): + return math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2) + def gather(self, session): + # Include altitude, not used here, for 3D plot experiments. + # Watch out for the NaN value from gps.py. + self.fixes.append((session.fix.latitude, session.fix.longitude, session.fix.altitude)) + return True + def header(self, session): + res = "# Position uncertainty, %s, %s, %ds cycle\n" % \ + (title, session.gps_id, session.cycle) + return res + def data(self, session): + res = "" + for i in range(len(self.recentered)): + (lat, lon) = self.recentered[i][:2] + (raw1, raw2, alt) = self.fixes[i] + res += "%f\t%f\t%f\t%f\t%f\n" % (lat, lon, raw1, raw2, alt) + return res + def plot(self, title, session): + if len(self.fixes) == 0: + sys.stderr.write("No fixes collected, can't estimate accuracy.") + sys.exit(1) + # centroid is just arithmetic avg of lat,lon + self.centroid = (sum(map(lambda x:x[0], self.fixes))/len(self.fixes), sum(map(lambda x:x[1], self.fixes))/len(self.fixes)) + # Sort fixes by distance from centroid + self.fixes.sort(lambda x, y: cmp(self.d(self.centroid, x), self.d(self.centroid, y))) + # Convert fixes to offsets from centroid in meters + self.recentered = map(lambda fix: gps.MeterOffset(self.centroid, fix[:2]), self.fixes) + # Compute CEP(50%) + cep_meters = gps.EarthDistance(self.centroid[:2], self.fixes[len(self.fixes)/2][:2]) + alt_sum = 0 + alt_num = 0 + lon_max = -9999 + for i in range(len(self.recentered)): + (lat, lon) = self.recentered[i][:2] + (raw1, raw2, alt) = self.fixes[i] + if not gps.isnan(alt): + alt_sum += alt + alt_num += 1 + if lon > lon_max : + lon_max = lon + if alt_num == 0: + alt_avg = gps.NaN + else: + alt_avg = alt_sum / alt_num + if self.centroid[0] < 0: + latstring = "%fS" % -self.centroid[0] + elif self.centroid[0] == 0: + latstring = "0" + else: + latstring = "%fN" % self.centroid[0] + if self.centroid[1] < 0: + lonstring = "%fW" % -self.centroid[1] + elif self.centroid[1] == 0: + lonstring = "0" + else: + lonstring = "%fE" % self.centroid[1] + fmt = "set autoscale\n" + fmt += 'set key below\n' + fmt += 'set key title "%s"\n' % time.asctime() + fmt += 'set size ratio -1\n' + fmt += 'set style line 2 pt 1\n' + fmt += 'set style line 3 pt 2\n' + fmt += 'set xlabel "Meters east from %s"\n' % lonstring + fmt += 'set ylabel "Meters north from %s"\n' % latstring + fmt += 'set border 15\n' + if not gps.isnan(alt_avg): + fmt += 'set y2label "Meters Altitude from %f"\n' % alt_avg + fmt += 'set ytics nomirror\n' + fmt += 'set y2tics\n' + fmt += 'cep=%f\n' % self.d((0,0), self.recentered[len(self.fixes)/2]) + fmt += 'set parametric\n' + fmt += 'set trange [0:2*pi]\n' + fmt += 'cx(t, r) = sin(t)*r\n' + fmt += 'cy(t, r) = cos(t)*r\n' + fmt += 'chlen = cep/20\n' + fmt += "set arrow from -chlen,0 to chlen,0 nohead\n" + fmt += "set arrow from 0,-chlen to 0,chlen nohead\n" + fmt += 'plot cx(t, cep),cy(t, cep) title "CEP (50%%) = %f meters", ' % (cep_meters) + fmt += ' "-" using 1:2 with points ls 3 title "%d GPS fixes" ' % (len(self.fixes)) + if not gps.isnan(alt_avg): + fmt += ', "-" using ( %f ):($5 < 100000 ? $5 - %f : 1/0) axes x1y2 with points ls 2 title " %d Altitude fixes, Average = %f" \n' % (lon_max +1, alt_avg, alt_num, alt_avg) + else: + fmt += "\n" + fmt += self.header(session) + fmt += self.data(session) + if not gps.isnan(alt_avg): + fmt += "e\n" + self.data(session) + return fmt + +class uninstrumented: + "Total times without instrumentation." + name = "uninstrumented" + def __init__(self): + self.stats = [] + def gather(self, session): + if session.fix.time: + seconds = time.time() - session.fix.time + self.stats.append(seconds) + return True + else: + return False + def header(self, session): + return "# Uninstrumented total latency, %s, %s, %dN%d, cycle %ds\n" % \ + (title, + session.gps_id, session.baudrate, + session.stopbits, session.cycle) + def data(self, session): + res = "" + for seconds in self.stats: + res += "%2.6lf\n" % seconds + return res + def plot(self, title, session): + fmt = ''' +set autoscale +set key below +set key title "Uninstrumented total latency, %s, %s, %dN%d, cycle %ds" +plot "-" using 0:1 title "Total time" with impulses +''' + res = fmt % (title, + session.gps_id, session.baudrate, + session.stopbits, session.cycle) + res += self.header(session) + return res + self.data(session) + +class rawplot: + "All measurement, no deductions." + name = "raw" + def __init__(self): + self.stats = [] + def gather(self, session): + self.stats.append(copy.copy(session.timings)) + return True + def header(self, session): + res = "# Raw latency data, %s, %s, %dN%d, cycle %ds\n" % \ + (title, + session.gps_id, session.baudrate, + session.stopbits, session.cycle) + res += "# tag len xmit " + for hn in ("T1", "D1", "E2", "T2", "D2"): + res += "%-13s" % hn + res += "\n#------- ----- --------------------" + for i in range(0, 5): + res += " " + ("-" * 11) + return res + "\n" + def data(self, session): + res = "" + for timings in self.stats: + res += "% 8s %4d %2.9f %2.9f %2.9f %2.9f %2.9f %2.9f\n" \ + % (timings.tag, + timings.len, + timings.xmit, + timings.recv - timings.xmit, + timings.decode - timings.recv, + timings.emit - timings.decode, + timings.c_recv - timings.emit, + timings.c_decode - timings.c_recv) + return res + def plot(self, file, session): + fmt = ''' +set autoscale +set key below +set key title "Raw latency data, %s, %s, %dN%d, cycle %ds" +plot \ + "-" using 0:8 title "D2 = Client decode time" with impulses, \ + "-" using 0:7 title "T2 = TCP/IP latency" with impulses, \ + "-" using 0:6 title "E2 = Daemon encode time" with impulses, \ + "-" using 0:5 title "D1 = Daemon decode time" with impulses, \ + "-" using 0:4 title "T1 = RS232 time" with impulses +''' + res = fmt % (title, + session.gps_id, session.baudrate, + session.stopbits, session.cycle) + res += self.header(session) + for dummy in range(0, 5): + res += self.data(session) + "e\n" + return res + +class splitplot: + "Discard base time, use color to indicate different tags." + name = "split" + sentences = [] + def __init__(self): + self.stats = [] + def gather(self, session): + self.stats.append(copy.copy(session.timings)) + if session.timings.tag not in self.sentences: + self.sentences.append(session.timings.tag) + return True + def header(self, session): + res = "# Split latency data, %s, %s, %dN%d, cycle %ds\n#" % \ + (title, + session.gps_id, session.baudrate, + session.stopbits, session.cycle) + for s in splitplot.sentences: + res += "%8s\t" % s + for hn in ("T1", "D1", "E2", "T2", "D2", "length"): + res += "%8s\t" % hn + res += "tag\n# " + for s in tuple(splitplot.sentences) + ("T1", "D1", "E2", "T2", "D2", "length"): + res += "---------\t" + return res + "--------\n" + def data(self, session): + res = "" + for timings in self.stats: + for s in splitplot.sentences: + if s == timings.tag: + res += "%2.6f\t" % timings.xmit + else: + res += "- \t" + res += "%2.6f\t%2.6f\t%2.6f\t%2.6f\t%2.6f\t%8d\t# %s\n" \ + % (timings.recv - timings.xmit, + timings.decode - timings.recv, + timings.emit - timings.decode, + timings.c_recv - timings.emit, + timings.c_decode - timings.c_recv, + timings.len, + timings.tag) + return res + def plot(self, title, session): + fixed = ''' +set autoscale +set key below +set key title "Filtered latency data, %s, %s, %dN%d, cycle %ds" +plot \ + "-" using 0:%d title "D2 = Client decode time" with impulses, \ + "-" using 0:%d title "T2 = TCP/IP latency" with impulses, \ + "-" using 0:%d title "E2 = Daemon encode time" with impulses, \ + "-" using 0:%d title "D1 = Daemon decode time" with impulses, \ + "-" using 0:%d title "T1 = RS3232 time" with impulses, \ +''' + sc = len(splitplot.sentences) + fmt = fixed % (title, + session.gps_id, session.baudrate, + session.stopbits, session.cycle, + sc+5, + sc+4, + sc+3, + sc+2, + sc+1) + for i in range(sc): + fmt += ' "-" using 0:%d title "%s" with impulses,' % \ + (i+1, self.sentences[i]) + res = fmt[:-1] + "\n" + res += self.header(session) + for dummy in range(sc+5): + res += self.data(session) + "e\n" + return res + +class cycle: + "Send-cycle analysis." + name = "cycle" + def __init__(self): + self.stats = [] + def gather(self, session): + self.stats.append(copy.copy(session.timings)) + return True + def plot(self, title, session): + msg = "" + def roundoff(n): + # Round a time to hundredths of a second + return round(n*100) / 100.0 + intervals = {} + last_seen = {} + for timing in self.stats: + # Throw out everything but the leader in each GSV group + if timing.tag[-3:] == "GSV" and last_command[-3:] == "GSV": + continue + last_command = timing.tag + # Record timings + received = timing.d_received() + if not timing.tag in intervals: + intervals[timing.tag] = [] + if timing.tag in last_seen: + intervals[timing.tag].append(roundoff(received - last_seen[timing.tag])) + last_seen[timing.tag] = received + + # Step three: get command frequencies and the basic send cycle time + frequencies = {} + for (key, interval_list) in intervals.items(): + frequencies[key] = {} + for interval in interval_list: + frequencies[key][interval] = frequencies[key].get(interval, 0) + 1 + # filter out noise + for key in frequencies: + distribution = frequencies[key] + for interval in distribution.keys(): + if distribution[interval] < 2: + del distribution[interval] + cycles = {} + for key in frequencies: + distribution = frequencies[key] + if len(frequencies[key].values()) == 1: + # The value is uniqe after filtering + cycles[key] = distribution.keys()[0] + else: + # Compute the mode + maxfreq = 0 + for (interval, frequency) in distribution.items(): + if distribution[interval] > maxfreq: + cycles[key] = interval + maxfreq = distribution[interval] + msg += "Cycle report %s, %s, %dN%d, cycle %ds" % \ + (title, + session.gps_id, session.baudrate, + session.stopbits, session.cycle) + msg += "The sentence set emitted by this GPS is: %s\n" % " ".join(intervals.keys()) + for key in cycles: + if len(frequencies[key].values()) == 1: + if cycles[key] == 1: + msg += "%s: is emitted once a second.\n" % key + else: + msg += "%s: is emitted once every %d seconds.\n" % (key, cycles[key]) + else: + if cycles[key] == 1: + msg += "%s: is probably emitted once a second.\n" % key + else: + msg += "%s: is probably emitted once every %d seconds.\n" % (key, cycles[key]) + sendcycle = min(*cycles.values()) + if sendcycle == 1: + msg += "Send cycle is once per second.\n" + else: + msg += "Send cycle is once per %d seconds.\n" % sendcycle + return msg + +formatters = (spaceplot, uninstrumented, rawplot, splitplot, cycle) + +def plotframe(await, fname, speed, threshold, title): + "Return a string containing a GNUplot script " + if fname: + for formatter in formatters: + if formatter.name == fname: + plotter = formatter() + break + else: + sys.stderr.write("gpsprof: no such formatter.\n") + sys.exit(1) + try: + session = gps.gps(verbose=verbose) + except socket.error: + sys.stderr.write("gpsprof: gpsd unreachable.\n") + sys.exit(1) + # Initialize + session.poll() + if session.version == None: + print >>sys.stderr, "gpsprof: requires gpsd to speak new protocol." + sys.exit(1) + session.send("?DEVICES;") + while session.poll() != -1: + if session.data["class"] == "DEVICES": + break + if len(session.data.devices) != 1: + print >>sys.stderr, "gpsprof: exactly one device must be attached." + sys.exit(1) + device = session.data.devices[0] + path = device["path"] + session.baudrate = device["bps"] + session.parity = device["bps"] + session.stopbits = device["stopbits"] + session.cycle = device["cycle"] + session.gps_id = device["driver"] + # Set parameters + if speed: + session.send('?DEVICE={"path":"%s","bps:":%d}' % (path, speed)) + session.poll() + if session.baudrate != speed: + sys.stderr.write("gpsprof: baud rate change failed.\n") + options = "" + if formatter not in (spaceplot, uninstrumented): + options = ',"timing":true' + try: + #session.set_raw_hook(lambda x: sys.stderr.write(`x`+"\n")) + session.send('?WATCH={"enable":true%s}' % options) + baton = Baton("gpsprof: looking for fix", "done") + countdown = await + basetime = time.time() + while countdown > 0: + if session.poll() == -1: + sys.stderr.write("gpsprof: gpsd has vanished.\n") + sys.exit(1) + baton.twirl() + if session.data["class"] == "WATCH": + if "timing" in options and not session.data.get("timing"): + sys.stderr.write("gpsprof: timing is not enabled.\n") + sys.exit(1) + # We can get some funky artifacts at start of session + # apparently due to RS232 buffering effects. Ignore + # them. + if threshold and time.time()-basetime < session.cycle * threshold: + continue + if session.fix.mode <= gps.MODE_NO_FIX: + continue + if countdown == await: + sys.stderr.write("first fix in %.2fsec, gathering %d samples..." % (time.time()-basetime,await)) + if plotter.gather(session): + countdown -= 1 + baton.end() + finally: + session.send('?WATCH={"enable":false,"timing":false}') + command = plotter.plot(title, session) + del session + return command + +if __name__ == '__main__': + try: + (options, arguments) = getopt.getopt(sys.argv[1:], "f:hm:n:s:t:v") + formatter = "space" + raw = False + speed = 0 + title = time.ctime() + threshold = 0 + await = 100 + verbose = False + for (switch, val) in options: + if (switch == '-f'): + formatter = val + elif (switch == '-m'): + threshold = int(val) + elif (switch == '-n'): + await = int(val) + elif (switch == '-s'): + speed = int(val) + elif (switch == '-t'): + title = val + elif (switch == '-v'): + verbose = True + elif (switch == '-h'): + sys.stderr.write(\ + "usage: gpsprof [-h] [-m threshold] [-n samplecount] \n" + + "\t[-f {" + "|".join(map(lambda x: x.name, formatters)) + "}] [-s speed] [-t title]\n") + sys.exit(0) + sys.stdout.write(plotframe(await,formatter,speed,threshold,title)) + except KeyboardInterrupt: + pass + +# The following sets edit modes for GNU EMACS +# Local Variables: +# mode:python +# End: |