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