Skip to content

Commit e39879a

Browse files
shancock884peterbarker
authored andcommitted
DFReader: Read unit data from log and add dump_verbose function to DFMessage
Cache units and multipliers in lookup tables when first scanning the file Handles both DFBinary and DFText files Store derived unit for each field as DFFormat class attribute Create get_unit method on DFFormat to return unit if defined or empty string Create dump_verbose function on DFMessage class, which outputs both value and unit for each field Also show the deg or deg/s value for rad or rad/s fields Separate code to detect quiet nan into a utility function, so it can be used by both __str__ and dump_verbose Improve display precision of values with a format multiplier, e.g.: (401952592*1e-7 => 40.195259199999995 vs 401952592/1e7 => 40.1952592) mavlogdump.py updated to call the dump_verbose method when --verbose specified Use hasattr to check method exists, in case of misaligned files
1 parent 35bc0c1 commit e39879a

File tree

2 files changed

+177
-36
lines changed

2 files changed

+177
-36
lines changed

DFReader.py

Lines changed: 174 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import os
1818
import mmap
1919
import platform
20+
import time
2021

2122
import struct
2223
import sys
@@ -50,9 +51,32 @@
5051
"Q": ("Q", None, long), # Backward compat
5152
}
5253

54+
MULT_TO_PREFIX = {
55+
0: "",
56+
1: "",
57+
1.0e-1: "d", # deci
58+
1.0e-2: "c", # centi
59+
1.0e-3: "m", # milli
60+
1.0e-6: "µ", # micro
61+
1.0e-9: "n" # nano
62+
}
63+
5364
def u_ord(c):
5465
return ord(c) if sys.version_info.major < 3 else c
5566

