device.py revision 87b68b020dd72c4cdcf3b8c1f9196c060f947991
1# Copyright 2013 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import its.error 16import os 17import os.path 18import sys 19import re 20import json 21import tempfile 22import time 23import unittest 24import subprocess 25 26class ItsSession(object): 27 """Controls a device over adb to run ITS scripts. 28 29 The script importing this module (on the host machine) prepares JSON 30 objects encoding CaptureRequests, specifying sets of parameters to use 31 when capturing an image using the Camera2 APIs. This class encapsualtes 32 sending the requests to the device, monitoring the device's progress, and 33 copying the resultant captures back to the host machine when done. 34 35 The device must have ItsService.apk installed. 36 37 The "adb logcat" command is used to receive messages from the service 38 running on the device. 39 40 Attributes: 41 proc: The handle to the process in which "adb logcat" is invoked. 42 logcat: The stdout stream from the logcat process. 43 """ 44 45 # TODO: Handle multiple connected devices. 46 # The adb program is used for communication with the device. Need to handle 47 # the case of multiple devices connected. Currently, uses the "-d" param 48 # to adb, which causes it to fail if there is more than one device. 49 ADB = "adb -d" 50 51 DEVICE_FOLDER_ROOT = '/sdcard/its' 52 DEVICE_FOLDER_CAPTURE = 'captures' 53 INTENT_CAPTURE = 'com.android.camera2.its.CAPTURE' 54 INTENT_3A = 'com.android.camera2.its.3A' 55 INTENT_GETPROPS = 'com.android.camera2.its.GETPROPS' 56 TAG = 'CAMERA-ITS-PY' 57 58 MSG_RECV = "RECV" 59 MSG_SIZE = "SIZE" 60 MSG_FILE = "FILE" 61 MSG_CAPT = "CAPT" 62 MSG_DONE = "DONE" 63 MSG_FAIL = "FAIL" 64 MSG_AF = "3A-F" 65 MSG_AE = "3A-E" 66 MSG_AWB = "3A-W" 67 MSGS = [MSG_RECV, MSG_SIZE, MSG_FILE, MSG_CAPT, MSG_DONE, 68 MSG_FAIL, MSG_AE, MSG_AF, MSG_AWB] 69 70 def __init__(self): 71 self.proc = None 72 reboot_device_on_argv() 73 self.__open_logcat() 74 75 def __del__(self): 76 self.__kill_logcat() 77 78 def __enter__(self): 79 return self 80 81 def __exit__(self, type, value, traceback): 82 return False 83 84 def __open_logcat(self): 85 """Opens the "adb logcat" stream. 86 87 Internal function, called by this class's constructor. 88 89 Gets the adb logcat stream that is intended for parsing by this python 90 script. Flushes it first to clear out existing messages. 91 92 Populates the proc and logcat members of this class. 93 """ 94 _run('%s logcat -c' % (self.ADB)) 95 self.proc = subprocess.Popen( 96 self.ADB.split() + ["logcat", "-s", "'%s:v'" % (self.TAG)], 97 stdout=subprocess.PIPE) 98 self.logcat = self.proc.stdout 99 100 def __get_next_msg(self): 101 """Gets the next message from the logcat stream. 102 103 Reads from the logcat stdout stream. Blocks until a new line is ready, 104 but exits in the event of a keyboard interrupt (to allow the script to 105 be Ctrl-C killed). 106 107 If the special message "FAIL" is received, kills the script; the test 108 shouldn't continue running if something went wrong. The user can then 109 manually inspect the device to see what the problem is, for example by 110 looking at logcat themself. 111 112 Returns: 113 The next string from the logcat stdout stream. 114 """ 115 while True: 116 # Get the next logcat line. 117 line = self.logcat.readline().strip() 118 # Get the message, which is the string following the "###" code. 119 idx = line.find('### ') 120 if idx >= 0: 121 msg = line[idx+4:] 122 if self.__unpack_msg(msg)[0] == self.MSG_FAIL: 123 raise its.error.Error('FAIL device msg received') 124 return msg 125 126 def __kill_logcat(self): 127 """Kill the logcat process. 128 129 Internal function called by this class's destructor. 130 """ 131 if self.proc: 132 self.proc.kill() 133 134 def __send_intent(self, intent_string, intent_params=None): 135 """Send an intent to the device. 136 137 Takes a Python object object specifying the operation to be performed 138 on the device, converts it to JSON, sends it to the device over adb, 139 then sends an intent to ItsService.apk running on the device with 140 the path to that JSON file (including starting the service). 141 142 Args: 143 intent_string: The string corresponding to the intent to send (3A 144 or capture). 145 intent_params: A Python dictionary object containing the operations 146 to perform; for a capture intent, the dict. contains either 147 captureRequest or captureRequestList key, and for a 3A intent, 148 the dictionary contains a 3A params key. 149 """ 150 _run('%s shell mkdir -p "%s"' % ( 151 self.ADB, self.DEVICE_FOLDER_ROOT)) 152 intent_args = "" 153 if intent_params: 154 with tempfile.NamedTemporaryFile( 155 mode="w", suffix=".json", delete=False) as f: 156 tmpfname = f.name 157 f.write(json.dumps(intent_params)) 158 _run('%s push %s %s' % ( 159 self.ADB, tmpfname, self.DEVICE_FOLDER_ROOT)) 160 os.remove(tmpfname) 161 intent_args = ' -d "file://%s/%s"' % ( 162 self.DEVICE_FOLDER_ROOT, os.path.basename(tmpfname)) 163 # TODO: Figure out why "--user 0" is needed, and fix the problem 164 _run(('%s shell am startservice --user 0 -t text/plain ' 165 '-a %s%s') % (self.ADB, intent_string, intent_args)) 166 167 def __start_capture(self, request): 168 self.__send_intent(self.INTENT_CAPTURE, request) 169 170 def __start_3a(self, params): 171 self.__send_intent(self.INTENT_3A, params) 172 173 def __start_getprops(self): 174 self.__send_intent(self.INTENT_GETPROPS) 175 176 def __unpack_msg(self, msg): 177 """Process a string containing a coded message from the device. 178 179 The logcat messages intended to be parsed by this script are of the 180 following form: 181 RECV - Indicates capture command was received 182 SIZE <WIDTH> <HEIGHT> - The width,height of the captured image 183 FILE <PATH> - The path on the device of the captured image 184 CAPT <I> of <N> - Indicates capt cmd #I out of #N was issued 185 DONE - Indicates the capture sequence completed 186 FAIL - Indicates an error occurred 187 188 Args: 189 msg: The string message from the device. 190 191 Returns: 192 Tuple containing the message type (a string) and the message 193 payload (a list). 194 """ 195 a = msg.split() 196 if a[0] not in self.MSGS: 197 raise its.error.Error('Invalid device message: %s' % (msg)) 198 return a[0], a[1:] 199 200 def __wait_for_camera_properties(self): 201 """Block until the requested camera properties object is available. 202 203 Monitors messages from the service on the device (via logcat), looking 204 for special coded messages that indicate the status of the request. 205 206 Returns: 207 The remote path (on the device) where the camera properties JSON 208 file is stored. 209 """ 210 fname = None 211 msg = self.__get_next_msg() 212 if self.__unpack_msg(msg)[0] != self.MSG_RECV: 213 raise its.error.Error('Device msg not RECV: %s' % (msg)) 214 while True: 215 msg = self.__get_next_msg() 216 msgtype, msgparams = self.__unpack_msg(msg) 217 if msgtype == self.MSG_FILE: 218 fname = msgparams[0] 219 elif msgtype == self.MSG_DONE: 220 return fname 221 222 def __wait_for_capture_done_single(self): 223 """Block until a single capture is done. 224 225 Monitors messages from the service on the device (via logcat), looking 226 for special coded messages that indicate the status of the captures. 227 228 Returns: 229 The remote path (on the device) where the image file was stored, 230 along with the image's width and height. 231 """ 232 fname = None 233 w = None 234 h = None 235 msg = self.__get_next_msg() 236 if self.__unpack_msg(msg)[0] != self.MSG_RECV: 237 raise its.error.Error('Device msg not RECV: %s' % (msg)) 238 while True: 239 msg = self.__get_next_msg() 240 msgtype, msgparams = self.__unpack_msg(msg) 241 if msgtype == self.MSG_SIZE: 242 w = int(msgparams[0]) 243 h = int(msgparams[1]) 244 elif msgtype == self.MSG_FILE: 245 fname = msgparams[0] 246 elif msgtype == self.MSG_DONE: 247 return fname, w, h 248 249 def __wait_for_capture_done_burst(self, num_req): 250 """Block until a burst of captures is done. 251 252 Monitors messages from the service on the device (via logcat), looking 253 for special coded messages that indicate the status of the captures. 254 255 Args: 256 num_req: The number of captures to wait for. 257 258 Returns: 259 The remote paths (on the device) where the image files were stored, 260 along with their width and height. 261 """ 262 fnames = [] 263 w = None 264 h = None 265 msg = self.__get_next_msg() 266 if self.__unpack_msg(msg)[0] != self.MSG_RECV: 267 raise its.error.Error('Device msg not RECV: %s' % (msg)) 268 while True: 269 msg = self.__get_next_msg() 270 msgtype, msgparams = self.__unpack_msg(msg) 271 if msgtype == self.MSG_SIZE: 272 w = int(msgparams[0]) 273 h = int(msgparams[1]) 274 elif msgtype == self.MSG_FILE: 275 fnames.append(msgparams[0]) 276 elif msgtype == self.MSG_DONE: 277 if len(fnames) != num_req or not w or not h: 278 raise its.error.Error('Missing FILE or SIZE device msg') 279 return fnames, w, h 280 281 def __get_json_path(self, image_fname): 282 """Get the path of the JSON metadata file associated with an image. 283 284 Args: 285 image_fname: Path of the image file (local or remote). 286 287 Returns: 288 The path of the associated JSON metadata file, which has the same 289 basename but different extension. 290 """ 291 base, ext = os.path.splitext(image_fname) 292 return base + ".json" 293 294 def __copy_captured_files(self, remote_fnames): 295 """Copy captured data from device back to host machine over adb. 296 297 Copy captured images and associated metadata from the device to the 298 host machine. The image and metadata files have the same basename, but 299 different file extensions; the captured image is .yuv/.jpg/.raw, and 300 the captured metadata is .json. 301 302 File names are unique, as each has the timestamp of the capture in it. 303 304 Deletes the files from the device after they have been transferred off. 305 306 Args: 307 remote_fnames: List of paths of the captured image files on the 308 remote device. 309 310 Returns: 311 List of paths of captured image files on the local host machine 312 (which is just in the current directory). 313 """ 314 local_fnames = [] 315 for fname in remote_fnames: 316 _run('%s pull %s .' % (self.ADB, fname)) 317 _run('%s pull %s .' % ( 318 self.ADB, self.__get_json_path(fname))) 319 local_fnames.append(os.path.basename(fname)) 320 _run('%s shell rm -rf %s/*' % (self.ADB, self.DEVICE_FOLDER_ROOT)) 321 return local_fnames 322 323 def __parse_captured_json(self, local_fnames): 324 """Parse the JSON objects that are returned alongside captured images. 325 326 Args: 327 local_fnames: List of paths of captured image on the local machine. 328 329 Returns: 330 List of Python objects obtained from loading the argument files 331 and converting from the JSON object form to native Python. 332 """ 333 return [json.load(open(self.__get_json_path(f))) for f in local_fnames] 334 335 def get_camera_properties(self): 336 """Get the camera properties object for the device. 337 338 Returns: 339 The Python dictionary object for the CameraProperties object. 340 """ 341 self.__start_getprops() 342 remote_fname = self.__wait_for_camera_properties() 343 _run('%s pull %s .' % (self.ADB, remote_fname)) 344 local_fname = os.path.basename(remote_fname) 345 return self.__parse_captured_json([local_fname])[0]['cameraProperties'] 346 347 def do_3a(self, region_ae, region_af, region_awb, 348 do_ae=True, do_awb=True, do_af=True): 349 """Perform a 3A operation on the device. 350 351 Triggers some or all of AE, AWB, and AF, and returns once they have 352 converged. Uses the vendor 3A that is implemented inside the HAL. 353 354 Throws an assertion if 3A fails to converge. 355 356 Args: 357 region_ae: Normalized rect. (x,y,w,h) specifying the AE region. 358 region_af: Normalized rect. (x,y,w,h) specifying the AF region. 359 region_awb: Normalized rect. (x,y,w,h) specifying the AWB region. 360 361 Returns: 362 Five values: 363 * AE sensitivity; None if do_ae is False 364 * AE exposure time; None if do_ae is False 365 * AWB gains (list); None if do_awb is False 366 * AWB transform (list); None if do_awb is false 367 * AF focus position; None if do_af is false 368 """ 369 params = {"regions" : {"ae": region_ae, 370 "awb": region_awb, 371 "af": region_af }, 372 "triggers": {"ae": do_ae, 373 "af": do_af } } 374 print "Running vendor 3A on device" 375 self.__start_3a(params) 376 ae_sens = None 377 ae_exp = None 378 awb_gains = None 379 awb_transform = None 380 af_dist = None 381 while True: 382 msg = self.__get_next_msg() 383 msgtype, msgparams = self.__unpack_msg(msg) 384 if msgtype == self.MSG_AE: 385 ae_sens = int(msgparams[0]) 386 ae_exp = int(msgparams[1]) 387 elif msgtype == self.MSG_AWB: 388 awb_gains = [float(x) for x in msgparams[:4]] 389 awb_transform = [float(x) for x in msgparams[4:]] 390 elif msgtype == self.MSG_AF: 391 af_dist = float(msgparams[0]) 392 elif msgtype == self.MSG_DONE: 393 if (do_ae and ae_sens == None or do_awb and awb_gains == None 394 or do_af and af_dist == None): 395 raise its.error.Error('3A failed to converge') 396 return ae_sens, ae_exp, awb_gains, awb_transform, af_dist 397 398 def do_capture(self, request, out_fname_prefix=None): 399 """Issue capture request(s), and read back the image(s) and metadata. 400 401 The main top-level function for capturing one or more images using the 402 device. Captures a single image if the request has the "captureRequest" 403 key, and captures a burst if it has "captureRequestList". 404 405 The request object may also contain an "outputSurface" field to specify 406 the width, height, and format of the captured image. Supported formats 407 are "yuv" and "jpeg". If no outputSurface field was passed inside the 408 request object, then the default is used, which is "yuv" (a YUV420 409 fully planar image) corresponding to a full sensor frame. 410 411 Example request 1: 412 413 { 414 "captureRequest": { 415 "android.sensor.exposureTime": 100*1000*1000, 416 "android.sensor.sensitivity": 100 417 } 418 } 419 420 Example request 2: 421 422 { 423 "captureRequestList": [ 424 { 425 "android.sensor.exposureTime": 100*1000*1000, 426 "android.sensor.sensitivity": 100 427 }, 428 { 429 "android.sensor.exposureTime": 100*1000*1000, 430 "android.sensor.sensitivity": 200 431 }], 432 "outputSurface": { 433 "width": 640, 434 "height": 480, 435 "format": "yuv" 436 } 437 } 438 439 Args: 440 request: The Python dictionary specifying the capture(s), which 441 will be converted to JSON and sent to the device. 442 out_fname_prefix: (Optionally) the file name prefix to use for the 443 captured files. If this arg is present, then the captured files 444 will be renamed appropriately. 445 446 Returns: 447 Four values: 448 * The path or list of paths of the captured images (depending on 449 whether the request was for a single or burst capture). The paths 450 are on the host machine. The captured metadata file(s) have the 451 same file names as their corresponding images, with a ".json" 452 extension. 453 * The width and height of the captured image(s). For a burst, all 454 are the same size. 455 * The Python dictionary or list of dictionaries (in the case of a 456 burst capture) containing the metadata of the captured image(s). 457 """ 458 if request.has_key("captureRequest"): 459 460 print "Capturing image (including a pre-shot for settings synch)" 461 462 # HACK: Take a pre-shot, to make sure the settings stick. 463 # TODO: Remove this code once it is no longer needed. 464 self.__start_capture(request) 465 self.__wait_for_capture_done_single() 466 467 self.__start_capture(request) 468 remote_fname, w, h = self.__wait_for_capture_done_single() 469 local_fname = self.__copy_captured_files([remote_fname])[0] 470 out_metadata_obj = self.__parse_captured_json([local_fname])[0] 471 if out_fname_prefix: 472 _, image_ext = os.path.splitext(local_fname) 473 os.rename(local_fname, out_fname_prefix + image_ext) 474 os.rename(self.__get_json_path(local_fname), 475 out_fname_prefix + ".json") 476 local_fname = out_fname_prefix + image_ext 477 return local_fname, w, h, out_metadata_obj 478 else: 479 if not request.has_key("captureRequestList"): 480 raise its.error.Error( 481 'Missing captureRequest or captureRequestList arg key') 482 n = len(request['captureRequestList']) 483 print "Capture burst of %d images" % (n) 484 self.__start_capture(request) 485 remote_fnames, w, h = self.__wait_for_capture_done_burst(n) 486 local_fnames = self.__copy_captured_files(remote_fnames) 487 out_metadata_objs = self.__parse_captured_json(local_fnames) 488 if out_fname_prefix is not None: 489 for i in range(len(local_fnames)): 490 _, image_ext = os.path.splitext(local_fnames[i]) 491 os.rename(local_fnames[i], 492 "%s-%04d%s" % (out_fname_prefix, i, image_ext)) 493 os.rename(self.__get_json_path(local_fnames[i]), 494 "%s-%04d.json" % (out_fname_prefix, i)) 495 local_fnames[i] = out_fname_prefix + image_ext 496 return local_fnames, w, h, out_metadata_objs 497 498def _run(cmd): 499 """Replacement for os.system, with hiding of stdout+stderr messages. 500 """ 501 with open(os.devnull, 'wb') as devnull: 502 subprocess.check_call( 503 cmd.split(), stdout=devnull, stderr=subprocess.STDOUT) 504 505def reboot_device(sleep_duration=30): 506 """Function to reboot a device and block until it is ready. 507 508 Can be used at the start of a test to get the device into a known good 509 state. Will disconnect any other adb sessions, so this function is not 510 a part of the ItsSession class (which encapsulates a session with a 511 device.) 512 513 Args: 514 sleep_duration: (Optional) the length of time to sleep (seconds) after 515 the device comes online before returning; this gives the device 516 time to finish booting. 517 """ 518 print "Rebooting device" 519 _run("%s reboot" % (ItsSession.ADB)); 520 _run("%s wait-for-device" % (ItsSession.ADB)) 521 time.sleep(sleep_duration) 522 print "Reboot complete" 523 524def reboot_device_on_argv(): 525 """Examine sys.argv, and reboot if the "reboot" arg is present. 526 527 If the script command line contains either: 528 529 reboot 530 reboot=30 531 532 then the device will be rebooted, and if the optional numeric arg is 533 present, then that will be the sleep duration passed to the reboot 534 call. 535 536 Returns: 537 Boolean, indicating whether the device was rebooted. 538 """ 539 for s in sys.argv[1:]: 540 if s[:6] == "reboot": 541 if len(s) > 7 and s[6] == "=": 542 duration = int(s[7:]) 543 reboot_device(duration) 544 elif len(s) == 6: 545 reboot_device() 546 return True 547 return False 548 549class __UnitTest(unittest.TestCase): 550 """Run a suite of unit tests on this module. 551 """ 552 553 # TODO: Add some unit tests. 554 None 555 556if __name__ == '__main__': 557 unittest.main() 558 559