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