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