def main(argv): last_time = 0 deltas = {} d = evemu.Device(argv[1], create=False) for e in d.events(): if not e.matches("EV_SYN", "SYN_REPORT"): print(" %s %s %d" % (evemu.event_get_name(e.type), \ evemu.event_get_name(e.type, e.code), \ e.value)) continue time = usec(e.sec, e.usec) dt = (time - last_time) / 1000 last_time = time print("%4dms ---- %s %s ----" % (dt, evemu.event_get_name(e.type), \ evemu.event_get_name(e.type, e.code))) deltas[dt] = dict.get(deltas, dt, 0) + 1 print("\nDistribution of deltas in ms:") for key, value in dict.iteritems(deltas): print(" %dms: %d" % (key, value))
def main(argv): d = evemu.Device(argv[1], create=False) xres = 1.0 * d.get_abs_resolution("ABS_MT_POSITION_X") yres = 1.0 * d.get_abs_resolution("ABS_MT_POSITION_Y") # Assume Apple trackpad resolutions if xres == 0 or yres == 0: print("WARNING: Using hardcoded resolutions") xres = 94 yres = 90 width, height = None, None ow, oh = 0, 0 for e in d.events(): if e.matches("EV_ABS", "ABS_MT_WIDTH_MAJOR"): width = e.value elif e.matches("EV_ABS", "ABS_MT_WIDTH_MINOR"): height = e.value elif e.matches("EV_SYN", "SYN_REPORT"): if width == None or height == None: continue w = width / xres h = height / yres if w == ow and h == oh: continue ow = w oh = h print("width: {} height {} phys: {}x{}mm".format( width, height, w, h))
def test_play_and_record(self): """ Verifies that a Device and play back prerecorded events. """ device = evemu.Device(self.get_device_file()) devnode = device.devnode events_file = self.get_events_file() # device.record() calls evemu_record() and is thus missing the # description that the input file has with open(events_file) as e: indata = extract_events(strip_comments(e.readlines())) recording_started = Event() q = Queue() record_process = Process(target=record, args=(recording_started, devnode, q)) record_process.start() recording_started.wait(100) device.play(open(events_file)) outdata = strip_comments(q.get()) record_process.join() self.assertEquals(len(indata), len(outdata)) fuzz = re.compile("E: \d+\.\d+ (.*)") for i in range(len(indata)): lhs = fuzz.match(indata[i]) self.assertTrue(lhs) rhs = fuzz.match(outdata[i]) self.assertTrue(rhs) self.assertEquals(lhs.group(1), rhs.group(1))
def main(argv): d = evemu.Device(argv[1], create=False) pmax = d.get_abs_maximum("ABS_PRESSURE") pmin = d.get_abs_minimum("ABS_PRESSURE") pressure = pmin time_offset = -1 print("#!/usr/bin/gnuplot") print("# This is a self-executing gnuplot file") print("#") print("set xlabel \"time\"") print("set ylabel \"pressure\"") print("set style data lines") print("plot '-' using 1:2 title 'pressure'") for e in d.events(): if time_offset < 0: time_offset = time_to_us(e.sec, e.usec) if e.matches("EV_ABS", "ABS_PRESSURE"): t = time_to_us(e.sec, e.usec) - time_offset print("%f %d" % (t, e.value)) print("e") print("pause -1")
def test_read_events_twice(self): device = evemu.Device(self.get_device_file(), create=False) events_file = self.get_events_file() with open(events_file) as ef: e1 = [(e.type, e.code, e.value) for e in device.events(ef)] e2 = [(e.type, e.code, e.value) for e in device.events(ef)] self.assertEquals(len(e1), len(e2)) self.assertEquals(e1, e2)
def main(argv): d = evemu.Device(argv[1], create=False) locations = {} x, y = 0, 0 for e in d.events(): if e.matches("EV_ABS", "ABS_X"): x = e.value elif e.matches("EV_ABS", "ABS_Y"): y = e.value elif e.matches("EV_SYN", "SYN_REPORT"): val = locations.get((x, y), 0) locations[(x, y)] = val + 1 xmax = d.get_abs_maximum("ABS_X") xmin = d.get_abs_minimum("ABS_X") ymax = d.get_abs_maximum("ABS_Y") ymin = d.get_abs_minimum("ABS_Y") # adjust for out-of-range coordinates xmax = max([x for (x, y) in locations.keys()] + [xmax]) ymax = max([y for (x, y) in locations.keys()] + [ymax]) xmin = min([x for (x, y) in locations.keys()] + [xmin]) ymin = min([y for (x, y) in locations.keys()] + [ymin]) w = xmax - xmin h = ymax - ymin stride = w xs = [x for (x, y) in locations.keys()] ys = [y for (x, y) in locations.keys()] xs.sort() ys.sort() xs = set(range(xmin, xmax)) - set(xs) ys = set(range(ymin, ymax)) - set(ys) # xs and ys contain all x/y coordinates that were never reached anywhere imgdata = [255] * (stride * (h + 1)) for x in xs: px = x - xmin for py in range(0, h): imgdata[py * stride + px] = 0 for y in ys: py = y - ymin for px in range(0, w): imgdata[py * stride + px] = 0 im = Image.new('L', (w, h + 1)) im.putdata(imgdata) im.save(OUTPUT_FILE)
def record(recording_started, device_node, q): """ Runs the recorder in a separate process because the evemu API is a blocking API. """ device = evemu.Device(device_node) with tempfile.TemporaryFile(mode='rt') as event_file: recording_started.set() device.record(event_file, 1000) event_file.flush() event_file.seek(0) outdata = event_file.readlines() q.put(outdata)
def main(argv): slot = 0 slots = [[0, 0], [0, 0]] tracking_ids = [-1, -1] tracking_ids_old = [-1, -1] distances = [] mm = [] horiz = [] vert = [] d = evemu.Device(argv[1], create=False) width = d.get_abs_maximum("ABS_MT_POSITION_X") - d.get_abs_minimum("ABS_MT_POSITION_X") height = d.get_abs_maximum("ABS_MT_POSITION_Y") - d.get_abs_minimum("ABS_MT_POSITION_Y") xres = max(1.0, d.get_abs_resolution("ABS_MT_POSITION_X") * 1.0) yres = max(1.0, d.get_abs_resolution("ABS_MT_POSITION_Y") * 1.0) print "Touchpad dimensions: %dx%dmm (%dx%d units)" % (width/xres, height/yres, width, height) for e in d.events(): if e.matches("EV_ABS", "ABS_MT_SLOT"): slot = e.value elif e.matches("EV_ABS", "ABS_MT_TRACKING_ID"): tracking_ids[slot] = e.value elif e.matches("EV_ABS", "ABS_MT_POSITION_X"): slots[slot][0] = e.value elif e.matches("EV_ABS", "ABS_MT_POSITION_Y"): slots[slot][1] = e.value elif e.matches("EV_SYN", "SYN_REPORT"): if tracking_ids[0] != -1 and tracking_ids[1] != -1 and \ (tracking_ids_old[0] == -1 or tracking_ids_old[1] == -1): dist = math.hypot(slots[0][0] - slots[1][0], slots[0][1] - slots[1][1]) distances.append(dist) dist = math.hypot(slots[0][0]/xres - slots[1][0]/xres, slots[0][1]/yres - slots[1][1]/yres) mm.append(dist) h = abs(slots[0][0]/xres - slots[1][0]/xres) horiz.append(h) v = abs(slots[0][1]/yres - slots[1][1]/yres) vert.append(v) print "New 2fg touch: distance %dmm (h %dmm v %dmm)" % (dist, h, v) tracking_ids_old[0] = tracking_ids[0] tracking_ids_old[1] = tracking_ids[1] print "Max distance: %dmm, %d units" % (max(mm), max(distances)) print "Min distance %dmm, %d units" % (min(mm), min(distances)) print "Max distance: %dmm horiz %dmm vert" % (max(horiz), max(vert)) print "Min distance %dmm horiz, %dmm vert" % (min(horiz), min(vert))
def test_describe(self): """ Verifies that a device description can be correctly extracted from a Device. """ # Get original description with open(self.get_device_file()) as f: data = strip_comments(f.readlines()) # Create a pseudo device with that description d = evemu.Device(self.get_device_file()) # get the description to a temporary file with tempfile.TemporaryFile(mode='rt') as t: d.describe(t) # read in the temporary file and compare to the original t.flush() t.seek(0) newdata = strip_comments(t.readlines()) self.assertEquals(data, newdata)
def main(argv): slots = [] xres, yres = 1, 1 d = evemu.Device(argv[1], create=False) nslots = d.get_abs_maximum("ABS_MT_SLOT") + 1 slots = [Slot() for _ in range(0, nslots)] print("Tracking %d slots" % nslots) slot = 0 for e in d.events(): s = slots[slot] if e.matches("EV_ABS", "ABS_MT_SLOT"): slot = e.value s = slots[slot] s.dirty = True elif e.matches("EV_ABS", "ABS_MT_TRACKING_ID"): if e.value == -1: s.state = SlotState.END else: s.state = SlotState.BEGIN elif e.matches("EV_ABS", "ABS_MT_POSITION_X"): s.x = e.value s.dirty = True elif e.matches("EV_ABS", "ABS_MT_POSITION_Y"): s.y = e.value s.dirty = True elif e.matches("EV_SYN", "SYN_REPORT"): if (slots[0].state == SlotState.END and slots[0].near_enough_to(slots[1])) or \ (slots[1].state == SlotState.END and slots[1].near_enough_to(slots[0])): print("{:2d}.{:06d}: possible slot jump".format(e.sec, e.usec)) for sl in slots: if sl.state == SlotState.BEGIN: sl.state = SlotState.UPDATE elif sl.state == SlotState.END: sl.state = SlotState.NONE
def main(argv): d = evemu.Device(argv[1], create=False) pressure = None dpressure = None vals = [] for e in d.events(): if e.matches("EV_ABS", "ABS_MT_TRACKING_ID"): if e.value == -1: pressure = None dpressure = None elif e.matches("EV_ABS", "ABS_PRESSURE"): if pressure is not None: dpressure = e.value - pressure pressure = e.value elif e.matches("EV_SYN", "SYN_REPORT"): if dpressure is not None and abs(dpressure) <= 2: continue vals.append((pressure, dpressure)) print("Pressure deltas <= 2 are filtered") for p, dp in vals: print("Pressure: {} delta {}".format(p, dp))
def test_construct_from_prop_file_file_nocreate(self): """ Verifies a device can be constructed from an evemu prop file file object, without creating a uinput device. """ d = evemu.Device(open(self.get_device_file()), create=False)
def test_construct_from_prop_file_file(self): """ Verifies a device can be constructed from an evemu prop file file object. """ d = evemu.Device(open(self.get_device_file()))
def test_construct_from_prop_file_name(self): """ Verifies a device can be constructed from an evemu prop file name. """ d = evemu.Device(self.get_device_file())
def test_construct_from_dev_node_file(self): """ Verifies a Device can be constructed from an existing input device node file object. """ d = evemu.Device(open("/dev/input/event0"))
def test_construct_from_dev_node_name(self): """ Verifies a Device can be constructed from an existing input device node name. """ d = evemu.Device("/dev/input/event0")
def parse_recordings_file(path): print("# processing {}".format(path)) vels = [] d = evemu.Device(path, create=False) if not d.has_event("EV_ABS", "ABS_MT_SLOT"): print("# single touch only, skipping") return None nslots = d.get_abs_maximum("ABS_MT_SLOT") + 1 slots = [Slot() for _ in range(0, nslots)] xres = 1.0 * d.get_abs_resolution("ABS_MT_POSITION_X") yres = 1.0 * d.get_abs_resolution("ABS_MT_POSITION_Y") slot = 0 for e in d.events(): s = slots[slot] if e.matches("EV_ABS", "ABS_MT_SLOT"): slot = e.value s = slots[slot] s.dirty = True elif e.matches("EV_ABS", "ABS_MT_TRACKING_ID"): if e.value == -1: s.state = SlotState.END else: s.state = SlotState.BEGIN s.time = e.sec * 1e6 + e.usec s.dx = 0 s.dy = 0 s.dirty = True elif e.matches("EV_ABS", "ABS_MT_POSITION_X"): if s.state == SlotState.UPDATE: s.dx = e.value - s.x s.x = e.value s.dirty = True elif e.matches("EV_ABS", "ABS_MT_POSITION_Y"): if s.state == SlotState.UPDATE: s.dy = e.value - s.y s.y = e.value s.dirty = True elif e.matches("EV_SYN", "SYN_REPORT"): for sl in slots: if sl.state != SlotState.NONE and sl.dirty: t = e.sec * 1e6 + e.usec sl.dt = t - sl.time sl.time = t if sl.state == SlotState.UPDATE and sl.dirty: dist = math.hypot(sl.dx/xres, sl.dy/yres) # in mm dt = sl.dt # in µs vel = 1000 * dist/dt # mm/ms == m/s vel = 1000 * vel # mm/s vels.append(vel) # print("{}".format(vel)) if sl.state == SlotState.BEGIN: sl.state = SlotState.UPDATE elif sl.state == SlotState.END: sl.state = SlotState.NONE sl.dirty = False nevents = len(vels) maxvel = max(vels) print("# Number of data points: {}".format(nevents)) print("# Highest velocity: {} mm/s".format(maxvel)) # divide into buckets for each 10mm/s increment increment = 10 nbuckets = int(maxvel/increment) + 1 buckets = [0] * nbuckets print("# Starting with {} buckets".format(nbuckets)) min_events = 5 for v in vels: bucket = int(v/increment) buckets[bucket] += 1 reduced_nevents = nevents for i in range(len(buckets) - 1, -1, -1): if buckets[i] >= min_events: break reduced_nevents -= buckets[i] # make sure we don't drop more than 5% of the data if nevents * 0.95 > reduced_nevents: break print("# Reducing to {} buckets ({} required per bucket)".format(i + 1, min_events)) del buckets[i+1:] nevents_new = sum(buckets) print("# Left with {} data points ({:.1f}% of data)".format(nevents_new, 100.0 * nevents_new/nevents)) speed = increment total_percent = 0 datapoints = {} for b in buckets: percent = 100.0 * b/nevents_new total_percent += percent datapoints[speed] = { "speed" : speed, "nevents" : b, "percent" : percent, "total-percent" : total_percent, } #print(".. {}mm/s: {:5} events, {:.1f}% {:.1f}% total".format(speed, b, percent, total_percent)) speed += increment return datapoints
def main(argv): deltas = [] x, y = None, None dx, dy = 0, 0 max_delta = [0, 0, 0] d = evemu.Device(argv[1], create=False) width = d.get_abs_maximum("ABS_MT_POSITION_X") - d.get_abs_minimum("ABS_MT_POSITION_X") height = d.get_abs_maximum("ABS_MT_POSITION_Y") - d.get_abs_minimum("ABS_MT_POSITION_Y") diag = veclen(width, height) xres = d.get_abs_resolution("ABS_MT_POSITION_X") * 1.0 yres = d.get_abs_resolution("ABS_MT_POSITION_Y") * 1.0 print "Touchpad dimensions: %dx%dmm" % (width/xres, height/yres) print "Touchpad diagonal: %.2f (0.25 == %.2f)" % (diag, 0.25 * diag) diag = veclen(width/xres, height/yres) print "Touchpad diagonal: %.2fmm (0.25 == %.2fmm)" % (diag, 0.25 * diag) slot = 0 for e in d.events(): if e.matches("EV_ABS", "ABS_MT_SLOT"): slot = e.value elif slot != 0: continue if e.matches("EV_ABS", "ABS_MT_TRACKING_ID"): if e.value == -1 and len(deltas) > 0: vdeltas = [ veclen(x/xres, y/xres) for (x, y) in deltas] print("%d.%d: %d deltas, average %.2f, max %.2f, total distance %.2f in mm" % (e.sec, e.usec, len(vdeltas), mean(vdeltas), max(vdeltas), sum(vdeltas))) xdeltas = [abs(x/xres) for (x, y) in deltas] ydeltas = [abs(y/yres) for (x, y) in deltas] print("... x: average %.2fmm, max %.2fmm, total distance %.2fmm" % (mean(xdeltas), max(xdeltas), sum(xdeltas))) print("... y: average %.2fmm, max %.2fmm, total distance %.2fmm" % (mean(ydeltas), max(ydeltas), sum(ydeltas))) max_delta[0] = max(max_delta[0], max(vdeltas)) max_delta[1] = max(max_delta[1], max(xdeltas)) max_delta[2] = max(max_delta[2], max(ydeltas)) else: deltas = [] x, y = None, None dx, dy = 0, 0 elif e.matches("EV_ABS", "ABS_MT_POSITION_X"): if x != None: dx = e.value - x x = e.value elif e.matches("EV_ABS", "ABS_MT_POSITION_Y"): if y != None: dy = e.value - y y = e.value elif e.matches("EV_SYN", "SYN_REPORT"): if dx != 0 and dy != 0: deltas.append((dx, dy)) dx, dy = 0, 0 print("Maximum recorded delta: %.2fmm" % (max_delta[0])) print("... x: %.2fmm" % max_delta[1]) print("... y: %.2fmm" % max_delta[2]) return
def setUp(self): self.d = evemu.Device(evemu_path, create=False)
def is_rel_device(path): d = evemu.Device(argv[1], create=False) return d.has_event("EV_REL", "REL_X")
def is_abs_device(path): d = evemu.Device(argv[1], create=False) return d.has_event("EV_ABS", "ABS_X")
def test_read_events(self): device = evemu.Device(self.get_device_file(), create=False) events_file = self.get_events_file() with open(events_file) as ef: events = [e for e in device.events(ef)] self.assertTrue(len(events) > 1)
# Command line: # export PYTHONPATH=/path/to/evemu/python # python convert-old-dumps-to-1.1.py myEvent.desc [myEvent.events] # # Make sure the print statement is disabled and the function is used. from __future__ import print_function import os import re import sys import evemu def usage(args): print("%s mydev.desc [mydev.events]" % os.path.basename(args[0])) return 1 if __name__ == "__main__": if len(sys.argv) < 2: exit(usage(sys.argv)) file_desc = sys.argv[1] d = evemu.Device(file_desc, create=False) d.describe(sys.stdout) if len(sys.argv) > 2: with open(sys.argv[2]) as f: for e in d.events(f): print(e)
def main(argv): slots = [] xres, yres = 1, 1 parser = argparse.ArgumentParser(description="Measure delta between event frames for each slot") parser.add_argument("--use-mm", action='store_true', help="Use mm instead of device deltas") parser.add_argument("--use-st", action='store_true', help="Use ABS_X/ABS_Y instead of device deltas") parser.add_argument("--use-absolute", action='store_true', help="Use absolute coordinates, not deltas") parser.add_argument("path", metavar="recording", nargs=1, help="Path to evemu recording") args = parser.parse_args() d = evemu.Device(args.path[0], create=False) nslots = d.get_abs_maximum("ABS_MT_SLOT") + 1 print("Tracking %d slots" % nslots) if nslots > 10: nslots = 10 print("Capping at %d slots" % nslots) slots = [Slot() for _ in range(0, nslots)] marker_begin_slot = " ++++++ | " marker_end_slot = " ------ | " marker_empty_slot = " *********** | " marker_no_data = " | " if args.use_mm: xres = 1.0 * d.get_abs_resolution("ABS_MT_POSITION_X") yres = 1.0 * d.get_abs_resolution("ABS_MT_POSITION_Y") marker_empty_slot = " ************* | " marker_no_data = " | " marker_begin_slot = " ++++++ | " marker_end_slot = " ------ | " if args.use_st: print("Warning: slot coordinates on FINGER/DOUBLETAP change may be incorrect") slot = 0 for e in d.events(): s = slots[slot] if args.use_st: # Note: this relies on the EV_KEY events to come in before the # x/y events, otherwise the last/first event in each slot will # be wrong. if e.matches("EV_KEY", "BTN_TOOL_FINGER"): slot = 0 s = slots[slot] s.dirty = True if e.value: s.state = SlotState.BEGIN else: s.state = SlotState.END elif e.matches("EV_KEY", "BTN_TOOL_DOUBLETAP"): slot = 1 s = slots[slot] s.dirty = True if e.value: s.state = SlotState.BEGIN else: s.state = SlotState.END elif e.matches("EV_ABS", "ABS_X"): if s.state == SlotState.UPDATE: s.dx = e.value - s.x s.x = e.value s.dirty = True elif e.matches("EV_ABS", "ABS_Y"): if s.state == SlotState.UPDATE: s.dy = e.value - s.y s.y = e.value s.dirty = True else: if e.matches("EV_ABS", "ABS_MT_SLOT"): slot = e.value s = slots[slot] s.dirty = True elif e.matches("EV_ABS", "ABS_MT_TRACKING_ID"): if e.value == -1: s.state = SlotState.END else: s.state = SlotState.BEGIN s.dx = 0 s.dy = 0 s.dirty = True elif e.matches("EV_ABS", "ABS_MT_POSITION_X"): if s.state == SlotState.UPDATE: s.dx = e.value - s.x s.x = e.value s.dirty = True elif e.matches("EV_ABS", "ABS_MT_POSITION_Y"): if s.state == SlotState.UPDATE: s.dy = e.value - s.y s.y = e.value s.dirty = True if e.matches("EV_SYN", "SYN_REPORT"): print("{:2d}.{:06d}: ".format(e.sec, e.usec), end='') for sl in slots: if sl.state == SlotState.NONE: print(marker_empty_slot, end='') elif sl.state == SlotState.BEGIN: print(marker_begin_slot, end='') elif sl.state == SlotState.END: print(marker_end_slot, end='') elif not sl.dirty: print(marker_no_data, end='') else: if sl.dx != 0 and sl.dy != 0: t = math.atan2(sl.dx, sl.dy) t += math.pi # in [0, 2pi] range now if t == 0: t = 0.01; else: t = t * 180.0 / math.pi directions = [ '↖↑', '↖←', '↙←', '↙↓', '↓↘', '→↘', '→↗', '↑↗'] direction = "{:3.0f}".format(t) direction = directions[int(t/45)] else: direction = '..' if args.use_mm: sl.dx /= xres sl.dy /= yres print("{} {:+3.2f}/{:+03.2f} | ".format(direction, sl.dx, sl.dy), end='') elif args.use_absolute: print("{} {:4d}/{:4d} | ".format(direction, sl.x, sl.y), end='') else: print("{} {:4d}/{:4d} | ".format(direction, sl.dx, sl.dy), end='') if sl.state == SlotState.BEGIN: sl.state = SlotState.UPDATE elif sl.state == SlotState.END: sl.state = SlotState.NONE sl.dirty = False print("")
def __init__(self, args=False): self.args = args self.device = evemu.Device("encoder.props") # Sleep 1s to be sure that the go app opens the file device time.sleep(1)
def setUp(self): super(DevicePropertiesTestCase, self).setUp() self._device = evemu.Device(self.get_device_file())
def main(argv): d = evemu.Device(argv[1], create=False) offsets = {} offset = 2 # timestamp count star separator print('Legend:') print(' time, count of keys down, * to mark <3ms timestamp') print(' + .... key down, | .... key is down, ^ .... key up') print(' ' * 12 + ' ' + ' ' + ' |', end='') for c in range(ord('a'), ord('z') + 1): print('{}'.format(chr(c)), end='') offsets[offset] = chr(c) offset += 1 for c in range(ord('0'), ord('9') + 1): print('{}'.format(chr(c)), end='') offsets[offset] = chr(c) offset += 1 offset += 1 print(' Spc Bksp Del Ret LShf RShf', end='') offsets[offset + 2] = 'space' offset += 4 offsets[offset + 2] = 'backspace' offset += 5 offsets[offset + 1] = 'delete' offset += 4 offsets[offset + 1] = 'enter' offset += 4 offsets[offset + 2] = 'leftshift' offset += 5 offsets[offset + 2] = 'rightshift' offset += 5 print('') max_offset = offset char = ['^', '+', '|', ' '] down = {} last_down = {} bounce = False for e in d.events(): if e.matches('EV_SYN', 'SYN_REPORT'): line = [] line.append('{:06d}.{:06d}'.format(e.sec, e.usec)) line.append(' {} '.format(len(down))) time = e.sec * 1000000 + e.usec if bounce: line.append('*') bounce = False else: line.append(' ') line.append('|') for i in range(2, max_offset): try: line.append(char[down[offsets[i]]]) except KeyError: line.append(' ') print(''.join(line)) for key in down: if down[key] == DOWN: down[key] = IS_DOWN elif down[key] == UP: down[key] = NONE d2 = {} for key in down.keys(): value = down[key] if value != NONE: d2[key] = value down = d2 if not e.matches('EV_KEY'): continue key = evemu.event_get_name(e.type, e.code) key = key[4:].lower() # remove KEY_ prefix down[key] = e.value if e.value: time = e.sec * 1000000 + e.usec if (key in last_down) and (time - last_down[key] < 70000): # 70 ms bounce = True else: last_down[key] = time