data/method/mavlink/pymavlink/tools/mavgraph.py

349 lines
12 KiB
Python
Raw Normal View History

2024-07-24 18:30:46 +08:00
#!/usr/bin/env python
'''
graph a MAVLink log file
Andrew Tridgell August 2011
'''
from __future__ import print_function
from builtins import input
from builtins import range
import datetime
import matplotlib
import os
import re
import sys
import time
from math import *
try:
from pymavlink.mavextra import *
except:
print("WARNING: Numpy missing, mathematical notation will not be supported.")
if sys.version_info[0] >= 3:
text_types = frozenset([str,])
else:
text_types = frozenset([unicode, str])
# cope with rename of raw_input in python3
try:
input = raw_input
except NameError:
pass
colourmap = {
'ardupilot' : {
'MANUAL' : (1.0, 0, 0),
'AUTO' : ( 0, 1.0, 0),
'LOITER' : ( 0, 0, 1.0),
'FBWA' : (1.0, 0.5, 0),
'RTL' : ( 1, 0, 0.5),
'STABILIZE' : (0.5, 1.0, 0),
'LAND' : ( 0, 1.0, 0.5),
'STEERING' : (0.5, 0, 1.0),
'HOLD' : ( 0, 0.5, 1.0),
'ALT_HOLD' : (1.0, 0.5, 0.5),
'CIRCLE' : (0.5, 1.0, 0.5),
'POSITION' : (1.0, 0.0, 1.0),
'GUIDED' : (0.5, 0.5, 1.0),
'ACRO' : (1.0, 1.0, 0),
'CRUISE' : ( 0, 1.0, 1.0)
},
'px4' : {
'MANUAL' : (1.0, 0, 0),
'SEATBELT' : ( 0.5, 0.5, 0),
'EASY' : ( 0, 1.0, 0),
'AUTO' : ( 0, 0, 1.0),
'UNKNOWN' : ( 1.0, 1.0, 1.0)
}
}
colourmap["apm"] = colourmap["ardupilot"]
edge_colour = (0.1, 0.1, 0.1)
lowest_x = None
highest_x = None
def plotit(x, y, fields, colors=[]):
'''plot a set of graphs using date for x axis'''
global lowest_x, highest_x
pylab.ion()
fig = pylab.figure(num=1, figsize=(12,6))
ax1 = fig.gca()
ax2 = None
xrange = 0.0
for i in range(0, len(fields)):
if len(x[i]) == 0: continue
if lowest_x is None or x[i][0] < lowest_x:
lowest_x = x[i][0]
if highest_x is None or x[i][-1] > highest_x:
highest_x = x[i][-1]
if highest_x is None or lowest_x is None:
return
xrange = highest_x - lowest_x
xrange *= 24 * 60 * 60
formatter = matplotlib.dates.DateFormatter('%H:%M:%S')
interval = 1
intervals = [ 1, 2, 5, 10, 15, 30, 60, 120, 240, 300, 600,
900, 1800, 3600, 7200, 5*3600, 10*3600, 24*3600 ]
for interval in intervals:
if xrange / interval < 15:
break
locator = matplotlib.dates.SecondLocator(interval=interval)
if not args.xaxis:
ax1.xaxis.set_major_locator(locator)
ax1.xaxis.set_major_formatter(formatter)
empty = True
ax1_labels = []
ax2_labels = []
for i in range(0, len(fields)):
if len(x[i]) == 0:
print("Failed to find any values for field %s" % fields[i])
continue
if i < len(colors):
color = colors[i]
else:
color = 'red'
(tz, tzdst) = time.tzname
if axes[i] == 2:
if ax2 is None:
ax2 = ax1.twinx()
ax = ax2
if not args.xaxis:
ax2.xaxis.set_major_locator(locator)
ax2.xaxis.set_major_formatter(formatter)
label = fields[i]
if label.endswith(":2"):
label = label[:-2]
ax2_labels.append(label)
else:
ax1_labels.append(fields[i])
ax = ax1
if args.xaxis:
if args.marker is not None:
marker = args.marker
else:
marker = '+'
if args.linestyle is not None:
linestyle = args.linestyle
else:
linestyle = 'None'
ax.plot(x[i], y[i], color=color, label=fields[i],
linestyle=linestyle, marker=marker)
else:
if args.marker is not None:
marker = args.marker
else:
marker = 'None'
if args.linestyle is not None:
linestyle = args.linestyle
else:
linestyle = '-'
if len(y[i]) > 0 and type(y[i][0]) in text_types:
# assume this is a piece of text to be rendered at a point in time
last_text_time = -1
last_text = None
for n in range(0, len(x[i])):
if last_text is None:
last_text = "[" + y[i][n] + "]"
last_text_time = x[i][n]
elif x[i][n] == last_text_time:
last_text += "[" + y[i][n] + "]"
else:
ax.text(x[i][n], 10, last_text,
rotation=90,
alpha=0.3,
verticalalignment='baseline')
last_text = None
last_label_time = x[i][n]
if last_text is not None:
ax.text(x[i][n], 10, last_text,
rotation=90,
alpha=0.3,
verticalalignment='baseline')
else:
ax.plot_date(x[i], y[i], color=color, label=fields[i],
linestyle=linestyle, marker=marker, tz=None)
empty = False
if args.flightmode is not None:
for i in range(len(modes)-1):
c = colourmap[args.flightmode].get(modes[i][1], edge_colour)
ax1.axvspan(modes[i][0], modes[i+1][0], fc=c, ec=edge_colour, alpha=0.1)
c = colourmap[args.flightmode].get(modes[-1][1], edge_colour)
ax1.axvspan(modes[-1][0], ax1.get_xlim()[1], fc=c, ec=edge_colour, alpha=0.1)
if ax1_labels != []:
ax1.legend(ax1_labels,loc=args.legend)
if ax2_labels != []:
ax2.legend(ax2_labels,loc=args.legend2)
if empty:
print("No data to graph")
return
return fig
from argparse import ArgumentParser
parser = ArgumentParser(description=__doc__)
parser.add_argument("--no-timestamps", dest="notimestamps", action='store_true', help="Log doesn't have timestamps")
parser.add_argument("--planner", action='store_true', help="use planner file format")
parser.add_argument("--condition", default=None, help="select packets by a condition")
parser.add_argument("--labels", default=None, help="comma separated field labels")
parser.add_argument("--legend", default='upper left', help="default legend position")
parser.add_argument("--legend2", default='upper right', help="default legend2 position")
parser.add_argument("--marker", default=None, help="point marker")
parser.add_argument("--linestyle", default=None, help="line style")
parser.add_argument("--xaxis", default=None, help="X axis expression")
parser.add_argument("--multi", action='store_true', help="multiple files with same colours")
parser.add_argument("--zero-time-base", action='store_true', help="use Z time base for DF logs")
parser.add_argument("--flightmode", default=None,
help="Choose the plot background according to the active flight mode of the specified type, e.g. --flightmode=apm for ArduPilot or --flightmode=px4 for PX4 stack logs. Cannot be specified with --xaxis.")
parser.add_argument("--dialect", default="ardupilotmega", help="MAVLink dialect")
parser.add_argument("--output", default=None, help="provide an output format")
parser.add_argument("--timeshift", type=float, default=0, help="shift time on first graph in seconds")
parser.add_argument("logs_fields", metavar="<LOG or FIELD>", nargs="+")
args = parser.parse_args()
from pymavlink import mavutil
if args.flightmode is not None and args.xaxis:
print("Cannot request flightmode backgrounds with an x-axis expression")
sys.exit(1)
if args.flightmode is not None and args.flightmode not in colourmap:
print("Unknown flight controller '%s' in specification of --flightmode (choose from %s)" % (args.flightmode, ",".join(colourmap.keys())))
sys.exit(1)
if args.output is not None:
matplotlib.use('Agg')
import pylab
filenames = []
fields = []
for f in args.logs_fields:
if os.path.exists(f):
filenames.append(f)
else:
fields.append(f)
msg_types = set()
multiplier = []
field_types = []
colors = [ 'red', 'green', 'blue', 'orange', 'olive', 'black', 'grey', 'yellow', 'brown', 'darkcyan', 'cornflowerblue', 'darkmagenta', 'deeppink', 'darkred']
# work out msg types we are interested in
x = []
y = []
modes = []
axes = []
first_only = []
re_caps = re.compile('[A-Z_][A-Z0-9_]+')
for f in fields:
caps = set(re.findall(re_caps, f))
msg_types = msg_types.union(caps)
field_types.append(caps)
y.append([])
x.append([])
axes.append(1)
first_only.append(False)
def add_data(t, msg, vars, flightmode):
'''add some data'''
mtype = msg.get_type()
if args.flightmode is not None and (len(modes) == 0 or modes[-1][1] != flightmode):
modes.append((t, flightmode))
if mtype not in msg_types:
return
for i in range(0, len(fields)):
if mtype not in field_types[i]:
continue
f = fields[i]
if f.endswith(":2"):
axes[i] = 2
f = f[:-2]
if f.endswith(":1"):
first_only[i] = True
f = f[:-2]
v = mavutil.evaluate_expression(f, vars)
if v is None:
continue
if args.xaxis is None:
xv = t
else:
xv = mavutil.evaluate_expression(args.xaxis, vars)
if xv is None:
continue
y[i].append(v)
x[i].append(xv)
def process_file(filename, timeshift):
'''process one file'''
print("Processing %s" % filename)
mlog = mavutil.mavlink_connection(filename, notimestamps=args.notimestamps, zero_time_base=args.zero_time_base, dialect=args.dialect)
vars = {}
all_messages = {}
while True:
msg = mlog.recv_match(args.condition)
if msg is None: break
try:
tdays = matplotlib.dates.date2num(datetime.datetime.fromtimestamp(msg._timestamp+timeshift))
except ValueError:
# this can happen if the log is corrupt
# ValueError: year is out of range
break
all_messages[msg.get_type()] = msg
add_data(tdays, msg, all_messages, mlog.flightmode)
if len(filenames) == 0:
print("No files to process")
sys.exit(1)
if args.labels is not None:
labels = args.labels.split(',')
if len(labels) != len(fields)*len(filenames):
print("Number of labels (%u) must match number of fields (%u)" % (
len(labels), len(fields)*len(filenames)))
sys.exit(1)
else:
labels = None
timeshift = args.timeshift
for fi in range(0, len(filenames)):
f = filenames[fi]
process_file(f, timeshift)
timeshift = 0
for i in range(0, len(x)):
if first_only[i] and fi != 0:
x[i] = []
y[i] = []
if labels:
lab = labels[fi*len(fields):(fi+1)*len(fields)]
else:
lab = fields[:]
if args.multi:
col = colors[:]
else:
col = colors[fi*len(fields):]
fig = plotit(x, y, lab, colors=col)
for i in range(0, len(x)):
x[i] = []
y[i] = []
if args.output is None:
pylab.show()
pylab.draw()
input('press enter to exit....')
else:
fname, fext = os.path.splitext(args.output)
if fext == '.html':
import mpld3
html = mpld3.fig_to_html(fig)
f_out = open(args.output, 'w')
f_out.write(html)
f_out.close()
else:
pylab.legend(loc=2,prop={'size':8})
pylab.savefig(args.output, bbox_inches='tight', dpi=200)