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 15 import its.error 16 import os 17 import os.path 18 import sys 19 import re 20 import json 21 import time 22 import unittest 23 import socket 24 import subprocess 25 import hashlib 26 import numpy 27 28 class ItsSession(object): 29 """Controls a device over adb to run ITS scripts. 30 31 The script importing this module (on the host machine) prepares JSON 32 objects encoding CaptureRequests, specifying sets of parameters to use 33 when capturing an image using the Camera2 APIs. This class encapsulates 34 sending the requests to the device, monitoring the device's progress, and 35 copying the resultant captures back to the host machine when done. TCP 36 forwarded over adb is the transport mechanism used. 37 38 The device must have CtsVerifier.apk installed. 39 40 Attributes: 41 sock: The open socket. 42 """ 43 44 # Open a connection to localhost:6000, forwarded to port 6000 on the device. 45 # TODO: Support multiple devices running over different TCP ports. 46 IPADDR = '127.0.0.1' 47 PORT = 6000 48 BUFFER_SIZE = 4096 49 50 # Seconds timeout on each socket operation. 51 SOCK_TIMEOUT = 10.0 52 53 PACKAGE = 'com.android.cts.verifier.camera.its' 54 INTENT_START = 'com.android.cts.verifier.camera.its.START' 55 ACTION_ITS_RESULT = 'com.android.cts.verifier.camera.its.ACTION_ITS_RESULT' 56 EXTRA_SUCCESS = 'camera.its.extra.SUCCESS' 57 58 # TODO: Handle multiple connected devices. 59 ADB = "adb -d" 60 61 # Definitions for some of the common output format options for do_capture(). 62 # Each gets images of full resolution for each requested format. 63 CAP_RAW = {"format":"raw"} 64 CAP_DNG = {"format":"dng"} 65 CAP_YUV = {"format":"yuv"} 66 CAP_JPEG = {"format":"jpeg"} 67 CAP_RAW_YUV = [{"format":"raw"}, {"format":"yuv"}] 68 CAP_DNG_YUV = [{"format":"dng"}, {"format":"yuv"}] 69 CAP_RAW_JPEG = [{"format":"raw"}, {"format":"jpeg"}] 70 CAP_DNG_JPEG = [{"format":"dng"}, {"format":"jpeg"}] 71 CAP_YUV_JPEG = [{"format":"yuv"}, {"format":"jpeg"}] 72 CAP_RAW_YUV_JPEG = [{"format":"raw"}, {"format":"yuv"}, {"format":"jpeg"}] 73 CAP_DNG_YUV_JPEG = [{"format":"dng"}, {"format":"yuv"}, {"format":"jpeg"}] 74 75 # Method to handle the case where the service isn't already running. 76 # This occurs when a test is invoked directly from the command line, rather 77 # than as a part of a separate test harness which is setting up the device 78 # and the TCP forwarding. 79 def __pre_init(self): 80 81 # This also includes the optional reboot handling: if the user 82 # provides a "reboot" or "reboot=N" arg, then reboot the device, 83 # waiting for N seconds (default 30) before returning. 84 for s in sys.argv[1:]: 85 if s[:6] == "reboot": 86 duration = 30 87 if len(s) > 7 and s[6] == "=": 88 duration = int(s[7:]) 89 print "Rebooting device" 90 _run("%s reboot" % (ItsSession.ADB)); 91 _run("%s wait-for-device" % (ItsSession.ADB)) 92 time.sleep(duration) 93 print "Reboot complete" 94 95 # TODO: Figure out why "--user 0" is needed, and fix the problem. 96 _run('%s shell am force-stop --user 0 %s' % (ItsSession.ADB, self.PACKAGE)) 97 _run(('%s shell am startservice --user 0 -t text/plain ' 98 '-a %s') % (ItsSession.ADB, self.INTENT_START)) 99 100 # Wait until the socket is ready to accept a connection. 101 proc = subprocess.Popen( 102 ItsSession.ADB.split() + ["logcat"], 103 stdout=subprocess.PIPE) 104 logcat = proc.stdout 105 while True: 106 line = logcat.readline().strip() 107 if line.find('ItsService ready') >= 0: 108 break 109 proc.kill() 110 111 # Setup the TCP-over-ADB forwarding. 112 _run('%s forward tcp:%d tcp:%d' % (ItsSession.ADB,self.PORT,self.PORT)) 113 114 def __init__(self): 115 if "noinit" not in sys.argv: 116 self.__pre_init() 117 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 118 self.sock.connect((self.IPADDR, self.PORT)) 119 self.sock.settimeout(self.SOCK_TIMEOUT) 120 self.__close_camera() 121 self.__open_camera() 122 123 def __del__(self): 124 if hasattr(self, 'sock') and self.sock: 125 self.__close_camera() 126 self.sock.close() 127 128 def __enter__(self): 129 return self 130 131 def __exit__(self, type, value, traceback): 132 return False 133 134 def __read_response_from_socket(self): 135 # Read a line (newline-terminated) string serialization of JSON object. 136 chars = [] 137 while len(chars) == 0 or chars[-1] != '\n': 138 ch = self.sock.recv(1) 139 if len(ch) == 0: 140 # Socket was probably closed; otherwise don't get empty strings 141 raise its.error.Error('Problem with socket on device side') 142 chars.append(ch) 143 line = ''.join(chars) 144 jobj = json.loads(line) 145 # Optionally read a binary buffer of a fixed size. 146 buf = None 147 if jobj.has_key("bufValueSize"): 148 n = jobj["bufValueSize"] 149 buf = bytearray(n) 150 view = memoryview(buf) 151 while n > 0: 152 nbytes = self.sock.recv_into(view, n) 153 view = view[nbytes:] 154 n -= nbytes 155 buf = numpy.frombuffer(buf, dtype=numpy.uint8) 156 return jobj, buf 157 158 def __open_camera(self): 159 # Get the camera ID to open as an argument. 160 camera_id = 0 161 for s in sys.argv[1:]: 162 if s[:7] == "camera=" and len(s) > 7: 163 camera_id = int(s[7:]) 164 cmd = {"cmdName":"open", "cameraId":camera_id} 165 self.sock.send(json.dumps(cmd) + "\n") 166 data,_ = self.__read_response_from_socket() 167 if data['tag'] != 'cameraOpened': 168 raise its.error.Error('Invalid command response') 169 170 def __close_camera(self): 171 cmd = {"cmdName":"close"} 172 self.sock.send(json.dumps(cmd) + "\n") 173 data,_ = self.__read_response_from_socket() 174 if data['tag'] != 'cameraClosed': 175 raise its.error.Error('Invalid command response') 176 177 def do_vibrate(self, pattern): 178 """Cause the device to vibrate to a specific pattern. 179 180 Args: 181 pattern: Durations (ms) for which to turn on or off the vibrator. 182 The first value indicates the number of milliseconds to wait 183 before turning the vibrator on. The next value indicates the 184 number of milliseconds for which to keep the vibrator on 185 before turning it off. Subsequent values alternate between 186 durations in milliseconds to turn the vibrator off or to turn 187 the vibrator on. 188 189 Returns: 190 Nothing. 191 """ 192 cmd = {} 193 cmd["cmdName"] = "doVibrate" 194 cmd["pattern"] = pattern 195 self.sock.send(json.dumps(cmd) + "\n") 196 data,_ = self.__read_response_from_socket() 197 if data['tag'] != 'vibrationStarted': 198 raise its.error.Error('Invalid command response') 199 200 def start_sensor_events(self): 201 """Start collecting sensor events on the device. 202 203 See get_sensor_events for more info. 204 205 Returns: 206 Nothing. 207 """ 208 cmd = {} 209 cmd["cmdName"] = "startSensorEvents" 210 self.sock.send(json.dumps(cmd) + "\n") 211 data,_ = self.__read_response_from_socket() 212 if data['tag'] != 'sensorEventsStarted': 213 raise its.error.Error('Invalid command response') 214 215 def get_sensor_events(self): 216 """Get a trace of all sensor events on the device. 217 218 The trace starts when the start_sensor_events function is called. If 219 the test runs for a long time after this call, then the device's 220 internal memory can fill up. Calling get_sensor_events gets all events 221 from the device, and then stops the device from collecting events and 222 clears the internal buffer; to start again, the start_sensor_events 223 call must be used again. 224 225 Events from the accelerometer, compass, and gyro are returned; each 226 has a timestamp and x,y,z values. 227 228 Note that sensor events are only produced if the device isn't in its 229 standby mode (i.e.) if the screen is on. 230 231 Returns: 232 A Python dictionary with three keys ("accel", "mag", "gyro") each 233 of which maps to a list of objects containing "time","x","y","z" 234 keys. 235 """ 236 cmd = {} 237 cmd["cmdName"] = "getSensorEvents" 238 self.sock.send(json.dumps(cmd) + "\n") 239 data,_ = self.__read_response_from_socket() 240 if data['tag'] != 'sensorEvents': 241 raise its.error.Error('Invalid command response') 242 return data['objValue'] 243 244 def get_camera_properties(self): 245 """Get the camera properties object for the device. 246 247 Returns: 248 The Python dictionary object for the CameraProperties object. 249 """ 250 cmd = {} 251 cmd["cmdName"] = "getCameraProperties" 252 self.sock.send(json.dumps(cmd) + "\n") 253 data,_ = self.__read_response_from_socket() 254 if data['tag'] != 'cameraProperties': 255 raise its.error.Error('Invalid command response') 256 return data['objValue']['cameraProperties'] 257 258 def do_3a(self, regions_ae=[[0,0,1,1,1]], 259 regions_awb=[[0,0,1,1,1]], 260 regions_af=[[0,0,1,1,1]], 261 do_ae=True, do_awb=True, do_af=True, 262 lock_ae=False, lock_awb=False, 263 get_results=False, 264 ev_comp=0): 265 """Perform a 3A operation on the device. 266 267 Triggers some or all of AE, AWB, and AF, and returns once they have 268 converged. Uses the vendor 3A that is implemented inside the HAL. 269 270 Throws an assertion if 3A fails to converge. 271 272 Args: 273 regions_ae: List of weighted AE regions. 274 regions_awb: List of weighted AWB regions. 275 regions_af: List of weighted AF regions. 276 do_ae: Trigger AE and wait for it to converge. 277 do_awb: Wait for AWB to converge. 278 do_af: Trigger AF and wait for it to converge. 279 lock_ae: Request AE lock after convergence, and wait for it. 280 lock_awb: Request AWB lock after convergence, and wait for it. 281 get_results: Return the 3A results from this function. 282 ev_comp: An EV compensation value to use when running AE. 283 284 Region format in args: 285 Arguments are lists of weighted regions; each weighted region is a 286 list of 5 values, [x,y,w,h, wgt], and each argument is a list of 287 these 5-value lists. The coordinates are given as normalized 288 rectangles (x,y,w,h) specifying the region. For example: 289 [[0.0, 0.0, 1.0, 0.5, 5], [0.0, 0.5, 1.0, 0.5, 10]]. 290 Weights are non-negative integers. 291 292 Returns: 293 Five values are returned if get_results is true:: 294 * AE sensitivity; None if do_ae is False 295 * AE exposure time; None if do_ae is False 296 * AWB gains (list); None if do_awb is False 297 * AWB transform (list); None if do_awb is false 298 * AF focus position; None if do_af is false 299 Otherwise, it returns five None values. 300 """ 301 print "Running vendor 3A on device" 302 cmd = {} 303 cmd["cmdName"] = "do3A" 304 cmd["regions"] = {"ae": sum(regions_ae, []), 305 "awb": sum(regions_awb, []), 306 "af": sum(regions_af, [])} 307 cmd["triggers"] = {"ae": do_ae, "af": do_af} 308 if lock_ae: 309 cmd["aeLock"] = True 310 if lock_awb: 311 cmd["awbLock"] = True 312 if ev_comp != 0: 313 cmd["evComp"] = ev_comp 314 self.sock.send(json.dumps(cmd) + "\n") 315 316 # Wait for each specified 3A to converge. 317 ae_sens = None 318 ae_exp = None 319 awb_gains = None 320 awb_transform = None 321 af_dist = None 322 converged = False 323 while True: 324 data,_ = self.__read_response_from_socket() 325 vals = data['strValue'].split() 326 if data['tag'] == 'aeResult': 327 ae_sens, ae_exp = [int(i) for i in vals] 328 elif data['tag'] == 'afResult': 329 af_dist = float(vals[0]) 330 elif data['tag'] == 'awbResult': 331 awb_gains = [float(f) for f in vals[:4]] 332 awb_transform = [float(f) for f in vals[4:]] 333 elif data['tag'] == '3aConverged': 334 converged = True 335 elif data['tag'] == '3aDone': 336 break 337 else: 338 raise its.error.Error('Invalid command response') 339 if converged and not get_results: 340 return None,None,None,None,None 341 if (do_ae and ae_sens == None or do_awb and awb_gains == None 342 or do_af and af_dist == None or not converged): 343 raise its.error.Error('3A failed to converge') 344 return ae_sens, ae_exp, awb_gains, awb_transform, af_dist 345 346 def do_capture(self, cap_request, out_surfaces=None): 347 """Issue capture request(s), and read back the image(s) and metadata. 348 349 The main top-level function for capturing one or more images using the 350 device. Captures a single image if cap_request is a single object, and 351 captures a burst if it is a list of objects. 352 353 The out_surfaces field can specify the width(s), height(s), and 354 format(s) of the captured image. The formats may be "yuv", "jpeg", 355 "dng", "raw", or "raw10". The default is a YUV420 frame ("yuv") 356 corresponding to a full sensor frame. 357 358 Note that one or more surfaces can be specified, allowing a capture to 359 request images back in multiple formats (e.g.) raw+yuv, raw+jpeg, 360 yuv+jpeg, raw+yuv+jpeg. If the size is omitted for a surface, the 361 default is the largest resolution available for the format of that 362 surface. At most one output surface can be specified for a given format, 363 and raw+dng, raw10+dng, and raw+raw10 are not supported as combinations. 364 365 Example of a single capture request: 366 367 { 368 "android.sensor.exposureTime": 100*1000*1000, 369 "android.sensor.sensitivity": 100 370 } 371 372 Example of a list of capture requests: 373 374 [ 375 { 376 "android.sensor.exposureTime": 100*1000*1000, 377 "android.sensor.sensitivity": 100 378 }, 379 { 380 "android.sensor.exposureTime": 100*1000*1000, 381 "android.sensor.sensitivity": 200 382 } 383 ] 384 385 Examples of output surface specifications: 386 387 { 388 "width": 640, 389 "height": 480, 390 "format": "yuv" 391 } 392 393 [ 394 { 395 "format": "jpeg" 396 }, 397 { 398 "format": "raw" 399 } 400 ] 401 402 The following variables defined in this class are shortcuts for 403 specifying one or more formats where each output is the full size for 404 that format; they can be used as values for the out_surfaces arguments: 405 406 CAP_RAW 407 CAP_DNG 408 CAP_YUV 409 CAP_JPEG 410 CAP_RAW_YUV 411 CAP_DNG_YUV 412 CAP_RAW_JPEG 413 CAP_DNG_JPEG 414 CAP_YUV_JPEG 415 CAP_RAW_YUV_JPEG 416 CAP_DNG_YUV_JPEG 417 418 If multiple formats are specified, then this function returns multiple 419 capture objects, one for each requested format. If multiple formats and 420 multiple captures (i.e. a burst) are specified, then this function 421 returns multiple lists of capture objects. In both cases, the order of 422 the returned objects matches the order of the requested formats in the 423 out_surfaces parameter. For example: 424 425 yuv_cap = do_capture( req1 ) 426 yuv_cap = do_capture( req1, yuv_fmt ) 427 yuv_cap, raw_cap = do_capture( req1, [yuv_fmt,raw_fmt] ) 428 yuv_caps = do_capture( [req1,req2], yuv_fmt ) 429 yuv_caps, raw_caps = do_capture( [req1,req2], [yuv_fmt,raw_fmt] ) 430 431 Args: 432 cap_request: The Python dict/list specifying the capture(s), which 433 will be converted to JSON and sent to the device. 434 out_surfaces: (Optional) specifications of the output image formats 435 and sizes to use for each capture. 436 437 Returns: 438 An object, list of objects, or list of lists of objects, where each 439 object contains the following fields: 440 * data: the image data as a numpy array of bytes. 441 * width: the width of the captured image. 442 * height: the height of the captured image. 443 * format: image the format, in ["yuv","jpeg","raw","raw10","dng"]. 444 * metadata: the capture result object (Python dictionary). 445 """ 446 cmd = {} 447 cmd["cmdName"] = "doCapture" 448 if not isinstance(cap_request, list): 449 cmd["captureRequests"] = [cap_request] 450 else: 451 cmd["captureRequests"] = cap_request 452 if out_surfaces is not None: 453 if not isinstance(out_surfaces, list): 454 cmd["outputSurfaces"] = [out_surfaces] 455 else: 456 cmd["outputSurfaces"] = out_surfaces 457 formats = [c["format"] if c.has_key("format") else "yuv" 458 for c in cmd["outputSurfaces"]] 459 formats = [s if s != "jpg" else "jpeg" for s in formats] 460 else: 461 formats = ['yuv'] 462 ncap = len(cmd["captureRequests"]) 463 nsurf = 1 if out_surfaces is None else len(cmd["outputSurfaces"]) 464 if len(formats) > len(set(formats)): 465 raise its.error.Error('Duplicate format requested') 466 if "dng" in formats and "raw" in formats or \ 467 "dng" in formats and "raw10" in formats or \ 468 "raw" in formats and "raw10" in formats: 469 raise its.error.Error('Different raw formats not supported') 470 print "Capturing %d frame%s with %d format%s [%s]" % ( 471 ncap, "s" if ncap>1 else "", nsurf, "s" if nsurf>1 else "", 472 ",".join(formats)) 473 self.sock.send(json.dumps(cmd) + "\n") 474 475 # Wait for ncap*nsurf images and ncap metadata responses. 476 # Assume that captures come out in the same order as requested in 477 # the burst, however individual images of different formats can come 478 # out in any order for that capture. 479 nbufs = 0 480 bufs = {"yuv":[], "raw":[], "raw10":[], "dng":[], "jpeg":[]} 481 mds = [] 482 widths = None 483 heights = None 484 while nbufs < ncap*nsurf or len(mds) < ncap: 485 jsonObj,buf = self.__read_response_from_socket() 486 if jsonObj['tag'] in ['jpegImage', 'yuvImage', 'rawImage', \ 487 'raw10Image', 'dngImage'] and buf is not None: 488 fmt = jsonObj['tag'][:-5] 489 bufs[fmt].append(buf) 490 nbufs += 1 491 elif jsonObj['tag'] == 'captureResults': 492 mds.append(jsonObj['objValue']['captureResult']) 493 outputs = jsonObj['objValue']['outputs'] 494 widths = [out['width'] for out in outputs] 495 heights = [out['height'] for out in outputs] 496 else: 497 # Just ignore other tags 498 None 499 rets = [] 500 for j,fmt in enumerate(formats): 501 objs = [] 502 for i in range(ncap): 503 obj = {} 504 obj["data"] = bufs[fmt][i] 505 obj["width"] = widths[j] 506 obj["height"] = heights[j] 507 obj["format"] = fmt 508 obj["metadata"] = mds[i] 509 objs.append(obj) 510 rets.append(objs if ncap>1 else objs[0]) 511 return rets if len(rets)>1 else rets[0] 512 513 def report_result(camera_id, success): 514 """Send a pass/fail result to the device, via an intent. 515 516 Args: 517 camera_id: The ID string of the camera for which to report pass/fail. 518 success: Boolean, indicating if the result was pass or fail. 519 520 Returns: 521 Nothing. 522 """ 523 resultstr = "%s=%s" % (camera_id, 'True' if success else 'False') 524 _run(('%s shell am broadcast ' 525 '-a %s --es %s %s') % (ItsSession.ADB, ItsSession.ACTION_ITS_RESULT, 526 ItsSession.EXTRA_SUCCESS, resultstr)) 527 528 529 def _run(cmd): 530 """Replacement for os.system, with hiding of stdout+stderr messages. 531 """ 532 with open(os.devnull, 'wb') as devnull: 533 subprocess.check_call( 534 cmd.split(), stdout=devnull, stderr=subprocess.STDOUT) 535 536 class __UnitTest(unittest.TestCase): 537 """Run a suite of unit tests on this module. 538 """ 539 540 # TODO: Add some unit tests. 541 None 542 543 if __name__ == '__main__': 544 unittest.main() 545 546