VTS integration framework initial refactor.

Bug=28004487

Commiting the integraton framework in the directory structure as
discussed offline.

Change-Id: I0314431e3b906a195290cd9b4630c93e07b494ca
diff --git a/utils/python/__init__.py b/utils/python/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/__init__.py
diff --git a/utils/python/controllers/__init__.py b/utils/python/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/__init__.py
diff --git a/utils/python/controllers/adb.py b/utils/python/controllers/adb.py
new file mode 100644
index 0000000..c6a0e96
--- /dev/null
+++ b/utils/python/controllers/adb.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from builtins import str
+
+import random
+import socket
+import subprocess
+import time
+
+class AdbError(Exception):
+    """Raised when there is an error in adb operations."""
+
+SL4A_LAUNCH_CMD=("am start -a com.googlecode.android_scripting.action.LAUNCH_SERVER "
+    "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT {} "
+    "com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher" )
+
+def get_available_host_port():
+    """Gets a host port number available for adb forward.
+
+    Returns:
+        An integer representing a port number on the host available for adb
+        forward.
+    """
+    while True:
+        port = random.randint(1024, 9900)
+        if is_port_available(port):
+            return port
+
+def is_port_available(port):
+    """Checks if a given port number is available on the system.
+
+    Args:
+        port: An integer which is the port number to check.
+
+    Returns:
+        True if the port is available; False otherwise.
+    """
+    # Make sure adb is not using this port so we don't accidentally interrupt
+    # ongoing runs by trying to bind to the port.
+    if port in list_occupied_adb_ports():
+        return False
+    s = None
+    try:
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        s.bind(('localhost', port))
+        return True
+    except socket.error:
+        return False
+    finally:
+        if s:
+            s.close()
+
+def list_occupied_adb_ports():
+    """Lists all the host ports occupied by adb forward.
+
+    This is useful because adb will silently override the binding if an attempt
+    to bind to a port already used by adb was made, instead of throwing binding
+    error. So one should always check what ports adb is using before trying to
+    bind to a port with adb.
+
+    Returns:
+        A list of integers representing occupied host ports.
+    """
+    out = AdbProxy().forward("--list")
+    clean_lines = str(out, 'utf-8').strip().split('\n')
+    used_ports = []
+    for line in clean_lines:
+        tokens = line.split(" tcp:")
+        if len(tokens) != 3:
+            continue
+        used_ports.append(int(tokens[1]))
+    return used_ports
+
+class AdbProxy():
+    """Proxy class for ADB.
+
+    For syntactic reasons, the '-' in adb commands need to be replaced with
+    '_'. Can directly execute adb commands on an object:
+    >> adb = AdbProxy(<serial>)
+    >> adb.start_server()
+    >> adb.devices() # will return the console output of "adb devices".
+    """
+    def __init__(self, serial="", log=None):
+        self.serial = serial
+        if serial:
+            self.adb_str = "adb -s {}".format(serial)
+        else:
+            self.adb_str = "adb"
+        self.log = log
+
+    def _exec_cmd(self, cmd):
+        """Executes adb commands in a new shell.
+
+        This is specific to executing adb binary because stderr is not a good
+        indicator of cmd execution status.
+
+        Args:
+            cmds: A string that is the adb command to execute.
+
+        Returns:
+            The output of the adb command run if exit code is 0.
+
+        Raises:
+            AdbError is raised if the adb command exit code is not 0.
+        """
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
+        (out, err) = proc.communicate()
+        ret = proc.returncode
+        total_output = "stdout: {}, stderr: {}, ret: {}".format(out, err, ret)
+        # TODO(angli): Fix this when global logger is done.
+        if self.log:
+            self.log.debug("{}\n{}".format(cmd, total_output))
+        if ret == 0:
+            return out
+        else:
+            raise AdbError(total_output)
+
+    def _exec_adb_cmd(self, name, arg_str):
+        return self._exec_cmd(' '.join((self.adb_str, name, arg_str)))
+
+    def tcp_forward(self, host_port, device_port):
+        """Starts tcp forwarding.
+
+        Args:
+            host_port: Port number to use on the computer.
+            device_port: Port number to use on the android device.
+        """
+        self.forward("tcp:{} tcp:{}".format(host_port, device_port))
+
+    def start_sl4a(self, port=8080):
+        """Starts sl4a server on the android device.
+
+        Args:
+            port: Port number to use on the android device.
+        """
+        MAX_SL4A_WAIT_TIME = 10
+        print(self.shell(SL4A_LAUNCH_CMD.format(port)))
+
+        for _ in range(MAX_SL4A_WAIT_TIME):
+            time.sleep(1)
+            if self.is_sl4a_running():
+                return
+        raise AdbError(
+                "com.googlecode.android_scripting process never started.")
+
+    def is_sl4a_running(self):
+        """Checks if the sl4a app is running on an android device.
+
+        Returns:
+            True if the sl4a app is running, False otherwise.
+        """
+        #Grep for process with a preceding S which means it is truly started.
+        out = self.shell('ps | grep "S com.googlecode.android_scripting"')
+        if len(out)==0:
+          return False
+        return True
+
+    def __getattr__(self, name):
+        def adb_call(*args):
+            clean_name = name.replace('_', '-')
+            arg_str = ' '.join(str(elem) for elem in args)
+            return self._exec_adb_cmd(clean_name, arg_str)
+        return adb_call
diff --git a/utils/python/controllers/android.py b/utils/python/controllers/android.py
new file mode 100644
index 0000000..cdd1412
--- /dev/null
+++ b/utils/python/controllers/android.py
@@ -0,0 +1,126 @@
+#/usr/bin/env python3.4
+#
+# Copyright (C) 2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""
+JSON RPC interface to android scripting engine.
+"""
+
+from builtins import str
+
+import json
+import os
+import socket
+import threading
+import time
+
+HOST = os.environ.get('AP_HOST', None)
+PORT = os.environ.get('AP_PORT', 9999)
+
+class SL4AException(Exception):
+    pass
+
+class SL4AAPIError(SL4AException):
+    """Raised when remote API reports an error."""
+
+class SL4AProtocolError(SL4AException):
+    """Raised when there is some error in exchanging data with server on device."""
+    NO_RESPONSE_FROM_HANDSHAKE = "No response from handshake."
+    NO_RESPONSE_FROM_SERVER = "No response from server."
+    MISMATCHED_API_ID = "Mismatched API id."
+
+def IDCounter():
+    i = 0
+    while True:
+        yield i
+        i += 1
+
+class Android(object):
+    COUNTER = IDCounter()
+
+    _SOCKET_CONNECT_TIMEOUT = 60
+
+    def __init__(self, cmd='initiate', uid=-1, port=PORT, addr=HOST, timeout=None):
+        self.lock = threading.RLock()
+        self.client = None  # prevent close errors on connect failure
+        self.uid = None
+        timeout_time = time.time() + self._SOCKET_CONNECT_TIMEOUT
+        while True:
+            try:
+                self.conn = socket.create_connection(
+                        (addr, port), max(1,timeout_time - time.time()))
+                self.conn.settimeout(timeout)
+                break
+            except (TimeoutError, socket.timeout):
+                print("Failed to create socket connection!")
+                raise
+            except (socket.error, IOError):
+                # TODO: optimize to only forgive some errors here
+                # error values are OS-specific so this will require
+                # additional tuning to fail faster
+                if time.time() + 1 >= timeout_time:
+                    print("Failed to create socket connection!")
+                    raise
+                time.sleep(1)
+
+        self.client = self.conn.makefile(mode="brw")
+
+        resp = self._cmd(cmd, uid)
+        if not resp:
+            raise SL4AProtocolError(SL4AProtocolError.NO_RESPONSE_FROM_HANDSHAKE)
+        result = json.loads(str(resp, encoding="utf8"))
+        if result['status']:
+            self.uid = result['uid']
+        else:
+            self.uid = -1
+
+    def close(self):
+        if self.conn is not None:
+            self.conn.close()
+            self.conn = None
+
+    def _cmd(self, command, uid=None):
+        if not uid:
+            uid = self.uid
+        self.client.write(
+            json.dumps({'cmd': command, 'uid': uid})
+                .encode("utf8")+b'\n')
+        self.client.flush()
+        return self.client.readline()
+
+    def _rpc(self, method, *args):
+        self.lock.acquire()
+        apiid = next(Android.COUNTER)
+        self.lock.release()
+        data = {'id': apiid,
+                'method': method,
+                'params': args}
+        request = json.dumps(data)
+        self.client.write(request.encode("utf8")+b'\n')
+        self.client.flush()
+        response = self.client.readline()
+        if not response:
+            raise SL4AProtocolError(SL4AProtocolError.NO_RESPONSE_FROM_SERVER)
+        result = json.loads(str(response, encoding="utf8"))
+        if result['error']:
+            raise SL4AAPIError(result['error'])
+        if result['id'] != apiid:
+            raise SL4AProtocolError(SL4AProtocolError.MISMATCHED_API_ID)
+        return result['result']
+
+    def __getattr__(self, name):
+        def rpc_call(*args):
+            return self._rpc(name, *args)
+        return rpc_call
diff --git a/utils/python/controllers/android_device.py b/utils/python/controllers/android_device.py
new file mode 100644
index 0000000..17ff80b
--- /dev/null
+++ b/utils/python/controllers/android_device.py
@@ -0,0 +1,731 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from builtins import str
+from builtins import open
+
+import os
+import time
+import traceback
+
+from vts.runners.host import logger as vts_logger
+from vts.runners.host import signals
+from vts.runners.host import utils
+from vts.runners.utils.pythoncontrollers import adb
+from vts.runners.utils.pythoncontrollers import android
+from vts.runners.utils.pythoncontrollers import event_dispatcher
+from vts.runners.utils.pythoncontrollers import fastboot
+
+VTS_CONTROLLER_CONFIG_NAME = "AndroidDevice"
+VTS_CONTROLLER_REFERENCE_NAME = "android_devices"
+
+ANDROID_DEVICE_PICK_ALL_TOKEN = "*"
+# Key name for adb logcat extra params in config file.
+ANDROID_DEVICE_ADB_LOGCAT_PARAM_KEY = "adb_logcat_param"
+ANDROID_DEVICE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
+ANDROID_DEVICE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
+
+class AndroidDeviceError(signals.ControllerError):
+    pass
+
+def create(configs, logger):
+    if not configs:
+        raise AndroidDeviceError(ANDROID_DEVICE_EMPTY_CONFIG_MSG)
+    elif configs == ANDROID_DEVICE_PICK_ALL_TOKEN:
+        ads = get_all_instances(logger=logger)
+    elif not isinstance(configs, list):
+        raise AndroidDeviceError(ANDROID_DEVICE_NOT_LIST_CONFIG_MSG)
+    elif isinstance(configs[0], str):
+        # Configs is a list of serials.
+        ads = get_instances(configs, logger)
+    else:
+        # Configs is a list of dicts.
+        ads = get_instances_with_configs(configs, logger)
+    connected_ads = list_adb_devices()
+    for ad in ads:
+        if ad.serial not in connected_ads:
+            raise AndroidDeviceError(("Android device %s is specified in config"
+                                     " but is not attached.") % ad.serial)
+        ad.startAdbLogcat()
+        try:
+            ad.getSl4aClient()
+            ad.ed.start()
+        except:
+            # This exception is logged here to help with debugging under py2,
+            # because "exception raised while processing another exception" is
+            # only printed under py3.
+            msg = "Failed to start sl4a on %s" % ad.serial
+            logger.exception(msg)
+            raise AndroidDeviceError(msg)
+    return ads
+
+def destroy(ads):
+    for ad in ads:
+        try:
+            ad.closeAllSl4aSession()
+        except:
+            pass
+        if ad.adb_logcat_process:
+            ad.stopAdbLogcat()
+
+def _parse_device_list(device_list_str, key):
+    """Parses a byte string representing a list of devices. The string is
+    generated by calling either adb or fastboot.
+
+    Args:
+        device_list_str: Output of adb or fastboot.
+        key: The token that signifies a device in device_list_str.
+
+    Returns:
+        A list of android device serial numbers.
+    """
+    clean_lines = str(device_list_str, 'utf-8').strip().split('\n')
+    results = []
+    for line in clean_lines:
+        tokens = line.strip().split('\t')
+        if len(tokens) == 2 and tokens[1] == key:
+            results.append(tokens[0])
+    return results
+
+def list_adb_devices():
+    """List all android devices connected to the computer that are detected by
+    adb.
+
+    Returns:
+        A list of android device serials. Empty if there's none.
+    """
+    out = adb.AdbProxy().devices()
+    return _parse_device_list(out, "device")
+
+def list_fastboot_devices():
+    """List all android devices connected to the computer that are in in
+    fastboot mode. These are detected by fastboot.
+
+    Returns:
+        A list of android device serials. Empty if there's none.
+    """
+    out = fastboot.FastbootProxy().devices()
+    return _parse_device_list(out, "fastboot")
+
+def get_instances(serials, logger=None):
+    """Create AndroidDevice instances from a list of serials.
+
+    Args:
+        serials: A list of android device serials.
+        logger: A logger to be passed to each instance.
+
+    Returns:
+        A list of AndroidDevice objects.
+    """
+    results = []
+    for s in serials:
+        results.append(AndroidDevice(s, logger=logger))
+    return results
+
+def get_instances_with_configs(configs, logger=None):
+    """Create AndroidDevice instances from a list of json configs.
+
+    Each config should have the required key-value pair "serial".
+
+    Args:
+        configs: A list of dicts each representing the configuration of one
+            android device.
+        logger: A logger to be passed to each instance.
+
+    Returns:
+        A list of AndroidDevice objects.
+    """
+    results = []
+    for c in configs:
+        try:
+            serial = c.pop("serial")
+        except KeyError:
+            raise AndroidDeviceError(('Required value "serial" is missing in '
+                'AndroidDevice config %s.') % c)
+        ad = AndroidDevice(serial, logger=logger)
+        ad.loadConfig(c)
+        results.append(ad)
+    return results
+
+def get_all_instances(include_fastboot=False, logger=None):
+    """Create AndroidDevice instances for all attached android devices.
+
+    Args:
+        include_fastboot: Whether to include devices in bootloader mode or not.
+        logger: A logger to be passed to each instance.
+
+    Returns:
+        A list of AndroidDevice objects each representing an android device
+        attached to the computer.
+    """
+    if include_fastboot:
+        serial_list = list_adb_devices() + list_fastboot_devices()
+        return get_instances(serial_list, logger=logger)
+    return get_instances(list_adb_devices(), logger=logger)
+
+def filter_devices(ads, func):
+    """Finds the AndroidDevice instances from a list that match certain
+    conditions.
+
+    Args:
+        ads: A list of AndroidDevice instances.
+        func: A function that takes an AndroidDevice object and returns True
+            if the device satisfies the filter condition.
+
+    Returns:
+        A list of AndroidDevice instances that satisfy the filter condition.
+    """
+    results = []
+    for ad in ads:
+        if func(ad):
+            results.append(ad)
+    return results
+
+def get_device(ads, **kwargs):
+    """Finds a unique AndroidDevice instance from a list that has specific
+    attributes of certain values.
+
+    Example:
+        get_device(android_devices, label="foo", phone_number="1234567890")
+        get_device(android_devices, model="angler")
+
+    Args:
+        ads: A list of AndroidDevice instances.
+        kwargs: keyword arguments used to filter AndroidDevice instances.
+
+    Returns:
+        The target AndroidDevice instance.
+
+    Raises:
+        AndroidDeviceError is raised if none or more than one device is
+        matched.
+    """
+    def _get_device_filter(ad):
+        for k, v in kwargs.items():
+            if not hasattr(ad, k):
+                return False
+            elif getattr(ad, k) != v:
+                return False
+        return True
+    filtered = filter_devices(ads, _get_device_filter)
+    if not filtered:
+        raise AndroidDeviceError(("Could not find a target device that matches"
+                                  " condition: %s.") % kwargs)
+    elif len(filtered) == 1:
+        return filtered[0]
+    else:
+        serials = [ad.serial for ad in filtered]
+        raise AndroidDeviceError("More than one device matched: %s" % serials)
+
+def takeBugReports(ads, test_name, begin_time):
+    """Takes bug reports on a list of android devices.
+
+    If you want to take a bug report, call this function with a list of
+    android_device objects in on_fail. But reports will be taken on all the
+    devices in the list concurrently. Bug report takes a relative long
+    time to take, so use this cautiously.
+
+    Args:
+        ads: A list of AndroidDevice instances.
+        test_name: Name of the test case that triggered this bug report.
+        begin_time: Logline format timestamp taken when the test started.
+    """
+    begin_time = vts_logger.normalizeLogLineTimestamp(begin_time)
+    def take_br(test_name, begin_time, ad):
+        ad.takeBugReport(test_name, begin_time)
+    args = [(test_name, begin_time, ad) for ad in ads]
+    utils.concurrent_exec(take_br, args)
+
+class AndroidDevice:
+    """Class representing an android device.
+
+    Each object of this class represents one Android device in ACTS, including
+    handles to adb, fastboot, and sl4a clients. In addition to direct adb
+    commands, this object also uses adb port forwarding to talk to the Android
+    device.
+
+    Attributes:
+        serial: A string that's the serial number of the Androi device.
+        h_port: An integer that's the port number for adb port forwarding used
+                on the computer the Android device is connected
+        d_port: An integer  that's the port number used on the Android device
+                for adb port forwarding.
+        log: A LoggerProxy object used for the class's internal logging.
+        log_path: A string that is the path where all logs collected on this
+                  android device should be stored.
+        adb_logcat_process: A process that collects the adb logcat.
+        adb_logcat_file_path: A string that's the full path to the adb logcat
+                              file collected, if any.
+        adb: An AdbProxy object used for interacting with the device via adb.
+        fastboot: A FastbootProxy object used for interacting with the device
+                  via fastboot.
+    """
+
+    def __init__(self, serial="", host_port=None, device_port=8080,
+                 logger=None):
+        self.serial = serial
+        self.h_port = host_port
+        self.d_port = device_port
+        self.log = logging.getLogger()
+        lp = self.log.log_path
+        self.log_path = os.path.join(lp, "AndroidDevice%s" % serial)
+        self._droid_sessions = {}
+        self._event_dispatchers = {}
+        self.adb_logcat_process = None
+        self.adb_logcat_file_path = None
+        self.adb = adb.AdbProxy(serial)
+        self.fastboot = fastboot.FastbootProxy(serial)
+        if not self.isBootloaderMode:
+            self.rootAdb()
+
+    def __del__(self):
+        if self.h_port:
+            self.adb.forward("--remove tcp:%d" % self.h_port)
+        if self.adb_logcat_process:
+            self.stopAdbLogcat()
+
+    @property
+    def isBootloaderMode(self):
+        """True if the device is in bootloader mode.
+        """
+        return self.serial in list_fastboot_devices()
+
+    @property
+    def isAdbRoot(self):
+        """True if adb is running as root for this device.
+        """
+        return "root" in self.adb.shell("id -u").decode("utf-8")
+
+    @property
+    def model(self):
+        """The Android code name for the device.
+        """
+        # If device is in bootloader mode, get mode name from fastboot.
+        if self.isBootloaderMode:
+            out = self.fastboot.getvar("product").strip()
+            # "out" is never empty because of the "total time" message fastboot
+            # writes to stderr.
+            lines = out.decode("utf-8").split('\n', 1)
+            if lines:
+                tokens = lines[0].split(' ')
+                if len(tokens) > 1:
+                    return tokens[1].lower()
+            return None
+        out = self.adb.shell('getprop | grep ro.build.product')
+        model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
+        if model == "sprout":
+            return model
+        else:
+            out = self.adb.shell('getprop | grep ro.product.name')
+            model = out.decode("utf-8").strip().split('[')[-1][:-1].lower()
+            return model
+
+    @property
+    def droid(self):
+        """The first sl4a session initiated on this device. None if there isn't
+        one.
+        """
+        try:
+            session_id = sorted(self._droid_sessions)[0]
+            return self._droid_sessions[session_id][0]
+        except IndexError:
+            return None
+
+    @property
+    def ed(self):
+        """The first event_dispatcher instance created on this device. None if
+        there isn't one.
+        """
+        try:
+            session_id = sorted(self._event_dispatchers)[0]
+            return self._event_dispatchers[session_id]
+        except IndexError:
+            return None
+
+    @property
+    def droids(self):
+        """A list of the active sl4a sessions on this device.
+
+        If multiple connections exist for the same session, only one connection
+        is listed.
+        """
+        keys = sorted(self._droid_sessions)
+        results = []
+        for k in keys:
+            results.append(self._droid_sessions[k][0])
+        return results
+
+    @property
+    def eds(self):
+        """A list of the event_dispatcher objects on this device.
+
+        The indexing of the list matches that of the droids property.
+        """
+        keys = sorted(self._event_dispatchers)
+        results = []
+        for k in keys:
+            results.append(self._event_dispatchers[k])
+        return results
+
+    @property
+    def isAdbLogcatOn(self):
+        """Whether there is an ongoing adb logcat collection.
+        """
+        if self.adb_logcat_process:
+            return True
+        return False
+
+    def loadConfig(self, config):
+        """Add attributes to the AndroidDevice object based on json config.
+
+        Args:
+            config: A dictionary representing the configs.
+
+        Raises:
+            AndroidDeviceError is raised if the config is trying to overwrite
+            an existing attribute.
+        """
+        for k, v in config.items():
+            if hasattr(self, k):
+                raise AndroidDeviceError(("Attempting to set existing "
+                    "attribute %s on %s") % (k, self.serial))
+            setattr(self, k, v)
+
+    def rootAdb(self):
+        """Change adb to root mode for this device.
+        """
+        if not self.isAdbRoot:
+            self.adb.root()
+            self.adb.wait_for_device()
+            self.adb.remount()
+            self.adb.wait_for_device()
+
+    def getSl4aClient(self, handle_event=True):
+        """Create an sl4a connection to the device.
+
+        Return the connection handler 'droid'. By default, another connection
+        on the same session is made for EventDispatcher, and the dispatcher is
+        returned to the caller as well.
+        If sl4a server is not started on the device, try to start it.
+
+        Args:
+            handle_event: True if this droid session will need to handle
+                events.
+
+        Returns:
+            droid: Android object used to communicate with sl4a on the android
+                device.
+            ed: An optional EventDispatcher to organize events for this droid.
+
+        Examples:
+            Don't need event handling:
+            >>> ad = AndroidDevice()
+            >>> droid = ad.getSl4aClient(False)
+
+            Need event handling:
+            >>> ad = AndroidDevice()
+            >>> droid, ed = ad.getSl4aClient()
+        """
+        if not self.h_port or not adb.is_port_available(self.h_port):
+            self.h_port = adb.get_available_host_port()
+        self.adb.tcp_forward(self.h_port, self.d_port)
+        try:
+            droid = self.start_new_session()
+        except:
+            self.adb.start_sl4a()
+            droid = self.start_new_session()
+        if handle_event:
+            ed = self.getSl4aEventDispatcher(droid)
+            return droid, ed
+        return droid
+
+    def getSl4aEventDispatcher(self, droid):
+        """Return an EventDispatcher for an sl4a session
+
+        Args:
+            droid: Session to create EventDispatcher for.
+
+        Returns:
+            ed: An EventDispatcher for specified session.
+        """
+        ed_key = self.serial + str(droid.uid)
+        if ed_key in self._event_dispatchers:
+            if self._event_dispatchers[ed_key] is None:
+                raise AndroidDeviceError("EventDispatcher Key Empty")
+            self.log.debug("Returning existing key %s for event dispatcher!",
+                           ed_key)
+            return self._event_dispatchers[ed_key]
+        event_droid = self.add_new_connection_to_session(droid.uid)
+        ed = event_dispatcher.EventDispatcher(event_droid)
+        self._event_dispatchers[ed_key] = ed
+        return ed
+
+    def _is_timestamp_in_range(self, target, begin_time, end_time):
+        low = vts_logger.logLineTimestampComparator(begin_time, target) <= 0
+        high = vts_logger.logLineTimestampComparator(end_time, target) >= 0
+        return low and high
+
+    def takeAdbLogExcerpt(self, tag, begin_time):
+        """Takes an excerpt of the adb logcat log from a certain time point to
+        current time.
+
+        Args:
+            tag: An identifier of the time period, usualy the name of a test.
+            begin_time: Logline format timestamp of the beginning of the time
+                period.
+        """
+        if not self.adb_logcat_file_path:
+            raise AndroidDeviceError(("Attempting to cat adb log when none has"
+                                      " been collected on Android device %s."
+                                      ) % self.serial)
+        end_time = vts_logger.getLogLineTimestamp()
+        self.log.debug("Extracting adb log from logcat.")
+        adb_excerpt_path = os.path.join(self.log_path, "AdbLogExcerpts")
+        utils.create_dir(adb_excerpt_path)
+        f_name = os.path.basename(self.adb_logcat_file_path)
+        out_name = f_name.replace("adblog,", "").replace(".txt", "")
+        out_name = ",{},{}.txt".format(begin_time, out_name)
+        tag_len = utils.MAX_FILENAME_LEN - len(out_name)
+        tag = tag[:tag_len]
+        out_name = tag + out_name
+        full_adblog_path = os.path.join(adb_excerpt_path, out_name)
+        with open(full_adblog_path, 'w', encoding='utf-8') as out:
+            in_file = self.adb_logcat_file_path
+            with open(in_file, 'r', encoding='utf-8', errors='replace') as f:
+                in_range = False
+                while True:
+                    line = None
+                    try:
+                        line = f.readline()
+                        if not line:
+                            break
+                    except:
+                        continue
+                    line_time = line[:vts_logger.log_line_timestamp_len]
+                    if not vts_logger.isValidLogLineTimestamp(line_time):
+                        continue
+                    if self._is_timestamp_in_range(line_time, begin_time,
+                        end_time):
+                        in_range = True
+                        if not line.endswith('\n'):
+                            line += '\n'
+                        out.write(line)
+                    else:
+                        if in_range:
+                            break
+
+    def startAdbLogcat(self):
+        """Starts a standing adb logcat collection in separate subprocesses and
+        save the logcat in a file.
+        """
+        if self.isAdbLogcatOn:
+            raise AndroidDeviceError(("Android device {} already has an adb "
+                                     "logcat thread going on. Cannot start "
+                                     "another one.").format(self.serial))
+        # Disable adb log spam filter.
+        self.adb.shell("logpersist.start")
+        f_name = "adblog,{},{}.txt".format(self.model, self.serial)
+        utils.create_dir(self.log_path)
+        logcat_file_path = os.path.join(self.log_path, f_name)
+        try:
+            extra_params = self.adb_logcat_param
+        except AttributeError:
+            extra_params = "-b all"
+        cmd = "adb -s {} logcat -v threadtime {} >> {}".format(
+            self.serial, extra_params, logcat_file_path)
+        self.adb_logcat_process = utils.start_standing_subprocess(cmd)
+        self.adb_logcat_file_path = logcat_file_path
+
+    def stopAdbLogcat(self):
+        """Stops the adb logcat collection subprocess.
+        """
+        if not self.isAdbLogcatOn:
+            raise AndroidDeviceError(("Android device {} does not have an "
+                                      "ongoing adb logcat collection."
+                                      ).format(self.serial))
+        utils.stop_standing_subprocess(self.adb_logcat_process)
+        self.adb_logcat_process = None
+
+    def takeBugReport(self, test_name, begin_time):
+        """Takes a bug report on the device and stores it in a file.
+
+        Args:
+            test_name: Name of the test case that triggered this bug report.
+            begin_time: Logline format timestamp taken when the test started.
+        """
+        br_path = os.path.join(self.log_path, "BugReports")
+        utils.create_dir(br_path)
+        base_name = ",{},{}.txt".format(begin_time, self.serial)
+        test_name_len = utils.MAX_FILENAME_LEN - len(base_name)
+        out_name = test_name[:test_name_len] + base_name
+        full_out_path = os.path.join(br_path, out_name.replace(' ', '\ '))
+        self.log.info("Taking bugreport for %s on %s", test_name, self.serial)
+        self.adb.bugreport(" > {}".format(full_out_path))
+        self.log.info("Bugreport for %s taken at %s", test_name, full_out_path)
+
+    def start_new_session(self):
+        """Start a new session in sl4a.
+
+        Also caches the droid in a dict with its uid being the key.
+
+        Returns:
+            An Android object used to communicate with sl4a on the android
+                device.
+
+        Raises:
+            SL4AException: Something is wrong with sl4a and it returned an
+            existing uid to a new session.
+        """
+        droid = android.Android(port=self.h_port)
+        if droid.uid in self._droid_sessions:
+            raise android.SL4AException(("SL4A returned an existing uid for a "
+                "new session. Abort."))
+        self._droid_sessions[droid.uid] = [droid]
+        return droid
+
+    def add_new_connection_to_session(self, session_id):
+        """Create a new connection to an existing sl4a session.
+
+        Args:
+            session_id: UID of the sl4a session to add connection to.
+
+        Returns:
+            An Android object used to communicate with sl4a on the android
+                device.
+
+        Raises:
+            AndroidDeviceError: Raised if the session it's trying to connect to
+            does not exist.
+        """
+        if session_id not in self._droid_sessions:
+            raise AndroidDeviceError("Session %d doesn't exist." % session_id)
+        droid = android.Android(cmd='continue', uid=session_id,
+            port=self.h_port)
+        return droid
+
+    def closeOneSl4aSession(self, session_id):
+        """Terminate a session in sl4a.
+
+        Send terminate signal to sl4a server; stop dispatcher associated with
+        the session. Clear corresponding droids and dispatchers from cache.
+
+        Args:
+            session_id: UID of the sl4a session to terminate.
+        """
+        if self._droid_sessions and (session_id in self._droid_sessions):
+            for droid in self._droid_sessions[session_id]:
+                droid.closeSl4aSession()
+                droid.close()
+            del self._droid_sessions[session_id]
+        ed_key = self.serial + str(session_id)
+        if ed_key in self._event_dispatchers:
+            self._event_dispatchers[ed_key].clean_up()
+            del self._event_dispatchers[ed_key]
+
+    def closeAllSl4aSession(self):
+        """Terminate all sl4a sessions on the AndroidDevice instance.
+
+        Terminate all sessions and clear caches.
+        """
+        if self._droid_sessions:
+            session_ids = list(self._droid_sessions.keys())
+            for session_id in session_ids:
+                try:
+                    self.closeOneSl4aSession(session_id)
+                except:
+                    msg = "Failed to terminate session %d." % session_id
+                    self.log.exception(msg)
+                    self.log.error(traceback.format_exc())
+            if self.h_port:
+                self.adb.forward("--remove tcp:%d" % self.h_port)
+                self.h_port = None
+
+    def runIperfClient(self, server_host, extra_args=""):
+        """Start iperf client on the device.
+
+        Return status as true if iperf client start successfully.
+        And data flow information as results.
+
+        Args:
+            server_host: Address of the iperf server.
+            extra_args: A string representing extra arguments for iperf client,
+                e.g. "-i 1 -t 30".
+
+        Returns:
+            status: true if iperf client start successfully.
+            results: results have data flow information
+        """
+        out = self.adb.shell("iperf3 -c {} {}".format(server_host, extra_args))
+        clean_out = str(out,'utf-8').strip().split('\n')
+        if "error" in clean_out[0].lower():
+            return False, clean_out
+        return True, clean_out
+
+    @utils.timeout(15 * 60)
+    def waitForBootCompletion(self):
+        """Waits for Android framework to broadcast ACTION_BOOT_COMPLETED.
+
+        This function times out after 15 minutes.
+        """
+        self.adb.wait_for_device()
+        while True:
+            try:
+                out = self.adb.shell("getprop sys.boot_completed")
+                completed = out.decode('utf-8').strip()
+                if completed == '1':
+                    return
+            except adb.AdbError:
+                # adb shell calls may fail during certain period of booting
+                # process, which is normal. Ignoring these errors.
+                pass
+            time.sleep(5)
+
+    def reboot(self):
+        """Reboots the device.
+
+        Terminate all sl4a sessions, reboot the device, wait for device to
+        complete booting, and restart an sl4a session.
+
+        This is a blocking method.
+
+        This is probably going to print some error messages in console. Only
+        use if there's no other option.
+
+        Example:
+            droid, ed = ad.reboot()
+
+        Returns:
+            An sl4a session with an event_dispatcher.
+
+        Raises:
+            AndroidDeviceError is raised if waiting for completion timed
+            out.
+        """
+        if self.isBootloaderMode:
+            self.fastboot.reboot()
+            return
+        has_adb_log = self.isAdbLogcatOn
+        if has_adb_log:
+            self.stopAdbLogcat()
+        self.closeAllSl4aSession()
+        self.adb.reboot()
+        self.waitForBootCompletion()
+        self.rootAdb()
+        droid, ed = self.getSl4aClient()
+        ed.start()
+        if has_adb_log:
+            self.startAdbLogcat()
+        return droid, ed
diff --git a/utils/python/controllers/attenuator.py b/utils/python/controllers/attenuator.py
new file mode 100644
index 0000000..7dd14f2
--- /dev/null
+++ b/utils/python/controllers/attenuator.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import importlib
+
+from vts.runners.host.keys import Config
+
+VTS_CONTROLLER_CONFIG_NAME = "Attenuator"
+VTS_CONTROLLER_REFERENCE_NAME = "attenuators"
+
+def create(configs, logger):
+    objs = []
+    for c in configs:
+        attn_model = c["Model"]
+        # Default to telnet.
+        protocol = "telnet"
+        if "Protocol" in c:
+            protocol = c["Protocol"]
+        module_name = "vts.utils.python.controllers.attenuator_lib.%s.%s" % (attn_model,
+            protocol)
+        module = importlib.import_module(module_name)
+        inst_cnt = c["InstrumentCount"]
+        attn_inst = module.AttenuatorInstrument(inst_cnt)
+        attn_inst.model = attn_model
+        insts = attn_inst.open(c[Config.key_address.value],
+            c[Config.key_port.value])
+        for i in range(inst_cnt):
+            attn = Attenuator(attn_inst, idx=i)
+            if "Paths" in c:
+                try:
+                    setattr(attn, "path", c["Paths"][i])
+                except IndexError:
+                    logger.error("No path specified for attenuator %d." % i)
+                    raise
+            objs.append(attn)
+    return objs
+
+def destroy(objs):
+    return
+
+r"""
+Base classes which define how attenuators should be accessed, managed, and manipulated.
+
+Users will instantiate a specific child class, but almost all operation should be performed
+on the methods and data members defined here in the base classes or the wrapper classes.
+"""
+
+
+class AttenuatorError(Exception):
+    r"""This is the Exception class defined for all errors generated by Attenuator-related modules.
+    """
+    pass
+
+
+class InvalidDataError(AttenuatorError):
+    r"""This exception is  thrown when an unexpected result is seen on the transport layer below
+    the module.
+
+    When this exception is seen, closing an re-opening the link to the attenuator instrument is
+    probably necessary. Something has gone wrong in the transport.
+    """
+    pass
+
+
+class InvalidOperationError(AttenuatorError):
+    r"""Certain methods may only be accessed when the instance upon which they are invoked is in
+    a certain state. This indicates that the object is not in the correct state for a method to be
+    called.
+    """
+    pass
+
+
+class AttenuatorInstrument():
+    r"""This is a base class that defines the primitive behavior of all attenuator
+    instruments.
+
+    The AttenuatorInstrument class is designed to provide a simple low-level interface for
+    accessing any step attenuator instrument comprised of one or more attenuators and a
+    controller. All AttenuatorInstruments should override all the methods below and call
+    AttenuatorInstrument.__init__ in their constructors. Outside of setup/teardown,
+    devices should be accessed via this generic "interface".
+    """
+    model = None
+    INVALID_MAX_ATTEN = 999.9
+
+    def __init__(self, num_atten=0):
+        r"""This is the Constructor for Attenuator Instrument.
+
+        Parameters
+        ----------
+        num_atten : This optional parameter is the number of attenuators contained within the
+        instrument. In some instances setting this number to zero will allow the driver to
+        auto-determine, the number of attenuators; however, this behavior is not guaranteed.
+
+        Raises
+        ------
+        NotImplementedError
+            This constructor should never be called directly. It may only be called by a child.
+
+        Returns
+        -------
+        self
+            Returns a newly constructed AttenuatorInstrument
+        """
+
+        if type(self) is AttenuatorInstrument:
+            raise NotImplementedError("Base class should not be instantiated directly!")
+
+        self.num_atten = num_atten
+        self.max_atten = AttenuatorInstrument.INVALID_MAX_ATTEN
+        self.properties = None
+
+    def set_atten(self, idx, value):
+        r"""This function sets the attenuation of an attenuator given its index in the instrument.
+
+        Parameters
+        ----------
+        idx : This zero-based index is the identifier for a particular attenuator in an
+        instrument.
+        value : This is a floating point value for nominal attenuation to be set.
+
+        Raises
+        ------
+        NotImplementedError
+            This constructor should never be called directly. It may only be called by a child.
+        """
+        raise NotImplementedError("Base class should not be called directly!")
+
+    def get_atten(self, idx):
+        r"""This function returns the current attenuation from an attenuator at a given index in
+        the instrument.
+
+        Parameters
+        ----------
+        idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+
+        Raises
+        ------
+        NotImplementedError
+            This constructor should never be called directly. It may only be called by a child.
+
+        Returns
+        -------
+        float
+            Returns a the current attenuation value
+        """
+        raise NotImplementedError("Base class should not be called directly!")
+
+
+class Attenuator():
+    r"""This class defines an object representing a single attenuator in a remote instrument.
+
+    A user wishing to abstract the mapping of attenuators to physical instruments should use this
+    class, which provides an object that obscures the physical implementation an allows the user
+    to think only of attenuators regardless of their location.
+    """
+
+    def __init__(self, instrument, idx=0, offset=0):
+        r"""This is the constructor for Attenuator
+
+        Parameters
+        ----------
+        instrument : Reference to an AttenuatorInstrument on which the Attenuator resides
+        idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+        offset : A power offset value for the attenuator to be used when performing future
+        operations. This could be used for either calibration or to allow group operations with
+        offsets between various attenuators.
+
+        Raises
+        ------
+        TypeError
+            Requires a valid AttenuatorInstrument to be passed in.
+        IndexError
+            The index of the attenuator in the AttenuatorInstrument must be within the valid range.
+
+        Returns
+        -------
+        self
+            Returns a newly constructed Attenuator
+        """
+        if not isinstance(instrument, AttenuatorInstrument):
+            raise TypeError("Must provide an Attenuator Instrument Ref")
+        self.model = instrument.model
+        self.instrument = instrument
+        self.idx = idx
+        self.offset = offset
+
+        if(self.idx >= instrument.num_atten):
+            raise IndexError("Attenuator index out of range for attenuator instrument")
+
+    def set_atten(self, value):
+        r"""This function sets the attenuation of Attenuator.
+
+        Parameters
+        ----------
+        value : This is a floating point value for nominal attenuation to be set.
+
+        Raises
+        ------
+        ValueError
+            The requested set value+offset must be less than the maximum value.
+        """
+
+        if value+self.offset > self.instrument.max_atten:
+            raise ValueError("Attenuator Value+Offset greater than Max Attenuation!")
+
+        self.instrument.set_atten(self.idx, value+self.offset)
+
+    def get_atten(self):
+        r"""This function returns the current attenuation setting of Attenuator, normalized by
+        the set offset.
+
+        Returns
+        -------
+        float
+            Returns a the current attenuation value
+        """
+
+        return self.instrument.get_atten(self.idx) - self.offset
+
+    def get_max_atten(self):
+        r"""This function returns the max attenuation setting of Attenuator, normalized by
+        the set offset.
+
+        Returns
+        -------
+        float
+            Returns a the max attenuation value
+        """
+        if (self.instrument.max_atten == AttenuatorInstrument.INVALID_MAX_ATTEN):
+            raise ValueError("Invalid Max Attenuator Value")
+
+        return self.instrument.max_atten - self.offset
+
+
+class AttenuatorGroup(object):
+    r"""This is a handy abstraction for groups of attenuators that will share behavior.
+
+    Attenuator groups are intended to further facilitate abstraction of testing functions from
+    the physical objects underlying them. By adding attenuators to a group, it is possible to
+    operate on functional groups that can be thought of in a common manner in the test. This
+    class is intended to provide convenience to the user and avoid re-implementation of helper
+    functions and small loops scattered throughout user code.
+
+    """
+
+    def __init__(self, name=""):
+        r"""This is the constructor for AttenuatorGroup
+
+        Parameters
+        ----------
+        name : The name is an optional parameter intended to further facilitate the passing of
+        easily tracked groups of attenuators throughout code. It is left to the user to use the
+        name in a way that meets their needs.
+
+        Returns
+        -------
+        self
+            Returns a newly constructed AttenuatorGroup
+        """
+        self.name = name
+        self.attens = []
+        self._value = 0
+
+    def add_from_instrument(self, instrument, indices):
+        r"""This function provides a way to create groups directly from the Attenuator Instrument.
+
+        This function will create Attenuator objects for all of the indices passed in and add
+        them to the group.
+
+        Parameters
+        ----------
+        instrument : A ref to the instrument from which attenuators will be added
+        indices : You pay pass in the indices either as a range, a list, or a single integer.
+
+        Raises
+        ------
+        TypeError
+            Requires a valid AttenuatorInstrument to be passed in.
+        """
+
+        if not instrument or not isinstance(instrument, AttenuatorInstrument):
+            raise TypeError("Must provide an Attenuator Instrument Ref")
+
+        if type(indices) is range or type(indices) is list:
+            for i in indices:
+                self.attens.append(Attenuator(instrument, i))
+        elif type(indices) is int:
+            self.attens.append(Attenuator(instrument, indices))
+
+    def add(self, attenuator):
+        r"""This function adds an already constructed Attenuator object to the AttenuatorGroup.
+
+        Parameters
+        ----------
+        attenuator : An Attenuator object.
+
+        Raises
+        ------
+        TypeError
+            Requires a valid Attenuator to be passed in.
+        """
+
+        if not isinstance(attenuator, Attenuator):
+            raise TypeError("Must provide an Attenuator")
+
+        self.attens.append(attenuator)
+
+    def synchronize(self):
+        r"""This function can be called to ensure all Attenuators within a group are set
+        appropriately.
+        """
+
+        self.set_atten(self._value)
+
+    def is_synchronized(self):
+        r"""This function queries all the Attenuators in the group to determine whether or not
+        they are synchronized.
+
+        Returns
+        -------
+        bool
+            True if the attenuators are synchronized.
+        """
+
+        for att in self.attens:
+            if att.get_atten() != self._value:
+                return False
+        return True
+
+    def set_atten(self, value):
+        r"""This function sets the attenuation value of all attenuators in the group.
+
+        Parameters
+        ----------
+        value : This is a floating point value for nominal attenuation to be set.
+
+        Returns
+        -------
+        bool
+            True if the attenuators are synchronized.
+        """
+
+        value = float(value)
+        for att in self.attens:
+            att.set_atten(value)
+        self._value = value
+
+    def get_atten(self):
+        r"""This function returns the current attenuation setting of AttenuatorGroup.
+
+        This returns a cached value that assumes the attenuators are synchronized. It avoids a
+        relatively expensive call for a common operation, and trusts the user to ensure
+        synchronization.
+
+        Returns
+        -------
+        float
+            Returns a the current attenuation value for the group, which is independent of any
+            individual attenuator offsets.
+        """
+
+        return float(self._value)
diff --git a/utils/python/controllers/attenuator_lib/__init__.py b/utils/python/controllers/attenuator_lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/__init__.py
diff --git a/utils/python/controllers/attenuator_lib/_tnhelper.py b/utils/python/controllers/attenuator_lib/_tnhelper.py
new file mode 100644
index 0000000..f1a35c9
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/_tnhelper.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python3.4
+
+#   Copyright 2016- The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""
+Helper module for common telnet capability to communicate with AttenuatorInstrument(s).
+
+User code shouldn't need to directly access this class.
+"""
+
+
+import telnetlib
+from vts.utils.python.controllers import attenuator
+
+
+def _ascii_string(uc_string):
+    return str(uc_string).encode('ASCII')
+
+
+class _TNHelper():
+    #This is an internal helper class for Telnet+SCPI command-based instruments.
+    #It should only be used by those implemention control libraries and not by any user code
+    # directly
+
+    def __init__(self, tx_cmd_separator="\n", rx_cmd_separator="\n", prompt=""):
+        self._tn = None
+
+        self.tx_cmd_separator = tx_cmd_separator
+        self.rx_cmd_separator = rx_cmd_separator
+        self.prompt = prompt
+
+    def open(self, host, port=23):
+        if self._tn:
+            self._tn.close()
+
+        self._tn = telnetlib.Telnet()
+        self._tn.open(host, port, 10)
+
+    def is_open(self):
+        return bool(self._tn)
+
+    def close(self):
+        if self._tn:
+            self._tn.close()
+            self._tn = None
+
+    def cmd(self, cmd_str, wait_ret=True):
+        if not isinstance(cmd_str, str):
+            raise TypeError("Invalid command string", cmd_str)
+
+        if not self.is_open():
+            raise attenuator.InvalidOperationError("Telnet connection not open for commands")
+
+        cmd_str.strip(self.tx_cmd_separator)
+        self._tn.read_until(_ascii_string(self.prompt), 2)
+        self._tn.write(_ascii_string(cmd_str+self.tx_cmd_separator))
+
+        if wait_ret is False:
+            return None
+
+        match_idx, match_val, ret_text = \
+            self._tn.expect([_ascii_string("\S+"+self.rx_cmd_separator)], 1)
+
+        if match_idx == -1:
+            raise attenuator.InvalidDataError("Telnet command failed to return valid data")
+
+        ret_text = ret_text.decode()
+        ret_text = ret_text.strip(self.tx_cmd_separator + self.rx_cmd_separator + self.prompt)
+
+        return ret_text
diff --git a/utils/python/controllers/attenuator_lib/aeroflex/__init__.py b/utils/python/controllers/attenuator_lib/aeroflex/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/aeroflex/__init__.py
diff --git a/utils/python/controllers/attenuator_lib/aeroflex/telnet.py b/utils/python/controllers/attenuator_lib/aeroflex/telnet.py
new file mode 100644
index 0000000..4ce5612
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/aeroflex/telnet.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3.4
+
+#   Copyright 2016- The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""
+Class for Telnet control of Aeroflex 832X and 833X Series Attenuator Modules
+
+This class provides a wrapper to the Aeroflex attenuator modules for purposes
+of simplifying and abstracting control down to the basic necessities. It is
+not the intention of the module to expose all functionality, but to allow
+interchangeable HW to be used.
+
+See http://www.aeroflex.com/ams/weinschel/PDFILES/IM-608-Models-8320-&-8321-preliminary.pdf
+"""
+
+
+from vts.utils.python.controllers import attenuator
+from vts.utils.python.controllers.attenuator_lib import _tnhelper
+
+
+class AttenuatorInstrument(attenuator.AttenuatorInstrument):
+
+    def __init__(self, num_atten=0):
+        super().__init__(num_atten)
+
+        self._tnhelper = _tnhelper._TNHelper(tx_cmd_separator="\r\n",
+                                             rx_cmd_separator="\r\n",
+                                             prompt=">")
+        self.properties = None
+
+    def open(self, host, port=23):
+        r"""Opens a telnet connection to the desired AttenuatorInstrument and queries basic
+        information.
+
+        Parameters
+        ----------
+        host : A valid hostname (IP address or DNS-resolvable name) to an MC-DAT attenuator
+        instrument.
+        port : An optional port number (defaults to telnet default 23)
+        """
+        self._tnhelper.open(host, port)
+
+        # work around a bug in IO, but this is a good thing to do anyway
+        self._tnhelper.cmd("*CLS", False)
+
+        if self.num_atten == 0:
+            self.num_atten = int(self._tnhelper.cmd("RFCONFIG? CHAN"))
+
+        configstr = self._tnhelper.cmd("RFCONFIG? ATTN 1")
+
+        self.properties = dict(zip(['model', 'max_atten', 'min_step',
+                                    'unknown', 'unknown2', 'cfg_str'],
+                                   configstr.split(", ", 5)))
+
+        self.max_atten = float(self.properties['max_atten'])
+
+    def is_open(self):
+        r"""This function returns the state of the telnet connection to the underlying
+        AttenuatorInstrument.
+
+        Returns
+        -------
+        Bool
+            True if there is a successfully open connection to the AttenuatorInstrument
+        """
+
+        return bool(self._tnhelper.is_open())
+
+    def close(self):
+        r"""Closes a telnet connection to the desired AttenuatorInstrument.
+
+        This should be called as part of any teardown procedure prior to the attenuator
+        instrument leaving scope.
+        """
+
+        self._tnhelper.close()
+
+    def set_atten(self, idx, value):
+        r"""This function sets the attenuation of an attenuator given its index in the instrument.
+
+        Parameters
+        ----------
+        idx : This zero-based index is the identifier for a particular attenuator in an
+        instrument.
+        value : This is a floating point value for nominal attenuation to be set.
+
+        Raises
+        ------
+        InvalidOperationError
+            This error occurs if the underlying telnet connection to the instrument is not open.
+        IndexError
+            If the index of the attenuator is greater than the maximum index of the underlying
+            instrument, this error will be thrown. Do not count on this check programmatically.
+        ValueError
+            If the requested set value is greater than the maximum attenuation value, this error
+            will be thrown. Do not count on this check programmatically.
+        """
+
+
+        if not self.is_open():
+            raise attenuator.InvalidOperationError("Connection not open!")
+
+        if idx >= self.num_atten:
+            raise IndexError("Attenuator index out of range!", self.num_atten, idx)
+
+        if value > self.max_atten:
+            raise ValueError("Attenuator value out of range!", self.max_atten, value)
+
+        self._tnhelper.cmd("ATTN " + str(idx+1) + " " + str(value), False)
+
+    def get_atten(self, idx):
+        r"""This function returns the current attenuation from an attenuator at a given index in
+        the instrument.
+
+        Parameters
+        ----------
+        idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+
+        Raises
+        ------
+        InvalidOperationError
+            This error occurs if the underlying telnet connection to the instrument is not open.
+
+        Returns
+        -------
+        float
+            Returns a the current attenuation value
+        """
+        if not self.is_open():
+            raise attenuator.InvalidOperationError("Connection not open!")
+
+#       Potentially redundant safety check removed for the moment
+#       if idx >= self.num_atten:
+#           raise IndexError("Attenuator index out of range!", self.num_atten, idx)
+
+        atten_val = self._tnhelper.cmd("ATTN? " + str(idx+1))
+
+        return float(atten_val)
diff --git a/utils/python/controllers/attenuator_lib/minicircuits/__init__.py b/utils/python/controllers/attenuator_lib/minicircuits/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/minicircuits/__init__.py
diff --git a/utils/python/controllers/attenuator_lib/minicircuits/telnet.py b/utils/python/controllers/attenuator_lib/minicircuits/telnet.py
new file mode 100644
index 0000000..1bcb98b
--- /dev/null
+++ b/utils/python/controllers/attenuator_lib/minicircuits/telnet.py
@@ -0,0 +1,153 @@
+#!/usr/bin/env python3.4
+
+#   Copyright 2016- The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+"""
+Class for Telnet control of Mini-Circuits RCDAT series attenuators
+
+This class provides a wrapper to the MC-RCDAT attenuator modules for purposes
+of simplifying and abstracting control down to the basic necessities. It is
+not the intention of the module to expose all functionality, but to allow
+interchangeable HW to be used.
+
+See http://www.minicircuits.com/softwaredownload/Prog_Manual-6-Programmable_Attenuator.pdf
+"""
+
+
+from vts.utils.python.controllers import attenuator
+from vts.utils.python.controllers.attenuator_lib import _tnhelper
+
+
+class AttenuatorInstrument(attenuator.AttenuatorInstrument):
+    r"""This provides a specific telnet-controlled implementation of AttenuatorInstrument for
+    Mini-Circuits RC-DAT attenuators.
+
+    With the exception of telnet-specific commands, all functionality is defined by the
+    AttenuatorInstrument class. Because telnet is a stateful protocol, the functionality of
+    AttenuatorInstrument is contingent upon a telnet connection being established.
+    """
+
+    def __init__(self, num_atten=0):
+        super().__init__(num_atten)
+        self._tnhelper = _tnhelper._TNHelper(tx_cmd_separator="\r\n",
+                                             rx_cmd_separator="\n\r",
+                                             prompt="")
+
+    def __del__(self):
+        if self.is_open():
+            self.close()
+
+    def open(self, host, port=23):
+        r"""Opens a telnet connection to the desired AttenuatorInstrument and queries basic
+        information.
+
+        Parameters
+        ----------
+        host : A valid hostname (IP address or DNS-resolvable name) to an MC-DAT attenuator
+        instrument.
+        port : An optional port number (defaults to telnet default 23)
+        """
+
+        self._tnhelper.open(host, port)
+
+        if self.num_atten == 0:
+            self.num_atten = 1
+
+        config_str = self._tnhelper.cmd("MN?")
+
+        if config_str.startswith("MN="):
+            config_str = config_str[len("MN="):]
+
+        self.properties = dict(zip(['model', 'max_freq', 'max_atten'], config_str.split("-", 2)))
+        self.max_atten = float(self.properties['max_atten'])
+
+    def is_open(self):
+        r"""This function returns the state of the telnet connection to the underlying
+        AttenuatorInstrument.
+
+        Returns
+        -------
+        Bool
+            True if there is a successfully open connection to the AttenuatorInstrument
+        """
+
+        return bool(self._tnhelper.is_open())
+
+    def close(self):
+        r"""Closes a telnet connection to the desired AttenuatorInstrument.
+
+        This should be called as part of any teardown procedure prior to the attenuator
+        instrument leaving scope.
+        """
+
+        self._tnhelper.close()
+
+    def set_atten(self, idx, value):
+        r"""This function sets the attenuation of an attenuator given its index in the instrument.
+
+        Parameters
+        ----------
+        idx : This zero-based index is the identifier for a particular attenuator in an
+        instrument.
+        value : This is a floating point value for nominal attenuation to be set.
+
+        Raises
+        ------
+        InvalidOperationError
+            This error occurs if the underlying telnet connection to the instrument is not open.
+        IndexError
+            If the index of the attenuator is greater than the maximum index of the underlying
+            instrument, this error will be thrown. Do not count on this check programmatically.
+        ValueError
+            If the requested set value is greater than the maximum attenuation value, this error
+            will be thrown. Do not count on this check programmatically.
+        """
+
+        if not self.is_open():
+            raise attenuator.InvalidOperationError("Connection not open!")
+
+        if idx >= self.num_atten:
+            raise IndexError("Attenuator index out of range!", self.num_atten, idx)
+
+        if value > self.max_atten:
+            raise ValueError("Attenuator value out of range!", self.max_atten, value)
+
+        self._tnhelper.cmd("SETATT=" + str(value))
+
+    def get_atten(self, idx):
+        r"""This function returns the current attenuation from an attenuator at a given index in
+        the instrument.
+
+        Parameters
+        ----------
+        idx : This zero-based index is the identifier for a particular attenuator in an instrument.
+
+        Raises
+        ------
+        InvalidOperationError
+            This error occurs if the underlying telnet connection to the instrument is not open.
+
+        Returns
+        -------
+        float
+            Returns a the current attenuation value
+        """
+
+        if not self.is_open():
+            raise attenuator.InvalidOperationError("Connection not open!")
+
+        atten_val = self._tnhelper.cmd("ATT?")
+
+        return float(atten_val)
diff --git a/utils/python/controllers/event_dispatcher.py b/utils/python/controllers/event_dispatcher.py
new file mode 100644
index 0000000..92b9c65
--- /dev/null
+++ b/utils/python/controllers/event_dispatcher.py
@@ -0,0 +1,423 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016- The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from concurrent.futures import ThreadPoolExecutor
+import queue
+import re
+import socket
+import threading
+import time
+import traceback
+
+class EventDispatcherError(Exception):
+    pass
+
+class IllegalStateError(EventDispatcherError):
+    """Raise when user tries to put event_dispatcher into an illegal state.
+    """
+
+class DuplicateError(EventDispatcherError):
+    """Raise when a duplicate is being created and it shouldn't.
+    """
+
+class EventDispatcher:
+    """Class managing events for an sl4a connection.
+    """
+
+    DEFAULT_TIMEOUT = 60
+
+    def __init__(self, droid):
+        self.droid = droid
+        self.started = False
+        self.executor = None
+        self.poller = None
+        self.event_dict = {}
+        self.handlers = {}
+        self.lock = threading.RLock()
+
+    def poll_events(self):
+        """Continuously polls all types of events from sl4a.
+
+        Events are sorted by name and store in separate queues.
+        If there are registered handlers, the handlers will be called with
+        corresponding event immediately upon event discovery, and the event
+        won't be stored. If exceptions occur, stop the dispatcher and return
+        """
+        while self.started:
+            event_obj = None
+            event_name = None
+            try:
+                event_obj = self.droid.eventWait(50000)
+            except:
+                if self.started:
+                    print("Exception happened during polling.")
+                    print(traceback.format_exc())
+                    raise
+            if not event_obj:
+                continue
+            elif 'name' not in event_obj:
+                print("Received Malformed event {}".format(event_obj))
+                continue
+            else:
+                event_name = event_obj['name']
+            # if handler registered, process event
+            if event_name in self.handlers:
+                self.handle_subscribed_event(event_obj, event_name)
+            if event_name == "EventDispatcherShutdown":
+                self.droid.closeSl4aSession()
+                break
+            else:
+                self.lock.acquire()
+                if event_name in self.event_dict:  # otherwise, cache event
+                    self.event_dict[event_name].put(event_obj)
+                else:
+                    q = queue.Queue()
+                    q.put(event_obj)
+                    self.event_dict[event_name] = q
+                self.lock.release()
+
+    def register_handler(self, handler, event_name, args):
+        """Registers an event handler.
+
+        One type of event can only have one event handler associated with it.
+
+        Args:
+            handler: The event handler function to be registered.
+            event_name: Name of the event the handler is for.
+            args: User arguments to be passed to the handler when it's called.
+
+        Raises:
+            IllegalStateError: Raised if attempts to register a handler after
+                the dispatcher starts running.
+            DuplicateError: Raised if attempts to register more than one
+                handler for one type of event.
+        """
+        if self.started:
+            raise IllegalStateError(("Can't register service after polling is"
+                " started"))
+        self.lock.acquire()
+        try:
+            if event_name in self.handlers:
+                raise DuplicateError(
+                    'A handler for {} already exists'.format(event_name))
+            self.handlers[event_name] = (handler, args)
+        finally:
+            self.lock.release()
+
+    def start(self):
+        """Starts the event dispatcher.
+
+        Initiates executor and start polling events.
+
+        Raises:
+            IllegalStateError: Can't start a dispatcher again when it's already
+                running.
+        """
+        if not self.started:
+            self.started = True
+            self.executor = ThreadPoolExecutor(max_workers=32)
+            self.poller = self.executor.submit(self.poll_events)
+        else:
+            raise IllegalStateError("Dispatcher is already started.")
+
+    def clean_up(self):
+        """Clean up and release resources after the event dispatcher polling
+        loop has been broken.
+
+        The following things happen:
+        1. Clear all events and flags.
+        2. Close the sl4a client the event_dispatcher object holds.
+        3. Shut down executor without waiting.
+        """
+        uid = self.droid.uid
+        if not self.started:
+            return
+        self.started = False
+        self.clear_all_events()
+        self.droid.close()
+        self.poller.set_result("Done")
+        # The polling thread is guaranteed to finish after a max of 60 seconds,
+        # so we don't wait here.
+        self.executor.shutdown(wait=False)
+
+    def pop_event(self, event_name, timeout=DEFAULT_TIMEOUT):
+        """Pop an event from its queue.
+
+        Return and remove the oldest entry of an event.
+        Block until an event of specified name is available or
+        times out if timeout is set.
+
+        Args:
+            event_name: Name of the event to be popped.
+            timeout: Number of seconds to wait when event is not present.
+                Never times out if None.
+
+        Returns:
+            event: The oldest entry of the specified event. None if timed out.
+
+        Raises:
+            IllegalStateError: Raised if pop is called before the dispatcher
+                starts polling.
+        """
+        if not self.started:
+            raise IllegalStateError(
+                "Dispatcher needs to be started before popping.")
+
+        e_queue = self.get_event_q(event_name)
+
+        if not e_queue:
+            raise TypeError(
+                "Failed to get an event queue for {}".format(event_name))
+
+        try:
+            # Block for timeout
+            if timeout:
+                return e_queue.get(True, timeout)
+            # Non-blocking poll for event
+            elif timeout == 0:
+                return e_queue.get(False)
+            else:
+            # Block forever on event wait
+                return e_queue.get(True)
+        except queue.Empty:
+            raise queue.Empty(
+                'Timeout after {}s waiting for event: {}'.format(
+                    timeout, event_name))
+
+    def wait_for_event(self, event_name, predicate,
+                       timeout=DEFAULT_TIMEOUT, *args, **kwargs):
+        """Wait for an event that satisfies a predicate to appear.
+
+        Continuously pop events of a particular name and check against the
+        predicate until an event that satisfies the predicate is popped or
+        timed out. Note this will remove all the events of the same name that
+        do not satisfy the predicate in the process.
+
+        Args:
+            event_name: Name of the event to be popped.
+            predicate: A function that takes an event and returns True if the
+                predicate is satisfied, False otherwise.
+            timeout: Number of seconds to wait.
+            *args: Optional positional args passed to predicate().
+            **kwargs: Optional keyword args passed to predicate().
+
+        Returns:
+            The event that satisfies the predicate.
+
+        Raises:
+            queue.Empty: Raised if no event that satisfies the predicate was
+                found before time out.
+        """
+        deadline = time.time() + timeout
+
+        while True:
+            event = None
+            try:
+                event = self.pop_event(event_name, 1)
+            except queue.Empty:
+                pass
+
+            if event and predicate(event, *args, **kwargs):
+                return event
+
+            if time.time() > deadline:
+                raise queue.Empty(
+                    'Timeout after {}s waiting for event: {}'.format(
+                        timeout, event_name))
+
+    def pop_events(self, regex_pattern, timeout):
+        """Pop events whose names match a regex pattern.
+
+        If such event(s) exist, pop one event from each event queue that
+        satisfies the condition. Otherwise, wait for an event that satisfies
+        the condition to occur, with timeout.
+
+        Results are sorted by timestamp in ascending order.
+
+        Args:
+            regex_pattern: The regular expression pattern that an event name
+                should match in order to be popped.
+            timeout: Number of seconds to wait for events in case no event
+                matching the condition exits when the function is called.
+
+        Returns:
+            results: Pop events whose names match a regex pattern.
+                Empty if none exist and the wait timed out.
+
+        Raises:
+            IllegalStateError: Raised if pop is called before the dispatcher
+                starts polling.
+            queue.Empty: Raised if no event was found before time out.
+        """
+        if not self.started:
+            raise IllegalStateError(
+                "Dispatcher needs to be started before popping.")
+        deadline = time.time() + timeout
+        while True:
+            #TODO: fix the sleep loop
+            results = self._match_and_pop(regex_pattern)
+            if len(results) != 0 or time.time() > deadline:
+                break
+            time.sleep(1)
+        if len(results) == 0:
+            raise queue.Empty(
+                'Timeout after {}s waiting for event: {}'.format(
+                    timeout, regex_pattern))
+
+        return sorted(results, key=lambda event : event['time'])
+
+    def _match_and_pop(self, regex_pattern):
+        """Pop one event from each of the event queues whose names
+        match (in a sense of regular expression) regex_pattern.
+        """
+        results = []
+        self.lock.acquire()
+        for name in self.event_dict.keys():
+            if re.match(regex_pattern, name):
+                q = self.event_dict[name]
+                if q:
+                    try:
+                        results.append(q.get(False))
+                    except:
+                        pass
+        self.lock.release()
+        return results
+
+    def get_event_q(self, event_name):
+        """Obtain the queue storing events of the specified name.
+
+        If no event of this name has been polled, wait for one to.
+
+        Returns:
+            queue: A queue storing all the events of the specified name.
+                None if timed out.
+            timeout: Number of seconds to wait for the operation.
+
+        Raises:
+            queue.Empty: Raised if the queue does not exist and timeout has
+                passed.
+        """
+        self.lock.acquire()
+        if not event_name in self.event_dict or self.event_dict[event_name] is None:
+            self.event_dict[event_name] = queue.Queue()
+        self.lock.release()
+
+        event_queue = self.event_dict[event_name]
+        return event_queue
+
+    def handle_subscribed_event(self, event_obj, event_name):
+        """Execute the registered handler of an event.
+
+        Retrieve the handler and its arguments, and execute the handler in a
+            new thread.
+
+        Args:
+            event_obj: Json object of the event.
+            event_name: Name of the event to call handler for.
+        """
+        handler, args = self.handlers[event_name]
+        self.executor.submit(handler, event_obj, *args)
+
+
+    def _handle(self, event_handler, event_name, user_args, event_timeout,
+        cond, cond_timeout):
+        """Pop an event of specified type and calls its handler on it. If
+        condition is not None, block until condition is met or timeout.
+        """
+        if cond:
+            cond.wait(cond_timeout)
+        event = self.pop_event(event_name, event_timeout)
+        return event_handler(event, *user_args)
+
+    def handle_event(self, event_handler, event_name, user_args,
+        event_timeout=None, cond=None, cond_timeout=None):
+        """Handle events that don't have registered handlers
+
+        In a new thread, poll one event of specified type from its queue and
+        execute its handler. If no such event exists, the thread waits until
+        one appears.
+
+        Args:
+            event_handler: Handler for the event, which should take at least
+                one argument - the event json object.
+            event_name: Name of the event to be handled.
+            user_args: User arguments for the handler; to be passed in after
+                the event json.
+            event_timeout: Number of seconds to wait for the event to come.
+            cond: A condition to wait on before executing the handler. Should
+                be a threading.Event object.
+            cond_timeout: Number of seconds to wait before the condition times
+                out. Never times out if None.
+
+        Returns:
+            worker: A concurrent.Future object associated with the handler.
+                If blocking call worker.result() is triggered, the handler
+                needs to return something to unblock.
+        """
+        worker = self.executor.submit(self._handle, event_handler, event_name,
+            user_args, event_timeout, cond, cond_timeout)
+        return worker
+
+    def pop_all(self, event_name):
+        """Return and remove all stored events of a specified name.
+
+        Pops all events from their queue. May miss the latest ones.
+        If no event is available, return immediately.
+
+        Args:
+            event_name: Name of the events to be popped.
+
+        Returns:
+           results: List of the desired events.
+
+        Raises:
+            IllegalStateError: Raised if pop is called before the dispatcher
+                starts polling.
+        """
+        if not self.started:
+            raise IllegalStateError(("Dispatcher needs to be started before "
+                "popping."))
+        results = []
+        try:
+            self.lock.acquire()
+            while True:
+                e = self.event_dict[event_name].get(block=False)
+                results.append(e)
+        except (queue.Empty, KeyError):
+            return results
+        finally:
+            self.lock.release()
+
+    def clear_events(self, event_name):
+        """Clear all events of a particular name.
+
+        Args:
+            event_name: Name of the events to be popped.
+        """
+        self.lock.acquire()
+        try:
+            q = self.get_event_q(event_name)
+            q.queue.clear()
+        except queue.Empty:
+            return
+        finally:
+            self.lock.release()
+
+    def clear_all_events(self):
+        """Clear all event queues and their cached events."""
+        self.lock.acquire()
+        self.event_dict.clear()
+        self.lock.release()
diff --git a/utils/python/controllers/fastboot.py b/utils/python/controllers/fastboot.py
new file mode 100644
index 0000000..096dfae
--- /dev/null
+++ b/utils/python/controllers/fastboot.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2016 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+from subprocess import Popen, PIPE
+
+def exe_cmd(*cmds):
+    """Executes commands in a new shell. Directing stderr to PIPE.
+
+    This is fastboot's own exe_cmd because of its peculiar way of writing
+    non-error info to stderr.
+
+    Args:
+        cmds: A sequence of commands and arguments.
+
+    Returns:
+        The output of the command run.
+
+    Raises:
+        Exception is raised if an error occurred during the command execution.
+    """
+    cmd = ' '.join(cmds)
+    proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
+    (out, err) = proc.communicate()
+    if not err:
+        return out
+    return err
+
+class FastbootError(Exception):
+    """Raised when there is an error in fastboot operations."""
+
+class FastbootProxy():
+    """Proxy class for fastboot.
+
+    For syntactic reasons, the '-' in fastboot commands need to be replaced
+    with '_'. Can directly execute fastboot commands on an object:
+    >> fb = FastbootProxy(<serial>)
+    >> fb.devices() # will return the console output of "fastboot devices".
+    """
+    def __init__(self, serial=""):
+        self.serial = serial
+        if serial:
+            self.fastboot_str = "fastboot -s {}".format(serial)
+        else:
+            self.fastboot_str = "fastboot"
+
+    def _exec_fastboot_cmd(self, name, arg_str):
+        return exe_cmd(' '.join((self.fastboot_str, name, arg_str)))
+
+    def args(self, *args):
+        return exe_cmd(' '.join((self.fastboot_str,) + args))
+
+    def __getattr__(self, name):
+        def fastboot_call(*args):
+            clean_name = name.replace('_', '-')
+            arg_str = ' '.join(str(elem) for elem in args)
+            return self._exec_fastboot_cmd(clean_name, arg_str)
+        return fastboot_call