Skip to content

Commit 0658dae

Browse files
committed
MAVExplorer: allow arbitrary expressions to be plotted on the map
this allows mavextra to construct new mappable objects with Lat and Lng fields, allowing for derived positions from logs this also generalises and simplifies the handling of instances
1 parent 14e77b1 commit 0658dae

File tree

2 files changed

+129
-135
lines changed

2 files changed

+129
-135
lines changed

MAVProxy/tools/MAVExplorer.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ def cmd_map(args):
455455
options.colour_source='flightmode'
456456
options.nkf_sample = 1
457457
if len(args) > 0:
458-
options.types = ','.join(args)
458+
options.types = ':'.join(args)
459459
if len(options.types) > 1:
460460
options.colour_source='type'
461461
mfv_mav_ret = mavflightview.mavflightview_mav(mestate.mlog, options, mestate.flightmode_selections)

MAVProxy/tools/mavflightview.py

+128-134
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,64 @@ def colour_for_point_type(mlog, point, instance, options):
253253
'violet', 'purple', 'grey', 'black']
254254
return map_colours[instance]
255255

256+
def message_to_latlon(type, m, is_expression=False):
257+
'''convert a message to a lat/lon'''
258+
if type in ['GPS','GPS2'] and not is_expression:
259+
status = getattr(m, 'Status', None)
260+
nsats = getattr(m, 'NSats', None)
261+
# prevent mapping when no fix
262+
if status is None:
263+
status = getattr(m, 'FixType', None)
264+
if status is None:
265+
return None
266+
if nsats is None:
267+
nsats = 0
268+
if status < 2 and nsats < 5:
269+
return None
270+
# flash log
271+
lat = m.Lat
272+
lng = getattr(m, 'Lng', None)
273+
if lng is None:
274+
lng = getattr(m, 'Lon', None)
275+
if lng is None:
276+
return None
277+
return lat, lng
278+
279+
if hasattr(m,'Lat') and hasattr(m,'Lng'):
280+
return m.Lat, m.Lng
281+
if hasattr(m,'Lat') and hasattr(m,'Lon'):
282+
return m.Lat, m.Lon
283+
if hasattr(m,'PN') and hasattr(m,'PE'):
284+
pos = mavextra.ekf1_pos(m)
285+
if pos is None:
286+
return None
287+
return pos[0], pos[1]
288+
if hasattr(m,'lat') and hasattr(m,'lon'):
289+
return m.lat*1.0e-7, m.lon*1.0e-7
290+
if hasattr(m,'lat') and hasattr(m,'lng'):
291+
return m.lat*1.0e-7, m.lng*1.0e-7
292+
if hasattr(m,'latitude') and hasattr(m,'longitude'):
293+
return m.latitude*1.0e-7, m.longitude*1.0e-7
294+
return None
295+
296+
class PosExpression:
297+
'''object repesenting a map expression, with the types that we need to look for in the log'''
298+
def __init__(self, expression):
299+
self.expression = expression
300+
re_caps = re.compile('[A-Z_][A-Z0-9_]+')
301+
caps = set(re.findall(re_caps, expression))
302+
self.recv_match_types = caps
303+
304+
def __repr__(self):
305+
return "Expression(%s,%s)" % (self.expression, self.recv_match_types)
306+
307+
def pos_expressions(type_list):
308+
'''return a list of PosExpression objects for a type list'''
309+
ret = []
310+
for t in type_list:
311+
ret.append(PosExpression(t))
312+
return ret
313+
256314
def mavflightview_mav(mlog, options=None, flightmode_selections=[]):
257315
'''create a map for a log file'''
258316
wp = mavwp.MAVWPLoader()
@@ -266,51 +324,45 @@ def mavflightview_mav(mlog, options=None, flightmode_selections=[]):
266324
if s:
267325
all_false = False
268326
idx = 0
269-
path = [[]]
270-
instances = {}
327+
path = []
271328
ekf_counter = 0
272329
nkf_counter = 0
330+
expressions = []
331+
273332
types = ['MISSION_ITEM', 'MISSION_ITEM_INT', 'CMD']
274333
if options.types is not None:
275-
types.extend(options.types.split(','))
334+
if options.types.find(':'):
335+
type_list = options.types.split(':')
336+
else:
337+
type_list = options.types.split(',')
338+
expressions.extend(pos_expressions(type_list))
276339
else:
277-
types.extend(['POS','GLOBAL_POSITION_INT'])
340+
expressions.extend(pos_expressions(['POS', 'GLOBAL_POSITION_INT']))
278341
if options.rawgps or options.dualgps:
279-
types.extend(['GPS', 'GPS_RAW_INT'])
342+
expressions.extend(pos_expressions(['GPS', 'GPS_RAW_INT']))
280343
if options.rawgps2 or options.dualgps:
281-
types.extend(['GPS2_RAW','GPS2'])
344+
expressions.extend(pos_expressions(['GPS2_RAW','GPS2']))
282345
if options.ekf:
283-
types.extend(['EKF1', 'GPS'])
346+
expressions.extend(pos_expressions(['EKF1', 'GPS']))
284347
if options.nkf:
285-
types.extend(['NKF1', 'GPS'])
348+
expressions.extend(pos_expressions(['NKF1', 'GPS']))
286349
if options.ahr2:
287-
types.extend(['AHR2', 'AHRS2', 'GPS'])
288-
289-
# handle forms like GPS[0], mapping to GPS for recv_match_types
290-
291-
# it may be possible to pass conditions in to recv_match_types,
292-
# but for now we filter to desired instances later.
293-
want_instances = {}
294-
recv_match_types = types[:]
295-
for i in range(len(recv_match_types)):
296-
match = re.match('(?P<name>.*)\[(?P<instancenum>[^\]]+)\]', recv_match_types[i])
297-
if match is not None:
298-
name = match.group("name")
299-
number = match.group("instancenum")
300-
if name not in want_instances:
301-
want_instances[name] = set()
302-
want_instances[name].add(number)
303-
recv_match_types[i] = name
350+
expressions.extend(pos_expressions(['AHR2', 'AHRS2', 'GPS']))
351+
352+
# find the union of message types we need from the log for all expressions
353+
recv_match_types = set()
354+
for e in expressions:
355+
recv_match_types.update(set(e.recv_match_types))
304356

