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 encapsualtes 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 ItsService.apk installed. 39 40 Attributes: 41 sock: The open socket. 42 """ 43 44 # TODO: Handle multiple connected devices. 45 # The adb program is used for communication with the device. Need to handle 46 # the case of multiple devices connected. Currently, uses the "-d" param 47 # to adb, which causes it to fail if there is more than one device. 48 ADB = "adb -d" 49 50 # Open a connection to localhost:6000, forwarded to port 6000 on the device. 51 # TODO: Support multiple devices running over different TCP ports. 52 IPADDR = '127.0.0.1' 53 PORT = 6000 54 BUFFER_SIZE = 4096 55 56 # Seconds timeout on each socket operation. 57 SOCK_TIMEOUT = 10.0 58 59 PACKAGE = 'com.android.camera2.its' 60 INTENT_START = 'com.android.camera2.its.START' 61 62 def __init__(self): 63 reboot_device_on_argv() 64 # TODO: Figure out why "--user 0" is needed, and fix the problem 65 _run('%s shell am force-stop --user 0 %s' % (self.ADB, self.PACKAGE)) 66 _run(('%s shell am startservice --user 0 -t text/plain ' 67 '-a %s') % (self.ADB, self.INTENT_START)) 68 _run('%s forward tcp:%d tcp:%d' % (self.ADB,self.PORT,self.PORT)) 69 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 70 self.sock.connect((self.IPADDR, self.PORT)) 71 self.sock.settimeout(self.SOCK_TIMEOUT) 72 73 def __del__(self): 74 if self.sock: 75 self.sock.close() 76 77 def __enter__(self): 78 return self 79 80 def __exit__(self, type, value, traceback): 81 return False 82 83 def __read_response_from_socket(self): 84 # Read a line (newline-terminated) string serialization of JSON object. 85 chars = [] 86 while len(chars) == 0 or chars[-1] != '\n': 87 chars.append(self.sock.recv(1)) 88 line = ''.join(chars) 89 jobj = json.loads(line) 90 # Optionally read a binary buffer of a fixed size. 91 buf = None 92 if jobj.has_key("bufValueSize"): 93 n = jobj["bufValueSize"] 94 buf = bytearray(n) 95 view = memoryview(buf) 96 while n > 0: 97 nbytes = self.sock.recv_into(view, n) 98 view = view[nbytes:] 99 n -= nbytes 100 buf = numpy.frombuffer(buf, dtype=numpy.uint8) 101 return jobj, buf 102 103 def get_camera_properties(self): 104 """Get the camera properties object for the device. 105 106 Returns: 107 The Python dictionary object for the CameraProperties object. 108 """ 109 cmd = {} 110 cmd["cmdName"] = "getCameraProperties" 111 self.sock.send(json.dumps(cmd) + "\n") 112 data,_ = self.__read_response_from_socket() 113 if data['tag'] != 'cameraProperties': 114 raise its.error.Error('Invalid command response') 115 return data['objValue']['cameraProperties'] 116 117 def do_3a(self, region_ae, region_awb, region_af, 118 do_ae=True, do_awb=True, do_af=True): 119 """Perform a 3A operation on the device. 120 121 Triggers some or all of AE, AWB, and AF, and returns once they have 122 converged. Uses the vendor 3A that is implemented inside the HAL. 123 124 Throws an assertion if 3A fails to converge. 125 126 Args: 127 region_ae: Normalized rect. (x,y,w,h) specifying the AE region. 128 region_awb: Normalized rect. (x,y,w,h) specifying the AWB region. 129 region_af: Normalized rect. (x,y,w,h) specifying the AF region. 130 131 Returns: 132 Five values: 133 * AE sensitivity; None if do_ae is False 134 * AE exposure time; None if do_ae is False 135 * AWB gains (list); None if do_awb is False 136 * AWB transform (list); None if do_awb is false 137 * AF focus position; None if do_af is false 138 """ 139 print "Running vendor 3A on device" 140 cmd = {} 141 cmd["cmdName"] = "do3A" 142 cmd["regions"] = {"ae": region_ae, "awb": region_awb, "af": region_af} 143 cmd["triggers"] = {"ae": do_ae, "af": do_af} 144 self.sock.send(json.dumps(cmd) + "\n") 145 146 # Wait for each specified 3A to converge. 147 ae_sens = None 148 ae_exp = None 149 awb_gains = None 150 awb_transform = None 151 af_dist = None 152 while True: 153 data,_ = self.__read_response_from_socket() 154 vals = data['strValue'].split() 155 if data['tag'] == 'aeResult': 156 ae_sens, ae_exp = [int(i) for i in vals] 157 elif data['tag'] == 'afResult': 158 af_dist = float(vals[0]) 159 elif data['tag'] == 'awbResult': 160 awb_gains = [float(f) for f in vals[:4]] 161 awb_transform = [float(f) for f in vals[4:]] 162 elif data['tag'] == '3aDone': 163 break 164 else: 165 raise its.error.Error('Invalid command response') 166 if (do_ae and ae_sens == None or do_awb and awb_gains == None 167 or do_af and af_dist == None): 168 raise its.error.Error('3A failed to converge') 169 return ae_sens, ae_exp, awb_gains, awb_transform, af_dist 170 171 def do_capture(self, cap_request, out_surface=None): 172 """Issue capture request(s), and read back the image(s) and metadata. 173 174 The main top-level function for capturing one or more images using the 175 device. Captures a single image if cap_request is a single object, and 176 captures a burst if it is a list of objects. 177 178 The out_surface field can specify the width, height, and format of 179 the captured image. The format may be "yuv" or "jpeg". The default is 180 a YUV420 frame ("yuv") corresponding to a full sensor frame. 181 182 Example of a single capture request: 183 184 { 185 "android.sensor.exposureTime": 100*1000*1000, 186 "android.sensor.sensitivity": 100 187 } 188 189 Example of a list of capture requests: 190 191 [ 192 { 193 "android.sensor.exposureTime": 100*1000*1000, 194 "android.sensor.sensitivity": 100 195 }, 196 { 197 "android.sensor.exposureTime": 100*1000*1000, 198 "android.sensor.sensitivity": 200 199 } 200 ] 201 202 Example of an output surface specification: 203 204 { 205 "width": 640, 206 "height": 480, 207 "format": "yuv" 208 } 209 210 Args: 211 cap_request: The Python dict/list specifying the capture(s), which 212 will be converted to JSON and sent to the device. 213 out_surface: (Optional) the width,height,format to use for all 214 captured images. 215 216 Returns: 217 An object or list of objects (depending on whether the request was 218 for a single or burst capture), where each object contains the 219 following fields: 220 * data: the image data as a numpy array of bytes. 221 * width: the width of the captured image. 222 * height: the height of the captured image. 223 * format: the format of the image, in ["yuv", "jpeg"]. 224 * metadata: the capture result object (Python dictionaty). 225 """ 226 cmd = {} 227 cmd["cmdName"] = "doCapture" 228 if not isinstance(cap_request, list): 229 cmd["captureRequests"] = [cap_request] 230 else: 231 cmd["captureRequests"] = cap_request 232 if out_surface is not None: 233 cmd["outputSurface"] = out_surface 234 n = len(cmd["captureRequests"]) 235 print "Capturing %d image%s" % (n, "s" if n>1 else "") 236 self.sock.send(json.dumps(cmd) + "\n") 237 238 # Wait for n images and n metadata responses from the device. 239 bufs = [] 240 mds = [] 241 fmts = [] 242 width = None 243 height = None 244 while len(bufs) < n or len(mds) < n: 245 jsonObj,buf = self.__read_response_from_socket() 246 if jsonObj['tag'] in ['jpegImage','yuvImage'] and buf is not None: 247 bufs.append(buf) 248 fmts.append(jsonObj['tag'][:-5]) 249 elif jsonObj['tag'] == 'captureResults': 250 mds.append(jsonObj['objValue']['captureResult']) 251 width = jsonObj['objValue']['width'] 252 height = jsonObj['objValue']['height'] 253 254 objs = [] 255 for i in range(n): 256 obj = {} 257 obj["data"] = bufs[i] 258 obj["width"] = width 259 obj["height"] = height 260 obj["format"] = fmts[i] 261 obj["metadata"] = mds[i] 262 objs.append(obj) 263 return objs if n>1 else objs[0] 264 265 def _run(cmd): 266 """Replacement for os.system, with hiding of stdout+stderr messages. 267 """ 268 with open(os.devnull, 'wb') as devnull: 269 subprocess.check_call( 270 cmd.split(), stdout=devnull, stderr=subprocess.STDOUT) 271 272 def reboot_device(sleep_duration=30): 273 """Function to reboot a device and block until it is ready. 274 275 Can be used at the start of a test to get the device into a known good 276 state. Will disconnect any other adb sessions, so this function is not 277 a part of the ItsSession class (which encapsulates a session with a 278 device.) 279 280 Args: 281 sleep_duration: (Optional) the length of time to sleep (seconds) after 282 the device comes online before returning; this gives the device 283 time to finish booting. 284 """ 285 print "Rebooting device" 286 _run("%s reboot" % (ItsSession.ADB)); 287 _run("%s wait-for-device" % (ItsSession.ADB)) 288 time.sleep(sleep_duration) 289 print "Reboot complete" 290 291 def reboot_device_on_argv(): 292 """Examine sys.argv, and reboot if the "reboot" arg is present. 293 294 If the script command line contains either: 295 296 reboot 297 reboot=30 298 299 then the device will be rebooted, and if the optional numeric arg is 300 present, then that will be the sleep duration passed to the reboot 301 call. 302 303 Returns: 304 Boolean, indicating whether the device was rebooted. 305 """ 306 for s in sys.argv[1:]: 307 if s[:6] == "reboot": 308 if len(s) > 7 and s[6] == "=": 309 duration = int(s[7:]) 310 reboot_device(duration) 311 elif len(s) == 6: 312 reboot_device() 313 return True 314 return False 315 316 class __UnitTest(unittest.TestCase): 317 """Run a suite of unit tests on this module. 318 """ 319 320 # TODO: Add some unit tests. 321 None 322 323 if __name__ == '__main__': 324 unittest.main() 325 326