00001
00002 '''
00003 APM DataFlash log file reader
00004
00005 Copyright Andrew Tridgell 2011
00006 Released under GNU GPL version 3 or later
00007
00008 Partly based on SDLog2Parser by Anton Babushkin
00009 '''
00010
00011 import struct, time, os
00012 from . import mavutil
00013
00014 FORMAT_TO_STRUCT = {
00015 "b": ("b", None, int),
00016 "B": ("B", None, int),
00017 "h": ("h", None, int),
00018 "H": ("H", None, int),
00019 "i": ("i", None, int),
00020 "I": ("I", None, int),
00021 "f": ("f", None, float),
00022 "n": ("4s", None, str),
00023 "N": ("16s", None, str),
00024 "Z": ("64s", None, str),
00025 "c": ("h", 0.01, float),
00026 "C": ("H", 0.01, float),
00027 "e": ("i", 0.01, float),
00028 "E": ("I", 0.01, float),
00029 "L": ("i", 1.0e-7, float),
00030 "d": ("d", None, float),
00031 "M": ("b", None, int),
00032 "q": ("q", None, long),
00033 "Q": ("Q", None, long),
00034 }
00035
00036 class DFFormat(object):
00037 def __init__(self, type, name, flen, format, columns):
00038 self.type = type
00039 self.name = name
00040 self.len = flen
00041 self.format = format
00042 self.columns = columns.split(',')
00043
00044 if self.columns == ['']:
00045 self.columns = []
00046
00047 msg_struct = "<"
00048 msg_mults = []
00049 msg_types = []
00050 for c in format:
00051 if ord(c) == 0:
00052 break
00053 try:
00054 (s, mul, type) = FORMAT_TO_STRUCT[c]
00055 msg_struct += s
00056 msg_mults.append(mul)
00057 msg_types.append(type)
00058 except KeyError as e:
00059 raise Exception("Unsupported format char: '%s' in message %s" % (c, name))
00060
00061 self.msg_struct = msg_struct
00062 self.msg_types = msg_types
00063 self.msg_mults = msg_mults
00064 self.colhash = {}
00065 for i in range(len(self.columns)):
00066 self.colhash[self.columns[i]] = i
00067
00068 def __str__(self):
00069 return "DFFormat(%s,%s,%s,%s)" % (self.type, self.name, self.format, self.columns)
00070
00071 def null_term(str):
00072 '''null terminate a string'''
00073 idx = str.find("\0")
00074 if idx != -1:
00075 str = str[:idx]
00076 return str
00077
00078 class DFMessage(object):
00079 def __init__(self, fmt, elements, apply_multiplier):
00080 self.fmt = fmt
00081 self._elements = elements
00082 self._apply_multiplier = apply_multiplier
00083 self._fieldnames = fmt.columns
00084
00085 def to_dict(self):
00086 d = {'mavpackettype': self.fmt.name}
00087
00088 for field in self._fieldnames:
00089 d[field] = self.__getattr__(field)
00090
00091 return d
00092
00093 def __getattr__(self, field):
00094 '''override field getter'''
00095 try:
00096 i = self.fmt.colhash[field]
00097 except Exception:
00098 raise AttributeError(field)
00099 v = self._elements[i]
00100 if self.fmt.format[i] != 'M' or self._apply_multiplier:
00101 v = self.fmt.msg_types[i](v)
00102 if self.fmt.msg_types[i] == str:
00103 v = null_term(v)
00104 if self.fmt.msg_mults[i] is not None and self._apply_multiplier:
00105 v *= self.fmt.msg_mults[i]
00106 return v
00107
00108 def get_type(self):
00109 return self.fmt.name
00110
00111 def __str__(self):
00112 ret = "%s {" % self.fmt.name
00113 col_count = 0
00114 for c in self.fmt.columns:
00115 ret += "%s : %s, " % (c, self.__getattr__(c))
00116 col_count += 1
00117 if col_count != 0:
00118 ret = ret[:-2]
00119 return ret + '}'
00120
00121 def get_msgbuf(self):
00122 '''create a binary message buffer for a message'''
00123 values = []
00124 for i in range(len(self.fmt.columns)):
00125 if i >= len(self.fmt.msg_mults):
00126 continue
00127 mul = self.fmt.msg_mults[i]
00128 name = self.fmt.columns[i]
00129 if name == 'Mode' and 'ModeNum' in self.fmt.columns:
00130 name = 'ModeNum'
00131 v = self.__getattr__(name)
00132 if mul is not None:
00133 v /= mul
00134 values.append(v)
00135 return struct.pack("BBB", 0xA3, 0x95, self.fmt.type) + struct.pack(self.fmt.msg_struct, *values)
00136
00137 def get_fieldnames(self):
00138 return self._fieldnames
00139
00140 class DFReaderClock():
00141 '''base class for all the different ways we count time in logs'''
00142
00143 def __init__(self):
00144 self.set_timebase(0)
00145 self.timestamp = 0
00146
00147 def _gpsTimeToTime(self, week, msec):
00148 '''convert GPS week and TOW to a time in seconds since 1970'''
00149 epoch = 86400*(10*365 + (1980-1969)/4 + 1 + 6 - 2)
00150 return epoch + 86400*7*week + msec*0.001 - 15
00151
00152 def set_timebase(self, base):
00153 self.timebase = base
00154
00155 def message_arrived(self, m):
00156 pass
00157
00158 def rewind_event(self):
00159 pass
00160
00161 class DFReaderClock_usec(DFReaderClock):
00162 '''DFReaderClock_usec - use microsecond timestamps from messages'''
00163 def __init__(self):
00164 DFReaderClock.__init__(self)
00165
00166 def find_time_base(self, gps, first_us_stamp):
00167 '''work out time basis for the log - even newer style'''
00168 t = self._gpsTimeToTime(gps.GWk, gps.GMS)
00169 self.set_timebase(t - gps.TimeUS*0.000001)
00170
00171 self.timestamp = self.timebase + first_us_stamp*0.000001
00172
00173 def type_has_good_TimeMS(self, type):
00174 '''The TimeMS in some messages is not from *our* clock!'''
00175 if type.startswith('ACC'):
00176 return False;
00177 if type.startswith('GYR'):
00178 return False;
00179 return True
00180
00181 def should_use_msec_field0(self, m):
00182 if not self.type_has_good_TimeMS(m.get_type()):
00183 return False
00184 if 'TimeMS' != m._fieldnames[0]:
00185 return False
00186 if self.timebase + m.TimeMS*0.001 < self.timestamp:
00187 return False
00188 return True;
00189
00190 def set_message_timestamp(self, m):
00191 if 'TimeUS' == m._fieldnames[0]:
00192
00193 m._timestamp = self.timebase + m.TimeUS*0.000001
00194 elif self.should_use_msec_field0(m):
00195
00196
00197 m._timestamp = self.timebase + m.TimeMS*0.001
00198 else:
00199 m._timestamp = self.timestamp
00200 self.timestamp = m._timestamp
00201
00202 class DFReaderClock_msec(DFReaderClock):
00203 '''DFReaderClock_msec - a format where many messages have TimeMS in their formats, and GPS messages have a "T" field giving msecs '''
00204 def find_time_base(self, gps, first_ms_stamp):
00205 '''work out time basis for the log - new style'''
00206 t = self._gpsTimeToTime(gps.Week, gps.TimeMS)
00207 self.set_timebase(t - gps.T*0.001)
00208 self.timestamp = self.timebase + first_ms_stamp*0.001
00209
00210 def set_message_timestamp(self, m):
00211 if 'TimeMS' == m._fieldnames[0]:
00212 m._timestamp = self.timebase + m.TimeMS*0.001
00213 elif m.get_type() in ['GPS','GPS2']:
00214 m._timestamp = self.timebase + m.T*0.001
00215 else:
00216 m._timestamp = self.timestamp
00217 self.timestamp = m._timestamp
00218
00219 class DFReaderClock_px4(DFReaderClock):
00220 '''DFReaderClock_px4 - a format where a starting time is explicitly given in a message'''
00221 def __init__(self):
00222 DFReaderClock.__init__(self)
00223 self.px4_timebase = 0
00224
00225 def find_time_base(self, gps):
00226 '''work out time basis for the log - PX4 native'''
00227 t = gps.GPSTime * 1.0e-6
00228 self.timebase = t - self.px4_timebase
00229
00230 def set_px4_timebase(self, time_msg):
00231 self.px4_timebase = time_msg.StartTime * 1.0e-6
00232
00233 def set_message_timestamp(self, m):
00234 m._timestamp = self.timebase + self.px4_timebase
00235
00236 def message_arrived(self, m):
00237 type = m.get_type()
00238 if type == 'TIME' and 'StartTime' in m._fieldnames:
00239 self.set_px4_timebase(m)
00240
00241 class DFReaderClock_gps_interpolated(DFReaderClock):
00242 '''DFReaderClock_gps_interpolated - for when the only real references in a message are GPS timestamps '''
00243 def __init__(self):
00244 DFReaderClock.__init__(self)
00245 self.msg_rate = {}
00246 self.counts = {}
00247 self.counts_since_gps = {}
00248
00249 def rewind_event(self):
00250 '''reset counters on rewind'''
00251 self.counts = {}
00252 self.counts_since_gps = {}
00253
00254 def message_arrived(self, m):
00255 type = m.get_type()
00256 if not type in self.counts:
00257 self.counts[type] = 1
00258 else:
00259 self.counts[type] += 1
00260
00261
00262 if not type in self.counts_since_gps:
00263 self.counts_since_gps[type] = 1
00264 else:
00265 self.counts_since_gps[type] += 1
00266
00267 if type == 'GPS' or type == 'GPS2':
00268 self.gps_message_arrived(m)
00269
00270 def gps_message_arrived(self, m):
00271 '''adjust time base from GPS message'''
00272
00273 gps_week = getattr(m, 'Week', None)
00274 gps_timems = getattr(m, 'TimeMS', None)
00275 if gps_week is None:
00276
00277 gps_week = getattr(m, 'GWk', None)
00278 gps_timems = getattr(m, 'GMS', None)
00279 if gps_week is None:
00280 if getattr(m, 'GPSTime', None) is not None:
00281
00282
00283
00284 return;
00285
00286 t = self._gpsTimeToTime(gps_week, gps_timems)
00287
00288 deltat = t - self.timebase
00289 if deltat <= 0:
00290 return
00291
00292 for type in self.counts_since_gps:
00293 rate = self.counts_since_gps[type] / deltat
00294 if rate > self.msg_rate.get(type, 0):
00295 self.msg_rate[type] = rate
00296 self.msg_rate['IMU'] = 50.0
00297 self.timebase = t
00298 self.counts_since_gps = {}
00299
00300 def set_message_timestamp(self, m):
00301 rate = self.msg_rate.get(m.fmt.name, 50.0)
00302 if int(rate) == 0:
00303 rate = 50
00304 count = self.counts_since_gps.get(m.fmt.name, 0)
00305 m._timestamp = self.timebase + count/rate
00306
00307
00308 class DFReader(object):
00309 '''parse a generic dataflash file'''
00310 def __init__(self):
00311
00312 self.clock = None
00313 self.timestamp = 0
00314 self.mav_type = mavutil.mavlink.MAV_TYPE_FIXED_WING
00315 self.verbose = False
00316 self.params = {}
00317
00318 def _rewind(self):
00319 '''reset state on rewind'''
00320 self.messages = { 'MAV' : self }
00321 self.flightmode = "UNKNOWN"
00322 self.percent = 0
00323 if self.clock:
00324 self.clock.rewind_event()
00325
00326 def init_clock_px4(self, px4_msg_time, px4_msg_gps):
00327 self.clock = DFReaderClock_px4()
00328 if not self._zero_time_base:
00329 self.clock.set_px4_timebase(px4_msg_time)
00330 self.clock.find_time_base(px4_msg_gps)
00331 return True
00332
00333 def init_clock_msec(self):
00334
00335 self.clock = DFReaderClock_msec()
00336
00337 def init_clock_usec(self):
00338 self.clock = DFReaderClock_usec()
00339
00340 def init_clock_gps_interpolated(self, clock):
00341 self.clock = clock
00342
00343 def init_clock(self):
00344 '''work out time basis for the log'''
00345
00346 self._rewind()
00347
00348
00349
00350 gps_clock = DFReaderClock_gps_interpolated()
00351 self.clock = gps_clock
00352
00353 px4_msg_time = None
00354 px4_msg_gps = None
00355 gps_interp_msg_gps1 = None
00356 gps_interp_msg_gps2 = None
00357 first_us_stamp = None
00358 first_ms_stamp = None
00359
00360 have_good_clock = False
00361 while True:
00362 m = self.recv_msg()
00363 if m is None:
00364 break;
00365
00366 type = m.get_type()
00367
00368 if first_us_stamp is None:
00369 first_us_stamp = getattr(m, "TimeUS", None);
00370
00371 if first_ms_stamp is None and (type != 'GPS' and type != 'GPS2'):
00372
00373
00374 first_ms_stamp = getattr(m, "TimeMS", None);
00375
00376 if type == 'GPS' or type == 'GPS2':
00377 if getattr(m, "TimeUS", 0) != 0 and \
00378 getattr(m, "GWk", 0) != 0:
00379 self.init_clock_usec()
00380 if not self._zero_time_base:
00381 self.clock.find_time_base(m, first_us_stamp)
00382 have_good_clock = True
00383 break
00384 if getattr(m, "T", 0) != 0 and \
00385 getattr(m, "Week", 0) != 0:
00386 if first_ms_stamp is None:
00387 first_ms_stamp = m.T
00388 self.init_clock_msec()
00389 if not self._zero_time_base:
00390 self.clock.find_time_base(m, first_ms_stamp)
00391 have_good_clock = True
00392 break
00393 if getattr(m, "GPSTime", 0) != 0:
00394 px4_msg_gps = m
00395 if getattr(m, "Week", 0) != 0:
00396 if gps_interp_msg_gps1 is not None and \
00397 (gps_interp_msg_gps1.TimeMS != m.TimeMS or \
00398 gps_interp_msg_gps1.Week != m.Week):
00399
00400
00401
00402
00403
00404 self.init_clock_gps_interpolated(gps_clock)
00405 have_good_clock = True
00406 break
00407 gps_interp_msg_gps1 = m
00408
00409 elif type == 'TIME':
00410 '''only px4-style logs use TIME'''
00411 if getattr(m, "StartTime", None) != None:
00412 px4_msg_time = m;
00413
00414 if px4_msg_time is not None and px4_msg_gps is not None:
00415 self.init_clock_px4(px4_msg_time, px4_msg_gps)
00416 have_good_clock = True
00417 break
00418
00419
00420 if not have_good_clock:
00421
00422
00423
00424 if first_us_stamp is not None:
00425 self.init_clock_usec()
00426 elif first_ms_stamp is not None:
00427 self.init_clock_msec()
00428
00429 self._rewind()
00430
00431 return
00432
00433 def _set_time(self, m):
00434 '''set time for a message'''
00435
00436 m._timestamp = self.timestamp
00437 if len(m._fieldnames) > 0 and self.clock is not None:
00438 self.clock.set_message_timestamp(m)
00439
00440 def recv_msg(self):
00441 return self._parse_next()
00442
00443 def _add_msg(self, m):
00444 '''add a new message'''
00445 type = m.get_type()
00446 self.messages[type] = m
00447
00448 if self.clock:
00449 self.clock.message_arrived(m)
00450
00451 if type == 'MSG':
00452 if m.Message.find("Rover") != -1:
00453 self.mav_type = mavutil.mavlink.MAV_TYPE_GROUND_ROVER
00454 elif m.Message.find("Plane") != -1:
00455 self.mav_type = mavutil.mavlink.MAV_TYPE_FIXED_WING
00456 elif m.Message.find("Copter") != -1:
00457 self.mav_type = mavutil.mavlink.MAV_TYPE_QUADROTOR
00458 elif m.Message.startswith("Antenna"):
00459 self.mav_type = mavutil.mavlink.MAV_TYPE_ANTENNA_TRACKER
00460 if type == 'MODE':
00461 if isinstance(m.Mode, str):
00462 self.flightmode = m.Mode.upper()
00463 elif 'ModeNum' in m._fieldnames:
00464 mapping = mavutil.mode_mapping_bynumber(self.mav_type)
00465 if mapping is not None and m.ModeNum in mapping:
00466 self.flightmode = mapping[m.ModeNum]
00467 else:
00468 self.flightmode = mavutil.mode_string_acm(m.Mode)
00469 if type == 'STAT' and 'MainState' in m._fieldnames:
00470 self.flightmode = mavutil.mode_string_px4(m.MainState)
00471 if type == 'PARM' and getattr(m, 'Name', None) is not None:
00472 self.params[m.Name] = m.Value
00473 self._set_time(m)
00474
00475 def recv_match(self, condition=None, type=None, blocking=False):
00476 '''recv the next message that matches the given condition
00477 type can be a string or a list of strings'''
00478 if type is not None and not isinstance(type, list):
00479 type = [type]
00480 while True:
00481 m = self.recv_msg()
00482 if m is None:
00483 return None
00484 if type is not None and not m.get_type() in type:
00485 continue
00486 if not mavutil.evaluate_condition(condition, self.messages):
00487 continue
00488 return m
00489
00490 def check_condition(self, condition):
00491 '''check if a condition is true'''
00492 return mavutil.evaluate_condition(condition, self.messages)
00493
00494 def param(self, name, default=None):
00495 '''convenient function for returning an arbitrary MAVLink
00496 parameter with a default'''
00497 if not name in self.params:
00498 return default
00499 return self.params[name]
00500
00501 class DFReader_binary(DFReader):
00502 '''parse a binary dataflash file'''
00503 def __init__(self, filename, zero_time_base=False):
00504 DFReader.__init__(self)
00505
00506 f = open(filename, mode='rb')
00507 self.data = f.read()
00508 self.data_len = len(self.data)
00509 f.close()
00510 self.HEAD1 = 0xA3
00511 self.HEAD2 = 0x95
00512 self.formats = {
00513 0x80 : DFFormat(0x80, 'FMT', 89, 'BBnNZ', "Type,Length,Name,Format,Columns")
00514 }
00515 self._zero_time_base = zero_time_base
00516 self.init_clock()
00517 self._rewind()
00518
00519 def _rewind(self):
00520 '''rewind to start of log'''
00521 DFReader._rewind(self)
00522 self.offset = 0
00523 self.remaining = self.data_len
00524
00525 def _parse_next(self):
00526 '''read one message, returning it as an object'''
00527 if self.data_len - self.offset < 3:
00528 return None
00529
00530 hdr = self.data[self.offset:self.offset+3]
00531 skip_bytes = 0
00532 skip_type = None
00533
00534 while (ord(hdr[0]) != self.HEAD1 or ord(hdr[1]) != self.HEAD2 or ord(hdr[2]) not in self.formats):
00535 if skip_type is None:
00536 skip_type = (ord(hdr[0]), ord(hdr[1]), ord(hdr[2]))
00537 skip_bytes += 1
00538 self.offset += 1
00539 if self.data_len - self.offset < 3:
00540 return None
00541 hdr = self.data[self.offset:self.offset+3]
00542 msg_type = ord(hdr[2])
00543 if skip_bytes != 0:
00544 if self.remaining < 528:
00545 return None
00546 print("Skipped %u bad bytes in log %s remaining=%u" % (skip_bytes, skip_type, self.remaining))
00547 self.remaining -= skip_bytes
00548
00549 self.offset += 3
00550 self.remaining -= 3
00551
00552 if not msg_type in self.formats:
00553 if self.verbose:
00554 print("unknown message type %02x" % msg_type)
00555 raise Exception("Unknown message type %02x" % msg_type)
00556 fmt = self.formats[msg_type]
00557 if self.remaining < fmt.len-3:
00558
00559 if self.verbose:
00560 print("out of data")
00561 return None
00562 body = self.data[self.offset:self.offset+(fmt.len-3)]
00563 elements = None
00564 try:
00565 elements = list(struct.unpack(fmt.msg_struct, body))
00566 except Exception:
00567 if self.remaining < 528:
00568
00569 return None
00570
00571
00572
00573 print("Failed to parse %s/%s with len %u (remaining %u)" % (fmt.name, fmt.msg_struct, len(body), self.remaining))
00574 if elements is None:
00575 return self._parse_next()
00576 name = null_term(fmt.name)
00577 if name == 'FMT':
00578
00579
00580 self.formats[elements[0]] = DFFormat(elements[0],
00581 null_term(elements[2]), elements[1],
00582 null_term(elements[3]), null_term(elements[4]))
00583
00584 self.offset += fmt.len-3
00585 self.remaining -= fmt.len-3
00586 m = DFMessage(fmt, elements, True)
00587 self._add_msg(m)
00588
00589 self.percent = 100.0 * (self.offset / float(self.data_len))
00590
00591 return m
00592
00593 def DFReader_is_text_log(filename):
00594 '''return True if a file appears to be a valid text log'''
00595 f = open(filename)
00596 ret = (f.read(8000).find('FMT, ') != -1)
00597 f.close()
00598 return ret
00599
00600 class DFReader_text(DFReader):
00601 '''parse a text dataflash file'''
00602 def __init__(self, filename, zero_time_base=False):
00603 DFReader.__init__(self)
00604
00605 f = open(filename, mode='r')
00606 self.lines = f.readlines()
00607 f.close()
00608 self.formats = {
00609 'FMT' : DFFormat(0x80, 'FMT', 89, 'BBnNZ', "Type,Length,Name,Format,Columns")
00610 }
00611 self._rewind()
00612 self._zero_time_base = zero_time_base
00613 self.init_clock()
00614 self._rewind()
00615
00616 def _rewind(self):
00617 '''rewind to start of log'''
00618 DFReader._rewind(self)
00619 self.line = 0
00620
00621 while self.line < len(self.lines):
00622 if self.lines[self.line].startswith("FMT, "):
00623 break
00624 self.line += 1
00625
00626 def _parse_next(self):
00627 '''read one message, returning it as an object'''
00628
00629 this_line = self.line
00630 while self.line < len(self.lines):
00631 s = self.lines[self.line].rstrip()
00632 elements = s.split(", ")
00633 this_line = self.line
00634
00635 self.line += 1
00636 if len(elements) >= 2:
00637
00638 break
00639
00640 if this_line >= len(self.lines):
00641 return None
00642
00643
00644 if len(elements) == 5 and elements[-1] == ',':
00645 elements[-1] = ''
00646 elements.append('')
00647
00648 self.percent = 100.0 * (this_line / float(len(self.lines)))
00649
00650 msg_type = elements[0]
00651
00652 if not msg_type in self.formats:
00653 return self._parse_next()
00654
00655 fmt = self.formats[msg_type]
00656
00657 if len(elements) < len(fmt.format)+1:
00658
00659 return self._parse_next()
00660
00661 elements = elements[1:]
00662
00663 name = fmt.name.rstrip('\0')
00664 if name == 'FMT':
00665
00666
00667 self.formats[elements[2]] = DFFormat(int(elements[0]), elements[2], int(elements[1]), elements[3], elements[4])
00668
00669 try:
00670 m = DFMessage(fmt, elements, False)
00671 except ValueError:
00672 return self._parse_next()
00673
00674 self._add_msg(m)
00675
00676 return m
00677
00678
00679 if __name__ == "__main__":
00680 import sys
00681 use_profiler = False
00682 if use_profiler:
00683 from line_profiler import LineProfiler
00684 profiler = LineProfiler()
00685 profiler.add_function(DFReader_binary._parse_next)
00686 profiler.add_function(DFReader_binary._add_msg)
00687 profiler.add_function(DFReader._set_time)
00688 profiler.enable_by_count()
00689
00690 filename = sys.argv[1]
00691 if filename.endswith('.log'):
00692 log = DFReader_text(filename)
00693 else:
00694 log = DFReader_binary(filename)
00695 while True:
00696 m = log.recv_msg()
00697 if m is None:
00698 break
00699
00700 if use_profiler:
00701 profiler.print_stats()
00702