• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 json
16import os
17import socket
18import string
19import subprocess
20import sys
21import time
22import unicodedata
23import unittest
24
25import its.error
26import numpy
27
28from collections import namedtuple
29
30class ItsSession(object):
31    """Controls a device over adb to run ITS scripts.
32
33    The script importing this module (on the host machine) prepares JSON
34    objects encoding CaptureRequests, specifying sets of parameters to use
35    when capturing an image using the Camera2 APIs. This class encapsulates
36    sending the requests to the device, monitoring the device's progress, and
37    copying the resultant captures back to the host machine when done. TCP
38    forwarded over adb is the transport mechanism used.
39
40    The device must have CtsVerifier.apk installed.
41
42    Attributes:
43        sock: The open socket.
44    """
45
46    # Open a connection to localhost:<host_port>, forwarded to port 6000 on the
47    # device. <host_port> is determined at run-time to support multiple
48    # connected devices.
49    IPADDR = '127.0.0.1'
50    REMOTE_PORT = 6000
51    BUFFER_SIZE = 4096
52
53    # LOCK_PORT is used as a mutex lock to protect the list of forwarded ports
54    # among all processes. The script assumes LOCK_PORT is available and will
55    # try to use ports between CLIENT_PORT_START and
56    # CLIENT_PORT_START+MAX_NUM_PORTS-1 on host for ITS sessions.
57    CLIENT_PORT_START = 6000
58    MAX_NUM_PORTS = 100
59    LOCK_PORT = CLIENT_PORT_START + MAX_NUM_PORTS
60
61    # Seconds timeout on each socket operation.
62    SOCK_TIMEOUT = 20.0
63    # Additional timeout in seconds when ITS service is doing more complicated
64    # operations, for example: issuing warmup requests before actual capture.
65    EXTRA_SOCK_TIMEOUT = 5.0
66
67    SEC_TO_NSEC = 1000*1000*1000.0
68
69    PACKAGE = 'com.android.cts.verifier.camera.its'
70    INTENT_START = 'com.android.cts.verifier.camera.its.START'
71    ACTION_ITS_RESULT = 'com.android.cts.verifier.camera.its.ACTION_ITS_RESULT'
72    EXTRA_VERSION = 'camera.its.extra.VERSION'
73    CURRENT_ITS_VERSION = '1.0'  # version number to sync with CtsVerifier
74    EXTRA_CAMERA_ID = 'camera.its.extra.CAMERA_ID'
75    EXTRA_RESULTS = 'camera.its.extra.RESULTS'
76    ITS_TEST_ACTIVITY = 'com.android.cts.verifier/.camera.its.ItsTestActivity'
77
78    # This string must be in sync with ItsService. Updated when interface
79    # between script and ItsService is changed.
80    ITS_SERVICE_VERSION = "1.0"
81
82    RESULT_PASS = 'PASS'
83    RESULT_FAIL = 'FAIL'
84    RESULT_NOT_EXECUTED = 'NOT_EXECUTED'
85    RESULT_VALUES = {RESULT_PASS, RESULT_FAIL, RESULT_NOT_EXECUTED}
86    RESULT_KEY = 'result'
87    SUMMARY_KEY = 'summary'
88
89    adb = "adb -d"
90    device_id = ""
91
92    # Definitions for some of the common output format options for do_capture().
93    # Each gets images of full resolution for each requested format.
94    CAP_RAW = {"format":"raw"}
95    CAP_DNG = {"format":"dng"}
96    CAP_YUV = {"format":"yuv"}
97    CAP_JPEG = {"format":"jpeg"}
98    CAP_RAW_YUV = [{"format":"raw"}, {"format":"yuv"}]
99    CAP_DNG_YUV = [{"format":"dng"}, {"format":"yuv"}]
100    CAP_RAW_JPEG = [{"format":"raw"}, {"format":"jpeg"}]
101    CAP_DNG_JPEG = [{"format":"dng"}, {"format":"jpeg"}]
102    CAP_YUV_JPEG = [{"format":"yuv"}, {"format":"jpeg"}]
103    CAP_RAW_YUV_JPEG = [{"format":"raw"}, {"format":"yuv"}, {"format":"jpeg"}]
104    CAP_DNG_YUV_JPEG = [{"format":"dng"}, {"format":"yuv"}, {"format":"jpeg"}]
105
106    # Predefine camera props. Save props extracted from the function,
107    # "get_camera_properties".
108    props = None
109
110    # Initialize the socket port for the host to forward requests to the device.
111    # This method assumes localhost's LOCK_PORT is available and will try to
112    # use ports between CLIENT_PORT_START and CLIENT_PORT_START+MAX_NUM_PORTS-1
113    def __init_socket_port(self):
114        NUM_RETRIES = 100
115        RETRY_WAIT_TIME_SEC = 0.05
116
117        # Bind a socket to use as mutex lock
118        socket_lock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
119        for i in range(NUM_RETRIES):
120            try:
121                socket_lock.bind((ItsSession.IPADDR, ItsSession.LOCK_PORT))
122                break
123            except socket.error or socket.timeout:
124                if i == NUM_RETRIES - 1:
125                    raise its.error.Error(self.device_id,
126                                          "socket lock returns error")
127                else:
128                    time.sleep(RETRY_WAIT_TIME_SEC)
129
130        # Check if a port is already assigned to the device.
131        command = "adb forward --list"
132        proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
133        output, error = proc.communicate()
134
135        port = None
136        used_ports = []
137        for line in output.split(os.linesep):
138            # each line should be formatted as:
139            # "<device_id> tcp:<host_port> tcp:<remote_port>"
140            forward_info = line.split()
141            if len(forward_info) >= 3 and \
142               len(forward_info[1]) > 4 and forward_info[1][:4] == "tcp:" and \
143               len(forward_info[2]) > 4 and forward_info[2][:4] == "tcp:":
144                local_p = int(forward_info[1][4:])
145                remote_p = int(forward_info[2][4:])
146                if forward_info[0] == self.device_id and \
147                   remote_p == ItsSession.REMOTE_PORT:
148                    port = local_p
149                    break
150                else:
151                    used_ports.append(local_p)
152
153        # Find the first available port if no port is assigned to the device.
154        if port is None:
155            for p in range(ItsSession.CLIENT_PORT_START,
156                           ItsSession.CLIENT_PORT_START +
157                           ItsSession.MAX_NUM_PORTS):
158                if p not in used_ports:
159                    # Try to run "adb forward" with the port
160                    command = "%s forward tcp:%d tcp:%d" % \
161                              (self.adb, p, self.REMOTE_PORT)
162                    proc = subprocess.Popen(command.split(),
163                                            stdout=subprocess.PIPE,
164                                            stderr=subprocess.PIPE)
165                    output, error = proc.communicate()
166
167                    # Check if there is no error
168                    if error is None or error.find("error") < 0:
169                        port = p
170                        break
171
172        if port is None:
173            raise its.error.Error(self.device_id, " cannot find an available " +
174                                  "port")
175
176        # Release the socket as mutex unlock
177        socket_lock.close()
178
179        # Connect to the socket
180        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
181        self.sock.connect((self.IPADDR, port))
182        self.sock.settimeout(self.SOCK_TIMEOUT)
183
184    # Reboot the device if needed and wait for the service to be ready for
185    # connection.
186    def __wait_for_service(self):
187        # This also includes the optional reboot handling: if the user
188        # provides a "reboot" or "reboot=N" arg, then reboot the device,
189        # waiting for N seconds (default 30) before returning.
190        for s in sys.argv[1:]:
191            if s[:6] == "reboot":
192                duration = 30
193                if len(s) > 7 and s[6] == "=":
194                    duration = int(s[7:])
195                print "Rebooting device"
196                _run("%s reboot" % (self.adb))
197                _run("%s wait-for-device" % (self.adb))
198                time.sleep(duration)
199                print "Reboot complete"
200
201        # Flush logcat so following code won't be misled by previous
202        # 'ItsService ready' log.
203        _run('%s logcat -c' % (self.adb))
204        time.sleep(1)
205
206        # TODO: Figure out why "--user 0" is needed, and fix the problem.
207        _run('%s shell am force-stop --user 0 %s' % (self.adb, self.PACKAGE))
208        _run(('%s shell am start-foreground-service --user 0 -t text/plain '
209              '-a %s') % (self.adb, self.INTENT_START))
210
211        # Wait until the socket is ready to accept a connection.
212        proc = subprocess.Popen(
213                self.adb.split() + ["logcat"],
214                stdout=subprocess.PIPE)
215        logcat = proc.stdout
216        while True:
217            line = logcat.readline().strip()
218            if line.find('ItsService ready') >= 0:
219                break
220        proc.kill()
221
222    def __init__(self, camera_id=None, hidden_physical_id=None):
223        self._camera_id = camera_id
224        self._hidden_physical_id = hidden_physical_id
225
226    def __enter__(self):
227        # Initialize device id and adb command.
228        self.device_id = get_device_id()
229        self.adb = "adb -s " + self.device_id
230
231        self.__wait_for_service()
232        self.__init_socket_port()
233
234        self.__close_camera()
235        self.__open_camera()
236        return self
237
238    def __exit__(self, type, value, traceback):
239        if hasattr(self, 'sock') and self.sock:
240            self.__close_camera()
241            self.sock.close()
242        return False
243
244    def __read_response_from_socket(self):
245        # Read a line (newline-terminated) string serialization of JSON object.
246        chars = []
247        while len(chars) == 0 or chars[-1] != '\n':
248            ch = self.sock.recv(1)
249            if len(ch) == 0:
250                # Socket was probably closed; otherwise don't get empty strings
251                raise its.error.Error('Problem with socket on device side')
252            chars.append(ch)
253        line = ''.join(chars)
254        jobj = json.loads(line)
255        # Optionally read a binary buffer of a fixed size.
256        buf = None
257        if jobj.has_key("bufValueSize"):
258            n = jobj["bufValueSize"]
259            buf = bytearray(n)
260            view = memoryview(buf)
261            while n > 0:
262                nbytes = self.sock.recv_into(view, n)
263                view = view[nbytes:]
264                n -= nbytes
265            buf = numpy.frombuffer(buf, dtype=numpy.uint8)
266        return jobj, buf
267
268    def __open_camera(self):
269        # Get the camera ID to open if it is an argument as a single camera.
270        # This allows passing camera=# to individual tests at command line
271        # and camera=#,#,# or an no camera argv with tools/run_all_tests.py.
272        #
273        # In case the camera is a logical multi-camera, to run ITS on the
274        # hidden physical sub-camera, pass camera=[logical ID]:[physical ID]
275        # to an individual test at the command line, and same applies to multiple
276        # camera IDs for tools/run_all_tests.py: camera=#,#:#,#:#,#
277        if not self._camera_id:
278            self._camera_id = 0
279            for s in sys.argv[1:]:
280                if s[:7] == "camera=" and len(s) > 7:
281                    camera_ids = s[7:].split(',')
282                    camera_id_combos = parse_camera_ids(camera_ids)
283                    if len(camera_id_combos) == 1:
284                        self._camera_id = camera_id_combos[0].id
285                        self._hidden_physical_id = camera_id_combos[0].sub_id
286
287        cmd = {"cmdName":"open", "cameraId":self._camera_id}
288        self.sock.send(json.dumps(cmd) + "\n")
289        data,_ = self.__read_response_from_socket()
290        if data['tag'] != 'cameraOpened':
291            raise its.error.Error('Invalid command response')
292
293    def __close_camera(self):
294        cmd = {"cmdName":"close"}
295        self.sock.send(json.dumps(cmd) + "\n")
296        data,_ = self.__read_response_from_socket()
297        if data['tag'] != 'cameraClosed':
298            raise its.error.Error('Invalid command response')
299
300    def do_vibrate(self, pattern):
301        """Cause the device to vibrate to a specific pattern.
302
303        Args:
304            pattern: Durations (ms) for which to turn on or off the vibrator.
305                The first value indicates the number of milliseconds to wait
306                before turning the vibrator on. The next value indicates the
307                number of milliseconds for which to keep the vibrator on
308                before turning it off. Subsequent values alternate between
309                durations in milliseconds to turn the vibrator off or to turn
310                the vibrator on.
311
312        Returns:
313            Nothing.
314        """
315        cmd = {}
316        cmd["cmdName"] = "doVibrate"
317        cmd["pattern"] = pattern
318        self.sock.send(json.dumps(cmd) + "\n")
319        data,_ = self.__read_response_from_socket()
320        if data['tag'] != 'vibrationStarted':
321            raise its.error.Error('Invalid command response')
322
323    def get_sensors(self):
324        """Get all sensors on the device.
325
326        Returns:
327            A Python dictionary that returns keys and booleans for each sensor.
328        """
329        cmd = {}
330        cmd["cmdName"] = "checkSensorExistence"
331        self.sock.send(json.dumps(cmd) + "\n")
332        data,_ = self.__read_response_from_socket()
333        if data['tag'] != 'sensorExistence':
334            raise its.error.Error('Invalid command response')
335        return data['objValue']
336
337    def start_sensor_events(self):
338        """Start collecting sensor events on the device.
339
340        See get_sensor_events for more info.
341
342        Returns:
343            Nothing.
344        """
345        cmd = {}
346        cmd["cmdName"] = "startSensorEvents"
347        self.sock.send(json.dumps(cmd) + "\n")
348        data,_ = self.__read_response_from_socket()
349        if data['tag'] != 'sensorEventsStarted':
350            raise its.error.Error('Invalid command response')
351
352    def get_sensor_events(self):
353        """Get a trace of all sensor events on the device.
354
355        The trace starts when the start_sensor_events function is called. If
356        the test runs for a long time after this call, then the device's
357        internal memory can fill up. Calling get_sensor_events gets all events
358        from the device, and then stops the device from collecting events and
359        clears the internal buffer; to start again, the start_sensor_events
360        call must be used again.
361
362        Events from the accelerometer, compass, and gyro are returned; each
363        has a timestamp and x,y,z values.
364
365        Note that sensor events are only produced if the device isn't in its
366        standby mode (i.e.) if the screen is on.
367
368        Returns:
369            A Python dictionary with three keys ("accel", "mag", "gyro") each
370            of which maps to a list of objects containing "time","x","y","z"
371            keys.
372        """
373        cmd = {}
374        cmd["cmdName"] = "getSensorEvents"
375        self.sock.send(json.dumps(cmd) + "\n")
376        timeout = self.SOCK_TIMEOUT + self.EXTRA_SOCK_TIMEOUT
377        self.sock.settimeout(timeout)
378        data,_ = self.__read_response_from_socket()
379        if data['tag'] != 'sensorEvents':
380            raise its.error.Error('Invalid command response')
381        self.sock.settimeout(self.SOCK_TIMEOUT)
382        return data['objValue']
383
384    def get_camera_ids(self):
385        """Get a list of camera device Ids that can be opened.
386
387        Returns:
388            a list of camera ID string
389        """
390        cmd = {}
391        cmd["cmdName"] = "getCameraIds"
392        self.sock.send(json.dumps(cmd) + "\n")
393        data,_ = self.__read_response_from_socket()
394        if data['tag'] != 'cameraIds':
395            raise its.error.Error('Invalid command response')
396        return data['objValue']['cameraIdArray']
397
398    def check_its_version_compatible(self):
399        """Check the java side ItsService is compatible with current host script.
400           Raise ItsException if versions are incompatible
401
402        Returns: None
403        """
404        cmd = {}
405        cmd["cmdName"] = "getItsVersion"
406        self.sock.send(json.dumps(cmd) + "\n")
407        data,_ = self.__read_response_from_socket()
408        if data['tag'] != 'ItsVersion':
409            raise its.error.Error('ItsService is incompatible with host python script')
410        server_version = data['strValue']
411        if self.ITS_SERVICE_VERSION != server_version:
412            raise its.error.Error('Version mismatch ItsService(%s) vs host script(%s)' % (
413                    server_version, ITS_SERVICE_VERSION))
414
415    def override_with_hidden_physical_camera_props(self, props):
416        """If current session is for a hidden physical camera, check that it is a valid
417           sub-camera backing the logical camera, and return the
418           characteristics of sub-camera. Otherwise, return "props" directly.
419
420        Returns: The properties of the hidden physical camera if possible
421        """
422        if self._hidden_physical_id:
423            e_msg = 'Camera %s is not a logical multi-camera' % self._camera_id
424            assert its.caps.logical_multi_camera(props), e_msg
425            physical_ids = its.caps.logical_multi_camera_physical_ids(props)
426            e_msg = 'Camera %s is not a hidden sub-camera of camera %s' % (
427                self._hidden_physical_id, self._camera_id)
428            assert self._hidden_physical_id in physical_ids, e_msg
429            props = self.get_camera_properties_by_id(self._hidden_physical_id)
430        return props
431
432    def get_camera_properties(self):
433        """Get the camera properties object for the device.
434
435        Returns:
436            The Python dictionary object for the CameraProperties object.
437        """
438        cmd = {}
439        cmd["cmdName"] = "getCameraProperties"
440        self.sock.send(json.dumps(cmd) + "\n")
441        data,_ = self.__read_response_from_socket()
442        if data['tag'] != 'cameraProperties':
443            raise its.error.Error('Invalid command response')
444        self.props = data['objValue']['cameraProperties']
445        return data['objValue']['cameraProperties']
446
447    def get_camera_properties_by_id(self, camera_id):
448        """Get the camera properties object for device with camera_id
449
450        Args:
451            camera_id: The ID string of the camera
452
453        Returns:
454            The Python dictionary object for the CameraProperties object. Empty
455            if no such device exists.
456
457        """
458        cmd = {}
459        cmd["cmdName"] = "getCameraPropertiesById"
460        cmd["cameraId"] = camera_id
461        self.sock.send(json.dumps(cmd) + "\n")
462        data,_ = self.__read_response_from_socket()
463        if data['tag'] != 'cameraProperties':
464            raise its.error.Error('Invalid command response')
465        return data['objValue']['cameraProperties']
466
467    def do_3a(self, regions_ae=[[0,0,1,1,1]],
468                    regions_awb=[[0,0,1,1,1]],
469                    regions_af=[[0,0,1,1,1]],
470                    do_ae=True, do_awb=True, do_af=True,
471                    lock_ae=False, lock_awb=False,
472                    get_results=False,
473                    ev_comp=0, mono_camera=False):
474        """Perform a 3A operation on the device.
475
476        Triggers some or all of AE, AWB, and AF, and returns once they have
477        converged. Uses the vendor 3A that is implemented inside the HAL.
478        Note: do_awb is always enabled regardless of do_awb flag
479
480        Throws an assertion if 3A fails to converge.
481
482        Args:
483            regions_ae: List of weighted AE regions.
484            regions_awb: List of weighted AWB regions.
485            regions_af: List of weighted AF regions.
486            do_ae: Trigger AE and wait for it to converge.
487            do_awb: Wait for AWB to converge.
488            do_af: Trigger AF and wait for it to converge.
489            lock_ae: Request AE lock after convergence, and wait for it.
490            lock_awb: Request AWB lock after convergence, and wait for it.
491            get_results: Return the 3A results from this function.
492            ev_comp: An EV compensation value to use when running AE.
493            mono_camera: Boolean for monochrome camera.
494
495        Region format in args:
496            Arguments are lists of weighted regions; each weighted region is a
497            list of 5 values, [x,y,w,h, wgt], and each argument is a list of
498            these 5-value lists. The coordinates are given as normalized
499            rectangles (x,y,w,h) specifying the region. For example:
500                [[0.0, 0.0, 1.0, 0.5, 5], [0.0, 0.5, 1.0, 0.5, 10]].
501            Weights are non-negative integers.
502
503        Returns:
504            Five values are returned if get_results is true::
505            * AE sensitivity; None if do_ae is False
506            * AE exposure time; None if do_ae is False
507            * AWB gains (list);
508            * AWB transform (list);
509            * AF focus position; None if do_af is false
510            Otherwise, it returns five None values.
511        """
512        print "Running vendor 3A on device"
513        cmd = {}
514        cmd["cmdName"] = "do3A"
515        cmd["regions"] = {"ae": sum(regions_ae, []),
516                          "awb": sum(regions_awb, []),
517                          "af": sum(regions_af, [])}
518        cmd["triggers"] = {"ae": do_ae, "af": do_af}
519        if lock_ae:
520            cmd["aeLock"] = True
521        if lock_awb:
522            cmd["awbLock"] = True
523        if ev_comp != 0:
524            cmd["evComp"] = ev_comp
525        if self._hidden_physical_id:
526            cmd["physicalId"] = self._hidden_physical_id
527        self.sock.send(json.dumps(cmd) + "\n")
528
529        # Wait for each specified 3A to converge.
530        ae_sens = None
531        ae_exp = None
532        awb_gains = None
533        awb_transform = None
534        af_dist = None
535        converged = False
536        while True:
537            data,_ = self.__read_response_from_socket()
538            vals = data['strValue'].split()
539            if data['tag'] == 'aeResult':
540                if do_ae:
541                    ae_sens, ae_exp = [int(i) for i in vals]
542            elif data['tag'] == 'afResult':
543                if do_af:
544                    af_dist = float(vals[0])
545            elif data['tag'] == 'awbResult':
546                awb_gains = [float(f) for f in vals[:4]]
547                awb_transform = [float(f) for f in vals[4:]]
548            elif data['tag'] == '3aConverged':
549                converged = True
550            elif data['tag'] == '3aDone':
551                break
552            else:
553                raise its.error.Error('Invalid command response')
554        if converged and not get_results:
555            return None,None,None,None,None
556        if (do_ae and ae_sens == None or (not mono_camera and do_awb and awb_gains == None)
557                or do_af and af_dist == None or not converged):
558
559            raise its.error.Error('3A failed to converge')
560        return ae_sens, ae_exp, awb_gains, awb_transform, af_dist
561
562    def is_stream_combination_supported(self, out_surfaces):
563        """Query whether a output surfaces combination is supported by the camera device.
564
565        This function hooks up to the isSessionConfigurationSupported() camera API
566        to query whether a particular stream combination is supported.
567
568        Refer to do_capture function for specification of out_surfaces field.
569        """
570        cmd = {}
571        cmd['cmdName'] = 'isStreamCombinationSupported'
572
573        if not isinstance(out_surfaces, list):
574            cmd['outputSurfaces'] = [out_surfaces]
575        else:
576            cmd['outputSurfaces'] = out_surfaces
577        formats = [c['format'] if 'format' in c else 'yuv'
578                   for c in cmd['outputSurfaces']]
579        formats = [s if s != 'jpg' else 'jpeg' for s in formats]
580
581        self.sock.send(json.dumps(cmd) + '\n')
582
583        data,_ = self.__read_response_from_socket()
584        if data['tag'] != 'streamCombinationSupport':
585            its.error.Error('Failed to query stream combination')
586
587        return data['strValue'] == 'supportedCombination'
588
589    def do_capture(self, cap_request,
590            out_surfaces=None, reprocess_format=None, repeat_request=None):
591        """Issue capture request(s), and read back the image(s) and metadata.
592
593        The main top-level function for capturing one or more images using the
594        device. Captures a single image if cap_request is a single object, and
595        captures a burst if it is a list of objects.
596
597        The optional repeat_request field can be used to assign a repeating
598        request list ran in background for 3 seconds to warm up the capturing
599        pipeline before start capturing. The repeat_requests will be ran on a
600        640x480 YUV surface without sending any data back. The caller needs to
601        make sure the stream configuration defined by out_surfaces and
602        repeat_request are valid or do_capture may fail because device does not
603        support such stream configuration.
604
605        The out_surfaces field can specify the width(s), height(s), and
606        format(s) of the captured image. The formats may be "yuv", "jpeg",
607        "dng", "raw", "raw10", "raw12", "rawStats" or "y8". The default is a YUV420
608        frame ("yuv") corresponding to a full sensor frame.
609
610        Optionally the out_surfaces field can specify physical camera id(s) if the
611        current camera device is a logical multi-camera. The physical camera id
612        must refer to a physical camera backing this logical camera device.
613
614        Note that one or more surfaces can be specified, allowing a capture to
615        request images back in multiple formats (e.g.) raw+yuv, raw+jpeg,
616        yuv+jpeg, raw+yuv+jpeg. If the size is omitted for a surface, the
617        default is the largest resolution available for the format of that
618        surface. At most one output surface can be specified for a given format,
619        and raw+dng, raw10+dng, and raw+raw10 are not supported as combinations.
620
621        If reprocess_format is not None, for each request, an intermediate
622        buffer of the given reprocess_format will be captured from camera and
623        the intermediate buffer will be reprocessed to the output surfaces. The
624        following settings will be turned off when capturing the intermediate
625        buffer and will be applied when reprocessing the intermediate buffer.
626            1. android.noiseReduction.mode
627            2. android.edge.mode
628            3. android.reprocess.effectiveExposureFactor
629
630        Supported reprocess format are "yuv" and "private". Supported output
631        surface formats when reprocessing is enabled are "yuv" and "jpeg".
632
633        Example of a single capture request:
634
635            {
636                "android.sensor.exposureTime": 100*1000*1000,
637                "android.sensor.sensitivity": 100
638            }
639
640        Example of a list of capture requests:
641
642            [
643                {
644                    "android.sensor.exposureTime": 100*1000*1000,
645                    "android.sensor.sensitivity": 100
646                },
647                {
648                    "android.sensor.exposureTime": 100*1000*1000,
649                    "android.sensor.sensitivity": 200
650                }
651            ]
652
653        Examples of output surface specifications:
654
655            {
656                "width": 640,
657                "height": 480,
658                "format": "yuv"
659            }
660
661            [
662                {
663                    "format": "jpeg"
664                },
665                {
666                    "format": "raw"
667                }
668            ]
669
670        The following variables defined in this class are shortcuts for
671        specifying one or more formats where each output is the full size for
672        that format; they can be used as values for the out_surfaces arguments:
673
674            CAP_RAW
675            CAP_DNG
676            CAP_YUV
677            CAP_JPEG
678            CAP_RAW_YUV
679            CAP_DNG_YUV
680            CAP_RAW_JPEG
681            CAP_DNG_JPEG
682            CAP_YUV_JPEG
683            CAP_RAW_YUV_JPEG
684            CAP_DNG_YUV_JPEG
685
686        If multiple formats are specified, then this function returns multiple
687        capture objects, one for each requested format. If multiple formats and
688        multiple captures (i.e. a burst) are specified, then this function
689        returns multiple lists of capture objects. In both cases, the order of
690        the returned objects matches the order of the requested formats in the
691        out_surfaces parameter. For example:
692
693            yuv_cap            = do_capture( req1                           )
694            yuv_cap            = do_capture( req1,        yuv_fmt           )
695            yuv_cap,  raw_cap  = do_capture( req1,        [yuv_fmt,raw_fmt] )
696            yuv_caps           = do_capture( [req1,req2], yuv_fmt           )
697            yuv_caps, raw_caps = do_capture( [req1,req2], [yuv_fmt,raw_fmt] )
698
699        The "rawStats" format processes the raw image and returns a new image
700        of statistics from the raw image. The format takes additional keys,
701        "gridWidth" and "gridHeight" which are size of grid cells in a 2D grid
702        of the raw image. For each grid cell, the mean and variance of each raw
703        channel is computed, and the do_capture call returns two 4-element float
704        images of dimensions (rawWidth / gridWidth, rawHeight / gridHeight),
705        concatenated back-to-back, where the first iamge contains the 4-channel
706        means and the second contains the 4-channel variances. Note that only
707        pixels in the active array crop region are used; pixels outside this
708        region (for example optical black rows) are cropped out before the
709        gridding and statistics computation is performed.
710
711        For the rawStats format, if the gridWidth is not provided then the raw
712        image width is used as the default, and similarly for gridHeight. With
713        this, the following is an example of a output description that computes
714        the mean and variance across each image row:
715
716            {
717                "gridHeight": 1,
718                "format": "rawStats"
719            }
720
721        Args:
722            cap_request: The Python dict/list specifying the capture(s), which
723                will be converted to JSON and sent to the device.
724            out_surfaces: (Optional) specifications of the output image formats
725                and sizes to use for each capture.
726            reprocess_format: (Optional) The reprocessing format. If not None,
727                reprocessing will be enabled.
728
729        Returns:
730            An object, list of objects, or list of lists of objects, where each
731            object contains the following fields:
732            * data: the image data as a numpy array of bytes.
733            * width: the width of the captured image.
734            * height: the height of the captured image.
735            * format: image the format, in [
736                        "yuv","jpeg","raw","raw10","raw12","rawStats","dng"].
737            * metadata: the capture result object (Python dictionary).
738        """
739        cmd = {}
740        if reprocess_format != None:
741            cmd["cmdName"] = "doReprocessCapture"
742            cmd["reprocessFormat"] = reprocess_format
743        else:
744            cmd["cmdName"] = "doCapture"
745
746        if repeat_request is not None and reprocess_format is not None:
747            raise its.error.Error('repeating request + reprocessing is not supported')
748
749        if repeat_request is None:
750            cmd["repeatRequests"] = []
751        elif not isinstance(repeat_request, list):
752            cmd["repeatRequests"] = [repeat_request]
753        else:
754            cmd["repeatRequests"] = repeat_request
755
756        if not isinstance(cap_request, list):
757            cmd["captureRequests"] = [cap_request]
758        else:
759            cmd["captureRequests"] = cap_request
760        if out_surfaces is not None:
761            if not isinstance(out_surfaces, list):
762                cmd["outputSurfaces"] = [out_surfaces]
763            else:
764                cmd["outputSurfaces"] = out_surfaces
765            formats = [c["format"] if "format" in c else "yuv"
766                       for c in cmd["outputSurfaces"]]
767            formats = [s if s != "jpg" else "jpeg" for s in formats]
768        else:
769            max_yuv_size = its.objects.get_available_output_sizes(
770                    "yuv", self.props)[0]
771            formats = ['yuv']
772            cmd["outputSurfaces"] = [{"format": "yuv",
773                                      "width" : max_yuv_size[0],
774                                      "height": max_yuv_size[1]}]
775
776        ncap = len(cmd["captureRequests"])
777        nsurf = 1 if out_surfaces is None else len(cmd["outputSurfaces"])
778
779        cam_ids = []
780        bufs = {}
781        yuv_bufs = {}
782        for i,s in enumerate(cmd["outputSurfaces"]):
783            if self._hidden_physical_id:
784                s['physicalCamera'] = self._hidden_physical_id
785
786            if 'physicalCamera' in s:
787                cam_id = s['physicalCamera']
788            else:
789                cam_id = self._camera_id
790
791            if cam_id not in cam_ids:
792                cam_ids.append(cam_id)
793                bufs[cam_id] = {"raw":[], "raw10":[], "raw12":[],
794                        "rawStats":[], "dng":[], "jpeg":[], "y8":[]}
795
796        for cam_id in cam_ids:
797            # Only allow yuv output to multiple targets
798            if cam_id == self._camera_id:
799                yuv_surfaces = [s for s in cmd["outputSurfaces"] if s["format"]=="yuv"\
800                                and "physicalCamera" not in s]
801                formats_for_id = [s["format"] for s in cmd["outputSurfaces"] if \
802                                 "physicalCamera" not in s]
803            else:
804                yuv_surfaces = [s for s in cmd["outputSurfaces"] if s["format"]=="yuv"\
805                                and "physicalCamera" in s and s["physicalCamera"] == cam_id]
806                formats_for_id = [s["format"] for s in cmd["outputSurfaces"] if \
807                                 "physicalCamera" in s and s["physicalCamera"] == cam_id]
808
809            n_yuv = len(yuv_surfaces)
810            # Compute the buffer size of YUV targets
811            yuv_maxsize_1d = 0
812            for s in yuv_surfaces:
813                if not ("width" in s and "height" in s):
814                    if self.props is None:
815                        raise its.error.Error('Camera props are unavailable')
816                    yuv_maxsize_2d = its.objects.get_available_output_sizes(
817                        "yuv", self.props)[0]
818                    yuv_maxsize_1d = yuv_maxsize_2d[0] * yuv_maxsize_2d[1] * 3 / 2
819                    break
820            yuv_sizes = [c["width"]*c["height"]*3/2
821                         if "width" in c and "height" in c
822                         else yuv_maxsize_1d
823                         for c in yuv_surfaces]
824            # Currently we don't pass enough metadta from ItsService to distinguish
825            # different yuv stream of same buffer size
826            if len(yuv_sizes) != len(set(yuv_sizes)):
827                raise its.error.Error(
828                        'ITS does not support yuv outputs of same buffer size')
829            if len(formats_for_id) > len(set(formats_for_id)):
830                if n_yuv != len(formats_for_id) - len(set(formats_for_id)) + 1:
831                    raise its.error.Error('Duplicate format requested')
832
833            yuv_bufs[cam_id] = {size:[] for size in yuv_sizes}
834
835        raw_formats = 0;
836        raw_formats += 1 if "dng" in formats else 0
837        raw_formats += 1 if "raw" in formats else 0
838        raw_formats += 1 if "raw10" in formats else 0
839        raw_formats += 1 if "raw12" in formats else 0
840        raw_formats += 1 if "rawStats" in formats else 0
841        if raw_formats > 1:
842            raise its.error.Error('Different raw formats not supported')
843
844        # Detect long exposure time and set timeout accordingly
845        longest_exp_time = 0
846        for req in cmd["captureRequests"]:
847            if "android.sensor.exposureTime" in req and \
848                    req["android.sensor.exposureTime"] > longest_exp_time:
849                longest_exp_time = req["android.sensor.exposureTime"]
850
851        extended_timeout = longest_exp_time / self.SEC_TO_NSEC + \
852                self.SOCK_TIMEOUT
853        if repeat_request:
854            extended_timeout += self.EXTRA_SOCK_TIMEOUT
855        self.sock.settimeout(extended_timeout)
856
857        print "Capturing %d frame%s with %d format%s [%s]" % (
858                  ncap, "s" if ncap>1 else "", nsurf, "s" if nsurf>1 else "",
859                  ",".join(formats))
860        self.sock.send(json.dumps(cmd) + "\n")
861
862        # Wait for ncap*nsurf images and ncap metadata responses.
863        # Assume that captures come out in the same order as requested in
864        # the burst, however individual images of different formats can come
865        # out in any order for that capture.
866        nbufs = 0
867        mds = []
868        physical_mds = []
869        widths = None
870        heights = None
871        while nbufs < ncap*nsurf or len(mds) < ncap:
872            jsonObj,buf = self.__read_response_from_socket()
873            if jsonObj['tag'] in ['jpegImage', 'rawImage', \
874                    'raw10Image', 'raw12Image', 'rawStatsImage', 'dngImage', 'y8Image'] \
875                    and buf is not None:
876                fmt = jsonObj['tag'][:-5]
877                bufs[self._camera_id][fmt].append(buf)
878                nbufs += 1
879            elif jsonObj['tag'] == 'yuvImage':
880                buf_size = numpy.product(buf.shape)
881                yuv_bufs[self._camera_id][buf_size].append(buf)
882                nbufs += 1
883            elif jsonObj['tag'] == 'captureResults':
884                mds.append(jsonObj['objValue']['captureResult'])
885                physical_mds.append(jsonObj['objValue']['physicalResults'])
886                outputs = jsonObj['objValue']['outputs']
887                widths = [out['width'] for out in outputs]
888                heights = [out['height'] for out in outputs]
889            else:
890                tagString = unicodedata.normalize('NFKD', jsonObj['tag']).encode('ascii', 'ignore');
891                for x in ['jpegImage', 'rawImage', \
892                        'raw10Image', 'raw12Image', 'rawStatsImage', 'yuvImage']:
893                    if tagString.startswith(x):
894                        if x == 'yuvImage':
895                            physicalId = jsonObj['tag'][len(x):]
896                            if physicalId in cam_ids:
897                                buf_size = numpy.product(buf.shape)
898                                yuv_bufs[physicalId][buf_size].append(buf)
899                                nbufs += 1
900                        else:
901                            physicalId = jsonObj['tag'][len(x):]
902                            if physicalId in cam_ids:
903                                fmt = x[:-5]
904                                bufs[physicalId][fmt].append(buf)
905                                nbufs += 1
906        rets = []
907        for j,fmt in enumerate(formats):
908            objs = []
909            if "physicalCamera" in cmd["outputSurfaces"][j]:
910                cam_id = cmd["outputSurfaces"][j]["physicalCamera"]
911            else:
912                cam_id = self._camera_id
913
914            for i in range(ncap):
915                obj = {}
916                obj["width"] = widths[j]
917                obj["height"] = heights[j]
918                obj["format"] = fmt
919                if cam_id == self._camera_id:
920                    obj["metadata"] = mds[i]
921                else:
922                    for physical_md in physical_mds[i]:
923                        if cam_id in physical_md:
924                            obj["metadata"] = physical_md[cam_id]
925                            break
926
927                if fmt == "yuv":
928                    buf_size = widths[j] * heights[j] * 3 / 2
929                    obj["data"] = yuv_bufs[cam_id][buf_size][i]
930                else:
931                    obj["data"] = bufs[cam_id][fmt][i]
932                objs.append(obj)
933            rets.append(objs if ncap > 1 else objs[0])
934        self.sock.settimeout(self.SOCK_TIMEOUT)
935        if len(rets) > 1 or (isinstance(rets[0], dict) and
936                             isinstance(cap_request, list)):
937            return rets
938        else:
939            return rets[0]
940
941def do_capture_with_latency(cam, req, sync_latency, fmt=None):
942    """Helper function to take enough frames with do_capture to allow sync latency.
943
944    Args:
945        cam:            camera object
946        req:            request for camera
947        sync_latency:   integer number of frames
948        fmt:            format for the capture
949    Returns:
950        single capture with the unsettled frames discarded
951    """
952    caps = cam.do_capture([req]*(sync_latency+1), fmt)
953    return caps[-1]
954
955
956def get_device_id():
957    """Return the ID of the device that the test is running on.
958
959    Return the device ID provided in the command line if it's connected. If no
960    device ID is provided in the command line and there is only one device
961    connected, return the device ID by parsing the result of "adb devices".
962    Also, if the environment variable ANDROID_SERIAL is set, use it as device
963    id. When both ANDROID_SERIAL and device argument present, device argument
964    takes priority.
965
966    Raise an exception if no device is connected; or the device ID provided in
967    the command line is not connected; or no device ID is provided in the
968    command line or environment variable and there are more than 1 device
969    connected.
970
971    Returns:
972        Device ID string.
973    """
974    device_id = None
975
976    # Check if device id is set in env
977    if "ANDROID_SERIAL" in os.environ:
978        device_id = os.environ["ANDROID_SERIAL"]
979
980    for s in sys.argv[1:]:
981        if s[:7] == "device=" and len(s) > 7:
982            device_id = str(s[7:])
983
984    # Get a list of connected devices
985    devices = []
986    command = "adb devices"
987    proc = subprocess.Popen(command.split(), stdout=subprocess.PIPE)
988    output, error = proc.communicate()
989    for line in output.split(os.linesep):
990        device_info = line.split()
991        if len(device_info) == 2 and device_info[1] == "device":
992            devices.append(device_info[0])
993
994    if len(devices) == 0:
995        raise its.error.Error("No device is connected!")
996    elif device_id is not None and device_id not in devices:
997        raise its.error.Error(device_id + " is not connected!")
998    elif device_id is None and len(devices) >= 2:
999        raise its.error.Error("More than 1 device are connected. " +
1000                "Use device=<device_id> to specify a device to test.")
1001    elif len(devices) == 1:
1002        device_id = devices[0]
1003
1004    return device_id
1005
1006def report_result(device_id, camera_id, results):
1007    """Send a pass/fail result to the device, via an intent.
1008
1009    Args:
1010        device_id: The ID string of the device to report the results to.
1011        camera_id: The ID string of the camera for which to report pass/fail.
1012        results: a dictionary contains all ITS scenes as key and result/summary
1013                 of current ITS run. See test_report_result unit test for
1014                 an example.
1015    Returns:
1016        Nothing.
1017    """
1018    ACTIVITY_START_WAIT = 1.5 # seconds
1019    adb = "adb -s " + device_id
1020
1021    # Start ItsTestActivity to receive test results
1022    cmd = "%s shell am start %s --activity-brought-to-front" % (adb, ItsSession.ITS_TEST_ACTIVITY)
1023    _run(cmd)
1024    time.sleep(ACTIVITY_START_WAIT)
1025
1026    # Validate/process results argument
1027    for scene in results:
1028        result_key = ItsSession.RESULT_KEY
1029        summary_key = ItsSession.SUMMARY_KEY
1030        if result_key not in results[scene]:
1031            raise its.error.Error('ITS result not found for ' + scene)
1032        if results[scene][result_key] not in ItsSession.RESULT_VALUES:
1033            raise its.error.Error('Unknown ITS result for %s: %s' % (
1034                    scene, results[result_key]))
1035        if summary_key in results[scene]:
1036            device_summary_path = "/sdcard/its_camera%s_%s.txt" % (
1037                    camera_id, scene)
1038            _run("%s push %s %s" % (
1039                    adb, results[scene][summary_key], device_summary_path))
1040            results[scene][summary_key] = device_summary_path
1041
1042    json_results = json.dumps(results)
1043    cmd = "%s shell am broadcast -a %s --es %s %s --es %s %s --es %s \'%s\'" % (
1044            adb, ItsSession.ACTION_ITS_RESULT,
1045            ItsSession.EXTRA_VERSION, ItsSession.CURRENT_ITS_VERSION,
1046            ItsSession.EXTRA_CAMERA_ID, camera_id,
1047            ItsSession.EXTRA_RESULTS, json_results)
1048    if len(cmd) > 4095:
1049        print "ITS command string might be too long! len:", len(cmd)
1050    _run(cmd)
1051
1052def adb_log(device_id, msg):
1053    """Send a log message to adb logcat
1054
1055    Args:
1056        device_id: The ID string of the adb device
1057        msg: the message string to be send to logcat
1058
1059    Returns:
1060        Nothing.
1061    """
1062    adb = "adb -s " + device_id
1063    cmd = "%s shell log -p i -t \"ItsTestHost\" %s" % (adb, msg)
1064    _run(cmd)
1065
1066def get_device_fingerprint(device_id):
1067    """ Return the Build FingerPrint of the device that the test is running on.
1068
1069    Returns:
1070        Device Build Fingerprint string.
1071    """
1072    device_bfp = None
1073
1074    # Get a list of connected devices
1075
1076    com = ('adb -s %s shell getprop | grep ro.build.fingerprint' % device_id)
1077    proc = subprocess.Popen(com.split(), stdout=subprocess.PIPE)
1078    output, error = proc.communicate()
1079    assert error is None
1080
1081    lst = string.split( \
1082            string.replace( \
1083            string.replace( \
1084            string.replace(output,
1085            '\n', ''), '[', ''), ']', ''), \
1086            ' ')
1087
1088    if lst[0].find('ro.build.fingerprint') != -1:
1089        device_bfp = lst[1]
1090
1091    return device_bfp
1092
1093def parse_camera_ids(ids):
1094    """ Parse the string of camera IDs into array of CameraIdCombo tuples.
1095    """
1096    CameraIdCombo = namedtuple('CameraIdCombo', ['id', 'sub_id'])
1097    id_combos = []
1098    for one_id in ids:
1099        one_combo = one_id.split(':')
1100        if len(one_combo) == 1:
1101            id_combos.append(CameraIdCombo(one_combo[0], None))
1102        elif len(one_combo) == 2:
1103            id_combos.append(CameraIdCombo(one_combo[0], one_combo[1]))
1104        else:
1105            assert(False), 'Camera id parameters must be either ID, or ID:SUB_ID'
1106    return id_combos
1107
1108def _run(cmd):
1109    """Replacement for os.system, with hiding of stdout+stderr messages.
1110    """
1111    with open(os.devnull, 'wb') as devnull:
1112        subprocess.check_call(
1113                cmd.split(), stdout=devnull, stderr=subprocess.STDOUT)
1114
1115
1116class __UnitTest(unittest.TestCase):
1117    """Run a suite of unit tests on this module.
1118    """
1119
1120    """
1121    # TODO: this test currently needs connected device to pass
1122    #       Need to remove that dependency before enabling the test
1123    def test_report_result(self):
1124        device_id = get_device_id()
1125        camera_id = "1"
1126        result_key = ItsSession.RESULT_KEY
1127        results = {"scene0":{result_key:"PASS"},
1128                   "scene1":{result_key:"PASS"},
1129                   "scene2":{result_key:"PASS"},
1130                   "scene3":{result_key:"PASS"},
1131                   "sceneNotExist":{result_key:"FAIL"}}
1132        report_result(device_id, camera_id, results)
1133    """
1134
1135if __name__ == '__main__':
1136    unittest.main()
1137
1138