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'''
9How 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
17The validator template is as follows:
18
19class 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
40Note 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
56import copy
57import numpy as np
58import os
59import re
60import sys
61
62import firmware_log
63import fuzzy
64import mtb
65
66from collections import namedtuple, OrderedDict
67from inspect import isfunction
68
69from common_util import print_and_exit, simple_system_output
70from firmware_constants import AXIS, GV, MTB, UNIT, VAL
71from geometry.elements import Point
72
73from 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.
77END_PERCENTAGE = 0.1
78
79# Define other constants below.
80VALIDATOR = 'Validator'
81
82
83def 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
124def 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
130def 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
138def 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
146def 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
162def 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
175def init_base_validator(device):
176    """Initialize the device for all the Validators to use"""
177    BaseValidator._device = device
178
179
180class 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
276class 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
325class 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
377class 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
506class 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
670class LinearityNormalFingerValidator(LinearityValidator):
671    """A dummy LinearityValidator to verify linearity for gestures performed
672    with normal fingers.
673    """
674    pass
675
676
677class LinearityFatFingerValidator(LinearityValidator):
678    """A dummy LinearityValidator to verify linearity for gestures performed
679    with fat fingers or thumb edge.
680    """
681    pass
682
683
684class 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
738class 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
776class CountTrackingIDNormalFingerValidator(CountTrackingIDValidator):
777    """A dummy CountTrackingIDValidator to collect data for
778    normal finger gestures.
779    """
780    pass
781
782
783class CountTrackingIDFatFingerValidator(CountTrackingIDValidator):
784    """A dummy CountTrackingIDValidator to collect data for fat finger gestures.
785    """
786    pass
787
788
789class 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
814class 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
825class 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
836class 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
860class 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
897class 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
930class 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
965class 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
1028class 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
1062class 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
1115class 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
1297class 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
1373class 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