305357
colour_source = getattr(options, "colour_source")
306-
re_caps = re.compile('[A-Z_][A-Z0-9_]+')
307358

308359
if colour_source is not None:
309360
# stolen from mavgraph.py
361+
re_caps = re.compile('[A-Z_][A-Z0-9_]+')
310362
caps = set(re.findall(re_caps, colour_source))
311-
recv_match_types.extend(caps)
363+
recv_match_types.update(caps)
312364

313-
print("Looking for types %s" % str(recv_match_types))
365+
print("Looking for types %s" % str(list(recv_match_types)))
314366

315367
last_timestamps = {}
316368
used_flightmodes = {}
@@ -377,126 +429,70 @@ def mavflightview_mav(mlog, options=None, flightmode_selections=[]):
377429

378430
if not mlog.check_condition(options.condition):
379431
continue
380-
if options.mode is not None and mlog.flightmode.lower() != options.mode.lower():
381-
continue
382432

383-
if not type in types and type not in want_instances:
384-
# may only be present for colour-source expressions to work
433+
if options.mode is not None and mlog.flightmode.lower() != options.mode.lower():
385434
continue
386435

387-
type_with_instance = type
388-
try:
389-
# remember that "m" here might be a mavlink message.
390-
instance_field = m.fmt.instance_field
391-
m_instance_field_value = eval(f"m.{instance_field}")
392-
if (type in want_instances and
393-
str(m_instance_field_value) not in want_instances[type]
394-
):
395-
continue
396-
397-
type_with_instance = '%s[%u]' % (type, m_instance_field_value)
398-
except Exception:
399-
pass
400-
401436
if not all_false and len(flightmode_selections) > 0 and idx < len(options._flightmodes) and m._timestamp >= options._flightmodes[idx][2]:
402437
idx += 1
403438
elif (idx < len(flightmode_selections) and flightmode_selections[idx]) or all_false or len(flightmode_selections) == 0:
404439
used_flightmodes[mlog.flightmode] = 1
405-
(lat, lng) = (None,None)
406-
if type in ['GPS','GPS2']:
407-
status = getattr(m, 'Status', None)
408-
nsats = getattr(m, 'NSats', None)
409-
if status is None:
410-
status = getattr(m, 'FixType', None)
411-
if status is None:
412-
print("Can't find status on GPS message")
413-
print(m)
414-
break
415-
if nsats is None:
416-
nsats = 0
417-
if status < 2 and nsats < 5:
440+
for instance in range(len(expressions)):
441+
expression = expressions[instance]
442+
if not type in expression.recv_match_types:
418443
continue
419-
# flash log
420-
lat = m.Lat
421-
lng = getattr(m, 'Lng', None)
422-
if lng is None:
423-
lng = getattr(m, 'Lon', None)
424-
if lng is None:
425-
print("Can't find longitude on GPS message")
426-
print(m)
427-
break
428-
elif type in ['EKF1', 'ANU1']:
429-
pos = mavextra.ekf1_pos(m)
430-
if pos is None:
444+
445+
# evaluate the expression
446+
is_expression = (type != expression.expression)
447+
if not is_expression:
448+
# this is a simple type as an expression
449+
v = m
450+
else:
451+
# we need to evaluate the expression to produce an object
452+
try:
453+
v = mavutil.evaluate_expression(expression.expression, mlog.messages)
454+
except Exception:
455+
continue
456+
if v is None:
431457
continue
432-
ekf_counter += 1
433-
if ekf_counter % options.ekf_sample != 0:
458+
latlng = message_to_latlon(type, v, is_expression)
459+
if latlng is None:
434460
continue
435-
(lat, lng, alt) = pos
436-
elif type in ['NKF1','XKF1']:
437-
pos = mavextra.ekf1_pos(m)
438-
if pos is None:
461+
lat,lng = latlng
462+
463+
while len(path) <= instance:
464+
path.append([])
465+
466+
# only plot thing we have a valid-looking location for:
467+
if abs(lat)<=0.01 and abs(lng)<=0.01:
439468
continue
440-
nkf_counter += 1
441-
if nkf_counter % options.nkf_sample != 0:
469+
470+
colour = colour_for_point(mlog, (lat, lng), instance, options)
471+
if colour is None:
442472
continue
443-
(lat, lng, alt) = pos
444-
elif type in ['ANU5']:
445-
(lat, lng) = (m.Alat*1.0e-7, m.Alng*1.0e-7)
446-
elif type in ['AHR2', 'POS', 'CHEK']:
447-
(lat, lng) = (m.Lat, m.Lng)
448-
elif type == 'AHRS2':
449-
(lat, lng) = (m.lat*1.0e-7, m.lng*1.0e-7)
450-
elif type == 'ORGN':
451-
(lat, lng) = (m.Lat, m.Lng)
452-
elif type == 'SIM':
453-
(lat, lng) = (m.Lat, m.Lng)
454-
elif type == 'GUID':
455-
if (m.Type == 0):
456-
(lat, lng) = (m.pX*1.0e-7, m.pY*1.0e-7)
457-
else:
458-
if hasattr(m,'Lat'):
459-
lat = m.Lat
460-
if hasattr(m,'Lon'):
461-
lng = m.Lon
462-
if hasattr(m,'Lng'):
463-
lng = m.Lng
464-
if hasattr(m,'lat'):
465-
lat = m.lat * 1.0e-7
466-
if hasattr(m,'lon'):
467-
lng = m.lon * 1.0e-7
468-
if hasattr(m,'lng'):
469-
lng = m.lng * 1.0e-7
470-
if hasattr(m,'latitude'):
471-
lat = m.latitude * 1.0e-7
472-
if hasattr(m,'longitude'):
473-
lng = m.longitude * 1.0e-7
474-
475-
if lat is None or lng is None:
476-
continue
477-
478-
# automatically add new types to instances
479-
if type_with_instance not in instances:
480-
instances[type_with_instance] = len(instances)
481-
while len(instances) >= len(path):
482-
path.append([])
483-
instance = instances[type_with_instance]
484473

