Home | History | Annotate | Download | only in firmware_TouchMTB
      1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Validators to verify if events conform to specified criteria."""
      6 
      7 
      8 '''
      9 How to add a new validator/gesture:
     10 (1) Implement a new validator class inheriting BaseValidator,
     11 (2) add proper method in mtb.Mtb class,
     12 (3) add the new validator in test_conf, and
     13         'from validators import the_new_validator'
     14     in alphabetical order, and
     15 (4) add the validator in relevant gestures; add a new gesture if necessary.
     16 
     17 The validator template is as follows:
     18 
     19 class XxxValidator(BaseValidator):
     20     """Validator to check ...
     21 
     22     Example:
     23         To check ...
     24           XxxValidator('<= 0.05, ~ +0.05', fingers=2)
     25     """
     26 
     27     def __init__(self, criteria_str, mf=None, fingers=1):
     28         name = self.__class__.__name__
     29         super(X..Validator, self).__init__(criteria_str, mf, name)
     30         self.fingers = fingers
     31 
     32     def check(self, packets, variation=None):
     33         """Check ..."""
     34         self.init_check(packets)
     35         xxx = self.packets.xxx()
     36         self.print_msg(...)
     37         return (self.fc.mf.grade(...), self.msg_list)
     38 
     39 
     40 Note that it is also possible to instantiate a validator as
     41           XxxValidator('<= 0.05, ~ +0.05', slot=0)
     42 
     43     Difference between fingers and slot:
     44       . When specifying 'fingers', e.g., fingers=2, the purpose is to pass
     45         the information about how many fingers there are in the gesture. In
     46         this case, the events in a specific slot is usually not important.
     47         An example is to check how many fingers there are when making a click:
     48             PhysicalClickValidator('== 0', fingers=2)
     49       . When specifying 'slot', e.g., slot=0, the purpose is pass the slot
     50         number to the validator to examine detailed events in that slot.
     51         An example of such usage:
     52             LinearityValidator('<= 0.03, ~ +0.07', slot=0)
     53 '''
     54 
     55 
     56 import copy
     57 import numpy as np
     58 import os
     59 import re
     60 import sys
     61 
     62 import firmware_log
     63 import fuzzy
     64 import mtb
     65 
     66 from collections import namedtuple, OrderedDict
     67 from inspect import isfunction
     68 
     69 from common_util import print_and_exit, simple_system_output
     70 from firmware_constants import AXIS, GV, MTB, UNIT, VAL
     71 from geometry.elements import Point
     72 
     73 from linux_input import EV_ABS, EV_STRINGS
     74 
     75 
     76 # Define the ratio of points taken at both ends of a line for edge tests.
     77 END_PERCENTAGE = 0.1
     78 
     79 # Define other constants below.
     80 VALIDATOR = 'Validator'
     81 
     82 
     83 def validate(packets, gesture, variation):
     84     """Validate a single gesture."""
     85     def _validate(validator, msg_list, score_list, vlogs):
     86         vlog = validator.check(packets, variation)
     87         if vlog is None:
     88             return False
     89         vlogs.append(copy.deepcopy(vlog))
     90         score = vlog.score
     91         if score is not None:
     92             score_list.append(score)
     93             # save the validator messages
     94             msg_validator_name = '%s' % vlog.name
     95             msg_criteria = '    criteria_str: %s' % vlog.criteria
     96             msg_score = 'score: %f' % score
     97             msg_list.append(os.linesep)
     98             msg_list.append(msg_validator_name)
     99             msg_list += vlog.details
    100             msg_list.append(msg_criteria)
    101             msg_list.append(msg_score)
    102         return score == 1.0
    103 
    104     if packets is None:
    105         return (None, None)
    106 
    107     msg_list = []
    108     score_list = []
    109     vlogs = []
    110     prerequisite_flag = True
    111 
    112     # If MtbSanityValidator does not pass, there exist some
    113     # critical problems which will be reported in its metrics.
    114     # No need to check the other validators.
    115     mtb_sanity_result = _validate(gesture.mtb_sanity_validator,
    116                                   msg_list, score_list, vlogs)
    117     if mtb_sanity_result:
    118         for validator in gesture.validators:
    119             _validate(validator, msg_list, score_list, vlogs)
    120 
    121     return (score_list, msg_list, vlogs)
    122 
    123 
    124 def get_parent_validators(validator_name):
    125     """Get the parents of a given validator."""
    126     validator = getattr(sys.modules[__name__], validator_name, None)
    127     return validator.__bases__ if validator else []
    128 
    129 
    130 def get_short_name(validator_name):
    131     """Get the short name of the validator.
    132 
    133     E.g, the short name of LinearityValidator is Linearity.
    134     """
    135     return validator_name.split(VALIDATOR)[0]
    136 
    137 
    138 def get_validator_name(short_name):
    139     """Convert the short_name to its corresponding validator name.
    140 
    141     E.g, the validator_name of Linearity is LinearityValidator.
    142     """
    143     return short_name + VALIDATOR
    144 
    145 
    146 def get_base_name_and_segment(validator_name):
    147     """Get the base name and segment of a validator.
    148 
    149     Examples:
    150         Ex 1: Linearity(BothEnds)Validator
    151             return ('Linearity', 'BothEnds')
    152         Ex 2: NoGapValidator
    153             return ('NoGap', None)
    154     """
    155     if '(' in validator_name:
    156         result = re.search('(.*)\((.*)\)%s' % VALIDATOR, validator_name)
    157         return (result.group(1), result.group(2))
    158     else:
    159         return (get_short_name(validator_name), None)
    160 
    161 
    162 def get_derived_name(validator_name, segment):
    163     """Get the derived name based on segment value.
    164 
    165     Example:
    166       validator_name: LinearityValidator
    167       segment: Middle
    168       derived_name: Linearity(Middle)Validator
    169     """
    170     short_name = get_short_name(validator_name)
    171     derived_name = '%s(%s)%s' % (short_name, segment, VALIDATOR)
    172     return derived_name
    173 
    174 
    175 def init_base_validator(device):
    176     """Initialize the device for all the Validators to use"""
    177     BaseValidator._device = device
    178 
    179 
    180 class BaseValidator(object):
    181     """Base class of validators."""
    182     aggregator = 'fuzzy.average'
    183     _device = None
    184 
    185     def __init__(self, criteria, mf=None, device=None, name=None):
    186         self.criteria_str = criteria() if isfunction(criteria) else criteria
    187         self.fc = fuzzy.FuzzyCriteria(self.criteria_str, mf=mf)
    188         self.device = device if device else BaseValidator._device
    189         self.packets = None
    190         self.vlog = firmware_log.ValidatorLog()
    191         self.vlog.name = name
    192         self.vlog.criteria = self.criteria_str
    193         self.mnprops = firmware_log.MetricNameProps()
    194 
    195     def init_check(self, packets=None):
    196         """Initialization before check() is called."""
    197         self.packets = mtb.Mtb(device=self.device, packets=packets)
    198         self.vlog.reset()
    199 
    200     def _is_direction_in_variation(self, variation, directions):
    201         """Is any element of directions list found in variation?"""
    202         for direction in directions:
    203             if direction in variation:
    204                 return True
    205         return False
    206 
    207     def is_horizontal(self, variation):
    208         """Is the direction horizontal?"""
    209         return self._is_direction_in_variation(variation,
    210                                                GV.HORIZONTAL_DIRECTIONS)
    211 
    212     def is_vertical(self, variation):
    213         """Is the direction vertical?"""
    214         return self._is_direction_in_variation(variation,
    215                                                GV.VERTICAL_DIRECTIONS)
    216 
    217     def is_diagonal(self, variation):
    218         """Is the direction diagonal?"""
    219         return self._is_direction_in_variation(variation,
    220                                                GV.DIAGONAL_DIRECTIONS)
    221 
    222     def get_direction(self, variation):
    223         """Get the direction."""
    224         # TODO(josephsih): raise an exception if a proper direction is not found
    225         if self.is_horizontal(variation):
    226             return GV.HORIZONTAL
    227         elif self.is_vertical(variation):
    228             return GV.VERTICAL
    229         elif self.is_diagonal(variation):
    230             return GV.DIAGONAL
    231 
    232     def get_direction_in_variation(self, variation):
    233         """Get the direction string from the variation list."""
    234         if isinstance(variation, tuple):
    235             for var in variation:
    236                 if var in GV.GESTURE_DIRECTIONS:
    237                     return var
    238         elif variation in GV.GESTURE_DIRECTIONS:
    239             return variation
    240         return None
    241 
    242     def log_details(self, msg):
    243         """Collect the detailed messages to be printed within this module."""
    244         prefix_space = ' ' * 4
    245         formatted_msg = '%s%s' % (prefix_space, msg)
    246         self.vlog.insert_details(formatted_msg)
    247 
    248     def get_threshold(self, criteria_str, op):
    249         """Search the criteria_str using regular expressions and get
    250         the threshold value.
    251 
    252         @param criteria_str: the criteria string to search
    253         """
    254         # In the search pattern, '.*?' is non-greedy, which will match as
    255         # few characters as possible.
    256         #   E.g., op = '>'
    257         #         criteria_str = '>= 200, ~ -100'
    258         #         pattern below would be '>.*?\s*(\d+)'
    259         #         result.group(1) below would be '200'
    260         pattern = '{}.*?\s*(\d+)'.format(op)
    261         result = re.search(pattern, criteria_str)
    262         return int(result.group(1)) if result else None
    263 
    264     def _get_axes_by_finger(self, finger):
    265         """Get list_x, list_y, and list_t for the specified finger.
    266 
    267         @param finger: the finger contact
    268         """
    269         points = self.packets.get_ordered_finger_path(self.finger, 'point')
    270         list_x = [p.x for p in points]
    271         list_y = [p.y for p in points]
    272         list_t = self.packets.get_ordered_finger_path(self.finger, 'syn_time')
    273         return (list_x, list_y, list_t)
    274 
    275 
    276 class DragLatencyValidator(BaseValidator):
    277     """ Validator to make check the touchpad's latency
    278 
    279     This is used in conjunction with a Quickstep latency measuring device. To
    280     compute the latencies, this validator imports the Quickstep software in the
    281     touchbot project and pulls the data from the Quickstep device and the
    282     packets collected by mtplot.  If there is no device plugged in, the
    283     validator will fail with an obviously erroneous value
    284     """
    285     def __init__(self, criteria_str, mf=None):
    286         name = self.__class__.__name__
    287         super(DragLatencyValidator, self).__init__(criteria_str, mf=mf,
    288                                                    name=name)
    289 
    290     def check(self, packets, variation=None):
    291         from quickstep import latency_measurement
    292         self.init_check(packets)
    293 
    294         # Reformat the touch events for latency measurement
    295         points = self.packets.get_ordered_finger_path(0, 'point')
    296         times = self.packets.get_ordered_finger_path(0, 'syn_time')
    297         finger_positions = [latency_measurement.FingerPosition(t, pt.x, pt.y)
    298                             for t, pt in zip(times, points)]
    299 
    300         # Find the sysfs entries for the Quickstep device and parse the logs
    301         laser_files = simple_system_output('find / -name laser')
    302         laser_crossings = []
    303         for f in laser_files.splitlines():
    304             laser_crossings = latency_measurement.get_laser_crossings(f)
    305             if laser_crossings:
    306                 break
    307 
    308         # Crunch the numbers using the Quickstep latency measurement module
    309         latencies = latency_measurement.measure_latencies(finger_positions,
    310                                                           laser_crossings)
    311         # If there is no Quickstep plugged in, there will be no readings, so
    312         # to keep the test suite from crashing insert a dummy value
    313         if not latencies:
    314             latencies = [9.999]
    315 
    316         avg = 1000.0 * sum(latencies) / len(latencies)
    317         self.vlog.score = self.fc.mf.grade(avg)
    318         self.log_details('Average drag latency (ms): %f' % avg)
    319         self.log_details('Max drag latency (ms): %f' % (1000 * max(latencies)))
    320         self.log_details('Min drag latency (ms): %f' % (1000 * min(latencies)))
    321         self.vlog.metrics = [firmware_log.Metric(self.mnprops.AVG_LATENCY, avg)]
    322         return self.vlog
    323 
    324 
    325 class DiscardInitialSecondsValidator(BaseValidator):
    326     """ Takes in another validator and validates
    327     all the data after the intial number of seconds specified
    328     """
    329     def __init__(self, validator, mf=None, device=None,
    330                  initial_seconds_to_discard=1):
    331         self.validator = validator
    332         self.initial_seconds_to_discard = initial_seconds_to_discard
    333         super(DiscardInitialSecondsValidator, self).__init__(
    334             validator.criteria_str, mf, device, validator.__class__.__name__)
    335 
    336     def _discard_initial_seconds(self, packets, seconds_to_discard):
    337         # Get the list of syn_time of all packets
    338         list_syn_time = self.packets.get_list_syn_time(None)
    339 
    340         # Get the time to cut the list at. list_syn_time is in seconds.
    341         cutoff_time = list_syn_time[0] + self.initial_seconds_to_discard
    342 
    343         # Find the index at which the list of timestamps is greater than
    344         # the cutoff time.
    345         cutoff_index = None
    346         for index, time in enumerate(list_syn_time):
    347             if time >= cutoff_time:
    348                 cutoff_index = index
    349                 break
    350 
    351         if not cutoff_index:
    352             return None
    353 
    354         # Create a packet representing the final state of the touchpad
    355         # at the end of the discarded seconds
    356         final_state_packet = mtb.create_final_state_packet(
    357             packets[:cutoff_index])
    358         if final_state_packet:
    359             return [final_state_packet] + packets[cutoff_index:]
    360         else:
    361             # If final_state_packet is [] which means all fingers have left
    362             # at this time instant, just exclude this empty packet.
    363             return packets[cutoff_index:]
    364 
    365     def check(self, packets, variation=None):
    366         self.init_check(packets)
    367         packets = self._discard_initial_seconds(packets,
    368                                                 self.initial_seconds_to_discard)
    369         if packets:
    370             return self.validator.check(packets, variation)
    371         else:
    372             print ('ERROR: The length of the test is '
    373                    'less than %d second(s) long.' %
    374                    self.initial_seconds_to_discard)
    375 
    376 
    377 class LinearityValidator1(BaseValidator):
    378     """Validator to verify linearity.
    379 
    380     Example:
    381         To check the linearity of the line drawn in finger 1:
    382           LinearityValidator1('<= 0.03, ~ +0.07', finger=1)
    383     """
    384     # Define the partial group size for calculating Mean Squared Error
    385     MSE_PARTIAL_GROUP_SIZE = 1
    386 
    387     def __init__(self, criteria_str, mf=None, device=None, finger=0,
    388                  segments=VAL.WHOLE):
    389         self._segments = segments
    390         self.finger = finger
    391         name = get_derived_name(self.__class__.__name__, segments)
    392         super(LinearityValidator1, self).__init__(criteria_str, mf, device,
    393                                                   name)
    394 
    395     def _simple_linear_regression(self, ax, ay):
    396         """Calculate the simple linear regression and returns the
    397            sum of squared residuals.
    398 
    399         It calculates the simple linear regression line for the points
    400         in the middle segment of the line. This exclude the points at
    401         both ends of the line which sometimes have wobbles. Then it
    402         calculates the fitting errors of the points at the specified segments
    403         against the computed simple linear regression line.
    404         """
    405         # Compute the simple linear regression line for the middle segment
    406         # whose purpose is to avoid wobbles on both ends of the line.
    407         mid_segment = self.packets.get_segments_x_and_y(ax, ay, VAL.MIDDLE,
    408                                                         END_PERCENTAGE)
    409         if not self._calc_simple_linear_regression_line(*mid_segment):
    410             return 0
    411 
    412         # Compute the fitting errors of the specified segments.
    413         if self._segments == VAL.BOTH_ENDS:
    414             bgn_segment = self.packets.get_segments_x_and_y(ax, ay, VAL.BEGIN,
    415                                                             END_PERCENTAGE)
    416             end_segment = self.packets.get_segments_x_and_y(ax, ay, VAL.END,
    417                                                             END_PERCENTAGE)
    418             bgn_error = self._calc_simple_linear_regression_error(*bgn_segment)
    419             end_error = self._calc_simple_linear_regression_error(*end_segment)
    420             return max(bgn_error, end_error)
    421         else:
    422             target_segment = self.packets.get_segments_x_and_y(ax, ay,
    423                     self._segments, END_PERCENTAGE)
    424             return self._calc_simple_linear_regression_error(*target_segment)
    425 
    426     def _calc_simple_linear_regression_line(self, ax, ay):
    427         """Calculate the simple linear regression line.
    428 
    429            ax: array x
    430            ay: array y
    431            This method tries to find alpha and beta in the formula
    432                 ay = alpha + beta . ax
    433            such that it has the least sum of squared residuals.
    434 
    435            Reference:
    436            - Simple linear regression:
    437              http://en.wikipedia.org/wiki/Simple_linear_regression
    438            - Average absolute deviation (or mean absolute deviation) :
    439              http://en.wikipedia.org/wiki/Average_absolute_deviation
    440         """
    441         # Convert the int list to the float array
    442         self._ax = 1.0 * np.array(ax)
    443         self._ay = 1.0 * np.array(ay)
    444 
    445         # If there are less than 2 data points, it is not a line at all.
    446         asize = self._ax.size
    447         if asize <= 2:
    448             return False
    449 
    450         Sx = self._ax.sum()
    451         Sy = self._ay.sum()
    452         Sxx = np.square(self._ax).sum()
    453         Sxy = np.dot(self._ax, self._ay)
    454         Syy = np.square(self._ay).sum()
    455         Sx2 = Sx * Sx
    456         Sy2 = Sy * Sy
    457 
    458         # compute Mean of x and y
    459         Mx = self._ax.mean()
    460         My = self._ay.mean()
    461 
    462         # Compute beta and alpha of the linear regression
    463         self._beta = 1.0 * (asize * Sxy - Sx * Sy) / (asize * Sxx - Sx2)
    464         self._alpha = My - self._beta * Mx
    465         return True
    466 
    467     def _calc_simple_linear_regression_error(self, ax, ay):
    468         """Calculate the fitting error based on the simple linear regression
    469         line characterized by the equation parameters alpha and beta.
    470         """
    471         # Convert the int list to the float array
    472         ax = 1.0 * np.array(ax)
    473         ay = 1.0 * np.array(ay)
    474 
    475         asize = ax.size
    476         partial = min(asize, max(1, self.MSE_PARTIAL_GROUP_SIZE))
    477 
    478         # spmse: squared root of partial mean squared error
    479         spmse = np.square(ay - self._alpha - self._beta * ax)
    480         spmse.sort()
    481         spmse = spmse[asize - partial : asize]
    482         spmse = np.sqrt(np.average(spmse))
    483         return spmse
    484 
    485     def check(self, packets, variation=None):
    486         """Check if the packets conforms to specified criteria."""
    487         self.init_check(packets)
    488         resolution_x, resolution_y = self.device.get_resolutions()
    489         (list_x, list_y) = self.packets.get_x_y(self.finger)
    490         # Compute average distance (fitting error) in pixels, and
    491         # average deviation on touch device in mm.
    492         if self.is_vertical(variation):
    493             ave_distance = self._simple_linear_regression(list_y, list_x)
    494             deviation = ave_distance / resolution_x
    495         else:
    496             ave_distance = self._simple_linear_regression(list_x, list_y)
    497             deviation = ave_distance / resolution_y
    498 
    499         self.log_details('ave fitting error: %.2f px' % ave_distance)
    500         msg_device = 'deviation finger%d: %.2f mm'
    501         self.log_details(msg_device % (self.finger, deviation))
    502         self.vlog.score = self.fc.mf.grade(deviation)
    503         return self.vlog
    504 
    505 
    506 class LinearityValidator(BaseValidator):
    507     """A validator to verify linearity based on x-t and y-t
    508 
    509     Example:
    510         To check the linearity of the line drawn in finger 1:
    511           LinearityValidator('<= 0.03, ~ +0.07', finger=1)
    512         Note: the finger number begins from 0
    513     """
    514     # Define the partial group size for calculating Mean Squared Error
    515     MSE_PARTIAL_GROUP_SIZE = 1
    516 
    517     def __init__(self, criteria_str, mf=None, device=None, finger=0,
    518                  segments=VAL.WHOLE):
    519         self._segments = segments
    520         self.finger = finger
    521         name = get_derived_name(self.__class__.__name__, segments)
    522         super(LinearityValidator, self).__init__(criteria_str, mf, device,
    523                                                   name)
    524 
    525     def _calc_residuals(self, line, list_t, list_y):
    526         """Calculate the residuals of the points in list_t, list_y against
    527         the line.
    528 
    529         @param line: the regression line of list_t and list_y
    530         @param list_t: a list of time instants
    531         @param list_y: a list of x/y coordinates
    532 
    533         This method returns the list of residuals, where
    534             residual[i] = line[t_i] - y_i
    535         where t_i is an element in list_t and
    536               y_i is a corresponding element in list_y.
    537 
    538         We calculate the vertical distance (y distance) here because the
    539         horizontal axis, list_t, always represent the time instants, and the
    540         vertical axis, list_y, could be either the coordinates in x or y axis.
    541         """
    542         return [float(line(t) - y) for t, y in zip(list_t, list_y)]
    543 
    544     def _do_simple_linear_regression(self, list_t, list_y):
    545         """Calculate the simple linear regression line and returns the
    546         sum of squared residuals.
    547 
    548         @param list_t: the list of time instants
    549         @param list_y: the list of x or y coordinates of touch contacts
    550 
    551         It calculates the residuals (fitting errors) of the points at the
    552         specified segments against the computed simple linear regression line.
    553 
    554         Reference:
    555         - Simple linear regression:
    556           http://en.wikipedia.org/wiki/Simple_linear_regression
    557         - numpy.polyfit(): used to calculate the simple linear regression line.
    558           http://docs.scipy.org/doc/numpy/reference/generated/numpy.polyfit.html
    559         """
    560         # At least 2 points to determine a line.
    561         if len(list_t) < 2 or len(list_y) < 2:
    562             return []
    563 
    564         mid_segment_t, mid_segment_y = self.packets.get_segments(
    565                 list_t, list_y, VAL.MIDDLE, END_PERCENTAGE)
    566 
    567         # Check to make sure there are enough samples to continue
    568         if len(mid_segment_t) <= 2 or len(mid_segment_y) <= 2:
    569             return []
    570 
    571         # Calculate the simple linear regression line.
    572         degree = 1
    573         regress_line = np.poly1d(np.polyfit(mid_segment_t, mid_segment_y,
    574                                             degree))
    575 
    576         # Compute the fitting errors of the specified segments.
    577         if self._segments == VAL.BOTH_ENDS:
    578             begin_segments = self.packets.get_segments(
    579                     list_t, list_y, VAL.BEGIN, END_PERCENTAGE)
    580             end_segments = self.packets.get_segments(
    581                     list_t, list_y, VAL.END, END_PERCENTAGE)
    582             begin_error = self._calc_residuals(regress_line, *begin_segments)
    583             end_error = self._calc_residuals(regress_line, *end_segments)
    584             return begin_error + end_error
    585         else:
    586             target_segments = self.packets.get_segments(
    587                     list_t, list_y, self._segments, END_PERCENTAGE)
    588             return self._calc_residuals(regress_line, *target_segments)
    589 
    590     def _calc_errors_single_axis(self, list_t, list_y):
    591         """Calculate various errors for axis-time.
    592 
    593         @param list_t: the list of time instants
    594         @param list_y: the list of x or y coordinates of touch contacts
    595         """
    596         # It is fine if axis-time is a horizontal line.
    597         errors_px = self._do_simple_linear_regression(list_t, list_y)
    598         if not errors_px:
    599             return (0, 0)
    600 
    601         # Calculate the max errors
    602         max_err_px = max(map(abs, errors_px))
    603 
    604         # Calculate the root mean square errors
    605         e2 = [e * e for e in errors_px]
    606         rms_err_px = (float(sum(e2)) / len(e2)) ** 0.5
    607 
    608         return (max_err_px, rms_err_px)
    609 
    610     def _calc_errors_all_axes(self, list_t, list_x, list_y):
    611         """Calculate various errors for all axes."""
    612         # Calculate max error and average squared error
    613         (max_err_x_px, rms_err_x_px) = self._calc_errors_single_axis(
    614                 list_t, list_x)
    615         (max_err_y_px, rms_err_y_px) = self._calc_errors_single_axis(
    616                 list_t, list_y)
    617 
    618         # Convert the unit from pixels to mms
    619         self.max_err_x_mm, self.max_err_y_mm = self.device.pixel_to_mm(
    620                 (max_err_x_px, max_err_y_px))
    621         self.rms_err_x_mm, self.rms_err_y_mm = self.device.pixel_to_mm(
    622                 (rms_err_x_px, rms_err_y_px))
    623 
    624     def _log_details_and_metrics(self, variation):
    625         """Log the details and calculate the metrics.
    626 
    627         @param variation: the gesture variation
    628         """
    629         list_x, list_y, list_t = self._get_axes_by_finger(self.finger)
    630         X, Y = AXIS.LIST
    631         # For horizontal lines, only consider x axis
    632         if self.is_horizontal(variation):
    633             self.list_coords = {X: list_x}
    634         # For vertical lines, only consider y axis
    635         elif self.is_vertical(variation):
    636             self.list_coords = {Y: list_y}
    637         # For diagonal lines, consider both x and y axes
    638         elif self.is_diagonal(variation):
    639             self.list_coords = {X: list_x, Y: list_y}
    640 
    641         self.max_err_mm = {}
    642         self.rms_err_mm = {}
    643         self.vlog.metrics = []
    644         mnprops = self.mnprops
    645         pixel_to_mm = self.device.pixel_to_mm_single_axis_by_name
    646         for axis, list_c in self.list_coords.items():
    647             max_err_px, rms_err_px = self._calc_errors_single_axis(
    648                     list_t, list_c)
    649             max_err_mm = pixel_to_mm(max_err_px, axis)
    650             rms_err_mm = pixel_to_mm(rms_err_px, axis)
    651             self.log_details('max_err[%s]: %.2f mm' % (axis, max_err_mm))
    652             self.log_details('rms_err[%s]: %.2f mm' % (axis, rms_err_mm))
    653             self.vlog.metrics.extend([
    654                 firmware_log.Metric(mnprops.MAX_ERR.format(axis), max_err_mm),
    655                 firmware_log.Metric(mnprops.RMS_ERR.format(axis), rms_err_mm),
    656             ])
    657             self.max_err_mm[axis] = max_err_mm
    658             self.rms_err_mm[axis] = rms_err_mm
    659 
    660     def check(self, packets, variation=None):
    661         """Check if the packets conforms to specified criteria."""
    662         self.init_check(packets)
    663         self._log_details_and_metrics(variation)
    664         # Calculate the score based on the max error
    665         max_err = max(self.max_err_mm.values())
    666         self.vlog.score = self.fc.mf.grade(max_err)
    667         return self.vlog
    668 
    669 
    670 class LinearityNormalFingerValidator(LinearityValidator):
    671     """A dummy LinearityValidator to verify linearity for gestures performed
    672     with normal fingers.
    673     """
    674     pass
    675 
    676 
    677 class LinearityFatFingerValidator(LinearityValidator):
    678     """A dummy LinearityValidator to verify linearity for gestures performed
    679     with fat fingers or thumb edge.
    680     """
    681     pass
    682 
    683 
    684 class RangeValidator(BaseValidator):
    685     """Validator to check the observed (x, y) positions should be within
    686     the range of reported min/max values.
    687 
    688     Example:
    689         To check the range of observed edge-to-edge positions:
    690           RangeValidator('<= 0.05, ~ +0.05')
    691     """
    692 
    693     def __init__(self, criteria_str, mf=None, device=None):
    694         self.name = self.__class__.__name__
    695         super(RangeValidator, self).__init__(criteria_str, mf, device,
    696                                              self.name)
    697 
    698     def check(self, packets, variation=None):
    699         """Check the left/right or top/bottom range based on the direction."""
    700         self.init_check(packets)
    701         valid_directions = [GV.CL, GV.CR, GV.CT, GV.CB]
    702         Range = namedtuple('Range', valid_directions)
    703         actual_range = Range(*self.packets.get_range())
    704         spec_range = Range(self.device.axis_x.min, self.device.axis_x.max,
    705                            self.device.axis_y.min, self.device.axis_y.max)
    706 
    707         direction = self.get_direction_in_variation(variation)
    708         if direction in valid_directions:
    709             actual_edge = getattr(actual_range, direction)
    710             spec_edge = getattr(spec_range, direction)
    711             short_of_range_px = abs(actual_edge - spec_edge)
    712         else:
    713             err_msg = 'Error: the gesture variation %s is not allowed in %s.'
    714             print_and_exit(err_msg % (variation, self.name))
    715 
    716         axis_spec = (self.device.axis_x if self.is_horizontal(variation)
    717                                         else self.device.axis_y)
    718         deviation_ratio = (float(short_of_range_px) /
    719                            (axis_spec.max - axis_spec.min))
    720         # Convert the direction to edge name.
    721         #   E.g., direction: center_to_left
    722         #         edge name: left
    723         edge_name = direction.split('_')[-1]
    724         metric_name = self.mnprops.RANGE.format(edge_name)
    725         short_of_range_mm = self.device.pixel_to_mm_single_axis(
    726                 short_of_range_px, axis_spec)
    727         self.vlog.metrics = [
    728             firmware_log.Metric(metric_name, short_of_range_mm)
    729         ]
    730         self.log_details('actual: px %s' % str(actual_edge))
    731         self.log_details('spec: px %s' % str(spec_edge))
    732         self.log_details('short of range: %d px == %f mm' %
    733                          (short_of_range_px, short_of_range_mm))
    734         self.vlog.score = self.fc.mf.grade(deviation_ratio)
    735         return self.vlog
    736 
    737 
    738 class CountTrackingIDValidator(BaseValidator):
    739     """Validator to check the count of tracking IDs.
    740 
    741     Example:
    742         To verify if there is exactly one finger observed:
    743           CountTrackingIDValidator('== 1')
    744     """
    745 
    746     def __init__(self, criteria_str, mf=None, device=None):
    747         name = self.__class__.__name__
    748         super(CountTrackingIDValidator, self).__init__(criteria_str, mf,
    749                                                        device, name)
    750 
    751     def check(self, packets, variation=None):
    752         """Check the number of tracking IDs observed."""
    753         self.init_check(packets)
    754 
    755         # Get the actual count of tracking id and log the details.
    756         actual_count_tid = self.packets.get_number_contacts()
    757         self.log_details('count of trackid IDs: %d' % actual_count_tid)
    758 
    759         # Only keep metrics with the criteria '== N'.
    760         # Ignore those with '>= N' which are used to assert that users have
    761         # performed correct gestures. As an example, we require that users
    762         # tap more than a certain number of times in the drumroll test.
    763         if '==' in self.criteria_str:
    764             expected_count_tid = int(self.criteria_str.split('==')[-1].strip())
    765             # E.g., expected_count_tid = 2
    766             #       actual_count_tid could be either smaller (e.g., 1) or
    767             #       larger (e.g., 3).
    768             metric_value = (actual_count_tid, expected_count_tid)
    769             metric_name = self.mnprops.TID
    770             self.vlog.metrics = [firmware_log.Metric(metric_name, metric_value)]
    771 
    772         self.vlog.score = self.fc.mf.grade(actual_count_tid)
    773         return self.vlog
    774 
    775 
    776 class CountTrackingIDNormalFingerValidator(CountTrackingIDValidator):
    777     """A dummy CountTrackingIDValidator to collect data for
    778     normal finger gestures.
    779     """
    780     pass
    781 
    782 
    783 class CountTrackingIDFatFingerValidator(CountTrackingIDValidator):
    784     """A dummy CountTrackingIDValidator to collect data for fat finger gestures.
    785     """
    786     pass
    787 
    788 
    789 class StationaryValidator(BaseValidator):
    790     """Check to make sure a finger we expect to remain still doesn't move.
    791 
    792     This class is inherited by both StationaryFingerValidator and
    793     StationaryTapValidator, and is not used directly as a validator.
    794     """
    795 
    796     def __init__(self, criteria, mf=None, device=None, slot=0):
    797         name = self.__class__.__name__
    798         super(StationaryValidator, self).__init__(criteria, mf, device, name)
    799         self.slot = slot
    800 
    801     def check(self, packets, variation=None):
    802         """Check the moving distance of the specified slot."""
    803         self.init_check(packets)
    804         max_distance = self.packets.get_max_distance(self.slot, UNIT.MM)
    805         msg = 'Max distance slot%d: %.2f mm'
    806         self.log_details(msg % (self.slot, max_distance))
    807         self.vlog.metrics = [
    808             firmware_log.Metric(self.mnprops.MAX_DISTANCE, max_distance)
    809         ]
    810         self.vlog.score = self.fc.mf.grade(max_distance)
    811         return self.vlog
    812 
    813 
    814 class StationaryFingerValidator(StationaryValidator):
    815     """A dummy StationaryValidator to check pulling effect by another finger.
    816 
    817     Example:
    818         To verify if the stationary finger specified by the slot is not
    819         pulled away more than 1.0 mm by another finger.
    820           StationaryFingerValidator('<= 1.0')
    821     """
    822     pass
    823 
    824 
    825 class StationaryTapValidator(StationaryValidator):
    826     """A dummy StationaryValidator to check the wobble of tap/click.
    827 
    828     Example:
    829         To verify if the tapping finger specified by the slot does not
    830         wobble larger than 1.0 mm.
    831           StationaryTapValidator('<= 1.0')
    832     """
    833     pass
    834 
    835 
    836 class NoGapValidator(BaseValidator):
    837     """Validator to make sure that there are no significant gaps in a line.
    838 
    839     Example:
    840         To verify if there is exactly one finger observed:
    841           NoGapValidator('<= 5, ~ +5', slot=1)
    842     """
    843 
    844     def __init__(self, criteria_str, mf=None, device=None, slot=0):
    845         name = self.__class__.__name__
    846         super(NoGapValidator, self).__init__(criteria_str, mf, device, name)
    847         self.slot = slot
    848 
    849     def check(self, packets, variation=None):
    850         """There should be no significant gaps in a line."""
    851         self.init_check(packets)
    852         # Get the largest gap ratio
    853         gap_ratio = self.packets.get_largest_gap_ratio(self.slot)
    854         msg = 'Largest gap ratio slot%d: %f'
    855         self.log_details(msg % (self.slot, gap_ratio))
    856         self.vlog.score = self.fc.mf.grade(gap_ratio)
    857         return self.vlog
    858 
    859 
    860 class NoReversedMotionValidator(BaseValidator):
    861     """Validator to measure the reversed motions in the specified slots.
    862 
    863     Example:
    864         To measure the reversed motions in slot 0:
    865           NoReversedMotionValidator('== 0, ~ +20', slots=0)
    866     """
    867     def __init__(self, criteria_str, mf=None, device=None, slots=(0,),
    868                  segments=VAL.MIDDLE):
    869         self._segments = segments
    870         name = get_derived_name(self.__class__.__name__, segments)
    871         self.slots = (slots,) if isinstance(slots, int) else slots
    872         parent = super(NoReversedMotionValidator, self)
    873         parent.__init__(criteria_str, mf, device, name)
    874 
    875     def _get_reversed_motions(self, slot, direction):
    876         """Get the reversed motions opposed to the direction in the slot."""
    877         return self.packets.get_reversed_motions(slot,
    878                                                  direction,
    879                                                  segment_flag=self._segments,
    880                                                  ratio=END_PERCENTAGE)
    881 
    882     def check(self, packets, variation=None):
    883         """There should be no reversed motions in a slot."""
    884         self.init_check(packets)
    885         sum_reversed_motions = 0
    886         direction = self.get_direction_in_variation(variation)
    887         for slot in self.slots:
    888             # Get the reversed motions.
    889             reversed_motions = self._get_reversed_motions(slot, direction)
    890             msg = 'Reversed motions slot%d: %s px'
    891             self.log_details(msg % (slot, reversed_motions))
    892             sum_reversed_motions += sum(map(abs, reversed_motions.values()))
    893         self.vlog.score = self.fc.mf.grade(sum_reversed_motions)
    894         return self.vlog
    895 
    896 
    897 class CountPacketsValidator(BaseValidator):
    898     """Validator to check the number of packets.
    899 
    900     Example:
    901         To verify if there are enough packets received about the first finger:
    902           CountPacketsValidator('>= 3, ~ -3', slot=0)
    903     """
    904 
    905     def __init__(self, criteria_str, mf=None, device=None, slot=0):
    906         self.name = self.__class__.__name__
    907         super(CountPacketsValidator, self).__init__(criteria_str, mf, device,
    908                                                     self.name)
    909         self.slot = slot
    910 
    911     def check(self, packets, variation=None):
    912         """Check the number of packets in the specified slot."""
    913         self.init_check(packets)
    914         # Get the number of packets in that slot
    915         actual_count_packets = self.packets.get_num_packets(self.slot)
    916         msg = 'Number of packets slot%d: %s'
    917         self.log_details(msg % (self.slot, actual_count_packets))
    918 
    919         # Add the metric for the count of packets
    920         expected_count_packets = self.get_threshold(self.criteria_str, '>')
    921         assert expected_count_packets, 'Check the criteria of %s' % self.name
    922         metric_value = (actual_count_packets, expected_count_packets)
    923         metric_name = self.mnprops.COUNT_PACKETS
    924         self.vlog.metrics = [firmware_log.Metric(metric_name, metric_value)]
    925 
    926         self.vlog.score = self.fc.mf.grade(actual_count_packets)
    927         return self.vlog
    928 
    929 
    930 class PinchValidator(BaseValidator):
    931     """Validator to check the pinch to zoom in/out.
    932 
    933     Example:
    934         To verify that the two fingers are drawing closer:
    935           PinchValidator('>= 200, ~ -100')
    936     """
    937 
    938     def __init__(self, criteria_str, mf=None, device=None):
    939         self.name = self.__class__.__name__
    940         super(PinchValidator, self).__init__(criteria_str, mf, device,
    941                                              self.name)
    942 
    943     def check(self, packets, variation):
    944         """Check the number of packets in the specified slot."""
    945         self.init_check(packets)
    946         # Get the relative motion of the two fingers
    947         slots = (0, 1)
    948         actual_relative_motion = self.packets.get_relative_motion(slots)
    949         if variation == GV.ZOOM_OUT:
    950             actual_relative_motion = -actual_relative_motion
    951         msg = 'Relative motions of the two fingers: %.2f px'
    952         self.log_details(msg % actual_relative_motion)
    953 
    954         # Add the metric for relative motion distance.
    955         expected_relative_motion = self.get_threshold(self.criteria_str, '>')
    956         assert expected_relative_motion, 'Check the criteria of %s' % self.name
    957         metric_value = (actual_relative_motion, expected_relative_motion)
    958         metric_name = self.mnprops.PINCH
    959         self.vlog.metrics = [firmware_log.Metric(metric_name, metric_value)]
    960 
    961         self.vlog.score = self.fc.mf.grade(actual_relative_motion)
    962         return self.vlog
    963 
    964 
    965 class PhysicalClickValidator(BaseValidator):
    966     """Validator to check the events generated by physical clicks
    967 
    968     Example:
    969         To verify the events generated by a one-finger physical click
    970           PhysicalClickValidator('== 1', fingers=1)
    971     """
    972 
    973     def __init__(self, criteria_str, fingers, mf=None, device=None):
    974         self.criteria_str = criteria_str
    975         self.name = self.__class__.__name__
    976         super(PhysicalClickValidator, self).__init__(criteria_str, mf, device,
    977                                                      self.name)
    978         self.fingers = fingers
    979 
    980     def _get_expected_number(self):
    981         """Get the expected number of counts from the criteria string.
    982 
    983         E.g., criteria_str: '== 1'
    984         """
    985         try:
    986             expected_count = int(self.criteria_str.split('==')[-1].strip())
    987         except Exception, e:
    988             print 'Error: %s in the criteria string of %s' % (e, self.name)
    989             exit(1)
    990         return expected_count
    991 
    992     def _add_metrics(self):
    993         """Add metrics"""
    994         fingers = self.fingers
    995         raw_click_count = self.packets.get_raw_physical_clicks()
    996 
    997         # This is for the metric:
    998         #   "of the n clicks, the % of clicks with the correct finger IDs"
    999         correct_click_count = self.packets.get_correct_physical_clicks(fingers)
   1000         value_with_TIDs = (correct_click_count, raw_click_count)
   1001         name_with_TIDs = self.mnprops.CLICK_CHECK_TIDS.format(self.fingers)
   1002 
   1003         # This is for the metric: "% of finger IDs with a click"
   1004         expected_click_count = self._get_expected_number()
   1005         value_clicks = (raw_click_count, expected_click_count)
   1006         name_clicks = self.mnprops.CLICK_CHECK_CLICK.format(self.fingers)
   1007 
   1008         self.vlog.metrics = [
   1009             firmware_log.Metric(name_with_TIDs, value_with_TIDs),
   1010             firmware_log.Metric(name_clicks, value_clicks),
   1011         ]
   1012 
   1013         return value_with_TIDs
   1014 
   1015     def check(self, packets, variation=None):
   1016         """Check the number of packets in the specified slot."""
   1017         self.init_check(packets)
   1018         correct_click_count, raw_click_count = self._add_metrics()
   1019         # Get the number of physical clicks made with the specified number
   1020         # of fingers.
   1021         msg = 'Count of %d-finger physical clicks: %s'
   1022         self.log_details(msg % (self.fingers, correct_click_count))
   1023         self.log_details('Count of physical clicks: %d' % raw_click_count)
   1024         self.vlog.score = self.fc.mf.grade(correct_click_count)
   1025         return self.vlog
   1026 
   1027 
   1028 class DrumrollValidator(BaseValidator):
   1029     """Validator to check the drumroll problem.
   1030 
   1031     All points from the same finger should be within 2 circles of radius X mm
   1032     (e.g. 2 mm)
   1033 
   1034     Example:
   1035         To verify that the max radius of all minimal enclosing circles generated
   1036         by alternately tapping the index and middle fingers is within 2.0 mm.
   1037           DrumrollValidator('<= 2.0')
   1038     """
   1039 
   1040     def __init__(self, criteria_str, mf=None, device=None):
   1041         name = self.__class__.__name__
   1042         super(DrumrollValidator, self).__init__(criteria_str, mf, device, name)
   1043 
   1044     def check(self, packets, variation=None):
   1045         """The moving distance of the points in any tracking ID should be
   1046         within the specified value.
   1047         """
   1048         self.init_check(packets)
   1049         # For each tracking ID, compute the minimal enclosing circles,
   1050         #     rocs = (radius_of_circle1, radius_of_circle2)
   1051         # Return a list of such minimal enclosing circles of all tracking IDs.
   1052         rocs = self.packets.get_list_of_rocs_of_all_tracking_ids()
   1053         max_radius = max(rocs)
   1054         self.log_details('Max radius: %.2f mm' % max_radius)
   1055         metric_name = self.mnprops.CIRCLE_RADIUS
   1056         self.vlog.metrics = [firmware_log.Metric(metric_name, roc)
   1057                              for roc in rocs]
   1058         self.vlog.score = self.fc.mf.grade(max_radius)
   1059         return self.vlog
   1060 
   1061 
   1062 class NoLevelJumpValidator(BaseValidator):
   1063     """Validator to check if there are level jumps
   1064 
   1065     When a user draws a horizontal line with thumb edge or a fat finger,
   1066     the line could comprise a horizontal line segment followed by another
   1067     horizontal line segment (or just dots) one level up or down, and then
   1068     another horizontal line segment again at different horizontal level, etc.
   1069     This validator is implemented to detect such level jumps.
   1070 
   1071     Such level jumps could also occur when drawing vertical or diagonal lines.
   1072 
   1073     Example:
   1074         To verify the level jumps in a one-finger tracking gesture:
   1075           NoLevelJumpValidator('<= 10, ~ +30', slots[0,])
   1076         where slots[0,] represent the slots with numbers larger than slot 0.
   1077         This kind of representation is required because when the thumb edge or
   1078         a fat finger is used, due to the difficulty in handling it correctly
   1079         in the touch device firmware, the tracking IDs and slot IDs may keep
   1080         changing. We would like to analyze all such slots.
   1081     """
   1082 
   1083     def __init__(self, criteria_str, mf=None, device=None, slots=0):
   1084         name = self.__class__.__name__
   1085         super(NoLevelJumpValidator, self).__init__(criteria_str, mf, device,
   1086                                                    name)
   1087         self.slots = slots
   1088 
   1089     def check(self, packets, variation=None):
   1090         """Check if there are level jumps."""
   1091         self.init_check(packets)
   1092         # Get the displacements of the slots.
   1093         slots = self.slots[0]
   1094         displacements = self.packets.get_displacements_for_slots(slots)
   1095 
   1096         # Iterate through the collected tracking IDs
   1097         jumps = []
   1098         for tid in displacements:
   1099             slot = displacements[tid][MTB.SLOT]
   1100             for axis in AXIS.LIST:
   1101                 disp = displacements[tid][axis]
   1102                 jump = self.packets.get_largest_accumulated_level_jumps(disp)
   1103                 jumps.append(jump)
   1104                 msg = '  accu jump (%d %s): %d px'
   1105                 self.log_details(msg % (slot, axis, jump))
   1106 
   1107         # Get the largest accumulated level jump
   1108         max_jump = max(jumps) if jumps else 0
   1109         msg = 'Max accu jump: %d px'
   1110         self.log_details(msg % (max_jump))
   1111         self.vlog.score = self.fc.mf.grade(max_jump)
   1112         return self.vlog
   1113 
   1114 
   1115 class ReportRateValidator(BaseValidator):
   1116     """Validator to check the report rate.
   1117 
   1118     Example:
   1119         To verify that the report rate is around 80 Hz. It gets 0 points
   1120         if the report rate drops below 60 Hz.
   1121           ReportRateValidator('== 80 ~ -20')
   1122     """
   1123 
   1124     def __init__(self, criteria_str, finger=None, mf=None, device=None,
   1125                  chop_off_pauses=True):
   1126         """Initialize ReportRateValidator
   1127 
   1128         @param criteria_str: the criteria string
   1129         @param finger: the ith contact if not None. When set to None, it means
   1130                 to examine all packets.
   1131         @param mf: the fuzzy member function to use
   1132         @param device: the touch device
   1133         """
   1134         self.name = self.__class__.__name__
   1135         self.criteria_str = criteria_str
   1136         self.finger = finger
   1137         if finger is not None:
   1138             msg = '%s: finger = %d (It is required that finger >= 0.)'
   1139             assert finger >= 0, msg % (self.name, finger)
   1140         self.chop_off_pauses = chop_off_pauses
   1141         super(ReportRateValidator, self).__init__(criteria_str, mf, device,
   1142                                                   self.name)
   1143 
   1144     def _chop_off_both_ends(self, points, distance):
   1145         """Chop off both ends of segments such that the points in the remaining
   1146         middle segment are distant from both ends by more than the specified
   1147         distance.
   1148 
   1149         When performing a gesture such as finger tracking, it is possible
   1150         that the finger will stay stationary for a while before it actually
   1151         starts moving. Likewise, it is also possible that the finger may stay
   1152         stationary before the finger leaves the touch surface. We would like
   1153         to chop off the stationary segments.
   1154 
   1155         Note: if distance is 0, the effect is equivalent to keep all points.
   1156 
   1157         @param points: a list of Points
   1158         @param distance: the distance within which the points are chopped off
   1159         """
   1160         def _find_index(points, distance, reversed_flag=False):
   1161             """Find the first index of the point whose distance with the
   1162             first point is larger than the specified distance.
   1163 
   1164             @param points: a list of Points
   1165             @param distance: the distance
   1166             @param reversed_flag: indicates if the points needs to be reversed
   1167             """
   1168             points_len = len(points)
   1169             if reversed_flag:
   1170                 points = reversed(points)
   1171 
   1172             ref_point = None
   1173             for i, p in enumerate(points):
   1174                 if ref_point is None:
   1175                     ref_point = p
   1176                 if ref_point.distance(p) >= distance:
   1177                     return (points_len - i - 1) if reversed_flag else i
   1178 
   1179             return None
   1180 
   1181         # There must be extra points in addition to the first and the last point
   1182         if len(points) <= 2:
   1183             return None
   1184 
   1185         begin_moving_index = _find_index(points, distance, reversed_flag=False)
   1186         end_moving_index = _find_index(points, distance, reversed_flag=True)
   1187 
   1188         if (begin_moving_index is None or end_moving_index is None or
   1189                 begin_moving_index > end_moving_index):
   1190             return None
   1191         return [begin_moving_index, end_moving_index]
   1192 
   1193     def _add_report_rate_metrics2(self):
   1194         """Calculate and add the metrics about report rate.
   1195 
   1196         Three metrics are required.
   1197         - % of time intervals that are > (1/60) second
   1198         - average time interval
   1199         - max time interval
   1200 
   1201         """
   1202         import test_conf as conf
   1203 
   1204         if self.finger:
   1205             finger_list = [self.finger]
   1206         else:
   1207             ordered_finger_paths_dict = self.packets.get_ordered_finger_paths()
   1208             finger_list = range(len(ordered_finger_paths_dict))
   1209 
   1210         # distance: the minimal moving distance within which the points
   1211         #           at both ends will be chopped off
   1212         distance = conf.MIN_MOVING_DISTANCE if self.chop_off_pauses else 0
   1213 
   1214         # Derive the middle moving segment in which the finger(s)
   1215         # moves significantly.
   1216         begin_time = float('infinity')
   1217         end_time = float('-infinity')
   1218         for finger in finger_list:
   1219             list_t = self.packets.get_ordered_finger_path(finger, 'syn_time')
   1220             points = self.packets.get_ordered_finger_path(finger, 'point')
   1221             middle = self._chop_off_both_ends(points, distance)
   1222             if middle:
   1223                 this_begin_index, this_end_index = middle
   1224                 this_begin_time = list_t[this_begin_index]
   1225                 this_end_time = list_t[this_end_index]
   1226                 begin_time = min(begin_time, this_begin_time)
   1227                 end_time = max(end_time, this_end_time)
   1228 
   1229         if (begin_time == float('infinity') or end_time == float('-infinity')
   1230                 or end_time <= begin_time):
   1231             print 'Warning: %s: cannot derive a moving segment.' % self.name
   1232             print 'begin_time: ', begin_time
   1233             print 'end_time: ', end_time
   1234             return
   1235 
   1236         # Get the list of SYN_REPORT time in the middle moving segment.
   1237         list_syn_time = filter(lambda t: t >= begin_time and t <= end_time,
   1238                                self.packets.get_list_syn_time(self.finger))
   1239 
   1240         # Each packet consists of a list of events of which The last one is
   1241         # the sync event. The unit of sync_intervals is ms.
   1242         sync_intervals = [1000.0 * (list_syn_time[i + 1] - list_syn_time[i])
   1243                           for i in range(len(list_syn_time) - 1)]
   1244 
   1245         max_report_interval = conf.max_report_interval
   1246 
   1247         # Calculate the metrics and add them to vlog.
   1248         long_intervals = [s for s in sync_intervals if s > max_report_interval]
   1249         metric_long_intervals = (len(long_intervals), len(sync_intervals))
   1250         ave_interval = sum(sync_intervals) / len(sync_intervals)
   1251         max_interval = max(sync_intervals)
   1252 
   1253         name_long_intervals_pct = self.mnprops.LONG_INTERVALS.format(
   1254             '%.2f' % max_report_interval)
   1255         name_ave_time_interval = self.mnprops.AVE_TIME_INTERVAL
   1256         name_max_time_interval = self.mnprops.MAX_TIME_INTERVAL
   1257 
   1258         self.vlog.metrics = [
   1259             firmware_log.Metric(name_long_intervals_pct, metric_long_intervals),
   1260             firmware_log.Metric(self.mnprops.AVE_TIME_INTERVAL, ave_interval),
   1261             firmware_log.Metric(self.mnprops.MAX_TIME_INTERVAL, max_interval),
   1262         ]
   1263 
   1264         self.log_details('%s: %f' % (self.mnprops.AVE_TIME_INTERVAL,
   1265                          ave_interval))
   1266         self.log_details('%s: %f' % (self.mnprops.MAX_TIME_INTERVAL,
   1267                          max_interval))
   1268         self.log_details('# long intervals > %s ms: %d' %
   1269                          (self.mnprops.max_report_interval_str,
   1270                           len(long_intervals)))
   1271         self.log_details('# total intervals: %d' % len(sync_intervals))
   1272 
   1273     def _get_report_rate(self, list_syn_time):
   1274         """Get the report rate in Hz from the list of syn_time.
   1275 
   1276         @param list_syn_time: a list of SYN_REPORT time instants
   1277         """
   1278         if len(list_syn_time) <= 1:
   1279             return 0
   1280         duration = list_syn_time[-1] - list_syn_time[0]
   1281         num_packets = len(list_syn_time) - 1
   1282         report_rate = float(num_packets) / duration
   1283         return report_rate
   1284 
   1285     def check(self, packets, variation=None):
   1286         """The Report rate should be within the specified range."""
   1287         self.init_check(packets)
   1288         # Get the list of syn_time based on the specified finger.
   1289         list_syn_time = self.packets.get_list_syn_time(self.finger)
   1290         # Get the report rate
   1291         self.report_rate = self._get_report_rate(list_syn_time)
   1292         self._add_report_rate_metrics2()
   1293         self.vlog.score = self.fc.mf.grade(self.report_rate)
   1294         return self.vlog
   1295 
   1296 
   1297 class MtbSanityValidator(BaseValidator):
   1298     """Validator to check if the MTB format is correct.
   1299 
   1300     A ghost finger is a slot with a positive TRACKING ID without a real
   1301     object such as a finger touching the device.
   1302 
   1303     Note that this object should be instantiated before any finger touching
   1304     the device so that a snapshot could be derived in the very beginning.
   1305 
   1306     There are potentially many things to check in the MTB format. However,
   1307     this validator will begin with a simple TRACKING ID examination.
   1308     A new slot should come with a positive TRACKING ID before the slot
   1309     can assign values to its attributes or set -1 to its TRACKING ID.
   1310     This is sort of different from a ghost finger case. A ghost finger
   1311     occurs when there exist slots with positive TRACKING IDs in the
   1312     beginning by syncing with the kernel before any finger touching the
   1313     device.
   1314 
   1315     Note that there is no need for this class to perform
   1316         self.init_check(packets)
   1317     """
   1318 
   1319     def __init__(self, criteria_str='== 0', mf=None, device=None,
   1320                  device_info=None):
   1321         name = self.__class__.__name__
   1322         super(MtbSanityValidator, self).__init__(criteria_str, mf, device, name)
   1323         if device_info:
   1324             self.device_info = device_info
   1325         else:
   1326             sys.path.append('../../bin/input')
   1327             import input_device
   1328             self.device_info = input_device.InputDevice(self.device.device_node)
   1329 
   1330     def _check_ghost_fingers(self):
   1331         """Check if there are ghost fingers by synching with the kernel."""
   1332         self.number_fingers = self.device_info.get_num_fingers()
   1333         self.slot_dict = self.device_info.get_slots()
   1334 
   1335         self.log_details('# fingers: %d' % self.number_fingers)
   1336         for slot_id, slot in self.slot_dict.items():
   1337             self.log_details('slot %d:' % slot_id)
   1338             for prop in slot:
   1339                 prop_name = EV_STRINGS[EV_ABS].get(prop, prop)
   1340                 self.log_details(' %s=%6d' % (prop_name, slot[prop].value))
   1341 
   1342         self.vlog.metrics.append(
   1343                 firmware_log.Metric(self.mnprops.GHOST_FINGERS,
   1344                                     (self.number_fingers, 0)),
   1345         )
   1346         return self.number_fingers
   1347 
   1348     def _check_mtb(self, packets):
   1349         """Check if there are MTB format problems."""
   1350         mtb_sanity = mtb.MtbSanity(packets)
   1351         errors = mtb_sanity.check()
   1352         number_errors = sum(errors.values())
   1353 
   1354         self.log_details('# MTB errors: %d' % number_errors)
   1355         for err_string, err_count in errors.items():
   1356             if err_count > 0:
   1357                 self.log_details('%s: %d' % (err_string, err_count))
   1358 
   1359         self.vlog.metrics.append(
   1360                 firmware_log.Metric(self.mnprops.MTB_SANITY_ERR,
   1361                                     (number_errors, 0)),
   1362         )
   1363         return number_errors
   1364 
   1365     def check(self, packets, variation=None):
   1366         """Check ghost fingers and MTB format."""
   1367         self.vlog.metrics = []
   1368         number_errors = self._check_ghost_fingers() + self._check_mtb(packets)
   1369         self.vlog.score = self.fc.mf.grade(number_errors)
   1370         return self.vlog
   1371 
   1372 
   1373 class HysteresisValidator(BaseValidator):
   1374     """Validator to check if there exists a cursor jump initially
   1375 
   1376     The movement hysteresis, if existing, set in the touchpad firmware
   1377     should not lead to an obvious cursor jump when a finger starts moving.
   1378 
   1379     Example:
   1380         To verify if there exists a cursor jump with distance ratio larger
   1381         than 2.0; i.e.,
   1382         distance(point0, point1) / distance(point1, point2) should be <= 2.0
   1383           HysteresisValidator('> 2.0')
   1384 
   1385     Raw data of tests/data/center_to_right_slow_link.dat:
   1386 
   1387     [block0]
   1388     Event: type 3 (EV_ABS), code 57 (ABS_MT_TRACKING_ID), value 508
   1389     Event: type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 906
   1390     Event: type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 720
   1391     Event: type 3 (EV_ABS), code 58 (ABS_MT_PRESSURE), value 24
   1392     Event: -------------- SYN_REPORT ------------
   1393 
   1394     [block1]
   1395     Event: type 3 (EV_ABS), code 58 (ABS_MT_PRESSURE), value 25
   1396     Event: -------------- SYN_REPORT ------------
   1397 
   1398     ...  more SYN_REPORT with ABS_MT_PRESSURE only  ...
   1399 
   1400     [block2]
   1401     Event: type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 939
   1402     Event: type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 727
   1403     Event: type 3 (EV_ABS), code 58 (ABS_MT_PRESSURE), value 34
   1404     Event: -------------- SYN_REPORT ------------
   1405 
   1406     [block3]
   1407     Event: type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 941
   1408     Event: type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 727
   1409     Event: type 3 (EV_ABS), code 58 (ABS_MT_PRESSURE), value 37
   1410     Event: -------------- SYN_REPORT ------------
   1411 
   1412     ...  more data  ...
   1413 
   1414     Let point0 represents the coordinates in block0.
   1415     Let point1 represents the coordinates in block2.
   1416     Let point2 represents the coordinates in block3.
   1417 
   1418     Note that the data in block1 only contain a number of pressure values
   1419     without any X/Y updates even when the finger is tracking to the right.
   1420     This is the undesirable hysteresis effect.
   1421 
   1422     Compute ratio = distance(point0, point1) / distance(point1, point2).
   1423     When ratio is high, it indicates the hysteresis effect.
   1424     """
   1425 
   1426     def __init__(self, criteria_str, finger=0, mf=None, device=None):
   1427         self.criteria_str = criteria_str
   1428         self.finger = finger
   1429         name = self.__class__.__name__
   1430         super(HysteresisValidator, self).__init__(criteria_str, mf, device,
   1431                                                   name)
   1432 
   1433     def _point_px_to_mm(self, point_px):
   1434         """Convert a point in px to a point in mm."""
   1435         return Point(*self.device.pixel_to_mm(point_px.value()))
   1436 
   1437     def _find_index_of_first_distinct_value(self, points):
   1438         """Find first index, idx, such that points[idx] != points[0]."""
   1439         for idx, point in enumerate(points):
   1440             if points[0].distance(points[idx]) > 0:
   1441                 return idx
   1442         return None
   1443 
   1444     def check(self, packets, variation=None):
   1445         """There is no jump larger than a threshold at the beginning."""
   1446         self.init_check(packets)
   1447         points_px = self.packets.get_ordered_finger_path(self.finger, 'point')
   1448         point1_idx = point2_idx = None
   1449         distance1 = distance2 = None
   1450 
   1451         if len(points_px) > 0:
   1452             point0_mm = self._point_px_to_mm(points_px[0])
   1453             point1_idx = self._find_index_of_first_distinct_value(points_px)
   1454 
   1455         if point1_idx is not None:
   1456             point1_mm = self._point_px_to_mm(points_px[point1_idx])
   1457             distance1 = point0_mm.distance(point1_mm)
   1458             if point1_idx + 1 <= len(points_px):
   1459                 point2_idx = self._find_index_of_first_distinct_value(
   1460                         points_px[point1_idx:]) + point1_idx
   1461 
   1462         if point2_idx is not None:
   1463             point2_mm = self._point_px_to_mm(points_px[point2_idx])
   1464             distance2 = point1_mm.distance(point2_mm)
   1465             ratio = (float('infinity') if distance1 == 0 else
   1466                      distance1 / distance2)
   1467         else:
   1468             ratio = float('infinity')
   1469 
   1470         self.log_details('init gap ratio: %.2f' % ratio)
   1471         self.log_details('dist(p0,p1): ' +
   1472                          ('None' if distance1 is None else '%.2f' % distance1))
   1473         self.log_details('dist(p1,p2): ' +
   1474                          ('None' if distance2 is None else '%.2f' % distance2))
   1475         self.vlog.metrics = [
   1476                 firmware_log.Metric(self.mnprops.MAX_INIT_GAP_RATIO, ratio),
   1477                 firmware_log.Metric(self.mnprops.AVE_INIT_GAP_RATIO, ratio),
   1478         ]
   1479         self.vlog.score = self.fc.mf.grade(ratio)
   1480         return self.vlog
   1481