# # Code to abstract the parsing of APM Dataflash log files, currently only used by the LogAnalyzer # # Initial code by Andrew Chapman (amchapman@gmail.com), 16th Jan 2014 # # AP_FLAKE8_CLEAN from __future__ import print_function, division import bisect import ctypes import sys import numpy from VehicleType import VehicleType, VehicleTypeString class Format(object): '''Data channel format as specified by the FMT lines in the log file''' def __init__(self, msgType, msgLen, name, types, labels): self.NAME = 'FMT' self.msgType = msgType self.msgLen = msgLen self.name = name self.types = types self.labels = labels.split(',') def __str__(self): return "%8s %s" % (self.name, repr(self.labels)) @staticmethod def trycastToFormatType(value, valueType): """ Using format characters from libraries/DataFlash/DataFlash.h to cast strings to basic python int/float/string types tries a cast, if it does not work, well, acceptable as the text logs do not match the format, e.g. MODE is expected to be int """ try: if valueType in "fcCeELd": return float(value) elif valueType in "bBhHiIMQq": return int(value) elif valueType in "nNZ": return str(value) except ValueError: pass return value def to_class(self): members = dict( NAME=self.name, labels=self.labels[:], ) fieldtypes = [i for i in self.types] fieldlabels = self.labels[:] # field access for (label, _type) in zip(fieldlabels, fieldtypes): def createproperty(name, format): # extra scope for variable sanity # scaling via _NAME and def NAME(self): return self._NAME / SCALE propertyname = name attributename = '_' + name p = property( lambda x: getattr(x, attributename), lambda x, v: setattr(x, attributename, Format.trycastToFormatType(v, format)), ) members[propertyname] = p members[attributename] = None createproperty(label, _type) # repr shows all values but the header members['__repr__'] = lambda x: "<{cls} {data}>".format( cls=x.__class__.__name__, data=' '.join(["{}:{}".format(k, getattr(x, '_' + k)) for k in x.labels]) ) def init(a, *x): if len(x) != len(a.labels): raise ValueError("Invalid Length") for (l, v) in zip(a.labels, x): try: setattr(a, l, v) except Exception as e: print("{} {} {} failed".format(a, l, v)) print(e) members['__init__'] = init # finally, create the class cls = type('Log__{:s}'.format(self.name), (object,), members) return cls class logheader(ctypes.LittleEndianStructure): _fields_ = [ ('head1', ctypes.c_uint8), ('head2', ctypes.c_uint8), ('msgid', ctypes.c_uint8), ] def __repr__(self): return "".format( self=self ) class BinaryFormat(ctypes.LittleEndianStructure): NAME = 'FMT' MSG = 128 SIZE = 0 FIELD_FORMAT = { 'a': ctypes.c_int16 * 32, 'b': ctypes.c_int8, 'B': ctypes.c_uint8, 'h': ctypes.c_int16, 'H': ctypes.c_uint16, 'i': ctypes.c_int32, 'I': ctypes.c_uint32, 'f': ctypes.c_float, 'd': ctypes.c_double, 'n': ctypes.c_char * 4, 'N': ctypes.c_char * 16, 'Z': ctypes.c_char * 64, 'c': ctypes.c_int16, # * 100, 'C': ctypes.c_uint16, # * 100, 'e': ctypes.c_int32, # * 100, 'E': ctypes.c_uint32, # * 100, 'L': ctypes.c_int32, 'M': ctypes.c_uint8, 'q': ctypes.c_int64, 'Q': ctypes.c_uint64, } FIELD_SCALE = { 'c': 100, 'C': 100, 'e': 100, 'E': 100, } _packed_ = True _fields_ = [ ('head', logheader), ('type', ctypes.c_uint8), ('length', ctypes.c_uint8), ('name', ctypes.c_char * 4), ('types', ctypes.c_char * 16), ('labels', ctypes.c_char * 64), ] def __repr__(self): return "<{cls} {data}>".format( cls=self.__class__.__name__, data=' '.join(["{}:{}".format(k, getattr(self, k)) for (k, _) in self._fields_[1:]]), ) def to_class(self): labels = self.labels.decode(encoding="utf-8") if self.labels else "" members = dict( NAME=self.name.decode(encoding="utf-8"), MSG=self.type, SIZE=self.length, labels=labels.split(","), _pack_=True, ) if type(self.types[0]) == str: fieldtypes = [i for i in self.types] else: fieldtypes = [chr(i) for i in self.types] fieldlabels = members["labels"] if self.labels and (len(fieldtypes) != len(fieldlabels)): print("Broken FMT message for {} .. ignoring".format(self.name), file=sys.stderr) return None fields = [('head', logheader)] # field access for (label, _type) in zip(fieldlabels, fieldtypes): def createproperty(name, format): # extra scope for variable sanity # scaling via _NAME and def NAME(self): return self._NAME / SCALE propertyname = name attributename = '_' + name scale = BinaryFormat.FIELD_SCALE.get(format, None) def get_message_attribute(x): ret = getattr(x, attributename) if str(format) in ['Z', 'n', 'N']: ret = ret.decode(encoding="utf-8") return ret p = property(get_message_attribute) if scale is not None: p = property(lambda x: getattr(x, attributename) / scale) members[propertyname] = p try: fields.append((attributename, BinaryFormat.FIELD_FORMAT[format])) except KeyError: print('ERROR: Failed to add FMT type: {}, with format: {}'.format(attributename, format)) raise createproperty(label, _type) members['_fields_'] = fields # repr shows all values but the header members['__repr__'] = lambda x: "<{cls} {data}>".format( cls=x.__class__.__name__, data=' '.join(["{}:{}".format(k, getattr(x, k)) for k in x.labels]) ) # finally, create the class cls = type('Log__%s' % self.name, (ctypes.LittleEndianStructure,), members) if ctypes.sizeof(cls) != cls.SIZE: print("size mismatch for {} expected {} got {}".format(cls, ctypes.sizeof(cls), cls.SIZE), file=sys.stderr) return None return cls BinaryFormat.SIZE = ctypes.sizeof(BinaryFormat) class Channel(object): '''storage for a single stream of data, i.e. all GPS.RelAlt values''' # TODO: rethink data storage, but do more thorough regression testing before refactoring it # TODO: store data as a scipy spline curve so we can more easily interpolate and sample the slope? def __init__(self): # store dupe data in dict and list for now, until we decide which is the better way to go self.dictData = {} # dict of linenum->value self.listData = [] # list of (linenum,value) def getSegment(self, startLine, endLine): '''returns a segment of this data (from startLine to endLine, inclusive) as a new Channel instance''' segment = Channel() segment.dictData = {k: v for k, v in self.dictData.items() if k >= startLine and k <= endLine} segment.listData = [(k, v) for k, v in self.listData if k >= startLine and k <= endLine] return segment def min(self): return min(self.dictData.values()) def max(self): return max(self.dictData.values()) def avg(self): return numpy.mean(self.dictData.values()) def getNearestValueFwd(self, lineNumber): '''Returns (value,lineNumber)''' index = bisect.bisect_left(self.listData, (lineNumber, -99999)) while index < len(self.listData): line = self.listData[index][0] if line >= lineNumber: return (self.listData[index][1], line) index += 1 raise ValueError("Error finding nearest value for line %d" % lineNumber) def getNearestValueBack(self, lineNumber): '''Returns (value,lineNumber)''' index = bisect.bisect_left(self.listData, (lineNumber, -99999)) - 1 while index >= 0: line = self.listData[index][0] if line <= lineNumber: return (self.listData[index][1], line) index -= 1 raise ValueError("Error finding nearest value for line %d" % lineNumber) def getNearestValue(self, lineNumber, lookForwards=True): """ Find the nearest data value to the given lineNumber, defaults to first looking forwards. Returns (value,lineNumber) """ if lookForwards: try: return self.getNearestValueFwd(lineNumber) except ValueError: return self.getNearestValueBack(lineNumber) else: try: return self.getNearestValueBack(lineNumber) except ValueError: return self.getNearestValueFwd(lineNumber) raise Exception("Error finding nearest value for line %d" % lineNumber) def getInterpolatedValue(self, lineNumber): (prevValue, prevValueLine) = self.getNearestValue(lineNumber, lookForwards=False) (nextValue, nextValueLine) = self.getNearestValue(lineNumber, lookForwards=True) if prevValueLine == nextValueLine: return prevValue weight = (lineNumber - prevValueLine) / float(nextValueLine - prevValueLine) return (weight * prevValue) + ((1 - weight) * nextValue) def getIndexOf(self, lineNumber): '''returns the index within this channel's listData of the given lineNumber, or raises an Exception if not found''' index = bisect.bisect_left(self.listData, (lineNumber, -99999)) if self.listData[index][0] == lineNumber: return index else: raise Exception("Error finding index for line %d" % lineNumber) class LogIterator: """ Smart iterator that can move through a log by line number and maintain an index into the nearest values of all data channels """ # TODO: LogIterator currently indexes the next available value rather than the nearest value, we should make it # configurable between next/nearest class LogIteratorSubValue: '''syntactic sugar to allow access by LogIterator[lineLabel][dataLabel]''' logdata = None iterators = None lineLabel = None def __init__(self, logdata, iterators, lineLabel): self.logdata = logdata self.lineLabel = lineLabel self.iterators = iterators def __getitem__(self, dataLabel): index = self.iterators[self.lineLabel][0] return self.logdata.channels[self.lineLabel][dataLabel].listData[index][1] iterators = {} # lineLabel -> (listIndex,lineNumber) logdata = None currentLine = None def __init__(self, logdata, lineNumber=0): self.logdata = logdata self.currentLine = lineNumber for lineLabel in self.logdata.formats: if lineLabel in self.logdata.channels: self.iterators[lineLabel] = () self.jump(lineNumber) def __iter__(self): return self def __getitem__(self, lineLabel): return LogIterator.LogIteratorSubValue(self.logdata, self.iterators, lineLabel) def next(self): '''increment iterator to next log line''' self.currentLine += 1 if self.currentLine > self.logdata.lineCount: return self for lineLabel in self.iterators.keys(): # check if the currentLine has gone past our the line we're pointing to for this type of data dataLabel = self.logdata.formats[lineLabel].labels[0] (index, lineNumber) = self.iterators[lineLabel] # if so, and it is not the last entry in the log, increment the indices for dataLabels under that lineLabel if (self.currentLine > lineNumber) and ( index < len(self.logdata.channels[lineLabel][dataLabel].listData) - 1 ): index += 1 lineNumber = self.logdata.channels[lineLabel][dataLabel].listData[index][0] self.iterators[lineLabel] = (index, lineNumber) return self __next__ = next def jump(self, lineNumber): '''jump iterator to specified log line''' self.currentLine = lineNumber for lineLabel in self.iterators.keys(): dataLabel = self.logdata.formats[lineLabel].labels[0] (value, lineNumber) = self.logdata.channels[lineLabel][dataLabel].getNearestValue(self.currentLine) self.iterators[lineLabel] = (self.logdata.channels[lineLabel][dataLabel].getIndexOf(lineNumber), lineNumber) class DataflashLogHelper: '''helper functions for dealing with log data, put here to keep DataflashLog class as a simple parser and data store''' @staticmethod def getTimeAtLine(logdata, lineNumber): '''returns the nearest GPS timestamp in milliseconds after the given line number''' if "GPS" not in logdata.channels: raise Exception("no GPS log data found") # older logs use 'TIme', newer logs use 'TimeMS' # even newer logs use TimeUS timeLabel = None for possible in "TimeMS", "Time", "TimeUS": if possible in logdata.channels["GPS"]: timeLabel = possible break if timeLabel is None: raise Exception("Unable to get time label") while lineNumber <= logdata.lineCount: if lineNumber in logdata.channels["GPS"][timeLabel].dictData: return logdata.channels["GPS"][timeLabel].dictData[lineNumber] lineNumber = lineNumber + 1 sys.stderr.write("didn't find GPS data for " + str(lineNumber) + " - using maxtime\n") return logdata.channels["GPS"][timeLabel].max() @staticmethod def findLoiterChunks(logdata, minLengthSeconds=0, noRCInputs=True): """ Returns a list of (to, from) pairs defining sections of the log which are in loiter mode, ordered from longest to shortest in time. If `noRCInputs == True` it only returns chunks with no control inputs """ # TODO: implement noRCInputs handling when identifying stable loiter chunks, for now we're ignoring it def chunkSizeCompare(chunk1, chunk2): chunk1Len = chunk1[1] - chunk1[0] chunk2Len = chunk2[1] - chunk2[0] if chunk1Len == chunk2Len: return 0 elif chunk1Len > chunk2Len: return -1 else: return 1 changes = [{"line": k, "modeName": v[0], "modeNum": v[1]} for k, v in sorted(logdata.modeChanges.items())] chunks = [] for i in range(len(changes)): if changes[i]["modeName"] == "LOITER": startLine = changes[i]["line"] try: endLine = changes[i + 1]["line"] except IndexError: endLine = logdata.lineCount chunkTimeSeconds = ( DataflashLogHelper.getTimeAtLine(logdata, endLine) - DataflashLogHelper.getTimeAtLine(logdata, startLine) + 1 ) / 1000.0 if chunkTimeSeconds > minLengthSeconds: chunks.append((startLine, endLine)) chunks.sort(key=lambda chunk: chunk[1] - chunk[0]) return chunks @staticmethod def isLogEmpty(logdata): '''returns an human readable error string if the log is essentially empty, otherwise returns None''' # naive check for now, see if the throttle output was ever above 20% throttleThreshold = 20 if logdata.vehicleType == VehicleType.Copter: throttleThreshold = 200 # copter uses 0-1000, plane+rover use 0-100 if "CTUN" in logdata.channels: try: maxThrottle = logdata.channels["CTUN"]["ThrOut"].max() except KeyError: # ThrOut was shorted to ThO at some stage... maxThrottle = logdata.channels["CTUN"]["ThO"].max() # at roughly the same time ThO became a range from 0 to 1 throttleThreshold = 0.2 if maxThrottle < throttleThreshold: return "Throttle never above 20%" return None class DataflashLog(object): """ ArduPilot Dataflash log file reader and container class. Keep this simple, add more advanced or specific functions to DataflashLogHelper class """ knownHardwareTypes = ["APM", "PX4", "MPNG"] def __init__(self, logfile=None, format="auto", ignoreBadlines=False): self.filename = None self.vehicleType = None # from VehicleType enumeration; value derived from header self.vehicleTypeString = None # set at same time has the enum value self.firmwareVersion = "" self.firmwareHash = "" self.freeRAM = 0 self.hardwareType = "" # APM 1, APM 2, PX4, MPNG, etc What is VRBrain? BeagleBone, etc? Needs more testing self.formats = {} # name -> Format self.parameters = {} # token -> value self.messages = {} # lineNum -> message self.modeChanges = {} # lineNum -> (mode,value) self.channels = {} # lineLabel -> {dataLabel:Channel} self.filesizeKB = 0 self.durationSecs = 0 self.lineCount = 0 self.skippedLines = 0 self.backpatch_these_modechanges = [] self.frame = None if logfile: self.read(logfile, format, ignoreBadlines) def getCopterType(self): '''returns quad/hex/octo/tradheli if this is a copter log''' if self.vehicleType != VehicleType.Copter: return None motLabels = [] if "MOT" in self.formats: # not listed in PX4 log header for some reason? motLabels = self.formats["MOT"].labels if "GGain" in motLabels: return "tradheli" elif len(motLabels) == 4: return "quad" elif len(motLabels) == 6: return "hex" elif len(motLabels) == 8: return "octo" else: return "" def num_motor_channels(self): motor_channels_for_frame = { "QUAD": 4, "HEXA": 6, "Y6": 6, "OCTA": 8, "OCTA_QUAD": 8, "DECA": 10, # "HELI": 1, # "HELI_DUAL": 2, "TRI": 3, "SINGLE": 1, "COAX": 2, "TAILSITTER": 1, "DODECA_HEXA": 12, } return motor_channels_for_frame[self.frame] def read(self, logfile, format="auto", ignoreBadlines=False): '''returns on successful log read (including bad lines if ignoreBadlines==True), will throw an Exception otherwise''' # TODO: dataflash log parsing code is pretty hacky, should re-write more methodically df_header = bytearray([0xA3, 0x95, 0x80, 0x80]) self.filename = logfile if self.filename == '': f = sys.stdin else: f = open(self.filename, 'rb') if format.lower() == 'bin': head = df_header elif format == 'log': head = "" elif format == 'auto': if self.filename == '': # assuming TXT format head = "" else: head = f.read(4) f.seek(0) else: raise ValueError("Unknown log format for {}: {}".format(self.filename, format)) if head == df_header: numBytes, lineNumber = self.read_binary(f, ignoreBadlines) pass else: numBytes, lineNumber = self.read_text(f, ignoreBadlines) # gather some general stats about the log self.lineCount = lineNumber self.filesizeKB = numBytes / 1024.0 # TODO: switch duration calculation to use TimeMS values rather than GPS timestemp if "GPS" in self.channels: # the GPS time label changed at some point, need to handle both timeLabel = None for i in 'TimeMS', 'TimeUS', 'Time': if i in self.channels["GPS"]: timeLabel = i break firstTimeGPS = int(self.channels["GPS"][timeLabel].listData[0][1]) lastTimeGPS = int(self.channels["GPS"][timeLabel].listData[-1][1]) if timeLabel == 'TimeUS': firstTimeGPS /= 1000 lastTimeGPS /= 1000 self.durationSecs = (lastTimeGPS - firstTimeGPS) / 1000 # TODO: calculate logging rate based on timestamps # ... msg_vehicle_to_vehicle_map = { "ArduCopter": VehicleType.Copter, "APM:Copter": VehicleType.Copter, "ArduPlane": VehicleType.Plane, "ArduRover": VehicleType.Rover, } # takes the vehicle type supplied via "MSG" and sets vehicleType from # the VehicleType enumeration def set_vehicleType_from_MSG_vehicle(self, MSG_vehicle): ret = self.msg_vehicle_to_vehicle_map.get(MSG_vehicle, None) if ret is None: raise ValueError("Unknown vehicle type (%s)" % (MSG_vehicle)) self.vehicleType = ret self.vehicleTypeString = VehicleTypeString[ret] def handleModeChange(self, lineNumber, e): if self.vehicleType == VehicleType.Copter: modes = { 0: 'STABILIZE', 1: 'ACRO', 2: 'ALT_HOLD', 3: 'AUTO', 4: 'GUIDED', 5: 'LOITER', 6: 'RTL', 7: 'CIRCLE', 9: 'LAND', 10: 'OF_LOITER', 11: 'DRIFT', 13: 'SPORT', 14: 'FLIP', 15: 'AUTOTUNE', 16: 'POSHOLD', 17: 'BRAKE', 18: 'THROW', 19: 'AVOID_ADSB', 20: 'GUIDED_NOGPS', 21: 'SMART_RTL', } try: if hasattr(e, 'ThrCrs'): self.modeChanges[lineNumber] = (modes[int(e.Mode)], e.ThrCrs) else: # assume it has ModeNum: self.modeChanges[lineNumber] = (modes[int(e.Mode)], e.ModeNum) except ValueError: if hasattr(e, 'ThrCrs'): self.modeChanges[lineNumber] = (e.Mode, e.ThrCrs) else: # some .log files have the name spelt out by name # rather than number, contrary to the format # string. Attempt to map that back to a number: uppername = str(e.Mode).upper() for num in modes: if modes[num].upper() == uppername: self.modeChanges[lineNumber] = (uppername, num) return # assume it has ModeNum: print("Unknown mode=%u" % e.ModeNum) self.modeChanges[lineNumber] = (e.Mode, "mode=%u" % e.ModeNum) elif self.vehicleType in [VehicleType.Plane, VehicleType.Copter, VehicleType.Rover]: self.modeChanges[lineNumber] = (e.Mode, e.ModeNum) else: # if you've gotten to here the chances are we don't # know what vehicle you're flying... raise Exception( "Unknown log type for MODE line vehicletype=({}) line=({})".format(self.vehicleTypeString, repr(e)) ) def backPatchModeChanges(self): for (lineNumber, e) in self.backpatch_these_modechanges: self.handleModeChange(lineNumber, e) def set_frame(self, frame): self.frame = frame def process(self, lineNumber, e): if e.NAME == 'FMT': cls = e.to_class() if cls is not None: # FMT messages can be broken ... if hasattr(e, 'type') and e.type not in self._formats: # binary log specific self._formats[e.type] = cls if cls.NAME not in self.formats: self.formats[cls.NAME] = cls elif e.NAME == "PARM": self.parameters[e.Name] = e.Value elif e.NAME == "MSG": tokens = e.Message.split(' ') if not self.frame: if "Frame" in tokens[0]: self.set_frame(tokens[1]) if not self.vehicleType: try: self.set_vehicleType_from_MSG_vehicle(tokens[0]) except ValueError: return self.backPatchModeChanges() self.firmwareVersion = tokens[1] if len(tokens) == 3: self.firmwareHash = tokens[2][1:-1] else: self.messages[lineNumber] = e.Message elif e.NAME == "MODE": if self.vehicleType is None: self.backpatch_these_modechanges.append((lineNumber, e)) else: self.handleModeChange(lineNumber, e) # anything else must be the log data else: groupName = e.NAME # first time seeing this type of log line, create the channel storage if groupName not in self.channels: self.channels[groupName] = {} for label in e.labels: self.channels[groupName][label] = Channel() # store each token in its relevant channel for label in e.labels: value = getattr(e, label) channel = self.channels[groupName][label] channel.dictData[lineNumber] = value channel.listData.append((lineNumber, value)) def read_text(self, f, ignoreBadlines): self.formats = {'FMT': Format} lineNumber = 0 numBytes = 0 knownHardwareTypes = ["APM", "PX4", "MPNG"] for line in f: lineNumber = lineNumber + 1 numBytes += len(line) + 1 line = line.decode(encoding="utf-8") try: line = line.strip('\n\r') tokens = line.split(', ') # first handle the log header lines if line == " Ready to drive." or line == " Ready to FLY.": continue if line == "----------------------------------------": # present in pre-3.0 logs raise Exception( "Log file seems to be in the older format (prior to self-describing logs), which isn't supported" ) if len(tokens) == 1: tokens2 = line.split(' ') if line == "": pass elif len(tokens2) == 1 and tokens2[0].isdigit(): # log index pass elif len(tokens2) == 3 and tokens2[0] == "Free" and tokens2[1] == "RAM:": self.freeRAM = int(tokens2[2]) elif tokens2[0] in knownHardwareTypes: # not sure if we can parse this more usefully, for now only need to report it back verbatim self.hardwareType = line elif (len(tokens2) == 2 or len(tokens2) == 3) and tokens2[1][ 0 ].lower() == "v": # e.g. ArduCopter V3.1 (5c6503e2) try: self.set_vehicleType_from_MSG_vehicle(tokens2[0]) except ValueError: pass self.firmwareVersion = tokens2[1] if len(tokens2) == 3: self.firmwareHash = tokens2[2][1:-1] else: errorMsg = "Error parsing line %d of log file: %s" % (lineNumber, self.filename) if ignoreBadlines: print(errorMsg + " (skipping line)", file=sys.stderr) self.skippedLines += 1 else: raise Exception("") else: if not tokens[0] in self.formats: raise ValueError("Unknown Format {}".format(tokens[0])) e = self.formats[tokens[0]](*tokens[1:]) self.process(lineNumber, e) except Exception as e: print("BAD LINE: " + str(line), file=sys.stderr) if not ignoreBadlines: raise Exception( "Error parsing line %d of log file %s - %s" % (lineNumber, self.filename, e.args[0]) ) return (numBytes, lineNumber) def read_binary(self, f, ignoreBadlines): lineNumber = 0 numBytes = 0 for e in self._read_binary(f, ignoreBadlines): lineNumber += 1 if e is None: continue numBytes += e.SIZE # print(e) self.process(lineNumber, e) return (numBytes, lineNumber) def _read_binary(self, f, ignoreBadlines): self._formats = {128: BinaryFormat} data = bytearray(f.read()) offset = 0 while len(data) > offset + ctypes.sizeof(logheader): h = logheader.from_buffer(data, offset) if not (h.head1 == 0xA3 and h.head2 == 0x95): if ignoreBadlines is False: raise ValueError(h) else: if h.head1 == 0xFF and h.head2 == 0xFF and h.msgid == 0xFF: print( "Assuming EOF due to dataflash block tail filled with \\xff... (offset={off})".format( off=offset ), file=sys.stderr, ) break offset += 1 continue if h.msgid in self._formats: typ = self._formats[h.msgid] if len(data) <= offset + typ.SIZE: break try: e = typ.from_buffer(data, offset) except ValueError: print( "data:{} offset:{} size:{} sizeof:{} sum:{}".format( len(data), offset, typ.SIZE, ctypes.sizeof(typ), offset + typ.SIZE ) ) raise offset += typ.SIZE else: raise ValueError(str(h) + "unknown type") yield e