485-
# only plot thing we have a valid-looking location for:
486-
if abs(lat)<=0.01 and abs(lng)<=0.01:
487-
continue
474+
tdays = grapher.timestamp_to_days(m._timestamp)
475+
point = (lat, lng, colour, tdays)
476+
477+
if options.rate == 0 or not expression.expression in last_timestamps or m._timestamp - last_timestamps[expression.expression] > 1.0/options.rate:
478+
last_timestamps[expression.expression] = m._timestamp
479+
path[instance].append(point)
488480

489-
colour = colour_for_point(mlog, (lat, lng), instance, options)
490-
if colour is None:
491-
continue
481+
# remove any empty paths and construct instances array
482+
paths2 = []
483+
instances = {}
484+
for instance in range(len(expressions)):
485+
e = expressions[instance]
486+
if instance >= len(path):
487+
break
488+
if len(path[instance]) == 0:
489+
continue
490+
paths2.append(path[instance])
491+
instances[e.expression] = instance
492492

493-
tdays = grapher.timestamp_to_days(m._timestamp)
494-
point = (lat, lng, colour, tdays)
493+
path = paths2
495494

496-
if options.rate == 0 or not type_with_instance in last_timestamps or m._timestamp - last_timestamps[type_with_instance] > 1.0/options.rate:
497-
last_timestamps[type_with_instance] = m._timestamp
498-
path[instance].append(point)
499-
if len(path[0]) == 0:
495+
if len(path) == 0:
500496
print("No points to plot")
501497
return None
502498

@@ -691,8 +687,6 @@ def __init__(self):
691687
parser.add_option("--debug", action='store_true', default=False, help="show debug info")
692688
parser.add_option("--multi", action='store_true', default=False, help="show multiple flights on one map")
693689
parser.add_option("--types", default=None, help="types of position messages to show")
694-
parser.add_option("--ekf-sample", type='int', default=1, help="sub-sampling of EKF messages")
695-
parser.add_option("--nkf-sample", type='int', default=1, help="sub-sampling of NKF messages")
696690
parser.add_option("--rate", type='int', default=0, help="maximum message rate to display (0 means all points)")
697691
parser.add_option("--colour-source", type="str", default="flightmode", help="expression with range 0f..255f used for point colour")
698692
parser.add_option("--no-flightmode-legend", action="store_false", default=True, dest="show_flightmode_legend", help="hide legend for colour used for flight modes")

0 commit comments

Comments
 (0)