Home | History | Annotate | Download | only in its
      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