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