blob: e3cdacdfa68f083d663ab4705e328e80d7f409f7 [file] [log] [blame]
# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""This module provides MTB parser and related packet methods."""
import copy
import logging
import math
import os
import re
import sys
from firmware_constants import AXIS, GV, MTB, VAL
sys.path.append('../../bin/input')
from linux_input import *
def make_pretty_packet(packet):
"""Convert the event list in a packet to a pretty format."""
pretty_packet = []
for event in packet:
pretty_event = []
pretty_event.append('Event:')
pretty_event.append('time %.6f,' % event[MTB.EV_TIME])
if event.get(MTB.SYN_REPORT):
pretty_event.append('-------------- SYN_REPORT ------------\n')
else:
ev_type = event[MTB.EV_TYPE]
pretty_event.append('type %d (%s),' % (ev_type, EV_TYPES[ev_type]))
ev_code = event[MTB.EV_CODE]
pretty_event.append('code %d (%s),' %
(ev_code, EV_STRINGS[ev_type][ev_code]))
pretty_event.append('value %d' % event[MTB.EV_VALUE])
pretty_packet.append(' '.join(pretty_event))
return '\n'.join(pretty_packet)
def convert_to_evemu_format(packets):
"""Convert the text event format to the evemu format."""
evemu_output = []
evemu_format = 'E: %.6f %04x %04x %d'
evemu_format_syn_report = 'E: %.6f 0000 0000 0'
for packet in packets:
for event in packet:
if event.get(MTB.SYN_REPORT):
evemu_event = evemu_format_syn_report % event[MTB.EV_TIME]
else:
evemu_event = evemu_format % (event[MTB.EV_TIME],
event[MTB.EV_TYPE],
event[MTB.EV_CODE],
event[MTB.EV_VALUE])
evemu_output.append(evemu_event)
return evemu_output
def convert_mtplot_file_to_evemu_file(mtplot_filename, evemu_ext='.evemu',
force=False):
"""Convert a mtplot event file to an evemu event file.
Example:
'one_finger_swipe.dat' is converted to 'one_finger_swipe.evemu.dat'
"""
if not os.path.isfile(mtplot_filename):
print 'Error: there is no such file: "%s".' % mtplot_filename
return
# Convert mtplot event format to evemu event format.
mtplot_packets = MtbParser().parse_file(mtplot_filename)
evemu_packets = convert_to_evemu_format(mtplot_packets)
# Create the evemu filename from the mtplot filename.
mtplot_root, mtplot_ext = os.path.splitext(mtplot_filename)
evemu_filename = mtplot_root + evemu_ext + mtplot_ext
# Make sure that the file to be created does not exist yet unless force flag
# is set to be True.
if os.path.isfile(evemu_filename) and not force:
print 'Warning: the "%s" already exists.' % evemu_filename
return
# Write the converted evemu events to the evemu file.
with open(evemu_filename, 'w') as evemu_f:
evemu_f.write('\n'.join(evemu_packets))
class MtbEvemu:
"""A simplified class provides MTB utilities for evemu event format."""
def __init__(self):
self.mtb = Mtb()
self.num_tracking_ids = 0
def _convert_event(self, event):
(tv_sec, tv_usec, ev_type, ev_code, ev_value) = event
ev_dict = {MTB.EV_TIME: tv_sec + 0.000001 * tv_usec,
MTB.EV_TYPE: ev_type,
MTB.EV_CODE: ev_code,
MTB.EV_VALUE: ev_value}
return ev_dict
def all_fingers_leaving(self):
"""Is there no finger on the pad?"""
return self.num_tracking_ids <= 0
def process_event(self, event):
"""Process the event and count existing fingers."""
converted_event = self._convert_event(event)
if self.mtb._is_new_contact(converted_event):
self.num_tracking_ids += 1
elif self.mtb._is_finger_leaving(converted_event):
self.num_tracking_ids -= 1
class Mtb:
"""An MTB class providing MTB format related utility methods."""
LEN_MOVING_AVERAGE = 2
LEVEL_JUMP_RATIO = 3
LEVEL_JUMP_MAXIUM_ALLOWED = 10
LEN_DISCARD = 5
def __init__(self, packets=None):
self.packets = packets
self._define_check_event_func_list()
def _define_check_event_func_list(self):
"""Define event function lists for various event cycles below."""
self.check_event_func_list = {}
self.MAX_FINGERS = 5
# One-finger touching the pad should generate the following events:
# BTN_TOUCH, and BTN_TOOL_FINGER: 0 -> 1 -> 0
self.check_event_func_list[1] = [self._is_BTN_TOUCH,
self._is_BTN_TOOL_FINGER]
# Two-finger touching the pad should generate the following events:
# BTN_TOUCH, and BTN_TOOL_DOUBLETAP: 0 -> 1 -> 0
self.check_event_func_list[2] = [self._is_BTN_TOUCH,
self._is_BTN_TOOL_DOUBLETAP]
# Three-finger touching the pad should generate the following events:
# BTN_TOUCH, and BTN_TOOL_TRIPLETAP: 0 -> 1 -> 0
self.check_event_func_list[3] = [self._is_BTN_TOUCH,
self._is_BTN_TOOL_TRIPLETAP]
# Four-finger touching the pad should generate the following events:
# BTN_TOUCH, and BTN_TOOL_QUADTAP: 0 -> 1 -> 0
self.check_event_func_list[4] = [self._is_BTN_TOUCH,
self._is_BTN_TOOL_QUADTAP]
# Five-finger touching the pad should generate the following events:
# BTN_TOUCH, and BTN_TOOL_QUINTTAP: 0 -> 1 -> 0
self.check_event_func_list[5] = [self._is_BTN_TOUCH,
self._is_BTN_TOOL_QUINTTAP]
# Physical click should generate the following events:
# BTN_LEFT: 0 -> 1 -> 0
self.check_event_func_click = [self._is_BTN_LEFT,]
def _is_ABS_MT_TRACKING_ID(self, event):
"""Is this event ABS_MT_TRACKING_ID?"""
return (not event.get(MTB.SYN_REPORT) and
event[MTB.EV_TYPE] == EV_ABS and
event[MTB.EV_CODE] == ABS_MT_TRACKING_ID)
def _is_new_contact(self, event):
"""Is this packet generating new contact (Tracking ID)?"""
return self._is_ABS_MT_TRACKING_ID(event) and event[MTB.EV_VALUE] != -1
def _is_finger_leaving(self, event):
"""Is the finger is leaving in this packet?"""
return self._is_ABS_MT_TRACKING_ID(event) and event[MTB.EV_VALUE] == -1
def _is_ABS_MT_SLOT(self, event):
"""Is this packet ABS_MT_SLOT?"""
return (not event.get(MTB.SYN_REPORT) and
event[MTB.EV_TYPE] == EV_ABS and
event[MTB.EV_CODE] == ABS_MT_SLOT)
def _is_ABS_MT_POSITION_X(self, event):
"""Is this packet ABS_MT_POSITION_X?"""
return (not event.get(MTB.SYN_REPORT) and
event[MTB.EV_TYPE] == EV_ABS and
event[MTB.EV_CODE] == ABS_MT_POSITION_X)
def _is_ABS_MT_POSITION_Y(self, event):
"""Is this packet ABS_MT_POSITION_Y?"""
return (not event.get(MTB.SYN_REPORT) and
event[MTB.EV_TYPE] == EV_ABS and
event[MTB.EV_CODE] == ABS_MT_POSITION_Y)
def _is_EV_KEY(self, event):
"""Is this an EV_KEY event?"""
return (not event.get(MTB.SYN_REPORT) and event[MTB.EV_TYPE] == EV_KEY)
def _is_BTN_LEFT(self, event):
"""Is this event BTN_LEFT?"""
return (self._is_EV_KEY(event) and event[MTB.EV_CODE] == BTN_LEFT)
def _is_BTN_TOOL_FINGER(self, event):
"""Is this event BTN_TOOL_FINGER?"""
return (self._is_EV_KEY(event) and
event[MTB.EV_CODE] == BTN_TOOL_FINGER)
def _is_BTN_TOOL_DOUBLETAP(self, event):
"""Is this event BTN_TOOL_DOUBLETAP?"""
return (self._is_EV_KEY(event) and
event[MTB.EV_CODE] == BTN_TOOL_DOUBLETAP)
def _is_BTN_TOOL_TRIPLETAP(self, event):
"""Is this event BTN_TOOL_TRIPLETAP?"""
return (self._is_EV_KEY(event) and
event[MTB.EV_CODE] == BTN_TOOL_TRIPLETAP)
def _is_BTN_TOOL_QUADTAP(self, event):
"""Is this event BTN_TOOL_QUADTAP?"""
return (self._is_EV_KEY(event) and
event[MTB.EV_CODE] == BTN_TOOL_QUADTAP)
def _is_BTN_TOOL_QUINTTAP(self, event):
"""Is this event BTN_TOOL_QUINTTAP?"""
return (self._is_EV_KEY(event) and
event[MTB.EV_CODE] == BTN_TOOL_QUINTTAP)
def _is_BTN_TOUCH(self, event):
"""Is this event BTN_TOUCH?"""
return (self._is_EV_KEY(event) and
event[MTB.EV_CODE] == BTN_TOUCH)
def _calc_movement_for_axis(self, x, prev_x):
"""Calculate the distance moved in an axis."""
return abs(x - prev_x) if prev_x is not None else 0
def _calc_distance(self, (x0, y0), (x1, y1)):
"""Calculate the distance between two points."""
dist_x = x1 - x0
dist_y = y1 - y0
return math.sqrt(dist_x * dist_x + dist_y * dist_y)
def _init_dict(self, keys, value):
"""Initialize a dictionary over the keys with the same given value.
Note: The following command does not always work:
dict.fromkeys(keys, value)
It works when value is a simple type, e.g., an integer.
However, if value is [] or {}, it does not work correctly.
The reason is that if the value is [] or {}, all the keys would
point to the same list or dictionary, which is not expected
in most cases.
"""
return dict([(key, copy.deepcopy(value)) for key in keys])
def get_number_contacts(self):
"""Get the number of contacts (Tracking IDs)."""
num_contacts = 0
for packet in self.packets:
for event in packet:
if self._is_new_contact(event):
num_contacts += 1
return num_contacts
def get_x_y(self, target_slot):
"""Extract x and y positions in the target slot."""
# The default slot is slot 0 if no slot number is assigned.
# The rationale is that evdev is a state machine. It only reports
# the change. Slot 0 would not be reported by evdev if last time
# the last finger left the pad was at slot 0.
slot = 0
# Should not write "list_x = list_y = []" below.
# They would end up with pointing to the same list.
list_x = []
list_y = []
prev_x = prev_y = None
target_slot_live = False
initial_default_slot_0 = True
for packet in self.packets:
if (slot == target_slot and slot == 0 and not target_slot_live and
initial_default_slot_0):
target_slot_live = True
initial_default_slot_0 = False
for event in packet:
if self._is_ABS_MT_SLOT(event):
slot = event[MTB.EV_VALUE]
if slot == target_slot and not target_slot_live:
target_slot_live = True
if slot != target_slot:
continue
# Update x value if available.
if self._is_ABS_MT_POSITION_X(event):
prev_x = event[MTB.EV_VALUE]
# Update y value if available.
elif self._is_ABS_MT_POSITION_Y(event):
prev_y = event[MTB.EV_VALUE]
# Check if the finger at the target_slot is leaving.
elif self._is_finger_leaving(event):
target_slot_live = False
# If target_slot is alive, and both x and y have
# been assigned values, append the x and y to the list no matter
# whether x or y position is reported in the current packet.
# This also handles the initial condition that no previous x or y
# is reported yet.
if target_slot_live and prev_x and prev_y:
list_x.append(prev_x)
list_y.append(prev_y)
return (list_x, list_y)
def get_points_for_every_tracking_id(self):
"""Extract points in every tracking id.
This method is applicable when fingers are contacting and leaving
the touchpad continuously. The same slot number, e.g., slot 0 or
slot 1, may be used for multiple times.
"""
# The default slot is slot 0 if no slot number is assigned.
slot = 0
# points is a dictionary of lists, where each list holds all of
# the points in a tracking id.
points = {}
tracking_ids_all = []
tracking_ids_live = []
slot_to_tracking_id = {}
x = {}
y = {}
for packet in self.packets:
for event in packet:
if self._is_ABS_MT_SLOT(event):
slot = event[MTB.EV_VALUE]
# Find a new tracking ID
if self._is_new_contact(event):
tracking_id = event[MTB.EV_VALUE]
tracking_ids_all.append(tracking_id)
tracking_ids_live.append(tracking_id)
points[tracking_id] = {}
points[tracking_id][MTB.POINTS] = []
points[tracking_id][MTB.SLOT] = slot
slot_to_tracking_id[slot] = tracking_id
x[tracking_id] = None
y[tracking_id] = None
# A tracking ID is leaving.
elif self._is_finger_leaving(event):
leaving_tracking_id = slot_to_tracking_id[slot]
tracking_ids_live.remove(leaving_tracking_id)
del slot_to_tracking_id[slot]
# Update x value if available.
elif self._is_ABS_MT_POSITION_X(event):
x[slot_to_tracking_id[slot]] = event[MTB.EV_VALUE]
# Update y value if available.
elif self._is_ABS_MT_POSITION_Y(event):
y[slot_to_tracking_id[slot]] = event[MTB.EV_VALUE]
for tracking_id in tracking_ids_live:
if x[tracking_id] and y[tracking_id]:
curr_point = (x[tracking_id], y[tracking_id])
points[tracking_id][MTB.POINTS].append(curr_point)
return points
def _calc_farthest_distance(self, points):
"""Calculate the farthest distance of points."""
# TODO(josephsih): track state across different tracking IDs.
# The evdev driver only reports the delta of a slot state. It may only
# reports x positions if y positions are exactly the same as those
# generated by previous finger with the same slot ID. In this special
# case, the points would be an empty list. If we could track the
# states including x, y positions and z pressure, we could fill in
# those information into the points. The empty points cases may happen
# when performing drumroll gestures.
if not points:
return 0
return max([self._calc_distance(point, points[0]) for point in points])
def get_max_distance_of_all_tracking_ids(self):
"""Get the max moving distance of all tracking IDs."""
points = self.get_points_for_every_tracking_id()
max_distance = float('-infinity')
for tracking_id in sorted(points.keys()):
slot_points = points[tracking_id][MTB.POINTS]
distance = self._calc_farthest_distance(slot_points)
max_distance = max(max_distance, distance)
return max_distance
def get_x_y_multiple_slots(self, target_slots):
"""Extract points in multiple slots.
Only the packets with all specified slots are extracted.
This is useful to collect packets for pinch to zoom.
"""
# Initialize slot_exists dictionary to False
slot_exists = dict.fromkeys(target_slots, False)
# Set the initial slot number to 0 because evdev is a state machine,
# and may not present slot 0.
slot = 0
# Initialize the following dict to []
# Don't use "dict.fromkeys(target_slots, [])"
list_x = self._init_dict(target_slots, [])
list_y = self._init_dict(target_slots, [])
x = self._init_dict(target_slots, None)
y = self._init_dict(target_slots, None)
for packet in self.packets:
for event in packet:
if self._is_ABS_MT_SLOT(event):
slot = event[MTB.EV_VALUE]
if slot not in target_slots:
continue
if self._is_ABS_MT_TRACKING_ID(event):
if self._is_new_contact(event):
slot_exists[slot] = True
elif self._is_finger_leaving(event):
slot_exists[slot] = False
elif self._is_ABS_MT_POSITION_X(event):
x[slot] = event[MTB.EV_VALUE]
elif self._is_ABS_MT_POSITION_Y(event):
y[slot] = event[MTB.EV_VALUE]
# Note:
# - All slot_exists must be True to append x, y positions for the
# slots.
# - All x and y values for all slots must have been reported once.
# (This handles the initial condition that no previous x or y
# is reported yet.)
# - If either x or y positions are reported in the current packet,
# append x and y to the list of that slot.
# (This handles the condition that only x or y is reported.)
# - Even in the case that neither x nor y is reported in current
# packet, cmt driver constructs and passes hwstate to gestures.
if (all(slot_exists.values()) and all(x.values()) and
all(y.values())):
for s in target_slots:
list_x[s].append(x[s])
list_y[s].append(y[s])
return (list_x, list_y)
def get_points_multiple_slots(self, target_slots):
"""Get the points in multiple slots."""
list_x, list_y = self.get_x_y_multiple_slots(target_slots)
points_list = [zip(list_x[slot], list_y[slot]) for slot in target_slots]
points_dict = dict(zip(target_slots, points_list))
return points_dict
def get_relative_motion(self, target_slots):
"""Get the relative motion of the two target slots."""
# The slots in target_slots could be (0, 1), (1, 2) or other
# possibilities.
slot_a, slot_b = target_slots
points_dict = self.get_points_multiple_slots(target_slots)
points_slot_a = points_dict[slot_a]
points_slot_b = points_dict[slot_b]
# if only 0 or 1 point observed, the relative motion is 0.
if len(points_slot_a) <= 1 or len(points_slot_b) <= 1:
return 0
distance_begin = self._calc_distance(points_slot_a[0], points_slot_b[0])
distance_end = self._calc_distance(points_slot_a[-1], points_slot_b[-1])
relative_motion = distance_end - distance_begin
return relative_motion
def get_points(self, target_slot):
"""Get the points in the target slot."""
list_x, list_y = self.get_x_y(target_slot)
return zip(list_x, list_y)
def get_distances(self, target_slot):
"""Get the distances of neighbor points in the target slot."""
points = self.get_points(target_slot)
distances = []
for index in range(len(points) - 1):
distance = self._calc_distance(points[index], points[index + 1])
distances.append(distance)
return distances
def get_distances_with_first_point(self, target_slot):
"""Get distances of the points in the target_slot with first point."""
points = self.get_points(target_slot)
if not points:
return [0,]
point0 = points[0]
distances = [self._calc_distance(point, point0) for point in points]
return distances
def get_range(self):
"""Get the min and max values of (x, y) positions."""
min_x = min_y = float('infinity')
max_x = max_y = float('-infinity')
for packet in self.packets:
for event in packet:
if self._is_ABS_MT_POSITION_X(event):
x = event[MTB.EV_VALUE]
min_x = min(min_x, x)
max_x = max(max_x, x)
elif self._is_ABS_MT_POSITION_Y(event):
y = event[MTB.EV_VALUE]
min_y = min(min_y, y)
max_y = max(max_y, y)
return (min_x, max_x, min_y, max_y)
def get_total_motion(self, target_slot):
"""Get the total motion in the target slot."""
prev_x = prev_y = None
accu_x = accu_y = 0
slot = None
for packet in self.packets:
for event in packet:
if self._is_ABS_MT_SLOT(event):
slot = event[MTB.EV_VALUE]
elif self._is_ABS_MT_POSITION_X(event) and slot == target_slot:
x = event[MTB.EV_VALUE]
accu_x += self._calc_movement_for_axis(x, prev_x)
prev_x = x
elif self._is_ABS_MT_POSITION_Y(event) and slot == target_slot:
y = event[MTB.EV_VALUE]
accu_y += self._calc_movement_for_axis(y, prev_y)
prev_y = y
return (accu_x, accu_y)
def get_largest_distance(self, target_slot):
"""Get the largest distance of point to the first point."""
distances = self.get_distances_with_first_point(target_slot)
return max(distances)
def get_largest_gap_ratio(self, target_slot):
"""Get the largest gap ratio in the target slot.
gap_ratio_with_prev = curr_gap / prev_gap
gap_ratio_with_next = curr_gap / next_gap
This function tries to find the largest gap_ratio_with_prev
with the restriction that gap_ratio_with_next is larger than
RATIO_THRESHOLD_CURR_GAP_TO_NEXT_GAP.
The ratio threshold is used to prevent the gaps detected in a swipe.
Note that in a swipe, the gaps tends to become larger and larger.
"""
RATIO_THRESHOLD_CURR_GAP_TO_NEXT_GAP = 1.2
GAP_LOWER_BOUND = 10
gaps = self.get_distances(target_slot)
gap_ratios = []
largest_gap_ratio = float('-infinity')
for index in range(1, len(gaps) - 1):
prev_gap = max(gaps[index - 1], 1)
curr_gap = gaps[index]
next_gap = max(gaps[index + 1], 1)
gap_ratio_with_prev = curr_gap / prev_gap
gap_ratio_with_next = curr_gap / next_gap
if (curr_gap >= GAP_LOWER_BOUND and
gap_ratio_with_prev > largest_gap_ratio and
gap_ratio_with_next > RATIO_THRESHOLD_CURR_GAP_TO_NEXT_GAP):
largest_gap_ratio = gap_ratio_with_prev
return largest_gap_ratio
def _is_large(self, numbers, index):
"""Is the number at the index a large number compared to the moving
average of the previous LEN_MOVING_AVERAGE numbers? This is used to
determine if a distance is a level jump."""
if index < self.LEN_MOVING_AVERAGE + 1:
return False
moving_sum = sum(numbers[index - self.LEN_MOVING_AVERAGE : index])
moving_average = float(moving_sum) / self.LEN_MOVING_AVERAGE
cond1 = numbers[index] >= self.LEVEL_JUMP_RATIO * moving_average
cond2 = numbers[index] >= self.LEVEL_JUMP_MAXIUM_ALLOWED
return cond1 and cond2
def _accumulate_level_jumps(self, level_jumps):
"""Accumulate level jumps."""
if not level_jumps:
return []
is_positive = level_jumps[0] > 0
tmp_sum = 0
accumulated_level_jumps = []
for jump in level_jumps:
# If current sign is the same as previous ones, accumulate it.
if (jump > 0) is is_positive:
tmp_sum += jump
# If current jump has changed its sign, reset the tmp_sum to
# accumulate the level_jumps onwards.
else:
accumulated_level_jumps.append(tmp_sum)
tmp_sum = jump
is_positive = not is_positive
if tmp_sum != 0:
accumulated_level_jumps.append(tmp_sum)
return accumulated_level_jumps
def get_largest_accumulated_level_jumps(self, displacements):
"""Get the largest accumulated level jumps in displacements."""
largest_accumulated_level_jumps = 0
if len(displacements) < self.LEN_MOVING_AVERAGE + 1:
return largest_accumulated_level_jumps
# Truncate some elements at both ends of the list which are not stable.
displacements = displacements[self.LEN_DISCARD: -self.LEN_DISCARD]
distances = map(abs, displacements)
# E.g., displacements= [5, 6, 5, 6, 20, 3, 5, 4, 6, 21, 4, ...]
# level_jumps = [20, 21, ...]
level_jumps = [disp for i, disp in enumerate(displacements)
if self._is_large(distances, i)]
# E.g., level_jumps= [20, 21, -18, -25, 22, 18, -19]
# accumulated_level_jumps = [41, -43, 40, -19]
# largest_accumulated_level_jumps = 43
accumulated_level_jumps = self._accumulate_level_jumps(level_jumps)
if accumulated_level_jumps:
abs_accumulated_level_jumps = map(abs, accumulated_level_jumps)
largest_accumulated_level_jumps = max(abs_accumulated_level_jumps)
return largest_accumulated_level_jumps
def get_displacement(self, target_slot):
"""Get the displacement in the target slot."""
displace = [map(lambda p0, p1: p1 - p0, axis[:len(axis) - 1], axis[1:])
for axis in self.get_x_y(target_slot)]
displacement_dict = dict(zip((AXIS.X, AXIS.Y), displace))
return displacement_dict
def calc_displacement(self, numbers):
"""Calculate the displacements in a list of numbers."""
if len(numbers) <= 1:
return []
return [numbers[i + 1] - numbers[i] for i in range(len(numbers) - 1)]
def get_displacements_for_slots(self, min_slot):
"""Get the displacements for slots >= min_slot."""
points = self.get_points_for_every_tracking_id()
slots_to_delete = []
# Collect those tracking IDs with slots < min_slot and delete them.
# Python does not allow to modify a dictionary while iterating over it.
for tid in points:
slot = points[tid][MTB.SLOT]
tid_points = points[tid][MTB.POINTS]
if (slot < min_slot) or (tid_points == []):
slots_to_delete.append(tid)
for tid in slots_to_delete:
del points[tid]
# Calculate the displacements of the coordinates in the tracking IDs.
displacements = {}
for tid in points:
list_x, list_y = zip(*points[tid][MTB.POINTS])
displacements[tid] = {}
displacements[tid][MTB.SLOT] = points[tid][MTB.SLOT]
displacements[tid][AXIS.X] = self.calc_displacement(list_x)
displacements[tid][AXIS.Y] = self.calc_displacement(list_y)
return displacements
def _get_segments(self, src_list, segment_flag, ratio):
"""Get the segments based on segment_flag and ratio."""
end_size = int(round(len(src_list) * ratio))
if segment_flag == VAL.WHOLE:
return src_list
elif segment_flag == VAL.MIDDLE:
return src_list[end_size: -end_size]
elif segment_flag == VAL.BEGIN:
return src_list[: end_size]
elif segment_flag == VAL.END:
return src_list[-end_size:]
elif segment_flag == VAL.BOTH_ENDS:
bgn_segment = src_list[: end_size]
end_segment = src_list[-end_size:]
return bgn_segment + end_segment
else:
return None
def get_segments_x_and_y(self, ax, ay, segment_flag, ratio):
"""Get the segments for both x and y axes."""
segment_x = self._get_segments(ax, segment_flag, ratio)
segment_y = self._get_segments(ay, segment_flag, ratio)
return (segment_x, segment_y)
def get_reversed_motions(self, target_slot, direction,
segment_flag=VAL.WHOLE, ratio=None):
"""Get the total reversed motions in the specified direction
in the target slot.
Only the reversed motions specified by the segment_flag are taken.
The segment_flag could be
VAL.BEGIN: the begin segment
VAL.MIDDLE : the middle segment
VAL.END : the end segment
VAL.BOTH_ENDS : the segments at both ends
VAL.WHOLE: the whole line
The ratio represents the ratio of the BEGIN or END segment to the whole
segment.
If direction is in HORIZONTAL_DIRECTIONS, consider only x axis.
If direction is in VERTICAL_DIRECTIONS, consider only y axis.
If direction is in DIAGONAL_DIRECTIONS, consider both x and y axes.
Assume that the displacements in GV.LR (moving from left to right)
in the X axis are:
[10, 12, 8, -9, -2, 6, 8, 11, 12, 5, 2]
Its total reversed motion = (-9) + (-2) = -11
"""
# Define the axis moving directions dictionary
POSITIVE = 'positive'
NEGATIVE = 'negative'
AXIS_MOVING_DIRECTIONS = {
GV.LR: {AXIS.X: POSITIVE},
GV.RL: {AXIS.X: NEGATIVE},
GV.TB: {AXIS.Y: POSITIVE},
GV.BT: {AXIS.Y: NEGATIVE},
GV.CR: {AXIS.X: POSITIVE},
GV.CL: {AXIS.X: NEGATIVE},
GV.CB: {AXIS.Y: POSITIVE},
GV.CT: {AXIS.Y: NEGATIVE},
GV.BLTR: {AXIS.X: POSITIVE, AXIS.Y: NEGATIVE},
GV.BRTL: {AXIS.X: NEGATIVE, AXIS.Y: NEGATIVE},
GV.TRBL: {AXIS.X: NEGATIVE, AXIS.Y: POSITIVE},
GV.TLBR: {AXIS.X: POSITIVE, AXIS.Y: POSITIVE},
}
axis_moving_directions = AXIS_MOVING_DIRECTIONS.get(direction)
func_positive = lambda n: n > 0
func_negative = lambda n: n < 0
reversed_functions = {POSITIVE: func_negative, NEGATIVE: func_positive}
displacement_dict = self.get_displacement(target_slot)
reversed_motions = {}
for axis in AXIS.LIST:
axis_moving_direction = axis_moving_directions.get(axis)
if axis_moving_direction is None:
continue
displacement = displacement_dict[axis]
displacement_segment = self._get_segments(displacement,
segment_flag, ratio)
reversed_func = reversed_functions[axis_moving_direction]
reversed_motions[axis] = sum(filter(reversed_func,
displacement_segment))
return reversed_motions
def get_num_packets(self, target_slot):
"""Get the number of packets in the target slot."""
list_x, list_y = self.get_x_y(target_slot)
return len(list_x)
def get_report_rate(self):
"""Get the report rate of the packets in Hz."""
first_sync_event = self.packets[0][-1]
first_sync_time = first_sync_event.get(MTB.EV_TIME)
last_sync_event = self.packets[-1][-1]
last_sync_time = last_sync_event.get(MTB.EV_TIME)
duration = last_sync_time - first_sync_time
num_packets = len(self.packets) - 1
report_rate = float(num_packets) / duration
return report_rate
def _call_check_event_func(self, event, expected_value, check_event_result,
check_event_func):
"""Call all functions in check_event_func and return the results.
Note that since check_event_result is a dictionary, it is passed
by reference.
"""
for func in check_event_func:
if func(event):
event_value = event[MTB.EV_VALUE]
check_event_result[func] = (event_value == expected_value)
break
def _get_event_cycles(self, check_event_func):
"""A generic method to get the number of event cycles.
For a tap, its event cycle looks like:
(1) finger touching the pad:
BTN_TOOL_FINGER: 0-> 1
BTN_TOUCH: 0 -> 1
(2) finger leaving the pad:
BTN_TOOL_FINGER: 1-> 0
BTN_TOUCH: 1 -> 0
For a one-finger physical click, its event cycle looks like:
(1) finger clicking and pressing:
BTN_LEFT : 0-> 1
BTN_TOOL_FINGER: 0-> 1
BTN_TOUCH: 0 -> 1
(2) finger leaving:
BTN_LEFT : 1-> 0
BTN_TOOL_FINGER: 1-> 0
BTN_TOUCH: 1 -> 0
This method counts how many such cycles there are in the packets.
"""
# Initialize all check_event_result to False
# when all_events_observed is False and all check_event_result are True
# => all_events_observed is set to True
# when all_events_observed is True and all check_event_result are True
# => all_events_observed is set to False, and
# count is increased by 1
check_event_result = self._init_dict(check_event_func, False)
all_events_observed = False
count = 0
for packet in self.packets:
for event in packet:
if all_events_observed:
expected_value = 0
self._call_check_event_func(event, expected_value,
check_event_result,
check_event_func)
if all(check_event_result.values()):
all_events_observed = False
check_event_result = self._init_dict(check_event_func,
False)
count += 1
else:
expected_value = 1
self._call_check_event_func(event, expected_value,
check_event_result,
check_event_func)
if all(check_event_result.values()):
all_events_observed = True
check_event_result = self._init_dict(check_event_func,
False)
return count
def _get_event_cycles_for_num_fingers(self, num_fingers):
return self._get_event_cycles(self.check_event_func_list[num_fingers])
def verify_exact_number_fingers_touch(self, num_fingers):
"""Verify the exact number of fingers touching the pad.
Example: for a two-finger touch
2-finger touch cycles should be equal to 1
3/4/5-finger touch cycles should be equal to 0
Don't care about 1-finger touch cycles which is not deterministic.
"""
range_fingers = range(1, self.MAX_FINGERS)
flag_check = self._init_dict(range_fingers, True)
for f in range_fingers:
cycles = self._get_event_cycles_for_num_fingers(f)
if f == num_fingers:
flag_check[f] = cycles == 1
elif f > num_fingers:
flag_check[f] = cycles == 0
return all(flag_check)
def get_physical_clicks(self, num_fingers):
"""Get the count of physical clicks for the given number of fingers."""
flag_fingers_touch = self.verify_exact_number_fingers_touch(num_fingers)
click_cycles = self._get_event_cycles(self.check_event_func_click)
return click_cycles if flag_fingers_touch else 0
class MtbParser:
"""Touchpad MTB event Parser."""
def __init__(self):
self._get_event_re_patt()
def _get_event_re_patt(self):
"""Construct the regular expression search pattern of MTB events.
An ordinary event looks like
Event: time 133082.748019, type 3 (EV_ABS), code 0 (ABS_X), value 316
A SYN_REPORT event looks like
Event: time 10788.289613, -------------- SYN_REPORT ------------
"""
# Get the pattern of an ordinary event
event_patt_time = 'Event:\s*time\s*(\d+\.\d+)'
event_patt_type = 'type\s*(\d+)\s*\(\w+\)'
event_patt_code = 'code\s*(\d+)\s*\(\w+\)'
event_patt_value = 'value\s*(-?\d+)'
event_sep = ',\s*'
event_patt = event_sep.join([event_patt_time,
event_patt_type,
event_patt_code,
event_patt_value])
self.event_re_patt = re.compile(event_patt, re.I)
# Get the pattern of the SYN_REPORT event
event_patt_type_SYN_REPORT = '-+\s*SYN_REPORT\s-+'
event_patt_SYN_REPORT = event_sep.join([event_patt_time,
event_patt_type_SYN_REPORT])
self.event_re_patt_SYN_REPORT = re.compile(event_patt_SYN_REPORT, re.I)
def _get_event_dict_ordinary(self, line):
"""Construct the event dictionary for an ordinary event."""
result = self.event_re_patt.search(line)
ev_dict = {}
if result is not None:
ev_dict[MTB.EV_TIME] = float(result.group(1))
ev_dict[MTB.EV_TYPE] = int(result.group(2))
ev_dict[MTB.EV_CODE] = int(result.group(3))
ev_dict[MTB.EV_VALUE] = int(result.group(4))
return ev_dict
def _get_event_dict_SYN_REPORT(self, line):
"""Construct the event dictionary for a SYN_REPORT event."""
result = self.event_re_patt_SYN_REPORT.search(line)
ev_dict = {}
if result is not None:
ev_dict[MTB.EV_TIME] = float(result.group(1))
ev_dict[MTB.SYN_REPORT] = True
return ev_dict
def _get_event_dict(self, line):
"""Construct the event dictionary."""
EVENT_FUNC_LIST = [self._get_event_dict_ordinary,
self._get_event_dict_SYN_REPORT]
for get_event_func in EVENT_FUNC_LIST:
ev_dict = get_event_func(line)
if ev_dict:
return ev_dict
return False
def _is_SYN_REPORT(self, ev_dict):
"""Determine if this event is SYN_REPORT."""
return ev_dict.get(MTB.SYN_REPORT, False)
def parse(self, raw_event):
"""Parse the raw event string into a list of event dictionary."""
ev_list = []
packets = []
start_flag = False
for line in raw_event:
ev_dict = self._get_event_dict(line)
if ev_dict:
start_flag = True
ev_list.append(ev_dict)
if self._is_SYN_REPORT(ev_dict):
packets.append(ev_list)
ev_list = []
elif start_flag:
logging.warn(' Warn: format problem in event:\n %s' % line)
return packets
def parse_file(self, file_name):
"""Parse raw device events in the given file name."""
packets = None
if os.path.isfile(file_name):
with open(file_name) as f:
packets = self.parse(f)
return packets
if __name__ == '__main__':
# Read a device file, and convert it to pretty packet format.
if len(sys.argv) != 2 or not os.path.exists(sys.argv[1]):
print 'Usage: %s device_file' % sys.argv[0]
exit(1)
with open(sys.argv[1]) as event_file:
packets = MtbParser().parse(event_file)
for packet in packets:
print make_pretty_packet(packet)