67+
def is_quiet_nan(val):
68+
'''determine if the argument is a quiet nan'''
69+
# Is this a float, and some sort of nan?
70+
if isinstance(val, float) and math.isnan(val):
71+
# quiet nans have more non-zero values:
72+
if sys.version_info.major >= 3:
73+
noisy_nan = bytearray([0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
74+
else:
75+
noisy_nan = "\x7f\xf8\x00\x00\x00\x00\x00\x00"
76+
return struct.pack(">d", val) != noisy_nan
77+
else:
78+
return False
79+
5680
class DFFormat(object):
5781
def __init__(self, type, name, flen, format, columns, oldfmt=None):
5882
self.type = type
@@ -61,8 +85,7 @@ def __init__(self, type, name, flen, format, columns, oldfmt=None):
6185
self.format = format
6286
self.columns = columns.split(',')
6387
self.instance_field = None
64-
self.unit_ids = None
65-
self.mult_ids = None
88+
self.units = None
6689

6790
if self.columns == ['']:
6891
self.columns = []
@@ -101,32 +124,64 @@ def __init__(self, type, name, flen, format, columns, oldfmt=None):
101124
if self.msg_fmts[i] == 'a':
102125
self.a_indexes.append(i)
103126

127+
# If this format was alrady defined, copy over units and instance info
104128
if oldfmt is not None:
105-
self.set_unit_ids(oldfmt.unit_ids)
106-
self.set_mult_ids(oldfmt.mult_ids)
107-
108-
def set_unit_ids(self, unit_ids):
129+
self.units = oldfmt.units
130+
if oldfmt.instance_field is not None:
131+
self.set_instance_field(self.colhash[oldfmt.instance_field])
132+
133+
def set_instance_field(self, instance_idx):
134+
'''set up the instance field for this format'''
135+
self.instance_field = self.columns[instance_idx]
136+
# work out offset and length of instance field in message
137+
pre_fmt = self.format[:instance_idx]
138+
pre_sfmt = ""
139+
for c in pre_fmt:
140+
(s, mul, type) = FORMAT_TO_STRUCT[c]
141+
pre_sfmt += s
142+
self.instance_ofs = struct.calcsize(pre_sfmt)
143+
(ifmt,) = self.format[instance_idx]
144+
self.instance_len = struct.calcsize(ifmt)
145+
146+
def set_unit_ids(self, unit_ids, unit_lookup):
109147
'''set unit IDs string from FMTU'''
110148
if unit_ids is None:
111149
return
112-
self.unit_ids = unit_ids
150+
# Does this unit string define an instance field?
113151
instance_idx = unit_ids.find('#')
114152
if instance_idx != -1:
115-
self.instance_field = self.columns[instance_idx]
116-
# work out offset and length of instance field in message
117-
pre_fmt = self.format[:instance_idx]
118-
pre_sfmt = ""
119-
for c in pre_fmt:
120-
(s, mul, type) = FORMAT_TO_STRUCT[c]
121-
pre_sfmt += s
122-
self.instance_ofs = struct.calcsize(pre_sfmt)
123-
(ifmt,) = self.format[instance_idx]
124-
self.instance_len = struct.calcsize(ifmt)
125-
153+
self.set_instance_field(instance_idx)
154+
# Build the units array from the IDs
155+
self.units = [""]*len(self.columns)
156+
for i in range(len(self.columns)):
157+
if i < len(unit_ids):
158+
if unit_ids[i] in unit_lookup:
159+
self.units[i] = unit_lookup[unit_ids[i]]
126160

127-
def set_mult_ids(self, mult_ids):
161+
def set_mult_ids(self, mult_ids, mult_lookup):
128162
'''set mult IDs string from FMTU'''
129-
self.mult_ids = mult_ids
163+
# Update the units based on the multiplier
164+
for i in range(len(self.units)):
165+
# If the format has its own multiplier, do not adjust the unit,
166+
# and if no unit is specified there is nothing to adjust
167+
if self.msg_mults[i] is not None or self.units[i] == "":
168+
continue
169+
# Get the unit multiplier from the lookup table
170+
if mult_ids[i] in mult_lookup:
171+
unitmult = mult_lookup[mult_ids[i]]
172+
# Combine the multipler and unit to derive the real unit
173+
if unitmult in MULT_TO_PREFIX:
174+
self.units[i] = MULT_TO_PREFIX[unitmult]+self.units[i]
175+
else:
176+
self.units[i] = "%.4g %s" % (unitmult, self.units[i])
177+
178+
def get_unit(self, col):
179+
'''Return the unit for the specified field'''
180+
if self.units is None:
181+
return ""
182+
else:
183+
idx = self.colhash[col]
184+
return self.units[idx]
130185

131186
def __str__(self):
132187
return ("DFFormat(%s,%s,%s,%s)" %
@@ -192,7 +247,14 @@ def __getattr__(self, field):
192247
if self.fmt.msg_types[i] == str:
193248
v = null_term(v)
194249
if self.fmt.msg_mults[i] is not None and self._apply_multiplier:
195-
v *= self.fmt.msg_mults[i]
250+
# For reasons relating to floating point accuracy, you get a more
251+
# accurate result by dividing by 1e2 or 1e7 than multiplying by
252+
# 1e-2 or 1e-7
253+
if self.fmt.msg_mults[i] > 0.0 and self.fmt.msg_mults[i] < 1.0:
254+
divisor = 1/self.fmt.msg_mults[i]
255+
v /= divisor
256+
else:
257+
v *= self.fmt.msg_mults[i]
196258
return v
197259

198260
def __setattr__(self, field, value):
@@ -214,15 +276,9 @@ def __str__(self):
214276
col_count = 0
215277
for c in self.fmt.columns:
216278
val = self.__getattr__(c)
217-
if isinstance(val, float) and math.isnan(val):
218-
# quiet nans have more non-zero values:
219-
if is_py3:
220-
noisy_nan = bytearray([0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
221-
else:
222-
noisy_nan = "\x7f\xf8\x00\x00\x00\x00\x00\x00"
223-
if struct.pack(">d", val) != noisy_nan:
224-
val = "qnan"
225-
279+
if is_quiet_nan(val):
280+
val = "qnan"
281+
# Add the value to the return string
226282
if is_py3:
227283
ret += "%s : %s, " % (c, val)
228284
else:
@@ -235,6 +291,38 @@ def __str__(self):
235291
ret = ret[:-2]
236292
return ret + '}'
237293

294+
def dump_verbose(self, f):
295+
is_py3 = sys.version_info >= (3,0)
296+
timestamp = "%s.%03u" % (
297+
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self._timestamp)),
298+
int(self._timestamp*1000.0)%1000)
299+
f.write("%s: %s\n" % (timestamp, self.fmt.name))
300+
for c in self.fmt.columns:
301+
# Get the value
302+
val = self.__getattr__(c)
303+
# Handle quiet nan
304+
if is_quiet_nan(val):
305+
val = "qnan"
306+
# Output the field label and value
307+
if is_py3:
308+
f.write(" %s: %s" % (c, val))
309+
else:
310+
try:
311+
f.write(" %s: %s" % (c, val))
312+
except UnicodeDecodeError:
313+
f.write(" %s: %s" % (c, to_string(val)))
314+
# Append the unit to the output
315+
unit = self.fmt.get_unit(c)
316+
if unit == "":
317+
# No unit specified - just output the newline
318+
f.write("\n")
319+
elif unit.startswith("rad"):
320+
# For rad or rad/s, add the degrees conversion too
321+
f.write(" %s (%s %s)\n" % (unit, math.degrees(val), unit.replace("rad","deg")))
322+
else:
323+
# Append the unit
324+
f.write(" %s\n" % (unit))
325+
238326
def get_msgbuf(self):
239327
'''create a binary message buffer for a message'''
240328
values = []
@@ -482,6 +570,8 @@ def __init__(self):
482570
'__MAV__': self, # avoids conflicts with messages actually called "MAV"
483571
}
484572
self.percent = 0
573+
self.unit_lookup = {} # lookup table of units defined by UNIT messages
574+
self.mult_lookup = {} # lookup table of multipliers defined by MULT messages
485575

486576
def _rewind(self):
487577
'''reset state on rewind'''
@@ -798,6 +888,8 @@ def init_arrays(self, progress_callback=None):
798888
self.counts.append(0)
799889
fmt_type = 0x80
800890
fmtu_type = None
891+
unit_type = None
892+
mult_type = None
801893
ofs = 0
802894
pct = 0
803895
HEAD1 = self.HEAD1
@@ -859,7 +951,13 @@ def init_arrays(self, progress_callback=None):
859951
self.id_to_name[mfmt.type] = mfmt.name
860952
if mfmt.name == 'FMTU':
861953
fmtu_type = mfmt.type
954+
if mfmt.name == 'UNIT':
955+
unit_type = mfmt.type
956+
if mfmt.name == 'MULT':
957+
mult_type = mfmt.type
862958

959+
# Handle FMTU messages by updating the DFFormat class with the
960+
# unit/multiplier information
863961
if fmtu_type is not None and mtype == fmtu_type:
864962
fmt = self.formats[mtype]
865963
body = self.data_map[ofs+3:ofs+mlen]
@@ -870,9 +968,33 @@ def init_arrays(self, progress_callback=None):
870968
if ftype in self.formats:
871969
fmt2 = self.formats[ftype]
872970
if 'UnitIds' in fmt.colhash:
873-
fmt2.set_unit_ids(null_term(elements[fmt.colhash['UnitIds']]))
971+
fmt2.set_unit_ids(null_term(elements[fmt.colhash['UnitIds']]), self.unit_lookup)
874972
if 'MultIds' in fmt.colhash:
875-
fmt2.set_mult_ids(null_term(elements[fmt.colhash['MultIds']]))
973+
fmt2.set_mult_ids(null_term(elements[fmt.colhash['MultIds']]), self.mult_lookup)
974+
975+
# Handle UNIT messages by updating the unit_lookup dictionary
976+
if unit_type is not None and mtype == unit_type:
977+
fmt = self.formats[mtype]
978+
body = self.data_map[ofs+3:ofs+mlen]
979+
if len(body)+3 < mlen:
980+
break
981+
elements = list(struct.unpack(fmt.msg_struct, body))
982+
self.unit_lookup[chr(elements[1])] = null_term(elements[2])
983+
984+
# Handle MULT messages by updating the mult_lookup dictionary
985+
if mult_type is not None and mtype == mult_type:
986+
fmt = self.formats[mtype]
987+
body = self.data_map[ofs+3:ofs+mlen]
988+
if len(body)+3 < mlen:
989+
break
990+
elements = list(struct.unpack(fmt.msg_struct, body))
991+
# Even though the multiplier value is logged as a double, the
992+
# values in log files look to be single-precision values that have
993+
# been cast to a double.
994+
# To ensure that the values saved here can be used to index the
995+
# MULT_TO_PREFIX table, we round them to 7 significant decimal digits
996+
mult = float("%.7g" % (elements[2]))
997+
self.mult_lookup[chr(elements[1])] = mult
876998

877999
ofs += mlen
8781000
if progress_callback is not None:
@@ -1038,8 +1160,8 @@ def _parse_next(self):
10381160
MultIds = elements[2]
10391161
if FmtType in self.formats:
10401162
fmt = self.formats[FmtType]
1041-
fmt.set_unit_ids(UnitIds)
1042-
fmt.set_mult_ids(MultIds)
1163+
fmt.set_unit_ids(UnitIds, self.unit_lookup)
1164+
fmt.set_mult_ids(MultIds, self.mult_lookup)
10431165

10441166
try:
10451167
self._add_msg(m)
@@ -1279,8 +1401,24 @@ def _parse_next(self):
12791401
fmtid = getattr(m, 'FmtType', None)
12801402
if fmtid is not None and fmtid in self.id_to_name:
12811403
fmtu = self.formats[self.id_to_name[fmtid]]
1282-
fmtu.set_unit_ids(getattr(m, 'UnitIds', None))
1283-
fmtu.set_mult_ids(getattr(m, 'MultIds', None))
1404+
fmtu.set_unit_ids(getattr(m, 'UnitIds', None), self.unit_lookup)
1405+
fmtu.set_mult_ids(getattr(m, 'MultIds', None), self.mult_lookup)
1406+
1407+
if m.get_type() == 'UNIT':
1408+
unitid = getattr(m, 'Id', None)
1409+
label = getattr(m, 'Label', None)
1410+
self.unit_lookup[chr(unitid)] = null_term(label)
1411+
1412+
if m.get_type() == 'MULT':
1413+
multid = getattr(m, 'Id', None)
1414+
mult = getattr(m, 'Mult', None)
1415+
# Even though the multiplier value is logged as a double, the
1416+
# values in log files look to be single-precision values that have
1417+
# been cast to a double.
1418+
# To ensure that the values saved here can be used to index the
1419+
# MULT_TO_PREFIX table, we round them to 7 significant decimal digits
1420+
mult = float("%.7g" % (mult))
1421+
self.mult_lookup[chr(multid)] = mult
12841422

12851423
self._add_msg(m)
12861424

tools/mavlogdump.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,9 @@ def match_type(mtype, patterns):
379379
elif args.verbose and istlog:
380380
mavutil.dump_message_verbose(sys.stdout, m)
381381
print("")
382+
elif args.verbose and hasattr(m,"dump_verbose"):
383+
m.dump_verbose(sys.stdout)
384+
print("")
382385
else:
383386
# Otherwise we output in a standard Python dict-style format
384387
s = "%s.%02u: %s" % (time.strftime("%Y-%m-%d %H:%M:%S",

0 commit comments

Comments
 (0)