1# Copyright (c) 2013 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"""Handle google gdata spreadsheet service.""" 6 7 8from optparse import OptionParser 9import glob 10import os 11import shutil 12import subprocess 13import tempfile 14 15import gdata.gauth 16import gdata.spreadsheets.client 17 18from authenticator import SpreadsheetAuthorizer 19import mtb 20import test_conf as conf 21 22from common_util import print_and_exit 23from firmware_constants import GV 24 25 26# Skip operations about the Summary worksheet. 27# There are two tables in the worksheet. But it seems that there are problems 28# with table operations in the new Google Sheets for present. 29skip_summary_flag = True 30 31 32SHEET_DESCRIPTION = ('This sheet is autogenerated from autotest/client/' 33 'site_tests/firmware_TouchMTB/spreadsheet.py') 34 35# Old Google Spreadsheet that suffered from 400K cells limit. 36# Leave it here for reference. 37# 'Touchpad relative pressure readings' spreadsheet key which is copied from the 38# url of the spreadsheet https://docs.google.com/a/google.com/spreadsheet/ccc? 39# key=0Ah6uZRmm2hgYdG4wX0JaQkVqa2gybTQwMnRfNmxsR1E 40 41# New style Google Spreadsheet: cells limit is 2 million. 42# 'Touchpad relative pressure readings (new Google Sheets)' spreadsheet key 43# which is copied from the url of the spreadsheet 44# https://docs.google.com/a/google.com/spreadsheets/d/1kE2lhfjTiq5o7Z5DqnJhMXrGbfUSa1rXbfhKopdIrYc/ 45TARGET_SPREADSHEET_KEY = '1kE2lhfjTiq5o7Z5DqnJhMXrGbfUSa1rXbfhKopdIrYc' 46 47RESULTS = ['result:', '=B4*I5+J5', '=C4*I5+J5', '=D4*I5+J5', '=E4*I5+J5', 48 '=F4*I5+J5', '=G4*I5+J5', '=H4*I5+J5'] 49ACTUALS = ['actual:', '11.945895975', '25.517564775', '46.566217775', 50 '76.976808975', '107.513063775', '151.746650975', '248.8453439'] 51AVERAGES = ['mean:', '=AVERAGE(B6:B405)', '=AVERAGE(C6:C405)', 52 '=AVERAGE(D6:D405)', '=AVERAGE(E6:E405)', '=AVERAGE(F6:F405)', 53 '=AVERAGE(G6:G405)', '=AVERAGE(H6:H405)', 'slope:', 'icept'] 54HEADERS = ['', 'size0', 'size1', 'size2', 'size3', 'size4', 'size5', 'size6', 55 '=transpose(Regress(B4:F4,B3:F3))'] 56 57MEASURED = ['H4', 'G4', 'F4', 'E4', 'D4', 'C4', 'B4'] 58COMPUTED = ['H2', 'G2', 'F2', 'E2', 'D2', 'C2', 'B2'] 59 60SLOPE_CELL = (5, 9) 61INTERCEPT_CELL = (5, 10) 62 63CELLS = [ 64 RESULTS, 65 ACTUALS, 66 AVERAGES, 67 HEADERS 68] 69 70CROSS_SHEET_CELL = '=\'%s\'!%s' 71SUMMARY_WORKSHEET_TITLE = 'Summary' 72TABLE_COMPUTED = 'Computed Pressures' 73TABLE_MEASURED = 'Measured Pressures' 74 75options = None 76ACTUAL_SURFACE_AREA = [ '11.945895975', '25.517564775', '46.566217775', 77 '76.976808975', '107.513063775', '151.746650975', 78 '248.8453439' ] 79results_table = { 'A': 'device', 'B': 'size6', 'C': 'size5', 'D': 'size4', 80 'E': 'size3', 'F': 'size2', 'G': 'size1', 'H': 'size0' } 81surface_title = [ 'surface area', '248.8453439', '151.746650975', 82 '107.513063775', '76.976808975', '46.566217775', 83 '25.517564775', '11.945895975' ] 84diameter_title = [ 'diameter (mm)', '17.8', '13.9', '11.7', '9.9', '7.7', 85 '5.7', '3.9'] 86 87def print_verbose(msg): 88 """Print the message if options.verbose is True. 89 90 @param msg: the message to print 91 """ 92 if options.verbose: 93 print msg 94 95 96class GestureEventFiles: 97 """Get gesture event files and parse the pressure values.""" 98 DEFAULT_RESULT_DIR = 'latest' 99 FILE_PATTERN = '{}.%s*.dat'.format(conf.PRESSURE_CALIBRATION) 100 PRESSURE_LIST_MAX_SIZE = 80 * 5 101 102 def __init__(self): 103 self._get_machine_ip() 104 self._get_result_dir() 105 self._get_gesture_event_files() 106 self._get_event_pressures() 107 self._get_list_of_pressure_dicts() 108 109 def __del__(self): 110 self._cleanup() 111 112 def _cleanup(self): 113 """Remove the temporary directory that holds the gesture event files.""" 114 if os.path.isdir(self.event_dir): 115 print 'Removing tmp directory "%s" .... ' % self.event_dir 116 try: 117 shutil.rmtree(self.event_dir) 118 except Exception as e: 119 msg = 'Error in removing tmp directory ("%s"): %s' 120 print_and_exit(msg % (self.event_dir, e)) 121 122 def _cleanup_and_exit(self, err_msg): 123 """Clean up and exit with the error message. 124 125 @param err_msg: the error message to print 126 """ 127 self._cleanup() 128 print_and_exit(err_msg) 129 130 def _get_machine_ip(self): 131 """Get the ip address of the chromebook machine.""" 132 if options.device: 133 self.machine_ip = options.device 134 return 135 msg = '\nEnter the ip address (xx.xx.xx.xx) of the chromebook machine: ' 136 self.machine_ip = raw_input(msg) 137 138 def _get_result_dir(self): 139 """Get the test result directory located in the chromebook machine.""" 140 if not options.result_dir: 141 print '\nEnter the test result directory located in the machine.' 142 print 'It is a directory under %s' % conf.log_root_dir 143 print ('If you have just performed the pressure calibration test ' 144 'on the machine,\n' 'you could just press ENTER to use ' 145 'the default "latest" directory.') 146 result_dir = raw_input('Enter test result directory: ') 147 if result_dir == '': 148 result_dir = self.DEFAULT_RESULT_DIR 149 else: 150 result_dir = options.result_dir 151 self.result_dir = os.path.join(conf.log_root_dir, result_dir) 152 153 def _get_gesture_event_files(self): 154 """Scp the gesture event files in the result_dir in machine_ip.""" 155 try: 156 self.event_dir = tempfile.mkdtemp(prefix='touch_firmware_test_') 157 except Exception as e: 158 err_msg = 'Error in creating tmp directory (%s): %s' 159 self._cleanup_and_exit(err_msg % (self.event_dir, e)) 160 161 # Try to scp the gesture event files from the chromebook machine to 162 # the event_dir created above on the host. 163 # An example gesture event file looks like 164 # pressure_calibration.size0-lumpy-fw_11.27-calibration-20130307.dat 165 filepath = os.path.join(self.result_dir, self.FILE_PATTERN % '') 166 cmd = ('scp -o UserKnownHostsFile=/dev/null ' + 167 '-o StrictHostKeyChecking=no root@%s:%s %s') % ( 168 self.machine_ip, filepath, self.event_dir) 169 try: 170 print ('scp gesture event files from "machine_ip:%s" to %s\n' % 171 (self.machine_ip, self.event_dir)) 172 subprocess.call(cmd.split()) 173 except subprocess.CalledProcessError as e: 174 self._cleanup_and_exit('Error in executing "%s": %s' % (cmd, e)) 175 176 def _get_event_pressures(self): 177 """Parse the gesture event files to get the pressure values.""" 178 self.pressures = {} 179 self.len_pressures = {} 180 for s in GV.SIZE_LIST: 181 # Get the gesture event file for every finger size. 182 filepath = os.path.join(self.event_dir, self.FILE_PATTERN % s) 183 event_files = glob.glob(filepath) 184 if not event_files: 185 err_msg = 'Error: there is no gesture event file for size %s' 186 self._cleanup_and_exit(err_msg % s) 187 188 # Use the latest event file for the size if there are multiple ones. 189 event_files.sort() 190 event_file = event_files[-1] 191 192 # Get the list of pressures in the event file. 193 mtb_packets = mtb.get_mtb_packets_from_file(event_file) 194 target_slot = 0 195 list_z = mtb_packets.get_slot_data(target_slot, 'pressure') 196 len_z = len(list_z) 197 if self.PRESSURE_LIST_MAX_SIZE > len_z: 198 bgn_index = 0 199 end_index = len_z 200 else: 201 # Get the middle segment of the list of pressures. 202 bgn_index = (len_z - self.PRESSURE_LIST_MAX_SIZE) / 2 203 end_index = (len_z + self.PRESSURE_LIST_MAX_SIZE) / 2 204 self.pressures[s] = list_z[bgn_index : end_index] 205 self.len_pressures[s] = len(self.pressures[s]) 206 207 def _get_list_of_pressure_dicts(self): 208 """Get a list of pressure dictionaries.""" 209 self.list_of_pressure_dicts = [] 210 for index in range(max(self.len_pressures.values())): 211 pressure_dict = {} 212 for s in GV.SIZE_LIST: 213 if index < self.len_pressures[s]: 214 pressure_dict[s] = str(self.pressures[s][index]) 215 self.list_of_pressure_dicts.append(pressure_dict) 216 print_verbose(' row %4d: %s' % (index, str(pressure_dict))) 217 218class PressureSpreadsheet(object): 219 """A spreadsheet class to perform pressures calibration in worksheets.""" 220 WORKSHEET_ROW_COUNT = 1000 221 WORKSHEET_COL_COUNT = 20 222 START_ROW_NUMBER = 2 223 DATA_BEGIN_COLUMN = 2 224 COMPUTED_TABLE_ROW = 12 225 MEASURED_TABLE_ROW = 3 226 227 def __init__(self, worksheet_title): 228 """Initialize the spreadsheet and the worksheet 229 230 @param spreadsheet_title: the spreadsheet title 231 @param worksheet_title: the worksheet title 232 """ 233 self.computed_table = None 234 self.feed = None 235 self.measured_table = None 236 self.number_records = 0 237 self.spreadsheet_key = TARGET_SPREADSHEET_KEY 238 self.ss_client = gdata.spreadsheets.client.SpreadsheetsClient() 239 self.summary_feed = None 240 self.worksheet_title = worksheet_title 241 authorizer = SpreadsheetAuthorizer() 242 if not authorizer.authorize(self.ss_client): 243 raise "Please check the access permission of the spreadsheet" 244 self._get_new_worksheet_by_title(worksheet_title) 245 if not skip_summary_flag: 246 self._get_summary_tables() 247 248 def _get_worksheet_entry_by_title(self, worksheet_title): 249 """Check if the worksheet title exists?""" 250 worksheet_feed = self.ss_client.get_worksheets(self.spreadsheet_key) 251 for entry in worksheet_feed.entry: 252 if entry.title.text == worksheet_title: 253 return entry 254 return None 255 256 def _get_new_worksheet_by_title(self, worksheet_title, 257 row_count=WORKSHEET_ROW_COUNT, 258 col_count=WORKSHEET_COL_COUNT): 259 """Create a new worksheet using the title. 260 261 If the worksheet title already exists, using a new title name such as 262 "Copy n of title", where n = 2, 3, ..., MAX_TITLE_DUP + 1 263 264 @param title: the worksheet title 265 @param row_count: the number of rows in the worksheet 266 @param col_count: the number of columns in the worksheet 267 268 """ 269 MAX_TITLE_DUP = 10 270 new_worksheet_title = worksheet_title 271 for i in range(2, MAX_TITLE_DUP + 2): 272 if not self._get_worksheet_entry_by_title(new_worksheet_title): 273 break 274 new_worksheet_title = 'Copy %d of %s' % (i, worksheet_title) 275 self.worksheet_title = new_worksheet_title 276 else: 277 msg = 'Too many duplicate copies of the worksheet title: %s.' 278 print_and_exit(msg % worksheet_title) 279 280 # Add the new worksheet and get the worksheet_id. 281 worksheet_entry = self.ss_client.add_worksheet(self.spreadsheet_key, 282 new_worksheet_title, 283 row_count, 284 col_count) 285 worksheet_id = worksheet_entry.get_worksheet_id() 286 self.feed = gdata.spreadsheets.data.build_batch_cells_update( 287 self.spreadsheet_key, worksheet_id) 288 self.feed.add_set_cell(1, 1, SHEET_DESCRIPTION) 289 290 291 def _insert_pressure_data(self, list_of_pressure_dicts): 292 """Insert the lists of pressures of all finger sizes to a worksheet. 293 294 @param list_of_pressure_dicts: a list of pressure dictionaries 295 """ 296 # Set column headers for figner sizes 297 for row in range(len(CELLS)): 298 for column in range(len(CELLS[row])): 299 self.feed.add_set_cell(self.START_ROW_NUMBER + row, 300 column + 1, 301 CELLS[row][column]) 302 303 # Insert the pressures row by row 304 row = self.START_ROW_NUMBER + len(CELLS) 305 for pressure_dict in list_of_pressure_dicts: 306 print_verbose(' %s' % str(pressure_dict)) 307 col = self.DATA_BEGIN_COLUMN 308 for size in GV.SIZE_LIST: 309 if size in pressure_dict: 310 self.feed.add_set_cell(row, col, pressure_dict[size]) 311 col = col + 1 312 row = row + 1 313 314 def _insert_summary_record(self, table, row_source): 315 """ Paste a record into measured/computed table in summary sheet from 316 a row in source sheet. 317 318 Append one record in the table first as the gdata api does not allow 319 formula in the record data for add_record(). In other word, we have 320 to fill data with add_set_cell() one-by-one instead. 321 322 @param table: Target table entry 323 @param row_source: row in source sheet 324 """ 325 record = { 'device' : self.worksheet_title } 326 self.ss_client.add_record(self.spreadsheet_key, 327 table.get_table_id(), 328 record) 329 row_target = int(table.data.start_row) + int(table.data.num_rows) 330 # As there will be one record inserted measured table, we need to 331 # increase the row number by one for computed_table 332 if table == self.computed_table: 333 row_target = row_target + 1 334 for i in range(len(row_source)): 335 formula = CROSS_SHEET_CELL % (self.worksheet_title, row_source[i]) 336 self.summary_feed.add_set_cell(row_target, 337 i + 2, 338 formula) 339 340 def insert_pressures_to_worksheet(self, list_of_pressure_dicts): 341 """Insert the lists of pressures of all finger sizes to a new worksheet. 342 343 @param list_of_pressure_dicts: a list of pressure dictionaries 344 """ 345 self.number_records = len(list_of_pressure_dicts) 346 print 'Insert the data to device worksheet...' 347 self._insert_pressure_data(list_of_pressure_dicts) 348 349 print 'Finalizing the insertion...' 350 self.ss_client.batch(self.feed, force=True) 351 352 if not skip_summary_flag: 353 print 'Insert the data to summary worksheet...' 354 self._insert_summary_record(self.computed_table, COMPUTED) 355 self._insert_summary_record(self.measured_table, MEASURED) 356 print 'Finalizing the insertion...' 357 self.ss_client.batch(self.summary_feed, force=True) 358 359 def _get_summary_table(self, title, header_row, start_row): 360 """Insert the lists of pressures of all finger sizes to a new worksheet. 361 362 @param title: Title of the table 363 @param header_row: Row of the header in the table 364 @param start_row: starting row of data in the table 365 """ 366 tables = self.ss_client.get_tables(self.spreadsheet_key) 367 for table in tables.entry: 368 if table.title.text == title: 369 return table 370 # table is not created yet 371 table = self.ss_client.add_table(self.spreadsheet_key, 372 title, 373 title, 374 SUMMARY_WORKSHEET_TITLE, 375 header_row, 376 0, 377 start_row, 378 gdata.spreadsheets.data.INSERT_MODE, 379 results_table) 380 # add additional table descriptions 381 if title == 'Computed Pressures': 382 row = self.COMPUTED_TABLE_ROW - 1 383 for col in range(len(surface_title)): 384 self.summary_feed.add_set_cell(row, col + 1, surface_title[col]) 385 for col in range(len(diameter_title)): 386 self.summary_feed.add_set_cell(row + 1, col + 1, 387 diameter_title[col]) 388 return table 389 390 def _delete_nonexist_table(self, title): 391 """Remove the nonexist table entries. 392 393 @param title: Title of the table 394 """ 395 tables = self.ss_client.get_tables(self.spreadsheet_key) 396 for table in tables.entry: 397 if table.title.text == title: 398 self.ss_client.delete(table) 399 400 def _get_summary_tables(self): 401 """Insert the results of all finger sizes to summary worksheet.""" 402 entry = self._get_worksheet_entry_by_title(SUMMARY_WORKSHEET_TITLE) 403 if not entry: 404 self._delete_nonexist_table(TABLE_MEASURED) 405 self._delete_nonexist_table(TABLE_COMPUTED) 406 entry = self.ss_client.add_worksheet(self.spreadsheet_key, 407 SUMMARY_WORKSHEET_TITLE, 408 self.WORKSHEET_ROW_COUNT, 409 self.WORKSHEET_COL_COUNT) 410 self.summary_feed = gdata.spreadsheets.data.build_batch_cells_update( 411 self.spreadsheet_key, entry.get_worksheet_id()) 412 table_row = self.MEASURED_TABLE_ROW 413 self.measured_table = self._get_summary_table(TABLE_MEASURED, 414 table_row, 415 table_row + 1) 416 table_row = self.COMPUTED_TABLE_ROW 417 self.computed_table = self._get_summary_table(TABLE_COMPUTED, 418 table_row, 419 table_row + 1) 420 421def get_worksheet_title(): 422 """Get the worksheet title.""" 423 worksheet_title = '' 424 if options.name: 425 return options.name 426 while not worksheet_title: 427 print '\nInput the name of the new worksheet to insert the events.' 428 print ('This is usually the board name with the firmware version, ' 429 'e.g., Lumpy 11.27') 430 worksheet_title = raw_input('Input the new worksheet name: ') 431 return worksheet_title 432 433def print_slope_intercept(worksheet_title): 434 """read calibration data from worksheet and print it on command line 435 436 @param worksheet_title title of the worksheet to pull info from 437 """ 438 # init client 439 ss_client = gdata.spreadsheets.client.SpreadsheetsClient() 440 authorizer = SpreadsheetAuthorizer() 441 if not authorizer.authorize(ss_client): 442 raise "Please check the access permission of the spreadsheet" 443 444 # look up the worksheet id 445 worksheet_id = None 446 worksheet_feed = ss_client.get_worksheets(TARGET_SPREADSHEET_KEY) 447 for entry in worksheet_feed.entry: 448 if entry.title.text == worksheet_title: 449 worksheet_id = entry.get_worksheet_id() 450 if not worksheet_id: 451 raise "cannot find worksheet" + worksheet_title 452 453 # print calibration info 454 slope_cell = ss_client.get_cell(TARGET_SPREADSHEET_KEY, 455 worksheet_id, 456 SLOPE_CELL[0], SLOPE_CELL[1]) 457 print "slope=" + slope_cell.cell.numeric_value 458 459 intercept_cell = ss_client.get_cell(TARGET_SPREADSHEET_KEY, 460 worksheet_id, 461 INTERCEPT_CELL[0], INTERCEPT_CELL[1]) 462 print "intercept=" + intercept_cell.cell.numeric_value 463 464def main(): 465 """Parse the gesture events and insert them to the spreadsheet.""" 466 worksheet_title = get_worksheet_title() 467 if options.print_info: 468 print_slope_intercept(worksheet_title) 469 return 470 471 # Get the gesture event files and parse the events. 472 list_of_pressure_dicts = GestureEventFiles().list_of_pressure_dicts 473 474 # Access the spreadsheet, and create a new worksheet to insert the events. 475 ss = PressureSpreadsheet(worksheet_title) 476 ss.insert_pressures_to_worksheet(list_of_pressure_dicts) 477 478if __name__ == '__main__': 479 parser = OptionParser() 480 parser.add_option('-d', '--device', 481 dest='device', default=None, 482 help='device ip address to connect to') 483 parser.add_option('--result-dir', 484 dest='result_dir', default=None, 485 help='test results directory on the device') 486 parser.add_option('-v', '--verbose', 487 dest='verbose', default=False, action='store_true', 488 help='verbose debug output') 489 parser.add_option('-n', '--name', 490 dest='name', default=None, 491 help='worksheet name') 492 parser.add_option('--print-info', 493 dest='print_info', default=False, action="store_true", 494 help='print pressure calibration info only') 495 (options, args) = parser.parse_args() 496 if len(args) > 0: 497 parser.print_help() 498 exit(-1) 499 500 options.verbose = options.verbose 501 device_ip = options.device 502 result_dir = options.result_dir 503 main() 504