Revert "Revert "Re-structure ACTS to Match New Projects""

This reverts commit 842ad4b8bc3e0aeda7117f1242902922feb16821.

Change-Id: I3007e53cb4955a80f2fbd3d416bce8e45101a8f3
diff --git a/acts/framework/MANIFEST.in b/acts/framework/MANIFEST.in
new file mode 100644
index 0000000..d16f272
--- /dev/null
+++ b/acts/framework/MANIFEST.in
@@ -0,0 +1,5 @@
+include setup.py README.txt sample_config.json
+recursive-include acts *.py
+global-exclude .DS_Store
+global-exclude *.pyc
+
diff --git a/acts/framework/README.txt b/acts/framework/README.txt
new file mode 100755
index 0000000..18853d2
--- /dev/null
+++ b/acts/framework/README.txt
@@ -0,0 +1,15 @@
+This package includes the Android Comms Testing Suite (ACTS) alpha release
+
+Dependencies:
+adb
+python3.4+
+python3.4-setuptools
+pyserial in Python3.4+
+
+Setup:
+    1. Install the dependencies.
+       On Ubuntu, sudo apt-get install Python3.4 python3-setuptools python3-serial
+    2. Run "python3.4 setup.py install" with elevated permissions
+
+To verify ACTS is ready to go, at the location for README, run
+    act.py -c sample_config.txt -tb SampleTestBed -tc SampleTest
\ No newline at end of file
diff --git a/acts/framework/acts/__init__.py b/acts/framework/acts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/__init__.py
diff --git a/acts/framework/acts/act.py b/acts/framework/acts/act.py
new file mode 100755
index 0000000..ea3fa7b
--- /dev/null
+++ b/acts/framework/acts/act.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2014 - 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 argparse
+import multiprocessing
+import signal
+import sys
+import traceback
+
+from acts.base_test import validate_test_name
+from acts.controllers.android_device import list_adb_devices
+from acts.keys import Config
+from acts.signals import TestAbortAll
+from acts.test_runner import TestRunner
+from acts.test_runner import USERError
+from acts.utils import abs_path
+from acts.utils import concurrent_exec
+from acts.utils import load_config
+from acts.utils import valid_filename_chars
+
+
+def _validate_test_config(test_config):
+    """Validates the raw configuration loaded from the config file.
+
+    Making sure all the required fields exist.
+    """
+    for k in Config.reserved_keys.value:
+        if k not in test_config:
+            raise USERError(("Required key {} missing in test "
+            "config.").format(k))
+
+def _validate_testbed_name(name):
+    """Validates the name of a test bed.
+
+    Since test bed names are used as part of the test run id, it needs to meet
+    certain requirements.
+
+    Args:
+        name: The test bed's name specified in config file.
+
+    Raises:
+        If the name does not meet any criteria, USERError is raised.
+    """
+    if not name:
+        raise USERError("Test bed names can't be empty.")
+    if type(name) is not str:
+        raise USERError("Test bed names have to be string.")
+    for l in name:
+        if l not in valid_filename_chars:
+            raise USERError("Char '%s' is not allowed in test bed names." % l)
+
+def _validate_testbed_configs(testbed_configs):
+    """Validates the testbed configurations.
+
+    Assign android device serials if necessary.
+    Checks for resource conflicts.
+
+    Args:
+        testbed_configs: A list of testbed configuration json objects.
+
+    Raises:
+        If any part of the configuration is invalid, USERError is raised.
+    """
+    connected_androids = list_adb_devices()
+    seen_androids = {}
+    seen_names = {}
+    # Cross checks testbed configs for resource conflicts.
+    for config in testbed_configs:
+        # Check for conflicts between multiple concurrent testbed configs.
+        # No need to call it if there's only one testbed config.
+        name = config[Config.key_testbed_name.value]
+        _validate_testbed_name(name)
+        # Test bed names should be unique.
+        if name in seen_names:
+            raise USERError("Duplicate testbed name {} found.".format(name))
+        seen_names[name] = 0
+        # If no android device specified, pick up all.
+        if Config.key_android_device.value not in config:
+            if len(testbed_configs) > 1:
+                raise USERError(("Test bed %s is picking up all Android device"
+                    "s, so it can't be selected with other test beds.") % name)
+            config[Config.key_android_device.value] = connected_androids
+        for c in config[Config.key_android_device.value]:
+            if isinstance(c, str):
+                serial = c
+            elif "serial" in c:
+                serial = c["serial"]
+            else:
+                raise USERError(('Required value "serial" is missing in '
+                    'AndroidDevice config %s.') % c)
+            if serial not in connected_androids:
+                raise USERError(("Could not find android device {} "
+                 "specified in test config {}.").format(serial, name))
+            # One android device should not be in more than one configs.
+            if serial in seen_androids:
+                raise USERError(("Android device {} is referred in both {} and"
+                    " {}.").format(serial, name, seen_androids[serial]))
+            seen_androids[serial] = name
+
+def _parse_one_test_specifier(item):
+    tokens = item.split(':')
+    if len(tokens) > 2:
+        raise USERError("Syntax error in test specifier {}".format(item))
+    if len(tokens) == 1:
+        # This should be considered a test class name
+        test_cls_name = tokens[0]
+        return (test_cls_name, None)
+    elif len(tokens) == 2:
+        # This should be considered a test class name followed by
+        # a list of test case names.
+        test_cls_name, test_case_names = tokens
+        clean_names = []
+        for elem in test_case_names.split(','):
+            test_case = elem.strip()
+            validate_test_name(test_case)
+            clean_names.append(test_case)
+        return (test_cls_name, clean_names)
+
+def parse_test_list(test_list):
+    """Parse user provided test list into internal format for test_runner.
+
+    Args:
+        test_list: A list of test classes/cases.
+    """
+    result = []
+    for elem in test_list:
+        result.append(_parse_one_test_specifier(elem))
+    return result
+
+def load_test_config_file(test_config_path, tb_filters=None):
+    """Processes the test configuration file provied by user.
+
+    Loads the configuration file into a json object, unpacks each testbed
+    config into its own json object, and validate the configuration in the
+    process.
+
+    Args:
+        test_config_path: Path to the test configuration file.
+
+    Returns:
+        A list of test configuration json objects to be passed to TestRunner.
+    """
+    try:
+        configs = load_config(test_config_path)
+        if tb_filters:
+            tbs = []
+            for tb in configs[Config.key_testbed.value]:
+                if tb[Config.key_testbed_name.value] in tb_filters:
+                    tbs.append(tb)
+            if len(tbs) != len(tb_filters):
+                print("Expect to find %d test bed configs, found %d." % (
+                    len(tb_filters), len(tbs)))
+                print("Check if you have the correct test bed names.")
+                return None
+            configs[Config.key_testbed.value] = tbs
+        _validate_test_config(configs)
+        _validate_testbed_configs(configs[Config.key_testbed.value])
+        k_log_path = Config.key_log_path.value
+        configs[k_log_path] = abs_path(configs[k_log_path])
+        tps = configs[Config.key_test_paths.value]
+    except USERError as e:
+        print("Something is wrong in the test configurations.")
+        print(str(e))
+        return None
+    except Exception as e:
+        print("Error loading test config {}".format(test_config_path))
+        print(traceback.format_exc())
+        return None
+    # Unpack testbeds into separate json objects.
+    beds = configs.pop(Config.key_testbed.value)
+    config_jsons = []
+    for b in beds:
+        j = dict(configs)
+        j[Config.key_testbed.value] = b
+        # Custom keys in each test bed config will be moved up an level to be
+        # picked up for user_params. If the key already exists in the upper
+        # level, the local one defined in test bed config overwrites the
+        # general one.
+        for k in list(b.keys()):
+            if k in j:
+                j[k] = b[k]
+                del b[k]
+            elif k not in Config.tb_config_reserved_keys.value:
+                j[k] = b[k]
+                del b[k]
+        config_jsons.append(j)
+    return config_jsons
+
+def _run_test(test_runner, repeat=1):
+    """Instantiate and runs TestRunner.
+
+    This is the function to start separate processes with.
+
+    Args:
+        test_runner: The test_runner instance to be executed.
+        repeat: Number of times to iterate the specified tests.
+    """
+    try:
+        for i in range(repeat):
+            test_runner.run()
+    except TestAbortAll:
+        return
+    except:
+        print("Exception when executing {}, iteration {}.".format(
+            test_runner.testbed_name, i))
+        print(traceback.format_exc())
+    finally:
+        test_runner.stop()
+
+def _gen_term_signal_handler(test_runners):
+    def termination_sig_handler(signal_num, frame):
+        for t in test_runners:
+            t.stop()
+        sys.exit(1)
+    return termination_sig_handler
+
+def _run_tests_parallel(process_args):
+    print("Executing {} concurrent test runs.".format(len(process_args)))
+    concurrent_exec(_run_test, process_args)
+
+def _run_tests_sequential(process_args):
+    for args in process_args:
+        _run_test(*args)
+
+def _parse_test_file(fpath):
+    try:
+        with open(fpath, 'r') as f:
+            tf = []
+            for line in f:
+                line = line.strip()
+                if not line:
+                    continue
+                if len(tf) and (tf[-1].endswith(':') or tf[-1].endswith(',')):
+                    tf[-1] += line
+                else:
+                    tf.append(line)
+            return tf
+    except:
+        print("Error loading test file.")
+        raise
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description=("Specify tests to run. If "
+                 "nothing specified, run all test cases found."))
+    parser.add_argument('-c', '--config', nargs=1, type=str, required=True,
+        metavar="<PATH>", help="Path to the test configuration file.")
+    parser.add_argument('--test_args', nargs='+', type=str,
+        metavar="Arg1 Arg2 ...",
+        help=("Command-line arguments to be passed to every test case in a "
+              "test run. Use with caution."))
+    parser.add_argument('-d', '--debug', action="store_true",
+        help=("Set this flag if manual debugging is required."))
+    parser.add_argument('-p', '--parallel', action="store_true",
+        help=("If set, tests will be executed on all testbeds in parallel. "
+              "Otherwise, tests are executed iteratively testbed by testbed."))
+    parser.add_argument('-r', '--repeat', type=int,
+        metavar="<NUMBER>",
+        help="Number of times to run the specified test cases.")
+    parser.add_argument('-tb', '--testbed', nargs='+', type=str,
+        metavar="[<TEST BED NAME1> <TEST BED NAME2> ...]",
+        help="Specify which test beds to run tests on.")
+    group = parser.add_mutually_exclusive_group(required=True)
+    group.add_argument('-tc', '--testclass', nargs='+', type=str,
+        metavar="[TestClass1 TestClass2:test_xxx ...]",
+        help="A list of test classes/cases to run.")
+    group.add_argument('-tf', '--testfile', nargs=1, type=str,
+        metavar="<PATH>",
+        help=("Path to a file containing a comma delimited list of test "
+              "classes to run."))
+
+    args = parser.parse_args()
+    test_list = None
+    repeat = 1
+    if args.testfile:
+        test_list = _parse_test_file(args.testfile[0])
+    elif args.testclass:
+        test_list = args.testclass
+    if args.repeat:
+        repeat = args.repeat
+    parsed_configs = load_test_config_file(args.config[0], args.testbed)
+    if not parsed_configs:
+        print("Encountered error when parsing the config file, abort!")
+        sys.exit(1)
+    # Prepare args for test runs
+    test_identifiers = parse_test_list(test_list)
+    test_runners = []
+    process_args = []
+    try:
+        for c in parsed_configs:
+            c[Config.ikey_cli_args.value] = args.test_args
+            t = TestRunner(c, test_identifiers)
+            test_runners.append(t)
+            process_args.append((t, repeat))
+    except:
+        print("Failed to instantiate test runner, abort.")
+        print(traceback.format_exc())
+        sys.exit(1)
+    # Register handler for term signals if in -i mode.
+    if not args.debug:
+        handler = _gen_term_signal_handler(test_runners)
+        signal.signal(signal.SIGTERM, handler)
+        signal.signal(signal.SIGINT, handler)
+    # Execute test runners.
+    if args.parallel and len(process_args) > 1:
+        _run_tests_parallel(process_args)
+    else:
+        _run_tests_sequential(process_args)
+    sys.exit(0)
diff --git a/acts/framework/acts/base_test.py b/acts/framework/acts/base_test.py
new file mode 100644
index 0000000..8f4c93c
--- /dev/null
+++ b/acts/framework/acts/base_test.py
@@ -0,0 +1,757 @@
+#!/usr/bin/python3.4
+#
+# Copyright 2014 - 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 os
+
+import acts.logger as logger
+
+from acts.keys import Config
+from acts.records import TestResult
+from acts.records import TestResultRecord
+from acts.signals import TestAbortClass
+from acts.signals import TestAbortAll
+from acts.signals import TestFailure
+from acts.signals import TestPass
+from acts.signals import TestSkip
+from acts.signals import TestSilent
+from acts.utils import concurrent_exec
+from acts.utils import create_dir
+from acts.utils import get_current_human_time
+
+MAX_FILENAME_LEN = 255
+DEFAULT_ADB_LOG_OFFSET = 5
+
+# Macro strings for test result reporting
+TEST_CASE_TOKEN = "[Test Case]"
+RESULT_LINE_TEMPLATE = TEST_CASE_TOKEN + " %s %s"
+
+def validate_test_name(name):
+    """Checks if a test name is valid.
+
+    To be valid, a test name needs to follow the naming convention: starts
+    with "test_". Also, the test class needs to actually have a function
+    named after the test.
+
+    Args:
+        name: name of a test case.
+
+    Raises:
+        BaseTestError is raised if the name is invalid.
+    """
+    if len(name) < 5 or name[:5] != "test_":
+        raise BaseTestError("Invalid test case name found: {}.".format(name))
+
+class BaseTestError(Exception):
+    """Raised for exceptions that occured in BaseTestClass."""
+
+class BaseTestClass(object):
+    """Base class for all test classes to inherit from.
+
+    This class gets all the controller objects from test_runner and executes
+    the test cases requested within itself.
+
+    Most attributes of this class are set at runtime based on the configuration
+    provided.
+
+    Attributes:
+        tests: A list of strings, each representing a test case name.
+        TAG: A string used to refer to a test class. Default is the test class
+            name.
+        droids: A list of SL4A client objects for convenience. Do NOT use, to
+            be deprecated.
+        eds: A list of event_dispatcher objects. Do NOT use, to be deprecated.
+        log: A logger object used for logging.
+        results: A TestResult object for aggregating test results from the
+            execution of test cases.
+    """
+
+    TAG = None
+
+    def __init__(self, configs):
+        self.tests = []
+        if not self.TAG:
+            self.TAG = self.__class__.__name__
+        # Set default value for optional config params.
+        if Config.key_adb_log_time_offset.value not in configs:
+            configs[Config.key_adb_log_time_offset.value] = DEFAULT_ADB_LOG_OFFSET
+        # Set all the controller objects and params.
+        for name, value in configs.items():
+            setattr(self, name, value)
+        # Set convenience references for android_device objects.
+        # TODO(angli): remove these and force tests to use the droids in ad
+        # objs directly.
+        if Config.ikey_android_device.value in configs:
+            self.droids = []
+            self.eds = []
+            for ad in self.android_devices:
+                self.droids.append(ad.droid)
+                self.eds.append(ad.ed)
+            if self.android_devices:
+                self.droid = self.droids[0]
+                self.ed = self.eds[0]
+        else:
+            self.log.warning("No attached android device found.")
+        self.results = TestResult()
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self._exec_func(self.clean_up)
+
+    def unpack_userparams(self, req_param_names, opt_param_names=[]):
+        """Unpacks user defined parameters in test config into individual
+        variables.
+
+        Instead of accessing the user param with self.user_params["xxx"], the
+        variable can be directly accessed with self.xxx.
+
+        All missing required params will be logged in error. If an optional
+        param is missing, log a note and continue. You can assert on the return
+        value of this funtion in setup_class to ensure the required user params
+        are found in test config and set.
+
+        Args:
+            req_param_names: A list of names of the required user params.
+            opt_param_names: A list of names of the optional user params.
+
+        Returns:
+            True if all required user params were set. False otherwise.
+        """
+        missing = False
+        for name in req_param_names:
+            if name not in self.user_params:
+                missing = True
+                self.log.error(("Missing required user param '%s' in "
+                    "configuration!") % name)
+                continue
+            setattr(self, name, self.user_params[name])
+        for name in opt_param_names:
+            if name not in self.user_params:
+                self.log.info(("Missing optional user param '%s' in "
+                    "configuration, continue.") % name)
+            else:
+                setattr(self, name, self.user_params[name])
+        return not missing
+
+    def _setup_class(self):
+        """Proxy function to guarantee the base implementation of setup_class
+        is called.
+        """
+        try:
+            return self.setup_class()
+        except TestAbortClass:
+            return False
+
+    def setup_class(self):
+        """Setup function that will be called before executing any test case in
+        the test class.
+
+        Implementation is optional.
+        """
+        return True
+
+    def teardown_class(self):
+        """Teardown function that will be called after all the selected test
+        cases in the test class have been executed.
+
+        Implementation is optional.
+        """
+        pass
+
+    def _setup_test(self, test_name):
+        """Proxy function to guarantee the base implementation of setup_test is
+        called.
+        """
+        try:
+            # Write test start token to adb log if android device is attached.
+            for ad in self.android_devices:
+                ad.droid.logV("%s BEGIN %s" % (TEST_CASE_TOKEN, test_name))
+        except:
+            pass
+        return self.setup_test()
+
+    def setup_test(self):
+        """Setup function that will be called every time before executing each
+        test case in the test class.
+
+        Implementation is optional.
+        """
+        return True
+
+    def _teardown_test(self, test_name):
+        """Proxy function to guarantee the base implementation of teardown_test
+        is called.
+        """
+        try:
+            # Write test end token to adb log if android device is attached.
+            for ad in self.android_devices:
+                ad.droid.logV("%s END %s" % (TEST_CASE_TOKEN, test_name))
+        except:
+            pass
+        return self.teardown_test()
+
+    def teardown_test(self):
+        """Teardown function that will be called every time a test case has
+        been executed.
+
+        Implementation is optional.
+        """
+        pass
+
+    def _on_fail(self, record):
+        """Proxy function to guarantee the base implementation of on_fail is
+        called.
+
+        Args:
+            record: The TestResultRecord object for the failed test case.
+        """
+        test_name = record.test_name
+        begin_time = logger.epoch_to_log_line_timestamp(record.begin_time)
+        end_time = logger.get_log_line_timestamp(self.adb_log_time_offset)
+        self.log.error(record.details)
+        self.log.info(RESULT_LINE_TEMPLATE % (test_name, record.result))
+        try:
+            self.cat_adb_log(test_name, begin_time, end_time)
+        except AttributeError:
+            pass
+        self.on_fail(test_name, begin_time)
+
+    def on_fail(self, test_name, begin_time):
+        """A function that is executed upon a test case failure.
+
+        User implementation is optional.
+
+        This should be the primary place to call take_bug_reports, e.g.
+        self.take_bug_reports(test_name, self.android_devices[0])
+
+        Args:
+            test_name: Name of the test that triggered this function.
+            begin_time: Logline format timestamp taken when the test started.
+        """
+        pass
+
+    def _on_success(self, record):
+        """Proxy function to guarantee the base implementation of on_success is
+        called.
+
+        Args:
+            record: The TestResultRecord object for the passed test case.
+        """
+        test_name = record.test_name
+        begin_time = logger.epoch_to_log_line_timestamp(record.begin_time)
+        msg = record.details
+        if msg:
+            self.log.info(msg)
+        self.log.info(RESULT_LINE_TEMPLATE % (test_name, record.result))
+        self.on_success(test_name, begin_time)
+
+    def on_success(self, test_name, begin_time):
+        """A function that is executed upon a test case passing.
+
+        Implementation is optional.
+
+        Args:
+            test_name: Name of the test that triggered this function.
+            begin_time: Logline format timestamp taken when the test started.
+        """
+        pass
+
+    def _on_skip(self, record):
+        """Proxy function to guarantee the base implementation of on_skip is
+        called.
+
+        Args:
+            record: The TestResultRecord object for the skipped test case.
+        """
+        test_name = record.test_name
+        begin_time = logger.epoch_to_log_line_timestamp(record.begin_time)
+        self.log.info(RESULT_LINE_TEMPLATE % (test_name, record.result))
+        self.log.info("Reason to skip: %s" % record.details)
+        self.on_skip(test_name, begin_time)
+
+    def on_skip(self, test_name, begin_time):
+        """A function that is executed upon a test case being skipped.
+
+        Implementation is optional.
+
+        Args:
+            test_name: Name of the test that triggered this function.
+            begin_time: Logline format timestamp taken when the test started.
+        """
+        pass
+
+    def on_exception(self, test_name, begin_time):
+        """A function that is executed upon an unhandled exception from a test
+        case.
+
+        Implementation is optional.
+
+        Args:
+            test_name: Name of the test that triggered this function.
+            begin_time: Logline format timestamp taken when the test started.
+        """
+        pass
+
+    def fail(self, msg, extras=None):
+        """Explicitly fail a test case.
+
+        Args:
+            msg: A string explaining the datails of the failure.
+            extras: An optional field for extra information to be included in
+                test result.
+
+        Raises:
+            TestFailure is raised to mark a test case as failed.
+        """
+        raise TestFailure(msg, extras)
+
+    def explicit_pass(self, msg, extras=None):
+        """Explicitly pass a test case.
+
+        A test with not uncaught exception will pass implicitly so the usage of
+        this is optional. It is intended for reporting extra information when a
+        test passes.
+
+        Args:
+            msg: A string explaining the details of the passed test.
+            extras: An optional field for extra information to be included in
+                test result.
+
+        Raises:
+            TestPass is raised to mark a test case as passed.
+        """
+        raise TestPass(msg, extras)
+
+    def assert_true(self, expr, msg, extras=None):
+        """Assert an expression evaluates to true, otherwise fail the test.
+
+        Args:
+            expr: The expression that is evaluated.
+            msg: A string explaining the datails in case of failure.
+            extras: An optional field for extra information to be included in
+                test result.
+        """
+        if not expr:
+            self.fail(msg, extras)
+
+    def skip(self, reason, extras=None):
+        """Skip a test case.
+
+        Args:
+            reason: The reason this test is skipped.
+            extras: An optional field for extra information to be included in
+                test result.
+
+        Raises:
+            TestSkip is raised to mark a test case as skipped.
+        """
+        raise TestSkip(reason, extras)
+
+    def skip_if(self, expr, reason, extras=None):
+        """Skip a test case if expression evaluates to True.
+
+        Args:
+            expr: The expression that is evaluated.
+            reason: The reason this test is skipped.
+            extras: An optional field for extra information to be included in
+                test result.
+        """
+        if expr:
+            self.skip(reason, extras)
+
+    def abort_class(self, reason, extras=None):
+        """Abort all subsequent test cases within the same test class in one
+        iteration.
+
+        If one test class is requested multiple times in a test run, this can
+        only abort one of the requested executions, NOT all.
+
+        Args:
+            reason: The reason to abort.
+            extras: An optional field for extra information to be included in
+                test result.
+
+        Raises:
+            TestAbortClass is raised to abort all subsequent tests in the test
+            class.
+        """
+        self.log.warning(("Abort %s, remaining test cases within the class"
+                " will not be executed. Reason: %s") % (self.TAG, str(reason)))
+        raise TestAbortClass(reason, extras)
+
+    def abort_class_if(self, expr, reason, extras=None):
+        """Abort all subsequent test cases within the same test class in one
+        iteration, if expression evaluates to True.
+
+        If one test class is requested multiple times in a test run, this can
+        only abort one of the requested executions, NOT all.
+
+        Args:
+            expr: The expression that is evaluated.
+            reason: The reason to abort.
+            extras: An optional field for extra information to be included in
+                test result.
+
+        Raises:
+            TestAbortClass is raised to abort all subsequent tests in the test
+            class.
+        """
+        if expr:
+            self.abort_class(reason, extras)
+
+    def abort_all(self, reason, extras=None):
+        """Abort all subsequent test cases, including the ones not in this test
+        class or iteration.
+
+        Args:
+            reason: The reason to abort.
+            extras: An optional field for extra information to be included in
+                test result.
+
+        Raises:
+            TestAbortAll is raised to abort all subsequent tests.
+        """
+        self.log.warning(("Abort test run, remaining test cases will not be "
+                "executed. Reason: %s") % (str(reason)))
+        raise TestAbortAll(reason, extras)
+
+    def abort_all_if(self, expr, reason, extras=None):
+        """Abort all subsequent test cases, if the expression evaluates to
+        True.
+
+        Args:
+            expr: The expression that is evaluated.
+            reason: The reason to abort.
+            extras: An optional field for extra information to be included in
+                test result.
+
+        Raises:
+            TestAbortAll is raised to abort all subsequent tests.
+        """
+        if expr:
+            self.abort_all(reason, extras)
+
+    def _is_timestamp_in_range(self, target, begin_time, end_time):
+        low = logger.logline_timestamp_comparator(begin_time, target) <= 0
+        high = logger.logline_timestamp_comparator(end_time, target) >= 0
+        return low and high
+
+    def cat_adb_log(self, tag, begin_time, end_time):
+        """Takes logs from adb logcat log.
+
+        Goes through adb logcat log and excerpt the log lines recorded during a
+        certain time period. The lines are saved into a file in the test
+        class's log directory.
+
+        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.
+            end_time: Logline format timestamp of the end of the time period.
+        """
+        self.log.debug("Extracting adb log from logcat.")
+        adb_excerpt_path = os.path.join(self.log_path, "AdbLogExcerpts")
+        create_dir(adb_excerpt_path)
+        for f_name in self.adb_logcat_files:
+            out_name = f_name.replace("adblog,", "").replace(".txt", "")
+            out_name = ",{},{}.txt".format(begin_time, out_name)
+            tag_len = 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 = os.path.join(self.adb_logcat_path, f_name)
+                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[:logger.log_line_timestamp_len]
+                        if not logger.is_valid_logline_timestamp(line_time):
+                            continue
+                        if self._is_timestamp_in_range(line_time, begin_time,
+                            end_time):
+                            in_range = True
+                            out.write(line + '\n')
+                        else:
+                            if in_range:
+                                break
+
+    def take_bug_reports(self, test_name, begin_time, android_devices):
+        """Takes bug report on a list of devices and stores it in the log
+        directory of the test class.
+
+        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:
+            test_name: Name of the test case that triggered this bug report.
+            begin_time: Logline format timestamp taken when the test started.
+            android_devices: android_device instances to take bugreport on.
+        """
+        br_path = os.path.join(self.log_path, "BugReports")
+        begin_time = logger.normalize_log_line_timestamp(begin_time)
+        create_dir(br_path)
+        args = [(test_name, begin_time, ad) for ad in android_devices]
+        concurrent_exec(self._take_bug_report, args)
+
+    def _take_bug_report(self, test_name, begin_time, ad):
+        """Takes a bug report on a device and stores it in the log directory of
+        the test class.
+
+        Args:
+            test_name: Name of the test case that triggered this bug report.
+            begin_time: Logline format timestamp taken when the test started.
+            ad: The AndroidDevice instance to take bugreport on.
+        """
+        serial = ad.serial
+        br_path = os.path.join(self.log_path, "BugReports")
+        base_name = ",{},{}.txt".format(begin_time, serial)
+        test_name_len = 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 test case {} on {}".
+            format(test_name, serial))
+        ad.adb.bugreport(" > %s" % full_out_path)
+        self.log.info("Finished taking bugreport on {}".format(serial))
+
+    def exec_one_testcase(self, test_name, test_func, pms=None):
+        """Executes one test case and update test results.
+
+        Executes one test case, create a TestResultRecord object with the
+        execution information, and add the record to the test class's test
+        results.
+
+        Args:
+            test_name: Name of the test.
+            test_func: The test function.
+            pms: Params to be passed to the test function.
+        """
+        is_generate_trigger = False
+        tr_record = TestResultRecord(test_name)
+        tr_record.test_begin()
+        self.log.info("[Test Case] %s" % test_name)
+        verdict = None
+        try:
+            self.skip_if(not self._exec_func(self._setup_test, test_name),
+                "Setup for %s failed." % test_name)
+            try:
+                if pms:
+                        verdict = test_func(*pms)
+                else:
+                    verdict = test_func()
+            except TypeError as e:
+                e_str = str(e)
+                if test_name in e_str:
+                    raise TestSkip(e_str + ". Got args: {}".format(pms))
+                raise e
+        except (TestFailure, AssertionError) as e:
+            tr_record.test_fail(e)
+            self._exec_func(self._on_fail, tr_record)
+        except TestSkip as e:
+            # Test skipped.
+            tr_record.test_skip(e)
+            self._exec_func(self._on_skip, tr_record)
+        except (TestAbortClass, TestAbortAll) as e:
+            # Abort signals, pass along.
+            tr_record.test_skip(e)
+            raise e
+        except TestPass as e:
+            # Explicit test pass.
+            tr_record.test_pass(e)
+            self._exec_func(self._on_success, tr_record)
+        except TestSilent as e:
+            # This is a trigger test for generated tests, suppress reporting.
+            is_generate_trigger = True
+            self.results.requested.remove(test_name)
+        except Exception as e:
+            # Exception happened during test.
+            self.log.exception("Exception in " + test_name)
+            tr_record.test_fail(e)
+            bt = logger.epoch_to_log_line_timestamp(tr_record.begin_time)
+            self._exec_func(self.on_exception, tr_record.test_name, bt)
+            self._exec_func(self._on_fail, tr_record)
+        else:
+            # Keep supporting return False for now.
+            # TODO(angli): Deprecate return False support.
+            if verdict or (verdict is None):
+                # Test passed.
+                tr_record.test_pass()
+                self._exec_func(self._on_success, tr_record)
+                return
+            # Test failed because it didn't return True.
+            # This should be removed eventually.
+            tr_record.test_fail()
+            self._exec_func(self._on_fail, tr_record)
+        finally:
+            self._exec_func(self._teardown_test, test_name)
+            if not is_generate_trigger:
+                self.results.add_record(tr_record)
+                self.reporter.write(repr(tr_record) + '\n')
+
+    def run_generated_testcases(self, test_func, settings, *args, tag="",
+                                name_func=None):
+        """Runs generated test cases.
+
+        Generated test cases are not written down as functions, but as a list
+        of parameter sets. This way we reduce code repetition and improve
+        test case scalability.
+
+        Args:
+            test_func: The common logic shared by all these generated test
+                cases. This function should take at least one argument, which
+                is a parameter set.
+            settings: A list of strings representing parameter sets. These are
+                usually json strings that get loaded in the test_func.
+            args: Additional args to be passed to the test_func.
+            tag: Name of this group of generated test cases. Ignored if
+                name_func is provided and operate properly
+            name_func: A function that takes a test setting and generates a
+                proper test name. The test name should be shorter than
+                MAX_FILENAME_LEN. Names over the limit will be truncated.
+
+        Returns:
+            A list of settings that did not pass.
+        """
+        failed_settings = []
+        for s in settings:
+            test_name = "{} {}".format(tag, s)
+            if name_func:
+                try:
+                    test_name = name_func(s, *args)
+                except:
+                    msg = ("Failed to get test name from test_func. Fall back "
+                        "to default %s") % test_name
+                    self.log.exception(msg)
+            self.results.requested.append(test_name)
+            if len(test_name) > MAX_FILENAME_LEN:
+                test_name = test_name[:MAX_FILENAME_LEN]
+            previous_success_cnt = len(self.results.passed)
+            self.exec_one_testcase(test_name, test_func, (s,) + args)
+            if len(self.results.passed) - previous_success_cnt != 1:
+                failed_settings.append(s)
+        return failed_settings
+
+    def _exec_func(self, func, *args):
+        """Executes a function with exception safeguard.
+
+        This will let TestAbortAll through so abort_all works in all procedure
+        functions.
+
+        Args:
+            func: Function to be executed.
+            args: Arguments to be passed to the function.
+
+        Returns:
+            Whatever the function returns, or False if unhandled exception
+            occured.
+        """
+        try:
+            return func(*args)
+        except TestAbortAll:
+            raise
+        except:
+            msg = "Exception happened when executing {} in {}.".format(
+                func.__name__, self.TAG)
+            self.log.exception(msg)
+            return False
+
+    def _get_test_funcs(self, test_case_names):
+        # All tests are selected if test_cases list is None.
+        test_names = self.tests
+        if test_case_names:
+            test_names = test_case_names
+        # Load functions based on test names. Also find the longest test name.
+        test_funcs = []
+        for test_name in test_names:
+            try:
+                # Validate test_name's format.
+                validate_test_name(test_name)
+                test_funcs.append((test_name, getattr(self, test_name)))
+            except AttributeError:
+                self.log.warning("%s does not have test case %s." % (
+                    self.TAG, test_name))
+            except BaseTestError as e:
+                self.log.warning(str(e))
+        return test_funcs
+
+    def run(self, test_names=None):
+        """Runs test cases within a test class by the order they
+        appear in the test list.
+
+        Being in the test_names list makes the test case "requested". If its
+        name passes validation, then it'll be executed, otherwise the name will
+        be skipped.
+
+        Args:
+            test_names: A list of names of the requested test cases. If None,
+                all test cases in the class are considered requested.
+
+        Returns:
+            A tuple of: The number of requested test cases, the number of test
+            cases executed, and the number of test cases passed.
+        """
+        # Total number of test cases requested by user.
+        if test_names:
+            self.results.requested = list(test_names)
+        elif self.tests:
+            self.results.requested = list(self.tests)
+        else:
+            # No test case specified and no default list, abort.
+            return self.results
+        # Setup for the class.
+        if not self._exec_func(self._setup_class):
+            self.log.error("Failed to setup {}, skipping.".format(self.TAG))
+            skip_signal = TestSkip("Test class %s failed to setup." % self.TAG)
+            self.results.skip_all(skip_signal)
+            self._exec_func(self.teardown_class)
+            return self.results
+        self.log.info("==========> %s <==========" % self.TAG)
+        tests = self._get_test_funcs(test_names)
+
+        # Run tests in order.
+        try:
+            for test_name, test_func in tests:
+                self.exec_one_testcase(test_name, test_func, self.cli_args)
+            return self.results
+        except TestAbortClass:
+            return self.results
+        except TestAbortAll as e:
+            # Piggy-back test results on this exception object so we don't lose
+            # results from this test class.
+            setattr(e, "results", self.results)
+            raise e
+        finally:
+            self._exec_func(self.teardown_class)
+            self.log.info("Summary for test class %s: %s" % (self.TAG,
+                self.results.summary_str()))
+
+    def clean_up(self):
+        """A function that is executed upon completion of all tests cases
+        selected in the test class.
+
+        This function should clean up objects initialized in the constructor by
+        user.
+        """
+        pass
diff --git a/acts/framework/acts/controllers/__init__.py b/acts/framework/acts/controllers/__init__.py
new file mode 100644
index 0000000..e2f5b49
--- /dev/null
+++ b/acts/framework/acts/controllers/__init__.py
@@ -0,0 +1,33 @@
+"""Modules under acts.controllers provide interfaces to hardware/software
+resources that ACTS manages.
+
+Top level controllers module are controller modules that need to be explicitly
+specified by users in test configuration files. Top level controller modules
+should have the following module level functions:
+
+def create(configs, logger):
+    '''Instantiates the controller class with the input configs.
+    Args:
+        configs: A list of dicts each representing config for one controller
+            object.
+        logger: The main logger used in the current test run.
+    Returns:
+        A list of controller objects.
+
+def destroy(objs):
+    '''Destroys a list of controller objects created by the "create" function
+    and releases all the resources.
+
+    Args:
+        objs: A list of controller objects created from this module.
+    '''
+"""
+
+"""This is a list of all the top level controller modules"""
+__all__ = [
+    "android_device",
+    "attenuator",
+    "monsoon",
+    "access_point",
+    "iperf_server"
+]
\ No newline at end of file
diff --git a/acts/framework/acts/controllers/access_point.py b/acts/framework/acts/controllers/access_point.py
new file mode 100755
index 0000000..f8d862b
--- /dev/null
+++ b/acts/framework/acts/controllers/access_point.py
@@ -0,0 +1,510 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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.
+
+import acts.jsonrpc as jsonrpc
+from acts.test_utils.wifi_test_utils import WifiEnums
+
+def create(configs, logger):
+    results = []
+    for c in configs:
+        addr = c[Config.key_address.value]
+        port = 80
+        if Config.key_port.value in c:
+            port = c[Config.key_port.value]
+        results.append(AP(addr, port))
+    return results
+
+def destroy(objs):
+    return
+
+class ServerError(Exception):
+    pass
+
+class ClientError(Exception):
+    pass
+
+"""
+Controller for OpenWRT routers.
+"""
+class AP():
+    """Interface to OpenWRT using the LuCI interface.
+
+    Works via JSON-RPC over HTTP. A generic access method is provided, as well
+    as more specialized methods.
+
+    Can also call LuCI methods generically:
+
+        ap_instance.sys.loadavg()
+        ap_instance.sys.dmesg()
+        ap_instance.fs.stat("/etc/hosts")
+    """
+    IFACE_DEFAULTS = {"mode": "ap", "disabled": "0",
+                      "encryption": "psk2", "network": "lan"}
+    RADIO_DEFAULTS = {"disabled": "0"}
+
+    def __init__(self, addr, port=80):
+        self._client = jsonrpc.JSONRPCClient(
+                        "http://""{}:{}/cgi-bin/luci/rpc/".format(addr, port))
+        self.RADIO_NAMES = []
+        keys = self._client.get_all("wireless").keys()
+        if "radio0" in keys:
+            self.RADIO_NAMES.append("radio0")
+        if "radio1" in keys:
+            self.RADIO_NAMES.append("radio1")
+
+    def section_id_lookup(self, cfg_name, key, value):
+        """Looks up the section id of a section.
+
+        Finds the section ids of the sections that have the specified key:value
+        pair in them.
+
+        Args:
+            cfg_name: Name of the configuration file to look in.
+            key: Key of the pair.
+            value: Value of the pair.
+
+        Returns:
+            A list of the section ids found.
+        """
+        section_ids = []
+        sections = self._client.get_all(cfg_name)
+        for section_id, section_cfg in sections.items():
+            if key in section_cfg and section_cfg[key] == value:
+                section_ids.append(section_id)
+        return section_ids
+
+    def _section_option_lookup(self, cfg_name, conditions, *target_keys):
+        """Looks up values of options in sections that match the conditions.
+
+        To match a condition, a section needs to have all the key:value pairs
+        specified in conditions.
+
+        Args:
+            cfg_name: Name of the configuration file to look in.
+            key: Key of the pair.
+            value: Value of the pair.
+            target_key: Key of the options we want to retrieve values from.
+
+        Returns:
+            A list of the values found.
+        """
+        results = []
+        sections = self._client.get_all(cfg_name)
+        for section_cfg in sections.values():
+            if self._match_conditions(conditions, section_cfg):
+                r = {}
+                for k in target_keys:
+                    if k not in section_cfg:
+                        break
+                    r[k] = section_cfg[k]
+                if r:
+                    results.append(r)
+        return results
+
+    @staticmethod
+    def _match_conditions(conds, cfg):
+        for cond in conds:
+            key, value = cond
+            if key not in cfg or cfg[key] != value:
+                return False
+        return True
+
+    def run(self, *cmd):
+        """Executes a terminal command on the AP.
+
+        Args:
+            cmd: A tuple of command strings.
+
+        Returns:
+            The terminal output of the command.
+        """
+        return self._client.sys("exec", *cmd)
+
+    def apply_configs(self, ap_config):
+        """Applies configurations to the access point.
+
+        Reads the configuration file, adds wifi interfaces, and sets parameters
+        based on the configuration file.
+
+        Args:
+            ap_config: A dict containing the configurations for the AP.
+        """
+        self.reset()
+        for k, v in ap_config.items():
+            if "radio" in k:
+                self._apply_radio_configs(k, v)
+            if "network" in k:
+                # TODO(angli) Implement this.
+                pass
+        self.apply_wifi_changes()
+
+    def _apply_radio_configs(self, radio_id, radio_config):
+        """Applies conigurations on a radio of the AP.
+
+        Sets the options in the radio config.
+        Adds wifi-ifaces to this radio based on the configurations.
+        """
+        for k, v in radio_config.items():
+            if k == "settings":
+                self._set_options('wireless', radio_id, v,
+                                  self.RADIO_DEFAULTS)
+            if k == "wifi-iface":
+                for cfg in v:
+                    cfg["device"] = radio_id
+                self._add_ifaces(v)
+
+    def reset(self):
+        """Resets the AP to a clean state.
+        
+        Deletes all wifi-ifaces.
+        Enable all the radios.
+        """
+        sections = self._client.get_all("wireless")
+        to_be_deleted = []
+        for section_id in sections.keys():
+            if section_id not in self.RADIO_NAMES:
+                to_be_deleted.append(section_id)
+        self.delete_ifaces_by_ids(to_be_deleted)
+        for r in self.RADIO_NAMES:
+            self.toggle_radio_state(r, True)
+
+    def toggle_radio_state(self, radio_name, state=None):
+        """Toggles the state of a radio.
+
+        If input state is None, toggle the state of the radio.
+        Otherwise, set the radio's state to input state.
+        State True is equivalent to 'disabled':'0'
+
+        Args:
+            radio_name: Name of the radio to change state.
+            state: State to set to, default is None.
+
+        Raises:
+            ClientError: If the radio specified does not exist on the AP.
+        """
+        if radio_name not in self.RADIO_NAMES:
+            raise ClientError("Trying to change none-existent radio's state")
+        cur_state = self._client.get("wireless", radio_name, "disabled")
+        cur_state = True if cur_state=='0' else False
+        if state == cur_state:
+            return
+        new_state = '1' if cur_state else '0'
+        self._set_option("wireless", radio_name, "disabled", new_state)
+        self.apply_wifi_changes()
+        return
+
+    def set_ssid_state(self, ssid, state):
+        """Sets the state of ssid (turns on/off).
+
+        Args:
+            ssid: The ssid whose state is being changed.
+            state: State to set the ssid to. Enable the ssid if True, disable
+                otherwise.
+        """
+        new_state = '0' if state else '1'
+        section_ids = self.section_id_lookup("wireless", "ssid", ssid)
+        for s_id in section_ids:
+            self._set_option("wireless", s_id, "disabled", new_state)
+
+    def get_ssids(self, conds):
+        """Gets all the ssids that match the conditions.
+
+        Params:
+            conds: An iterable of tuples, each representing a key:value pair
+                an ssid must have to be included.
+
+        Returns:
+            A list of ssids that contain all the specified key:value pairs.
+        """
+        results = []
+        for s in self._section_option_lookup("wireless", conds, "ssid"):
+            results.append(s["ssid"])
+        return results
+
+    def get_active_ssids(self):
+        """Gets the ssids that are currently not disabled.
+
+        Returns:
+            A list of ssids that are currently active.
+        """
+        conds = (("disabled", "0"),)
+        return self.get_ssids(conds)
+
+    def get_active_ssids_info(self, *keys):
+        """Gets the specified info of currently active ssids
+
+        If frequency is requested, it'll be retrieved from the radio section
+        associated with this ssid.
+
+        Params:
+            keys: Names of the fields to include in the returned info.
+                e.g. "frequency".
+
+        Returns:
+            Values of the requested info.
+        """
+        conds = (("disabled", "0"),)
+        keys = [w.replace("frequency","device") for w in keys]
+        if "device" not in keys:
+            keys.append("device")
+        info = self._section_option_lookup("wireless", conds, "ssid", *keys)
+        results = []
+        for i in info:
+            radio = i["device"]
+            # Skip this info the radio its ssid is on is disabled.
+            disabled = self._client.get("wireless", radio, "disabled")
+            if disabled != '0':
+                continue
+            c = int(self._client.get("wireless", radio, "channel"))
+            if radio == "radio0":
+                i["frequency"] = WifiEnums.channel_2G_to_freq[c]
+            elif radio == "radio1":
+                i["frequency"] = WifiEnums.channel_5G_to_freq[c]
+            results.append(i)
+        return results
+
+    def get_radio_option(self, key, idx=0):
+        """Gets an option from the configured settings of a radio.
+
+        Params:
+            key: Name of the option to retrieve.
+            idx: Index of the radio to retrieve the option from. Default is 0.
+
+        Returns:
+            The value of the specified option and radio.
+        """
+        r = None
+        if idx == 0:
+            r = "radio0"
+        elif idx == 1:
+            r = "radio1"
+        return self._client.get("wireless", r, key)
+
+    def apply_wifi_changes(self):
+        """Applies committed wifi changes by restarting wifi.
+
+        Raises:
+            ServerError: Something funny happened restarting wifi on the AP.
+        """
+        s = self._client.commit('wireless')
+        resp = self.run('wifi')
+        return resp
+        # if resp != '' or not s:
+        #     raise ServerError(("Exception in refreshing wifi changes, commit"
+        #                        " status: ") + str(s) + ", wifi restart response: "
+        #                        + str(resp))
+
+    def set_wifi_channel(self, channel, device='radio0'):
+        self.set('wireless', device, 'channel', channel)
+
+    def _add_ifaces(self, configs):
+        """Adds wifi-ifaces in the AP's wireless config based on a list of
+        configuration dict.
+
+        Args:
+            configs: A list of dicts each representing a wifi-iface config.
+        """
+        for config in configs:
+            self._add_cfg_section('wireless', 'wifi-iface',
+                              config, self.IFACE_DEFAULTS)
+
+    def _add_cfg_section(self, cfg_name, section, options, defaults=None):
+        """Adds a section in a configuration file.
+
+        Args:
+            cfg_name: Name of the config file to add a section to.
+                e.g. 'wireless'.
+            section: Type of the secion to add. e.g. 'wifi-iface'.
+            options: A dict containing all key:value pairs of the options.
+                e.g. {'ssid': 'test', 'mode': 'ap'}
+
+        Raises:
+            ServerError: Uci add call returned False.
+        """
+        section_id = self._client.add(cfg_name, section)
+        if not section_id:
+            raise ServerError(' '.join(("Failed adding", section, "in",
+                              cfg_name)))
+        self._set_options(cfg_name, section_id, options, defaults)
+
+    def _set_options(self, cfg_name, section_id, options, defaults):
+        """Sets options in a section.
+
+        Args:
+            cfg_name: Name of the config file to add a section to.
+                e.g. 'wireless'.
+            section_id: ID of the secion to add options to. e.g. 'cfg000864'.
+            options: A dict containing all key:value pairs of the options.
+                e.g. {'ssid': 'test', 'mode': 'ap'}
+
+        Raises:
+            ServerError: Uci set call returned False.
+        """
+        # Fill the fields not defined in config with default values.
+        if defaults:
+            for k, v in defaults.items():
+                if k not in options:
+                    options[k] = v
+        # Set value pairs defined in config.
+        for k, v in options.items():
+            self._set_option(cfg_name, section_id, k, v)
+
+    def _set_option(self, cfg_name, section_id, k, v):
+        """Sets an option in a config section.
+
+        Args:
+            cfg_name: Name of the config file the section is in.
+                e.g. 'wireless'.
+            section_id: ID of the secion to set option in. e.g. 'cfg000864'.
+            k: Name of the option.
+            v: Value to set the option to.
+
+        Raises:
+            ServerError: If the rpc called returned False.
+        """
+        status = self._client.set(cfg_name, section_id, k, v)
+        if not status:
+            # Delete whatever was added.
+                raise ServerError(' '.join(("Failed adding option", str(k),
+                                  ':', str(d), "to", str(section_id))))
+
+    def delete_ifaces_by_ids(self, ids):
+        """Delete wifi-ifaces that are specified by the ids from the AP's
+        wireless config.
+
+        Args:
+            ids: A list of ids whose wifi-iface sections to be deleted.
+        """
+        for i in ids:
+            self._delete_cfg_section_by_id('wireless', i)
+
+    def delete_ifaces(self, key, value):
+        """Delete wifi-ifaces that contain the specified key:value pair.
+
+        Args:
+            key: Key of the pair.
+            value: Value of the pair.
+        """
+        self._delete_cfg_sections('wireless', key, value)
+
+    def _delete_cfg_sections(self, cfg_name, key, value):
+        """Deletes config sections that have the specified key:value pair.
+
+        Finds the ids of sections that match a key:value pair in the specified
+        config file and delete the section.
+
+        Args:
+            cfg_name: Name of the config file to delete sections from.
+                e.g. 'wireless'.
+            key: Name of the option to be matched.
+            value: Value of the option to be matched.
+
+        Raises:
+            ClientError: Could not find any section that has the key:value
+                pair.
+        """
+        section_ids = self.section_id_lookup(cfg_name, key, value)
+        if not section_ids:
+            raise ClientError(' '.join(("Could not find any section that has ",
+                              key, ":", value)))
+        for section_id in section_ids:
+            self._delete_cfg_section_by_id(cfg_name, section_id)
+
+    def _delete_cfg_section_by_id(self, cfg_name, section_id):
+        """Deletes the config section with specified id.
+
+        Args:
+            cfg_name: Name of the config file to the delete a section from.
+                e.g. 'wireless'.
+            section_id: ID of the section to be deleted. e.g. 'cfg0d3777'.
+
+        Raises:
+            ServerError: Uci delete call returned False.
+        """
+        self._client.delete(cfg_name, section_id)
+
+    def _get_iw_info(self):
+        results = []
+        text = self.run("iw dev").replace('\t', '')
+        interfaces = text.split("Interface")
+        for intf in interfaces:
+            if len(intf.strip()) < 6:
+                # This is a PHY mark.
+                continue
+            # This is an interface line.
+            intf = intf.replace(', ', '\n')
+            lines = intf.split('\n')
+            r = {}
+            for l in lines:
+                if ' ' in l:
+                    # Only the lines with space are processed.
+                    k, v = l.split(' ', 1)
+                    if k == "addr":
+                        k = "bssid"
+                    if "wlan" in v:
+                        k = "interface"
+                    if k == "channel":
+                        vs = v.split(' ', 1)
+                        v = int(vs[0])
+                        r["frequency"] = int(vs[1].split(' ', 1)[0][1:5])
+                    if k[-1] == ':':
+                        k = k[:-1]
+                    r[k] = v
+            results.append(r)
+        return results
+
+    def get_active_bssids_info(self, radio, *args):
+        wlan = None
+        if radio == "radio0":
+            wlan = "wlan0"
+        if radio == "radio1":
+            wlan = "wlan1"
+        infos = self._get_iw_info()
+        bssids = []
+        for i in infos:
+            if wlan in i["interface"]:
+                r = {}
+                for k,v in i.items():
+                    if k in args:
+                        r[k] = v
+                r["bssid"] = i["bssid"].upper()
+                bssids.append(r)
+        return bssids
+
+    def toggle_bssid_state(self, bssid):
+        if bssid == self.get_bssid("radio0"):
+            self.toggle_radio_state("radio0")
+            return True
+        elif bssid == self.get_bssid("radio1"):
+            self.toggle_radio_state("radio1")
+            return True
+        return False
+
+    def __getattr__(self, name):
+        return _LibCaller(self._client, name)
+
+class _LibCaller:
+    def __init__(self, client, *args):
+        self._client = client
+        self._args = args
+
+    def __getattr__(self, name):
+        return _LibCaller(self._client, *self._args+(name,))
+
+    def __call__(self, *args):
+        return self._client.call("/".join(self._args[:-1]),
+                                 self._args[-1],
+                                 *args)
diff --git a/acts/framework/acts/controllers/adb.py b/acts/framework/acts/controllers/adb.py
new file mode 100644
index 0000000..363ce19
--- /dev/null
+++ b/acts/framework/acts/controllers/adb.py
@@ -0,0 +1,138 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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 time
+
+from acts.utils import exe_cmd
+
+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 "
+    "-n com.googlecode.android_scripting/.activity.ScriptingLayerServiceLauncher "
+    "--ei com.googlecode.android_scripting.extra.USE_SERVICE_PORT {}")
+
+def get_available_host_ports(num):
+    """Gets available host port numbers for adb forward.
+
+    Starting from 9999 and counting down, check if the port is already used by
+    adb forward. If so, continue to the next number.
+
+    Args:
+        num: Number of port numbers needed.
+
+    Returns:
+        A list of integers each representing a port number available for adb
+        forward.
+    """
+    used_ports = list_occupied_ports()
+    results = []
+    port = 9999
+    cnt = 0
+    while cnt < num and port > 1024:
+        if port not in used_ports:
+            results.append(port)
+            cnt += 1
+        port -= 1
+    return results
+
+def list_occupied_ports():
+    """Lists all the host ports occupied by adb forward.
+
+    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
+
+def is_port_availble(port):
+    """Checks if a port number on the host is available.
+
+    Args:
+        port: The host port number to check.
+
+    Returns:
+        True is this port is available for adb forward.
+    """
+    return port not in list_occupied_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=""):
+        self.serial = serial
+        if serial:
+            self.adb_str = "adb -s {}".format(serial)
+        else:
+            self.adb_str = "adb"
+
+    def _exec_adb_cmd(self, name, arg_str):
+        return exe_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.
+        """
+        self.shell(SL4A_LAUNCH_CMD.format(port))
+        # TODO(angli): Make is_sl4a_running reliable so we don't have to do a
+        # dumb wait.
+        time.sleep(3)
+        if not self.is_sl4a_running():
+            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/acts/framework/acts/controllers/android.py b/acts/framework/acts/controllers/android.py
new file mode 100644
index 0000000..bd67d1f
--- /dev/null
+++ b/acts/framework/acts/controllers/android.py
@@ -0,0 +1,107 @@
+# 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.
+"""
+
+import json
+import os
+import socket
+import threading
+
+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()
+
+    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
+        try:
+            self.conn = socket.create_connection((addr, port), 60)
+            self.conn.settimeout(timeout)
+        except (TimeoutError, socket.timeout):
+            print("Failed to create socket connection!")
+            raise
+        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/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py
new file mode 100644
index 0000000..4081c0e
--- /dev/null
+++ b/acts/framework/acts/controllers/android_device.py
@@ -0,0 +1,561 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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 time
+import traceback
+
+import acts.controllers.android as android
+
+from acts.controllers.adb import AdbProxy
+from acts.controllers.adb import is_port_availble
+from acts.controllers.adb import get_available_host_ports
+from acts.controllers.fastboot import FastbootProxy
+from acts.event_dispatcher import EventDispatcher
+from acts.logger import LoggerProxy
+from acts.signals import ControllerError
+from acts.utils import exe_cmd
+
+def create(configs, logger):
+    if not configs:
+        ads = get_all_instances()
+    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)
+    for ad in ads:
+        try:
+            ad.get_droid()
+            ad.ed.start()
+        except:
+            raise ControllerError("Failed to start sl4a on %s" % ad.serial)
+    return ads
+
+def destroy(ads):
+    for ad in ads:
+        try:
+            ad.terminate_all_sessions()
+        except:
+            pass
+
+class AndroidDeviceError(Exception):
+    pass
+
+class DoesNotExistError(AndroidDeviceError):
+    """Raised when something that does not exist is referenced.
+    """
+
+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 = 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 = 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 ControllerError(('Requried value "serial" is missing in '
+                'AndroidDevice config %s.') % c)
+        ad = AndroidDevice(serial, logger=logger)
+        ad.load_config(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("No device matched")
+    elif len(filtered) == 1:
+        return filtered[0]
+    else:
+        serials = [ad.serial for ad in filtered]
+        raise AndroidDeviceError("More than one device matched: " % serials)
+
+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.
+        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 = LoggerProxy(logger)
+        self._droid_sessions = {}
+        self._event_dispatchers = {}
+        self.adb = AdbProxy(serial)
+        self.fastboot = FastbootProxy(serial)
+        if not self.is_bootloader:
+            self.root_adb()
+
+    def __del__(self):
+        if self.h_port:
+            self.adb.forward("--remove tcp:%d" % self.h_port)
+
+    @property
+    def is_bootloader(self):
+        """True if the device is in bootloader mode.
+        """
+        return self.serial in list_fastboot_devices()
+
+    @property
+    def is_adb_root(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.is_bootloader:
+            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
+
+    def load_config(self, config):
+        """Add attributes to the AndroidDevice object based on json config.
+
+        Args:
+            config: A dictionary representing the configs.
+
+        Raises:
+            ControllerError is raised if the config is trying to overwrite an
+            existing attribute.
+        """
+        for k, v in config.items():
+            if hasattr(self, k):
+                raise ControllerError(("Attempting to set existing attribute "
+                    "%s on %s") % (k, self.serial))
+            setattr(self, k, v)
+
+    def root_adb(self):
+        """Change adb to root mode for this device.
+        """
+        if not self.is_adb_root:
+            self.adb.root()
+            self.adb.wait_for_device()
+            self.adb.remount()
+            self.adb.wait_for_device()
+
+    def get_droid(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.get_droid(False)
+
+            Need event handling:
+            >>> ad = AndroidDevice()
+            >>> droid, ed = ad.get_droid()
+        """
+        if not self.h_port or not is_port_availble(self.h_port):
+            self.h_port = get_available_host_ports(1)[0]
+        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.get_dispatcher(droid)
+            return droid, ed
+        return droid
+
+    def get_dispatcher(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 = EventDispatcher(event_droid)
+        self._event_dispatchers[ed_key] = ed
+        return ed
+
+    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:
+            DoesNotExistError: Raised if the session it's trying to connect to
+            does not exist.
+        """
+        if session_id not in self._droid_sessions:
+            raise DoesNotExistError("Session %d doesn't exist." % session_id)
+        droid = android.Android(cmd='continue', uid=session_id,
+            port=self.h_port)
+        return droid
+
+    def terminate_session(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 terminate_all_sessions(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.terminate_session(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 run_iperf_client(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
+
+    def wait_for_boot_completion(self):
+        """Waits for the device to boot up.
+
+        Returns:
+            True if the device successfully finished booting, False otherwise.
+        """
+        timeout = time.time() + 60*15 # wait for 15 minutes
+        while True:
+            out = self.adb.shell("getprop sys.boot_completed")
+            completed = out.decode('utf-8').strip()
+            if completed == '1':
+                return True
+            if time.time() > timeout:
+                return False
+            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.is_bootloader:
+            self.fastboot.reboot()
+            return
+        self.terminate_all_sessions()
+        self.adb.reboot()
+        time.sleep(5)
+        if not self.wait_for_boot_completion():
+            raise AndroidDeviceError("Reboot timed out on %s." % self.serial)
+        self.root_adb()
+        droid, ed = self.get_droid()
+        ed.start()
+        return droid, ed
diff --git a/acts/framework/acts/controllers/attenuator.py b/acts/framework/acts/controllers/attenuator.py
new file mode 100644
index 0000000..7eede0b
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator.py
@@ -0,0 +1,373 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2015 - 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 acts.keys import Config
+
+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 = "acts.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/acts/framework/acts/controllers/attenuator_lib/__init__.py b/acts/framework/acts/controllers/attenuator_lib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator_lib/__init__.py
diff --git a/acts/framework/acts/controllers/attenuator_lib/_tnhelper.py b/acts/framework/acts/controllers/attenuator_lib/_tnhelper.py
new file mode 100644
index 0000000..fdb54e6
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator_lib/_tnhelper.py
@@ -0,0 +1,82 @@
+#!/usr/bin/python3.4
+
+#   Copyright 2014- 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 acts.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/acts/framework/acts/controllers/attenuator_lib/aeroflex/__init__.py b/acts/framework/acts/controllers/attenuator_lib/aeroflex/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator_lib/aeroflex/__init__.py
diff --git a/acts/framework/acts/controllers/attenuator_lib/aeroflex/telnet.py b/acts/framework/acts/controllers/attenuator_lib/aeroflex/telnet.py
new file mode 100644
index 0000000..5a68f63
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator_lib/aeroflex/telnet.py
@@ -0,0 +1,150 @@
+#!/usr/bin/python3.4
+
+#   Copyright 2014- 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 acts.controllers import attenuator
+from acts.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/acts/framework/acts/controllers/attenuator_lib/minicircuits/__init__.py b/acts/framework/acts/controllers/attenuator_lib/minicircuits/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator_lib/minicircuits/__init__.py
diff --git a/acts/framework/acts/controllers/attenuator_lib/minicircuits/telnet.py b/acts/framework/acts/controllers/attenuator_lib/minicircuits/telnet.py
new file mode 100644
index 0000000..25bd347
--- /dev/null
+++ b/acts/framework/acts/controllers/attenuator_lib/minicircuits/telnet.py
@@ -0,0 +1,157 @@
+#!/usr/bin/python3.4
+
+#   Copyright 2014- 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 acts.controllers import attenuator
+from acts.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!")
+
+#       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("ATT?")
+
+        return float(atten_val)
diff --git a/acts/framework/acts/controllers/fastboot.py b/acts/framework/acts/controllers/fastboot.py
new file mode 100644
index 0000000..a54301e
--- /dev/null
+++ b/acts/framework/acts/controllers/fastboot.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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
diff --git a/acts/framework/acts/controllers/iperf_server.py b/acts/framework/acts/controllers/iperf_server.py
new file mode 100644
index 0000000..97afd88
--- /dev/null
+++ b/acts/framework/acts/controllers/iperf_server.py
@@ -0,0 +1,78 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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 os
+from signal import SIGTERM
+import subprocess
+
+from acts.utils import create_dir
+from acts.utils import start_standing_subprocess
+from acts.utils import stop_standing_subprocess
+
+def create(configs, logger):
+    log_path = os.path.dirname(logger.handlers[1].baseFilename)
+    results = []
+    for c in configs:
+        try:
+            results.append(IPerfServer(c, log_path))
+        except:
+            pass
+    return results
+
+def destroy(objs):
+    for ipf in objs:
+        try:
+            ipf.stop()
+        except:
+            pass
+
+class IPerfServer():
+    """Class that handles iperf3 operations.
+    """
+    def __init__(self, port, log_path):
+        self.port = port
+        self.log_path = os.path.join(os.path.expanduser(log_path), "iPerf")
+        self.iperf_str = "iperf3 -s -p {}".format(port)
+        self.iperf_process = None
+        self.exec_count = 0
+        self.started = False
+
+    def start(self, extra_args="", tag=""):
+        """Starts iperf server on specified port.
+
+        Args:
+            extra_args: A string representing extra arguments to start iperf
+                server with.
+            tag: Appended to log file name to identify logs from different
+                iperf runs.
+        """
+        if self.started:
+            return
+        create_dir(self.log_path)
+        self.exec_count += 1
+        if tag:
+            tag = tag + ','
+        out_file_name = "IPerfServer,{},{}{}.log".format(self.port, tag,
+            self.exec_count)
+        full_out_path = os.path.join(self.log_path, out_file_name)
+        cmd = "{} {} > {}".format(self.iperf_str, extra_args, full_out_path)
+        self.iperf_process = start_standing_subprocess(cmd)
+        self.started = True
+
+    def stop(self):
+        if self.started:
+            stop_standing_subprocess(self.iperf_process)
+            self.started = False
diff --git a/acts/framework/acts/controllers/monsoon.py b/acts/framework/acts/controllers/monsoon.py
new file mode 100644
index 0000000..617556f
--- /dev/null
+++ b/acts/framework/acts/controllers/monsoon.py
@@ -0,0 +1,815 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2015 - 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.
+
+"""Interface for a USB-connected Monsoon power meter
+(http://msoon.com/LabEquipment/PowerMonitor/).
+"""
+
+_new_author_ = 'angli@google.com (Ang Li)'
+_author_ = 'kens@google.com (Ken Shirriff)'
+
+import fcntl
+import os
+import select
+import struct
+import sys
+import time
+import traceback
+import collections
+
+# http://pyserial.sourceforge.net/
+# On ubuntu, apt-get install python3-pyserial
+import serial
+
+from acts.logger import LoggerProxy
+from acts.controllers.android_device import list_adb_devices
+from acts.utils import timeout
+
+def create(configs, logger):
+    objs = []
+    for c in configs:
+        objs.append(Monsoon(serial=c, logger=logger))
+    return objs
+
+def destroy(objs):
+    return
+
+class MonsoonError(Exception):
+    """Raised for exceptions encountered in monsoon lib."""
+
+class MonsoonProxy:
+    """Class that directly talks to monsoon over serial.
+
+    Provides a simple class to use the power meter, e.g.
+    mon = monsoon.Monsoon()
+    mon.SetVoltage(3.7)
+    mon.StartDataCollection()
+    mydata = []
+    while len(mydata) < 1000:
+        mydata.extend(mon.CollectData())
+    mon.StopDataCollection()
+
+    See http://wiki/Main/MonsoonProtocol for information on the protocol.
+    """
+
+    def __init__(self, device=None, serialno=None, wait=1):
+        """Establish a connection to a Monsoon.
+
+        By default, opens the first available port, waiting if none are ready.
+        A particular port can be specified with "device", or a particular
+        Monsoon can be specified with "serialno" (using the number printed on
+        its back). With wait=0, IOError is thrown if a device is not
+        immediately available.
+        """
+        self._coarse_ref = self._fine_ref = self._coarse_zero = 0
+        self._fine_zero = self._coarse_scale = self._fine_scale = 0
+        self._last_seq = 0
+        self.start_voltage = 0
+
+        if device:
+            self.ser = serial.Serial(device, timeout=1)
+            return
+        # Try all devices connected through USB virtual serial ports until we
+        # find one we can use.
+        while True:
+            for dev in os.listdir("/dev"):
+                prefix = "ttyACM"
+                # Prefix is different on Mac OS X.
+                if sys.platform == "darwin":
+                    prefix = "tty.usbmodem"
+                if not dev.startswith(prefix):
+                    continue
+                tmpname = "/tmp/monsoon.%s.%s" % (os.uname()[0], dev)
+                self._tempfile = open(tmpname, "w")
+                try:
+                    os.chmod(tmpname, 0o666)
+                except OSError as e:
+                    pass
+
+                try:  # use a lockfile to ensure exclusive access
+                    fcntl.lockf(self._tempfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                except IOError as e:
+                    # TODO(angli): get rid of all print statements.
+                    print("device %s is in use" % dev, file=sys.stderr)
+                    continue
+
+                try:  # try to open the device
+                    self.ser = serial.Serial("/dev/%s" % dev, timeout=1)
+                    self.StopDataCollection()  # just in case
+                    self._FlushInput()  # discard stale input
+                    status = self.GetStatus()
+                except Exception as e:
+                    print("error opening device %s: %s" % (dev, e),
+                        file=sys.stderr)
+                    print(traceback.format_exc())
+                    continue
+
+                if not status:
+                    print("no response from device %s" % dev, file=sys.stderr)
+                elif serialno and status["serialNumber"] != serialno:
+                    print(("Note: another device serial #%d seen on %s" %
+                        (status["serialNumber"], dev)), file=sys.stderr)
+                else:
+                    self.start_voltage = status["voltage1"]
+                    return
+
+            self._tempfile = None
+            if not wait: raise IOError("No device found")
+            print("Waiting for device...", file=sys.stderr)
+            time.sleep(1)
+
+    def GetStatus(self):
+        """Requests and waits for status.
+
+        Returns:
+            status dictionary.
+        """
+        # status packet format
+        STATUS_FORMAT = ">BBBhhhHhhhHBBBxBbHBHHHHBbbHHBBBbbbbbbbbbBH"
+        STATUS_FIELDS = [
+                "packetType", "firmwareVersion", "protocolVersion",
+                "mainFineCurrent", "usbFineCurrent", "auxFineCurrent",
+                "voltage1", "mainCoarseCurrent", "usbCoarseCurrent",
+                "auxCoarseCurrent", "voltage2", "outputVoltageSetting",
+                "temperature", "status", "leds", "mainFineResistor",
+                "serialNumber", "sampleRate", "dacCalLow", "dacCalHigh",
+                "powerUpCurrentLimit", "runTimeCurrentLimit", "powerUpTime",
+                "usbFineResistor", "auxFineResistor",
+                "initialUsbVoltage", "initialAuxVoltage",
+                "hardwareRevision", "temperatureLimit", "usbPassthroughMode",
+                "mainCoarseResistor", "usbCoarseResistor", "auxCoarseResistor",
+                "defMainFineResistor", "defUsbFineResistor",
+                "defAuxFineResistor", "defMainCoarseResistor",
+                "defUsbCoarseResistor", "defAuxCoarseResistor", "eventCode",
+                "eventData", ]
+
+        self._SendStruct("BBB", 0x01, 0x00, 0x00)
+        while 1:  # Keep reading, discarding non-status packets
+            read_bytes = self._ReadPacket()
+            if not read_bytes:
+                return None
+            calsize = struct.calcsize(STATUS_FORMAT)
+            if len(read_bytes) != calsize or read_bytes[0] != 0x10:
+                print("Wanted status, dropped type=0x%02x, len=%d" % (
+                    read_bytes[0], len(read_bytes)), file=sys.stderr)
+                continue
+            status = dict(zip(STATUS_FIELDS, struct.unpack(STATUS_FORMAT,
+                read_bytes)))
+            assert status["packetType"] == 0x10
+            for k in status.keys():
+                if k.endswith("VoltageSetting"):
+                    status[k] = 2.0 + status[k] * 0.01
+                elif k.endswith("FineCurrent"):
+                    pass # needs calibration data
+                elif k.endswith("CoarseCurrent"):
+                    pass # needs calibration data
+                elif k.startswith("voltage") or k.endswith("Voltage"):
+                    status[k] = status[k] * 0.000125
+                elif k.endswith("Resistor"):
+                    status[k] = 0.05 + status[k] * 0.0001
+                    if k.startswith("aux") or k.startswith("defAux"):
+                        status[k] += 0.05
+                elif k.endswith("CurrentLimit"):
+                    status[k] = 8 * (1023 - status[k]) / 1023.0
+            return status
+
+    def RampVoltage(self, start, end):
+        v = start
+        if v < 3.0: v = 3.0 # protocol doesn't support lower than this
+        while (v < end):
+            self.SetVoltage(v)
+            v += .1
+            time.sleep(.1)
+        self.SetVoltage(end)
+
+    def SetVoltage(self, v):
+        """Set the output voltage, 0 to disable.
+        """
+        if v == 0:
+            self._SendStruct("BBB", 0x01, 0x01, 0x00)
+        else:
+            self._SendStruct("BBB", 0x01, 0x01, int((v - 2.0) * 100))
+
+    def GetVoltage(self):
+        """Get the output voltage.
+
+        Returns:
+            Current Output Voltage (in unit of v).
+        """
+        return self.GetStatus()["outputVoltageSetting"]
+
+    def SetMaxCurrent(self, i):
+        """Set the max output current.
+        """
+        assert i >= 0 and i <= 8
+        val = 1023 - int((i/8)*1023)
+        self._SendStruct("BBB", 0x01, 0x0a, val & 0xff)
+        self._SendStruct("BBB", 0x01, 0x0b, val >> 8)
+
+    def SetMaxPowerUpCurrent(self, i):
+        """Set the max power up current.
+        """
+        assert i >= 0 and i <= 8
+        val = 1023 - int((i/8)*1023)
+        self._SendStruct("BBB", 0x01, 0x08, val & 0xff)
+        self._SendStruct("BBB", 0x01, 0x09, val >> 8)
+
+    def SetUsbPassthrough(self, val):
+        """Set the USB passthrough mode: 0 = off, 1 = on,  2 = auto.
+        """
+        self._SendStruct("BBB", 0x01, 0x10, val)
+
+    def GetUsbPassthrough(self):
+        """Get the USB passthrough mode: 0 = off, 1 = on,  2 = auto.
+
+        Returns:
+            Current USB passthrough mode.
+        """
+        return self.GetStatus()["usbPassthroughMode"]
+
+    def StartDataCollection(self):
+        """Tell the device to start collecting and sending measurement data.
+        """
+        self._SendStruct("BBB", 0x01, 0x1b, 0x01) # Mystery command
+        self._SendStruct("BBBBBBB", 0x02, 0xff, 0xff, 0xff, 0xff, 0x03, 0xe8)
+
+    def StopDataCollection(self):
+        """Tell the device to stop collecting measurement data.
+        """
+        self._SendStruct("BB", 0x03, 0x00) # stop
+
+    def CollectData(self):
+        """Return some current samples. Call StartDataCollection() first.
+        """
+        while 1:  # loop until we get data or a timeout
+            _bytes = self._ReadPacket()
+            if not _bytes:
+                return None
+            if len(_bytes) < 4 + 8 + 1 or _bytes[0] < 0x20 or _bytes[0] > 0x2F:
+                print("Wanted data, dropped type=0x%02x, len=%d" % (
+                    _bytes[0], len(_bytes)), file=sys.stderr)
+                continue
+
+            seq, _type, x, y = struct.unpack("BBBB", _bytes[:4])
+            data = [struct.unpack(">hhhh", _bytes[x:x+8])
+                            for x in range(4, len(_bytes) - 8, 8)]
+
+            if self._last_seq and seq & 0xF != (self._last_seq + 1) & 0xF:
+                print("Data sequence skipped, lost packet?", file=sys.stderr)
+            self._last_seq = seq
+
+            if _type == 0:
+                if not self._coarse_scale or not self._fine_scale:
+                    print("Waiting for calibration, dropped data packet.",
+                        file=sys.stderr)
+                    continue
+                out = []
+                for main, usb, aux, voltage in data:
+                    if main & 1:
+                        coarse = ((main & ~1) - self._coarse_zero)
+                        out.append(coarse * self._coarse_scale)
+                    else:
+                        out.append((main - self._fine_zero) * self._fine_scale)
+                return out
+            elif _type == 1:
+                self._fine_zero = data[0][0]
+                self._coarse_zero = data[1][0]
+            elif _type == 2:
+                self._fine_ref = data[0][0]
+                self._coarse_ref = data[1][0]
+            else:
+                print("Discarding data packet type=0x%02x" % _type,
+                    file=sys.stderr)
+                continue
+
+            # See http://wiki/Main/MonsoonProtocol for details on these values.
+            if self._coarse_ref != self._coarse_zero:
+                self._coarse_scale = 2.88 / (self._coarse_ref - self._coarse_zero)
+            if self._fine_ref != self._fine_zero:
+                self._fine_scale = 0.0332 / (self._fine_ref - self._fine_zero)
+
+    def _SendStruct(self, fmt, *args):
+        """Pack a struct (without length or checksum) and send it.
+        """
+        data = struct.pack(fmt, *args)
+        data_len = len(data) + 1
+        checksum = (data_len + sum(data)) % 256
+        out = bytes([data_len]) + data + bytes([checksum])
+        self.ser.write(out)
+
+    def _ReadPacket(self):
+        """Read a single data record as a string (without length or checksum).
+        """
+        len_char = self.ser.read(1)
+        if not len_char:
+            print("Reading from serial port timed out.", file=sys.stderr)
+            return None
+
+        data_len = ord(len_char)
+        if not data_len:
+            return ""
+        result = self.ser.read(int(data_len))
+        if len(result) != data_len:
+            print("Length mismatch, expected %d bytes, got %d bytes." % data_len,
+                len(result))
+            return None
+        body = result[:-1]
+        checksum = (sum(result[:-1]) + data_len) % 256
+        if result[-1] != checksum:
+            print("Invalid checksum from serial port!", file=sys.stderr)
+            print("Expected {}, got {}".format(hex(checksum), hex(result[-1])))
+            return None
+        return result[:-1]
+
+    def _FlushInput(self):
+        """ Flush all read data until no more available. """
+        self.ser.flush()
+        flushed = 0
+        while True:
+            ready_r, ready_w, ready_x = select.select([self.ser], [],
+                [self.ser], 0)
+            if len(ready_x) > 0:
+                print("exception from serial port", file=sys.stderr)
+                return None
+            elif len(ready_r) > 0:
+                flushed += 1
+                self.ser.read(1)  # This may cause underlying buffering.
+                self.ser.flush()  # Flush the underlying buffer too.
+            else:
+                break
+        # if flushed > 0:
+        #     print("dropped >%d bytes" % flushed, file=sys.stderr)
+
+class MonsoonData:
+    """A class for reporting power measurement data from monsoon.
+
+    Data means the measured current value in Amps.
+    """
+    # Number of digits for long rounding.
+    lr = 8
+    # Number of digits for short rounding
+    sr = 6
+    # Delimiter for writing multiple MonsoonData objects to text file.
+    delimiter = "\n\n==========\n\n"
+
+    def __init__(self, data_points, timestamps, hz, voltage, offset=0):
+        """Instantiates a MonsoonData object.
+
+        Args:
+            data_points: A list of current values in Amp (float).
+            timestamps: A list of epoch timestamps (int).
+            hz: The hertz at which the data points are measured.
+            voltage: The voltage at which the data points are measured.
+            offset: The number of initial data points to discard
+                in calculations.
+        """
+        self._data_points = data_points
+        self._timestamps = timestamps
+        self.offset = offset
+        msg = "offset number must be smaller than the number of data points."
+        assert self.offset <= len(self._data_points), msg
+        self.data_points = self._data_points[self.offset:]
+        self.timestamps = self._timestamps[self.offset:]
+        self.hz = hz
+        self.voltage = voltage
+        self.tag = None
+        self._validate_data()
+
+    @property
+    def average_current(self):
+        """Average current in the unit of mA.
+        """
+        len_data_pt = len(self.data_points)
+        if len_data_pt == 0:
+            return 0
+        cur = sum(self.data_points) * 1000 / len_data_pt
+        return round(cur, self.sr)
+
+    @property
+    def total_charge(self):
+        """Total charged used in the unit of mAh.
+        """
+        charge = (sum(self.data_points) / self.hz) * 1000 / 3600
+        return round(charge, self.sr)
+
+    @property
+    def total_power(self):
+        """Total power used.
+        """
+        power = self.average_current * self.voltage
+        return round(power, self.sr)
+
+    @staticmethod
+    def from_string(data_str):
+        """Creates a MonsoonData object from a string representation generated
+        by __str__.
+
+        Args:
+            str: The string representation of a MonsoonData.
+
+        Returns:
+            A MonsoonData object.
+        """
+        lines = data_str.strip().split('\n')
+        msg = "Invalid format. Is this string generated by MonsoonData class?"
+        assert len(lines) > 4, msg
+        assert "Average Current:" in lines[1], msg
+        assert "Voltage: " in lines[2], msg
+        assert "Total Power: " in lines[3], msg
+        assert "Taken at " in lines[4]
+        assert lines[5] == "Time" + ' ' * 7 + "Amp", msg
+        hz_str = lines[4].split()[2]
+        hz = int(hz_str[:-2])
+        voltage_str = lines[2].split()[1]
+        voltage = int(voltage[:-1])
+        lines = lines[6:]
+        t = []
+        v = []
+        for l in lines:
+            try:
+                timestamp, value = l.split(' ')
+                t.append(int(timestamp))
+                v.append(float(value))
+            except ValueError:
+                raise AssertionError(msg)
+        return MonsoonData(v, t, hz, voltage)
+
+    @staticmethod
+    def save_to_text_file(monsoon_data, file_path):
+        """Save multiple MonsoonData objects to a text file.
+
+        Args:
+            monsoon_data: A list of MonsoonData objects to write to a text
+                file.
+            file_path: The full path of the file to save to, including the file
+                name.
+        """
+        with open(file_path, 'w') as f:
+            for md in monsoon_data:
+                f.write(str(md))
+                f.write(MonsoonData.delimiter)
+
+    @staticmethod
+    def from_text_file(file_path):
+        """Load MonsoonData objects from a text file generated by
+        MonsoonData.save_to_text_file.
+
+        Args:
+            file_path: The full path of the file load from, including the file
+                name.
+
+        Returns:
+            A list of MonsoonData objects.
+        """
+        results = []
+        with open(file_path, 'r') as f:
+            data_strs = f.read().split(MonsoonData.delimiter)
+            for data_str in data_strs:
+                results.append(MonsoonData.from_string(data_str))
+        return results
+
+    def _validate_data(self):
+        """Verifies that the data points contained in the class are valid.
+        """
+        msg = "Error! Expected {} timestamps, found {}.".format(
+            len(self._data_points), len(self._timestamps))
+        assert len(self._data_points) == len(self._timestamps), msg
+
+    def update_offset(self, new_offset):
+        """Updates how many data points to skip in caculations.
+
+        Always use this function to update offset instead of directly setting
+        self.offset.
+
+        Args:
+            new_offset: The new offset.
+        """
+        self.offset = new_offset
+        self.data_points = self._data_points[self.offset:]
+        self.timestamps = self._timestamps[self.offset:]
+
+    def get_data_with_timestamps(self):
+        """Returns the data points with timestamps.
+
+        Returns:
+            A list of tuples in the format of (timestamp, data)
+        """
+        result = []
+        for t, d in zip(self.timestamps, self.data_points):
+            result.append(t, round(d, self.lr))
+        return result
+
+    def get_average_record(self, n):
+        """Returns a list of average current numbers, each representing the
+        average over the last n data points.
+
+        Args:
+            n: Number of data points to average over.
+
+        Returns:
+            A list of average current values.
+        """
+        history_deque = collections.deque()
+        averages = []
+        for d in self.data_points:
+            history_deque.appendleft(d)
+            if len(history_deque) > n:
+                history_deque.pop()
+            avg = sum(history_deque) / len(history_deque)
+            averages.append(round(avg, self.lr))
+        return averages
+
+    def _header(self):
+        strs = [""]
+        if self.tag:
+            strs.append(self.tag)
+        else:
+            strs.append("Monsoon Measurement Data")
+        strs.append("Average Current: {}mA.".format(self.average_current))
+        strs.append("Voltage: {}V.".format(self.voltage))
+        strs.append("Total Power: {}mW.".format(self.total_power))
+        strs.append("Taken at {}Hz with an offset of {} samples.".format(
+            self.hz, self.offset))
+        return "\n".join(strs)
+
+    def __len__(self):
+        return len(self.data_points)
+
+    def __str__(self):
+        strs = []
+        strs.append(self._header())
+        strs.append("Time" + ' ' * 7 + "Amp")
+        for t, d in zip(self.timestamps, self.data_points):
+            strs.append("{} {}".format(t, round(d, self.sr)))
+        return "\n".join(strs)
+
+    def __repr__(self):
+        return self._header()
+
+class Monsoon:
+    """The wrapper class for test scripts to interact with monsoon.
+    """
+    def __init__(self, *args, **kwargs):
+        serial = kwargs["serial"]
+        device = None
+        if "logger" in kwargs:
+            self.log = LoggerProxy(kwargs["logger"])
+        else:
+            self.log = LoggerProxy()
+        if "device" in kwargs:
+            device = kwargs["device"]
+        self.mon = MonsoonProxy(serialno=serial, device=device)
+
+    def set_voltage(self, volt, ramp=False):
+        """Sets the output voltage of monsoon.
+
+        Args:
+            volt: Voltage to set the output to.
+            ramp: If true, the output voltage will be increased gradually to
+                prevent tripping Monsoon overvoltage.
+        """
+        if ramp:
+            self.mon.RampVoltage(mon.start_voltage, volt)
+        else:
+            self.mon.SetVoltage(volt)
+
+    def set_max_current(self, cur):
+        """Sets monsoon's max output current.
+
+        Args:
+            cur: The max current in A.
+        """
+        self.mon.SetMaxCurrent(cur)
+
+    def set_max_init_current(self, cur):
+        """Sets the max power-up/inital current.
+
+        Args:
+            cur: The max initial current allowed in mA.
+        """
+        self.mon.SetMaxPowerUpCurrent(cur)
+
+    @property
+    def status(self):
+        """Gets the status params of monsoon.
+
+        Returns:
+            A dictionary where each key-value pair represents a monsoon status
+            param.
+        """
+        return self.mon.GetStatus()
+
+    def take_samples(self, sample_hz, sample_num, sample_offset=0, live=False):
+        """Take samples of the current value supplied by monsoon.
+
+        This is the actual measurement for power consumption. This function
+        blocks until the number of samples requested has been fulfilled.
+
+        Args:
+            hz: Number of points to take for every second.
+            sample_num: Number of samples to take.
+            offset: The number of initial data points to discard in MonsoonData
+                calculations. sample_num is extended by offset to compensate.
+            live: Print each sample in console as measurement goes on.
+
+        Returns:
+            A MonsoonData object representing the data obtained in this
+            sampling. None if sampling is unsuccessful.
+        """
+        sys.stdout.flush()
+        voltage = self.mon.GetVoltage()
+        self.log.info("Taking samples at %dhz for %ds, voltage %fv." % (
+            sample_hz, sample_num/sample_hz, voltage))
+        sample_num += sample_offset
+        # Make sure state is normal
+        self.mon.StopDataCollection()
+        status = self.mon.GetStatus()
+        native_hz = status["sampleRate"] * 1000
+
+        # Collect and average samples as specified
+        self.mon.StartDataCollection()
+
+        # In case sample_hz doesn't divide native_hz exactly, use this
+        # invariant: 'offset' = (consumed samples) * sample_hz -
+        # (emitted samples) * native_hz
+        # This is the error accumulator in a variation of Bresenham's
+        # algorithm.
+        emitted = offset = 0
+        collected = []
+        # past n samples for rolling average
+        history_deque = collections.deque()
+        current_values = []
+        timestamps = []
+
+        try:
+            last_flush = time.time()
+            while emitted < sample_num or sample_num == -1:
+                # The number of raw samples to consume before emitting the next
+                # output
+                need = int((native_hz - offset + sample_hz - 1) / sample_hz)
+                if need > len(collected):     # still need more input samples
+                    samples = self.mon.CollectData()
+                    if not samples:
+                        break
+                    collected.extend(samples)
+                else:
+                    # Have enough data, generate output samples.
+                    # Adjust for consuming 'need' input samples.
+                    offset += need * sample_hz
+                    # maybe multiple, if sample_hz > native_hz
+                    while offset >= native_hz:
+                        # TODO(angli): Optimize "collected" operations.
+                        this_sample = sum(collected[:need]) / need
+                        this_time = int(time.time())
+                        timestamps.append(this_time)
+                        if live:
+                            self.log.info("%s %s" % (this_time, this_sample))
+                        current_values.append(this_sample)
+                        sys.stdout.flush()
+                        offset -= native_hz
+                        emitted += 1 # adjust for emitting 1 output sample
+                    collected = collected[need:]
+                    now = time.time()
+                    if now - last_flush >= 0.99: # flush every second
+                        sys.stdout.flush()
+                        last_flush = now
+        except Exception as e:
+            pass
+        self.mon.StopDataCollection()
+        try:
+            return MonsoonData(current_values, timestamps, sample_hz,
+                voltage, offset=sample_offset)
+        except:
+            return None
+
+    @timeout(60)
+    def usb(self, state):
+        """Sets the monsoon's USB passthrough mode. This is specific to the
+        USB port in front of the monsoon box which connects to the powered
+        device, NOT the USB that is used to talk to the monsoon itself.
+
+        "Off" means USB always off.
+        "On" means USB always on.
+        "Auto" means USB is automatically turned off when sampling is going on,
+        and turned back on when sampling finishes.
+
+        Args:
+            stats: The state to set the USB passthrough to.
+
+        Returns:
+            True if the state is legal and set. False otherwise.
+        """
+        state_lookup = {
+            "off": 0,
+            "on": 1,
+            "auto": 2
+        }
+        state = state.lower()
+        if state in state_lookup:
+            current_state = self.mon.GetUsbPassthrough()
+            while(current_state != state_lookup[state]):
+                self.mon.SetUsbPassthrough(state_lookup[state])
+                time.sleep(1)
+                current_state = self.mon.GetUsbPassthrough()
+            return True
+        return False
+
+    @timeout(15)
+    def _wait_for_device(self, ad):
+        while ad.serial not in list_adb_devices():
+            pass
+        ad.adb.wait_for_device()
+
+    def execute_sequence_and_measure(self, hz, duration, step_funcs, ad, offset_sec=20, *args, **kwargs):
+        """Executes a sequence of steps and take samples in-between.
+
+        For each step function, the following steps are followed:
+        1. The function is executed to put the android device in a state.
+        2. If the function returns False, skip to next step function.
+        3. If the function returns True, sl4a session is disconnected.
+        4. Monsoon takes samples.
+        5. Sl4a is reconnected.
+
+        Because it takes some time for the device to calm down after the usb
+        connection is cut, an offset is set for each measurement. The default
+        is 20s.
+
+        Args:
+            hz: Number of samples to take per second.
+            durations: Number(s) of minutes to take samples for in each step.
+                If this is an integer, all the steps will sample for the same
+                amount of time. If this is an iterable of the same length as
+                step_funcs, then each number represents the number of minutes
+                to take samples for after each step function.
+                e.g. If durations[0] is 10, we'll sample for 10 minutes after
+                step_funcs[0] is executed.
+            step_funcs: A list of funtions, whose first param is an android
+                device object. If a step function returns True, samples are
+                taken after this step, otherwise we move on to the next step
+                function.
+            ad: The android device object connected to this monsoon.
+            offset_sec: The number of seconds of initial data to discard.
+            *args, **kwargs: Extra args to be passed into each step functions.
+
+        Returns:
+            The MonsoonData objects from samplings.
+        """
+        sample_nums = []
+        try:
+            if len(duration) != len(step_funcs):
+                raise MonsoonError(("The number of durations need to be the "
+                    "same as the number of step functions."))
+            for d in duration:
+                sample_nums.append(d * 60 * hz)
+        except TypeError:
+            num = duration * 60 * hz
+            sample_nums = [num] * len(step_funcs)
+        results = []
+        oset = offset_sec * hz
+        for func, num in zip(step_funcs, sample_nums):
+            try:
+                self.usb("auto")
+                step_name = func.__name__
+                self.log.info("Executing step function %s." % step_name)
+                take_sample = func(ad, *args, **kwargs)
+                if not take_sample:
+                    self.log.info("Skip taking samples for %s" % step_name)
+                    continue
+                time.sleep(1)
+                ad.terminate_all_sessions()
+                time.sleep(1)
+                self.log.info("Taking samples for %s." % step_name)
+                data = self.take_samples(hz, num, sample_offset=oset)
+                assert data, "Sampling for %s failed." % step_name
+                self.log.info("Sample summary: %s" % repr(data))
+                # self.log.debug(str(data))
+                data.tag = step_name
+                results.append(data)
+            except Exception:
+                msg = "Exception happened during step %s, abort!" % func.__name__
+                self.log.exception(msg)
+                return results
+            finally:
+                self.mon.StopDataCollection()
+                self.usb("on")
+                self._wait_for_device(ad)
+                # Wait for device to come back online.
+                time.sleep(10)
+                droid, ed = ad.get_droid(True)
+                ed.start()
+                # Release wake lock to put device into sleep.
+                droid.goToSleepNow()
+        return results
\ No newline at end of file
diff --git a/acts/framework/acts/controllers/tel/__init__.py b/acts/framework/acts/controllers/tel/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/controllers/tel/__init__.py
diff --git a/acts/framework/acts/controllers/tel/_anritsu_utils.py b/acts/framework/acts/controllers/tel/_anritsu_utils.py
new file mode 100644
index 0000000..21f3a2f
--- /dev/null
+++ b/acts/framework/acts/controllers/tel/_anritsu_utils.py
@@ -0,0 +1,232 @@
+#!/usr/bin/python3.4
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+#   Copyright 2014- 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.
+
+"""
+Utility functions for for Anritsu Signalling Tester.
+"""
+
+OPERATION_COMPLETE = 1
+NO_ERROR = 0
+
+ANRITSU_ERROR_CODES = {
+    0: 'No errors occurred',
+    2: 'The specified file does not exist',
+    14: 'The buffer size is insufficient',
+    29: 'The save destination is a write-protected file.',
+    80: 'A file with the same name already exists.'
+        ' (If Overwrite is specified to 0.)',
+    87: 'The specified value is wrong.',
+    112: 'The disk space is insufficient.',
+    183: 'SmartStudio is already running.',
+    1060: 'The control software has not been started or has already terminated',
+    1067: 'SmartStudio, control software or SMS Centre could not start due to'
+          'a problem or problems resulting from OS or the MD8475A system.',
+    1229: 'Connecting to the server failed.',
+    1235: 'A request is suspended.',
+    1460: 'The operation is terminated due to the expiration of the'
+          ' timeout period.',
+    9999: 'A GPIB command error occurred.',
+    536870912: 'The license could not be confirmed.',
+    536870913: 'The specified file cannot be loaded by the SmartStudio.',
+    536870914: 'The specified process ID does not exist.',
+    536870915: 'The received data does not exist.',
+    536870916: 'Simulation is not running.',
+    536870917: 'Simulation is running.',
+    536870918: 'Test Case has never been executed.',
+    536870919: 'The resource cannot be obtained.',
+    536870920: 'A resource protocol error, such as download error or'
+               ' license error, occurred.',
+    536870921: 'The function call has been in invalid status.',
+    536870922: 'The current Simulation Model does not allow the operation.',
+    536870923: 'The Cell name to be set does not exist.',
+    536870924: 'The test is being executed.',
+    536870925: 'The current UE status does not correspond to the'
+               ' test parameters.',
+    536870926: 'There is no LOG information because the simulation'
+               ' has not been executed.',
+    536870927: 'Measure Export has already been executed.',
+    536870928: 'SmartStudio is not connected to the SMS Centre.',
+    536870929: 'SmartStudio failed to send an SMS message to the SMS Centre.',
+    536870930: 'SmartStudio has successfully sent an SMS message'
+               ' to the SMS Centre,but the SMS Centre judges it as an error.',
+    536870931: 'The processing that is unavailable with the current system'
+               ' status has been executed.',
+    536870932: 'The option could not be confirmed.',
+    536870933: 'Measure Export has been stopped.',
+    536870934: 'SmartStudio cannot load the specified file because the'
+               ' version is old.',
+    536870935: 'The data with the specified PDN number does not exist.',
+    536870936: 'The data with the specified Dedicated number does not exist.',
+    536870937: 'The PDN data cannot be added because the upper limit of the'
+               ' number of PDN data has been reached.',
+    536870938: 'The number of antennas, which cannot be set to the current'
+               ' Simulation Model,has been specified.',
+    536870939: 'Calibration of path loss failed.',
+    536870940: 'There is a parameter conflict.',
+    536870941: 'The DL Ref Power setting is out of the setting range'
+               ' at W-CDMA (Evolution).',
+    536870942: 'DC-HSDPA is not available for the current channel setting.',
+    536870943: 'The specified Packet Rate cannot be used by the current'
+               ' Simulation Model.',
+    536870944: 'The W-CDMA Cell parameter F-DPCH is set to Enable.',
+    536870945: 'Target is invalid.',
+    536870946: 'The PWS Centre detects an error.',
+    536870947: 'The Ec/Ior setting is invalid.',
+    536870948: 'The combination of Attach Type and TA Update Type is invalid.',
+    536870949: 'The license of the option has expired.',
+    536870950: 'The Ping command is being executed.',
+    536870951: 'The Ping command is not being executed.',
+    536870952: 'The current Test Case parameter setting is wrong.',
+    536870953: 'The specified IP address is the same as that of Default Gateway'
+               'specified by Simulation parameter.',
+    536870954: 'TFT IE conversion failed.',
+    536875008: 'An error exists in the parameter configuration.'
+               '(This error applies only to the current version.)',
+    536936448: 'License verification failed.',
+    536936449: 'The IMS Services cannot load the specified file.',
+    536936462: 'Simulation is not performed and no log information exists.',
+    536936467: 'The executed process is inoperable in the current status'
+               ' of Visual User Agent.',
+    536936707: 'The specified Virtual Network is not running.',
+    536936709: 'The specified Virtual Network is running. '
+               'Any one of the Virtual Networks is running.',
+    536936727: 'The specified Virtual Network does not exist.',
+    536936729: 'When the Virtual Network already exists.',
+    554762241: 'The RF Measurement launcher cannot be accessed.',
+    554762242: 'License check of the RF Measurement failed.',
+    554762243: 'Function is called when RF Measurement cannot be set.',
+    554762244: 'RF Measurement has been already started.',
+    554762245: 'RF Measurement failed to start due to a problem resulting'
+               ' from OS or the MD8475A system.',
+    554762246: 'RF Measurement is not started or is already terminated.',
+    554762247: 'There is a version mismatch between RF Measurement and CAL.',
+    554827777: 'The specified value for RF Measurement is abnormal.',
+    554827778: 'GPIB command error has occurred in RF Measurement.',
+    554827779: 'Invalid file path was specified to RF Measurement.',
+    554827780: 'RF Measurement argument is NULL pointer.',
+    555810817: 'RF Measurement is now performing the measurement.',
+    555810818: 'RF Measurement is now not performing the measurement.',
+    555810819: 'RF Measurement is not measured yet. (There is no result '
+               'information since measurement is not performed.)',
+    555810820: 'An error has occurred when RF Measurement'
+               ' starts the measurement.',
+    555810821: 'Simulation has stopped when RF Measurement is '
+               'performing the measurement.',
+    555810822: 'An error has been retrieved from the Platform when '
+               'RF Measurement is performing the measurement.',
+    555810823: 'Measurement has been started in the system state where RF '
+               'Measurement is invalid.',
+    556859393: 'RF Measurement is now saving a file.',
+    556859394: 'There is insufficient disk space when saving'
+               'a Measure Result file of RF Measurement.',
+    556859395: 'An internal error has occurred or USB cable has been'
+               ' disconnected when saving a Measure Result'
+               ' file of RF Measurement.',
+    556859396: 'A write-protected file was specified as the save destination'
+               ' when saving a Measure Result file of RF Measurement.',
+    568328193: 'An internal error has occurred in RF Measurement.',
+    687865857: 'Calibration Measure DSP is now being measured.',
+    687865858: 'Calibration measurement failed.',
+    687865859: 'Calibration slot is empty or its system does not apply.',
+    687865860: 'Unexpected command is received from Calibration HWC.',
+    687865861: 'Failed to receive the Calibration measurement result.',
+    687865862: 'Failed to open the correction value file on the'
+               ' Calibration HDD.',
+    687865863: 'Failed to move the pointer on the Calibration correction'
+               ' value table.',
+    687865864: 'Failed to write the correction value to the Calibration'
+               ' correction value file on the Calibration HDD.',
+    687865865: 'Failed to load the correction value from the Calibration HDD.',
+    687865866: 'Failed to create a directory to which the correction value '
+               'file on the Calibration HDD is saved.',
+    687865867: 'Correction data has not been written in the'
+               ' Calibration-specified correction table.',
+    687865868: 'Data received from Calibration HWC does not exist.',
+    687865869: 'Data has not been written to the Flash ROM'
+               ' of Calibration BASE UNIT.',
+    687865870: 'Correction data has not been written to the'
+               ' Calibration-specified sector.',
+    687866111: 'An calibration error other than described above occurred.',
+}
+
+
+def _error_code_tostring(error_code):
+    ''' returns the description of the error from the error code
+    returned by anritsu MD8475A '''
+    try:
+        error_string = ANRITSU_ERROR_CODES[error_code]
+    except KeyError:
+        error_string = "Error : {} ".format(error_code)
+
+    return error_string
+
+
+class AnritsuUtils():
+    def gsm_encode(text):
+        '''To encode text string with GSM 7-bit alphabet for common symbols'''
+        table = {' ': '%20', '!': '%21', '\"': '%22', '#': '%23', '$': '%24',
+                 '/': '%2F', '%': '%25', '&': '%26', '\'': '%27', '(': '%28',
+                 ')': '%29', '*': '%2A', '+': '%2B', ',': '%2C', ':': '%3A',
+                 ';': '%3B', '<': '%3C', '=': '%3D', '>': '%3E', '?': '%3F',
+                 '@': '%40', '[': '%5B', ']': '%5D', '_': '%5F', 'é': '%C3%A9'}
+        coded_str = ""
+        for char in text:
+            if char in table:
+                coded_str += table[char]
+            else:
+                coded_str += char
+        return coded_str
+
+    def gsm_decode(text):
+        '''To decode text string with GSM 7-bit alphabet for common symbols'''
+        table = {'%20': ' ', '%21': '!', '%22': '\"', '%23': '#', '%24': '$',
+                 '%2F': '/', '%25': '%', '%26': '&', '%27': '\'', '%28': '(',
+                 '%29': ')', '%2A': '*', '%2B': '+', '%2C': ',', '%3A': ':',
+                 '%3B': ';', '%3C': '<', '%3D': '=', '%3E': '>', '%3F': '?',
+                 '%40': '@', '%5B': '[', '%5D': ']', '%5F': '_', '%C3%A9': 'é'}
+        coded_str = text
+        for char in table:
+            if char in text:
+                coded_str = coded_str.replace(char, table[char])
+        return coded_str
+
+    def cdma_encode(text):
+        '''To encode text string with GSM 7-bit alphabet for common symbols'''
+        table = {' ': '%20', '!': '%21', '\"': '%22', '#': '%23', '$': '%24',
+                 '/': '%2F', '%': '%25', '&': '%26', '\'': '%27', '(': '%28',
+                 ')': '%29', '*': '%2A', '+': '%2B', ',': '%2C', ':': '%3A',
+                 ';': '%3B', '<': '%3C', '=': '%3D', '>': '%3E', '?': '%3F',
+                 '@': '%40', '[': '%5B', ']': '%5D', '_': '%5F'}
+        coded_str = ""
+        for char in text:
+            if char in table:
+                coded_str += table[char]
+            else:
+                coded_str += char
+        return coded_str
+
+class AnritsuError(Exception):
+    '''Exception for errors related to Anritsu.'''
+    def __init__(self, error, command=None):
+        self._error_code = error
+        self._error_message = _error_code_tostring(self._error_code)
+        if command is not None:
+            self._error_message = "Command {} returned the error: '{}'".format(
+                                  command, self._error_message)
+
+    def __str__(self):
+        return self._error_message
diff --git a/acts/framework/acts/controllers/tel/md8475a.py b/acts/framework/acts/controllers/tel/md8475a.py
new file mode 100644
index 0000000..42f125d
--- /dev/null
+++ b/acts/framework/acts/controllers/tel/md8475a.py
@@ -0,0 +1,3183 @@
+#!/usr/bin/python3.4
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+#   Copyright 2014- 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.
+
+"""
+Controller interface for Anritsu Signalling Tester MD8475A.
+"""
+
+import time
+import socket
+from enum import Enum
+from enum import IntEnum
+
+from . _anritsu_utils import *
+
+TERMINATOR = "\0"
+SMARTSTUDIO_LAUNCH_WAIT_TIME = 90
+SMARTSTUDIO_SIMULATION_START_WAIT_TIME = 120
+REGISTRATION_STATE_WAIT_TIME = 240
+COMMUNICATION_STATE_WAIT_TIME = 240
+ANRITSU_SOCKET_BUFFER_SIZE = 8192
+COMMAND_COMPLETE_WAIT_TIME = 90
+SETTLING_TIME = 1
+WAIT_TIME_IDENTITY_RESPONSE = 5
+
+IMSI_READ_USERDATA_WCDMA = "081501"
+IMEI_READ_USERDATA_WCDMA = "081502"
+IMEISV_READ_USERDATA_WCDMA = "081503"
+IMSI_READ_USERDATA_LTE = "075501"
+IMEI_READ_USERDATA_LTE = "075502"
+IMEISV_READ_USERDATA_LTE = "075503"
+IMSI_READ_USERDATA_GSM = "081501"
+IMEI_READ_USERDATA_GSM = "081502"
+IMEISV_READ_USERDATA_GSM = "081503"
+IDENTITY_REQ_DATA_LEN = 24
+SEQ_LOG_MESSAGE_START_INDEX = 60
+
+class ProcessingStatus(Enum):
+    ''' MD8475A processing status for UE,Packet,Voice,Video,SMS,
+        PPP, PWS '''
+    PROCESS_STATUS_NONE = "NONE"
+    PROCESS_STATUS_NOTRUN = "NOTRUN"
+    PROCESS_STATUS_POWEROFF = "POWEROFF"
+    PROCESS_STATUS_REGISTRATION = "REGISTRATION"
+    PROCESS_STATUS_DETACH = "DETACH"
+    PROCESS_STATUS_IDLE = "IDLE"
+    PROCESS_STATUS_ORIGINATION = "ORIGINATION"
+    PROCESS_STATUS_HANDOVER = "HANDOVER"
+    PROCESS_STATUS_UPDATING = "UPDATING"
+    PROCESS_STATUS_TERMINATION = "TERMINATION"
+    PROCESS_STATUS_COMMUNICATION = "COMMUNICATION"
+    PROCESS_STATUS_UERELEASE = "UERELEASE"
+    PROCESS_STATUS_NWRELEASE = "NWRELEASE"
+
+
+class BtsNumber(Enum):
+    '''ID number for MD8475A supported BTS '''
+    BTS1 = "BTS1"
+    BTS2 = "BTS2"
+    BTS3 = "BTS3"
+    BTS4 = "BTS4"
+
+
+class BtsTechnology(Enum):
+    ''' BTS system technology'''
+    LTE = "LTE"
+    WCDMA = "WCDMA"
+    TDSCDMA = "TDSCDMA"
+    GSM = "GSM"
+    CDMA1X = "CDMA1X"
+    EVDO = "EVDO"
+
+
+class BtsBandwidth(Enum):
+    ''' Values for Cell Bandwidth '''
+    LTE_BANDWIDTH_1dot4MHz = "1.4MHz"
+    LTE_BANDWIDTH_3MHz = "3MHz"
+    LTE_BANDWIDTH_5MHz = "5MHz"
+    LTE_BANDWIDTH_10MHz = "10MHz"
+    LTE_BANDWIDTH_15MHz = "15MHz"
+    LTE_BANDWIDTH_20MHz = "20MHz"
+
+
+class BtsPacketRate(Enum):
+    ''' Values for Cell Packet rate '''
+    LTE_MANUAL = "MANUAL"
+    LTE_BESTEFFORT = "BESTEFFORT"
+    WCDMA_DLHSAUTO_REL7_UL384K = "DLHSAUTO_REL7_UL384K"
+    WCDMA_DL18_0M_UL384K = "DL18_0M_UL384K"
+    WCDMA_DL21_6M_UL384K = "DL21_6M_UL384K"
+    WCDMA_DLHSAUTO_REL7_ULHSAUTO = "DLHSAUTO_REL7_ULHSAUTO"
+    WCDMA_DL18_0M_UL1_46M = "DL18_0M_UL1_46M"
+    WCDMA_DL18_0M_UL2_0M = "DL18_0M_UL2_0M"
+    WCDMA_DL18_0M_UL5_76M = "DL18_0M_UL5_76M"
+    WCDMA_DL21_6M_UL1_46M = "DL21_6M_UL1_46M"
+    WCDMA_DL21_6M_UL2_0M = "DL21_6M_UL2_0M"
+    WCDMA_DL21_6M_UL5_76M = "DL21_6M_UL5_76M"
+    WCDMA_DLHSAUTO_REL8_UL384K = "DLHSAUTO_REL8_UL384K"
+    WCDMA_DL23_4M_UL384K = "DL23_4M_UL384K"
+    WCDMA_DL28_0M_UL384K = "DL28_0M_UL384K"
+    WCDMA_DL36_0M_UL384K = "DL36_0M_UL384K"
+    WCDMA_DL43_2M_UL384K = "DL43_2M_UL384K"
+    WCDMA_DLHSAUTO_REL8_ULHSAUTO = "DLHSAUTO_REL8_ULHSAUTO"
+    WCDMA_DL23_4M_UL1_46M = "DL23_4M_UL1_46M"
+    WCDMA_DL23_4M_UL2_0M = "DL23_4M_UL2_0M"
+    WCDMA_DL23_4M_UL5_76M = "DL23_4M_UL5_76M"
+    WCDMA_DL28_0M_UL1_46M = "DL28_0M_UL1_46M"
+    WCDMA_DL28_0M_UL2_0M = "DL28_0M_UL2_0M"
+    WCDMA_DL28_0M_UL5_76M = "L28_0M_UL5_76M"
+    WCDMA_DL36_0M_UL1_46M = "DL36_0M_UL1_46M"
+    WCDMA_DL36_0M_UL2_0M = "DL36_0M_UL2_0M"
+    WCDMA_DL36_0M_UL5_76M = "DL36_0M_UL5_76M"
+    WCDMA_DL43_2M_UL1_46M = "DL43_2M_UL1_46M"
+    WCDMA_DL43_2M_UL2_0M = "DL43_2M_UL2_0M"
+    WCDMA_DL43_2M_UL5_76M = "L43_2M_UL5_76M"
+
+
+class BtsPacketWindowSize(Enum):
+    ''' Values for Cell Packet window size '''
+    WINDOW_SIZE_1 = 1
+    WINDOW_SIZE_8 = 8
+    WINDOW_SIZE_16 = 16
+    WINDOW_SIZE_32 = 32
+    WINDOW_SIZE_64 = 64
+    WINDOW_SIZE_128 = 128
+    WINDOW_SIZE_256 = 256
+    WINDOW_SIZE_512 = 512
+    WINDOW_SIZE_768 = 768
+    WINDOW_SIZE_1024 = 1024
+    WINDOW_SIZE_1536 = 1536
+    WINDOW_SIZE_2047 = 2047
+
+
+class BtsServiceState(Enum):
+    ''' Values for BTS service state '''
+    SERVICE_STATE_IN = "IN"
+    SERVICE_STATE_OUT = "OUT"
+
+
+class BtsCellBarred(Enum):
+    ''' Values for Cell barred parameter '''
+    NOTBARRED = "NOTBARRED"
+    BARRED = "BARRED"
+
+
+class BtsAccessClassBarred(Enum):
+    ''' Values for Access class barred parameter '''
+    NOTBARRED = "NOTBARRED"
+    EMERGENCY = "EMERGENCY"
+    BARRED = "BARRED"
+    USERSPECIFIC = "USERSPECIFIC"
+
+
+class BtsLteEmergencyAccessClassBarred(Enum):
+    ''' Values for Lte emergency access class barred parameter '''
+    NOTBARRED = "NOTBARRED"
+    BARRED = "BARRED"
+
+
+class BtsNwNameEnable(Enum):
+    ''' Values for BT network name enable parameter '''
+    NAME_ENABLE = "ON"
+    NAME_DISABLE = "OFF"
+
+
+class IPAddressType(Enum):
+    ''' Values for IP address type '''
+    IPV4 = "IPV4"
+    IPV6 = "IPV6"
+    IPV4V6 = "IPV4V6"
+
+
+class TriggerMessageIDs(Enum):
+    ''' ID for Trigger messages  '''
+    RRC_CONNECTION_REQ = 111101
+    RRC_CONN_REESTABLISH_REQ = 111100
+    ATTACH_REQ = 141141
+    DETACH_REQ = 141145
+    MM_LOC_UPDATE_REQ = 221108
+    GMM_ATTACH_REQ = 241101
+    GMM_RA_UPDATE_REQ = 241108
+    IDENTITY_REQUEST_LTE = 141155
+    IDENTITY_REQUEST_WCDMA = 241115
+    IDENTITY_REQUEST_GSM = 641115
+
+
+class TriggerMessageReply(Enum):
+    ''' Values for Trigger message reply parameter '''
+    ACCEPT = "ACCEPT"
+    REJECT = "REJECT"
+    IGNORE = "IGNORE"
+    NONE = "NONE"
+    ILLEGAL = "ILLEGAL"
+
+
+class TestProcedure(Enum):
+    ''' Values for different Test procedures in MD8475A '''
+    PROCEDURE_BL = "BL"
+    PROCEDURE_SELECTION = "SELECTION"
+    PROCEDURE_RESELECTION = "RESELECTION"
+    PROCEDURE_REDIRECTION = "REDIRECTION"
+    PROCEDURE_HO = "HO"
+    PROCEDURE_HHO = "HHO"
+    PROCEDURE_SHO = "SHO"
+    PROCEDURE_MEASUREMENT = "MEASUREMENT"
+    PROCEDURE_CELLCHANGE = "CELLCHANGE"
+    PROCEDURE_MULTICELL = "MULTICELL"
+
+
+class TestPowerControl(Enum):
+    ''' Values for power control in test procedure '''
+    POWER_CONTROL_ENABLE = "ENABLE"
+    POWER_CONTROL_DISABLE = "DISABLE"
+
+
+class TestMeasurement(Enum):
+    ''' Values for mesaurement in test procedure '''
+    MEASUREMENT_ENABLE = "ENABLE"
+    MEASUREMENT_DISABLE = "DISABLE"
+
+'''MD8475A processing states'''
+_PROCESS_STATES = {
+    "NONE": ProcessingStatus.PROCESS_STATUS_NONE,
+    "NOTRUN": ProcessingStatus.PROCESS_STATUS_NOTRUN,
+    "POWEROFF": ProcessingStatus.PROCESS_STATUS_POWEROFF,
+    "REGISTRATION": ProcessingStatus.PROCESS_STATUS_REGISTRATION,
+    "DETACH": ProcessingStatus.PROCESS_STATUS_DETACH,
+    "IDLE": ProcessingStatus.PROCESS_STATUS_IDLE,
+    "ORIGINATION": ProcessingStatus.PROCESS_STATUS_ORIGINATION,
+    "HANDOVER": ProcessingStatus.PROCESS_STATUS_HANDOVER,
+    "UPDATING": ProcessingStatus.PROCESS_STATUS_UPDATING,
+    "TERMINATION": ProcessingStatus.PROCESS_STATUS_TERMINATION,
+    "COMMUNICATION": ProcessingStatus.PROCESS_STATUS_COMMUNICATION,
+    "UERELEASE": ProcessingStatus.PROCESS_STATUS_UERELEASE,
+    "NWRELEASE": ProcessingStatus.PROCESS_STATUS_NWRELEASE,
+}
+
+
+class VirtualPhoneStatus(IntEnum):
+    ''' MD8475A virtual phone status for UE voice and UE video
+        PPP, PWS '''
+    STATUS_IDLE = 0
+    STATUS_VOICECALL_ORIGINATION = 1
+    STATUS_VOICECALL_INCOMING = 2
+    STATUS_VOICECALL_INPROGRESS = 3
+    STATUS_VOICECALL_DISCONNECTING = 4
+    STATUS_VOICECALL_DISCONNECTED = 5
+    STATUS_VIDEOCALL_ORIGINATION = 6
+    STATUS_VIDEOCALL_INCOMING = 7
+    STATUS_VIDEOCALL_INPROGRESS = 8
+    STATUS_VIDEOCALL_DISCONNECTING = 9
+    STATUS_VIDEOCALL_DISCONNECTED = 10
+
+
+'''Virtual Phone Status '''
+_VP_STATUS = {
+    "0": VirtualPhoneStatus.STATUS_IDLE,
+    "1": VirtualPhoneStatus.STATUS_VOICECALL_ORIGINATION,
+    "2": VirtualPhoneStatus.STATUS_VOICECALL_INCOMING,
+    "3": VirtualPhoneStatus.STATUS_VOICECALL_INPROGRESS,
+    "4": VirtualPhoneStatus.STATUS_VOICECALL_DISCONNECTING,
+    "5": VirtualPhoneStatus.STATUS_VOICECALL_DISCONNECTED,
+    "6": VirtualPhoneStatus.STATUS_VIDEOCALL_ORIGINATION,
+    "7": VirtualPhoneStatus.STATUS_VIDEOCALL_INCOMING,
+    "8": VirtualPhoneStatus.STATUS_VIDEOCALL_INPROGRESS,
+    "9": VirtualPhoneStatus.STATUS_VIDEOCALL_DISCONNECTING,
+    "10": VirtualPhoneStatus.STATUS_VIDEOCALL_DISCONNECTED,
+}
+
+class VirtualPhoneAutoAnswer(Enum):
+    ''' Virtual phone auto answer enable values'''
+    ON = "ON"
+    OFF = "OFF"
+
+class CsfbType(Enum):
+    ''' CSFB Type values'''
+    CSFB_TYPE_REDIRECTION = "REDIRECTION"
+    CSFB_TYPE_HANDOVER = "HO"
+
+
+class ReturnToEUTRAN(Enum):
+    '''Return to EUTRAN setting values '''
+    RETEUTRAN_ENABLE = "ENABLE"
+    RETEUTRAN_DISABLE = "DISABLE"
+
+
+class CTCHSetup(Enum):
+    '''CTCH setting values '''
+    CTCH_ENABLE = "ENABLE"
+    CTCH_DISABLE = "DISABLE"
+
+class UEIdentityType(Enum):
+    '''UE Identity type values '''
+    IMSI = "IMSI"
+    IMEI = "IMEI"
+    IMEISV = "IMEISV"
+
+class CBCHSetup(Enum):
+    '''CBCH setting values '''
+    CBCH_ENABLE = "ENABLE"
+    CBCH_DISABLE = "DISABLE"
+
+class MD8475A():
+    """Class to communicate with Anritsu MD8475A Signalling Tester.
+       This uses GPIB command to interface with Anritsu MD8475A """
+
+    def __init__(self, ip_address, log_handle):
+        self._error_reporting = True
+        self._ipaddr = ip_address
+        self.log = log_handle
+
+        # Open socket connection to Signaling Tester
+        self.log.info("Opening Socket Connection with "
+              "Signaling Tester ({}) ".format(self._ipaddr))
+        try:
+            self._sock = socket.create_connection((self._ipaddr, 28002),
+                                                  timeout=30)
+            self.send_query("*IDN?", 60)
+            self.log.info("Communication with Signaling Tester OK.")
+            self.log.info("Opened Socket connection to ({})"
+                  "with handle ({})".format(self._ipaddr, self._sock))
+            # launching Smart Studio Application needed for the simulation
+            ret = self.launch_smartstudio()
+        except socket.timeout:
+            raise AnritsuError("Timeout happened while conencting to"
+                               " Anritsu MD8475A")
+        except socket.error:
+            raise AnritsuError("Socket creation error")
+
+    def get_BTS(self, btsnumber):
+        """ Returns the BTS object based on the BTS number provided
+
+        Args:
+            btsnumber: BTS number (BTS1, BTS2)
+
+        Returns:
+            BTS object
+        """
+        return _BaseTransceiverStation(self, btsnumber)
+
+    def get_AnritsuTestCases(self):
+        """ Returns the Anritsu Test Case Module Object
+
+        Args:
+            None
+
+        Returns:
+            Anritsu Test Case Module Object
+        """
+        return _AnritsuTestCases(self)
+
+    def get_VirtualPhone(self):
+        """ Returns the Anritsu Virtual Phone Module Object
+
+        Args:
+            None
+
+        Returns:
+            Anritsu Virtual Phone Module Object
+        """
+        return _VirtualPhone(self)
+
+    def get_PDN(self, pdn_number):
+        """ Returns the PDN Module Object
+
+        Args:
+            None
+
+        Returns:
+            Anritsu PDN Module Object
+        """
+        return _PacketDataNetwork(self, pdn_number)
+
+    def get_TriggerMessage(self):
+        """ Returns the Anritsu Trigger Message Module Object
+
+        Args:
+            None
+
+        Returns:
+            Anritsu Trigger Message Module Object
+        """
+        return _TriggerMessage(self)
+
+    def send_query(self, query, sock_timeout=10):
+        """ Sends a Query message to Anritsu and return response
+
+        Args:
+            query - Query string
+
+        Returns:
+            query response
+        """
+        self.log.info("--> {}".format(query))
+        querytoSend = (query + TERMINATOR).encode('utf-8')
+        self._sock.settimeout(sock_timeout)
+        try:
+            self._sock.send(querytoSend)
+            result = self._sock.recv(ANRITSU_SOCKET_BUFFER_SIZE).rstrip(TERMINATOR.encode('utf-8'))
+            response = result.decode('utf-8')
+            self.log.info('<-- {}'.format(response))
+            return response
+        except socket.timeout:
+            raise AnritsuError("Timeout: Response from Anritsu")
+        except socket.error:
+            raise AnritsuError("Socket Error")
+
+    def send_command(self, command, sock_timeout=20):
+        """ Sends a Command message to Anritsu
+
+        Args:
+            command - command string
+
+        Returns:
+            None
+        """
+        self.log.info("--> {}".format(command))
+        if self._error_reporting:
+            cmdToSend = (command + ";ERROR?" + TERMINATOR).encode('utf-8')
+            self._sock.settimeout(sock_timeout)
+            try:
+                self._sock.send(cmdToSend)
+                err = self._sock.recv(ANRITSU_SOCKET_BUFFER_SIZE).rstrip(TERMINATOR.encode('utf-8'))
+                error = int(err.decode('utf-8'))
+                if error != NO_ERROR:
+                    raise AnritsuError(error,  command)
+                else:
+                    # check operation status
+                    status = self.send_query("*OPC?")
+                    if int(status) != OPERATION_COMPLETE:
+                        raise AnritsuError("Operation not completed")
+            except socket.timeout:
+                raise AnritsuError("Timeout for Command Response from Anritsu")
+            except socket.error:
+                raise AnritsuError("Socket Error for Anritsu command")
+            except Exception as e:
+                raise AnritsuError(e,  command)
+        else:
+            cmdToSend = (command + TERMINATOR).encode('utf-8')
+            try:
+                self._sock.send(cmdToSend)
+            except socket.error:
+                raise AnritsuError("Socket Error", command)
+            return
+
+    def launch_smartstudio(self):
+        """ launch the Smart studio application
+            This should be done before stating simulation
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        # check the Smart Studio status . If Smart Studio doesn't exist ,
+        # start it.if it is running, stop it. Smart Studio should be in
+        # NOTRUN (Simulation Stopped) state to start new simulation
+        stat = self.send_query("STAT?", 30)
+        if stat == "NOTEXIST":
+            self.log.info("Launching Smart Studio Application,"
+                  "it takes about a minute.")
+            time_to_wait = SMARTSTUDIO_LAUNCH_WAIT_TIME
+            sleep_interval = 15
+            waiting_time = 0
+
+            err = self.send_command("RUN", 120)
+            stat = self.send_query("STAT?")
+            while stat != "NOTRUN":
+                time.sleep(sleep_interval)
+                waiting_time = waiting_time + sleep_interval
+                if waiting_time <= time_to_wait:
+                    stat = self.send_query("STAT?")
+                else:
+                    raise AnritsuError("Timeout: Smart Studio launch")
+        elif stat == "RUNNING":
+            # Stop simulation if necessary
+            self.send_command("STOP", 60)
+            stat = self.send_query("STAT?")
+
+        # The state of the Smart Studio should be NOTRUN at this point
+        # after the one of the steps from above
+        if stat != "NOTRUN":
+            self.log.info("Can not launch Smart Studio, "
+                  "please shut down all the Smart Studio SW components")
+            raise AnritsuError("Could not run SmartStudio")
+
+    def close_smartstudio(self):
+        """ Closes the Smart studio application
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        self.stop_simulation()
+        self.send_command("EXIT", 60)
+
+    def get_smartstudio_status(self):
+        """ Gets the Smart studio status
+
+        Args:
+            None
+
+        Returns:
+            Smart studio status
+        """
+        return self.send_query("STAT?")
+
+    def start_simulation(self):
+        """ Starting the simulation of the network model.
+            simulation model or simulation parameter file
+            should be set before starting the simulation
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        time_to_wait = SMARTSTUDIO_SIMULATION_START_WAIT_TIME
+        sleep_interval = 2
+        waiting_time = 0
+
+        self.send_command("START", 120)
+
+        #FIXME. Doing this since Anritsu has some issue with CALLSTAT? in CDMA1X
+        simmodel = self.get_simulation_model();
+        list = simmodel.split(',')
+        if list[0] == 'CDMA1X':
+            time.sleep(15)
+            self.log.info("CDMA1X: Not waiting for POWEROFF state, returning")
+            return
+
+        callstat = self.send_query("CALLSTAT?").split(",")
+        while callstat[0] != "POWEROFF":
+            time.sleep(sleep_interval)
+            waiting_time = waiting_time + sleep_interval
+            if waiting_time <= time_to_wait:
+                callstat = self.send_query("CALLSTAT?").split(",")
+            else:
+                raise AnritsuError("Timeout: Starting simulation")
+
+    def stop_simulation(self):
+        """ Stop simulation operation
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        stat = self.send_query("STAT?")
+        # Stop simulation if its is RUNNING
+        if stat == "RUNNING":
+            self.send_command("STOP", 60)
+            stat = self.send_query("STAT?")
+            if stat != "NOTRUN":
+                self.log.info("Failed to stop simulation")
+                raise AnritsuError("Failed to stop simulation")
+
+    def reset(self):
+        """ reset simulation parameters
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.send_command("*RST", COMMAND_COMPLETE_WAIT_TIME)
+
+    def load_simulation_paramfile(self, filepath):
+        """ loads simulation model parameter file
+        Args:
+          filepath : simulation model parameter file path
+
+        Returns:
+            None
+        """
+        self.stop_simulation()
+        cmd = "LOADSIMPARAM \"" + filepath + '\";ERROR?'
+        self.send_query(cmd)
+
+    def load_cell_paramfile(self, filepath):
+        """ loads cell model parameter file
+
+        Args:
+          filepath : cell model parameter file path
+
+        Returns:
+            None
+        """
+        self.stop_simulation()
+        cmd = "LOADCELLPARAM \"" + filepath + '\";ERROR?'
+        status = int(self.send_query(cmd))
+        if status != NO_ERROR :
+            raise AnritsuError(status, cmd)
+
+    def set_simulation_model(self, bts1, bts2=None, bts3=None, bts4=None):
+        """ Sets the simulation model
+
+        Args:
+            bts1 - BTS1 RAT
+            bts1 - BTS2 RAT
+            bts3 - Not used now
+            bts4 - Not used now
+
+        Returns:
+            None
+        """
+        self.stop_simulation()
+        simmodel = bts1.value
+        if bts2 is not None:
+            simmodel = simmodel + "," + bts2.value
+        cmd = "SIMMODEL " + simmodel
+        self.send_command(cmd, COMMAND_COMPLETE_WAIT_TIME)
+
+    def get_simulation_model(self):
+        """ Gets the simulation model
+
+        Args:
+            None
+
+        Returns:
+            Current simulation model
+        """
+        cmd = "SIMMODEL?"
+        return self.send_query(cmd)
+
+    def set_simulation_state_to_poweroff(self):
+        """ Sets the simulation state to POWER OFF
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.send_command("RESETSIMULATION POWEROFF")
+        time_to_wait = 30
+        sleep_interval = 2
+        waiting_time = 0
+
+        callstat = self.send_query("CALLSTAT?").split(",")
+        while callstat[0] != "POWEROFF":
+            time.sleep(sleep_interval)
+            waiting_time = waiting_time + sleep_interval
+            if waiting_time <= time_to_wait:
+                callstat = self.send_query("CALLSTAT?").split(",")
+            else:
+                break
+
+    def set_simulation_state_to_idle(self, btsnumber):
+        """ Sets the simulation state to IDLE
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        if not isinstance(btsnumber, BtsNumber):
+            raise ValueError(' The parameter should be of type "BtsNumber" ')
+        cmd = "RESETSIMULATION IDLE," + btsnumber.value
+        self.send_command(cmd)
+        time_to_wait = 30
+        sleep_interval = 2
+        waiting_time = 0
+
+        callstat = self.send_query("CALLSTAT?").split(",")
+        while callstat[0] != "IDLE":
+            time.sleep(sleep_interval)
+            waiting_time = waiting_time + sleep_interval
+            if waiting_time <= time_to_wait:
+                callstat = self.send_query("CALLSTAT?").split(",")
+            else:
+                break
+
+    def wait_for_registration_state(self):
+        """ Waits for UE registration state on Anritsu
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.log.info("wait for IDLE/COMMUNICATION state on anritsu.")
+        time_to_wait = REGISTRATION_STATE_WAIT_TIME
+        sleep_interval = 1
+        waiting_time = 0
+
+        #FIXME. Doing this since Anritsu has some issue with CALLSTAT? in CDMA1X
+        simmodel = self.get_simulation_model();
+        list = simmodel.split(',')
+        if list[0] == 'CDMA1X':
+            time.sleep(15)
+            self.log.info("CDMA1X: Not waiting for IDLE/COMMUNICATION state,"
+                          " returning")
+            return
+
+        callstat = self.send_query("CALLSTAT?").split(",")
+        while callstat[0] != "IDLE" and callstat[1] != "COMMUNICATION":
+            time.sleep(sleep_interval)
+            waiting_time = waiting_time + sleep_interval
+            if waiting_time <= time_to_wait:
+                callstat = self.send_query("CALLSTAT?").split(",")
+            else:
+                raise AnritsuError("UE failed to register on network")
+
+    def wait_for_communication_state(self):
+        """ Waits for UE communication state on Anritsu
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.log.info("wait for COMMUNICATION state on anritsu")
+        time_to_wait = COMMUNICATION_STATE_WAIT_TIME
+        sleep_interval = 1
+        waiting_time = 0
+
+        #FIXME. Doing this since Anritsu has some issue with CALLSTAT? in CDMA1X
+        simmodel = self.get_simulation_model();
+        list = simmodel.split(',')
+        if list[0] == 'CDMA1X':
+            time.sleep(15)
+            self.log.info("CDMA1X: Not waiting for COMMUNICATION state,"
+                          " returning")
+            return
+
+        callstat = self.send_query("CALLSTAT?").split(",")
+        while callstat[1] != "COMMUNICATION":
+            time.sleep(sleep_interval)
+            waiting_time = waiting_time + sleep_interval
+            if waiting_time <= time_to_wait:
+                callstat = self.send_query("CALLSTAT?").split(",")
+            else:
+                raise AnritsuError("UE failed to register on network")
+
+    def get_camping_cell(self):
+        """ Gets the current camping cell information
+
+        Args:
+          None
+
+        Returns:
+            returns a tuple (BTS number, RAT Technology) '
+        """
+        bts_number, rat_info = self.send_query("CAMPINGCELL?").split(",")
+        return bts_number, rat_info
+
+    def start_testcase(self):
+        """ Starts a test case on Anritsu
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.send_command("STARTTEST")
+
+    def get_testcase_status(self):
+        """ Gets the current test case status on Anritsu
+
+        Args:
+          None
+
+        Returns:
+            current test case status
+        """
+        return self.send_query("TESTSTAT?")
+
+    @property
+    def gateway_ipv4addr(self):
+        """ Gets the IPv4 address of the default gateway
+
+        Args:
+          None
+
+        Returns:
+            current UE status
+        """
+        return self.send_query("DGIPV4?")
+
+    @gateway_ipv4addr.setter
+    def gateway_ipv4addr(self, ipv4_addr):
+        """ sets the IPv4 address of the default gateway
+        Args:
+            ipv4_addr: IPv4 address of the default gateway
+
+        Returns:
+            None
+        """
+        cmd = "DGIPV4 " + ipv4_addr
+        self.send_command(cmd)
+
+    def get_ue_status(self):
+        """ Gets the current UE status on Anritsu
+
+        Args:
+          None
+
+        Returns:
+            current UE status
+        """
+        UE_STATUS_INDEX = 0
+        ue_status = self.send_query("CALLSTAT?").split(",")[UE_STATUS_INDEX]
+        return _PROCESS_STATES[ue_status]
+
+    def get_packet_status(self):
+        """ Gets the current Packet status on Anritsu
+
+        Args:
+          None
+
+        Returns:
+            current Packet status
+        """
+        PACKET_STATUS_INDEX = 1
+        packet_status = self.send_query("CALLSTAT?").split(",")[PACKET_STATUS_INDEX]
+        return _PROCESS_STATES[packet_status]
+
+    def disconnect(self):
+        """ Disconnect the Anritsu box from test PC
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        # exit smart studio application
+        self.close_smartstudio()
+        self._sock.close()
+
+    def machine_reboot(self):
+        """ Reboots the Anritsu Machine
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.send_command("REBOOT")
+
+    def save_sequence_log(self, fileName):
+        """ Saves the Anritsu Sequence logs to file
+
+        Args:
+          fileName: log file name
+
+        Returns:
+            None
+        """
+        cmd = 'SAVESEQLOG "{}"'.format(fileName)
+        self.send_command(cmd)
+
+    def clear_sequence_log(self):
+        """ Clears the Anritsu Sequence logs
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.send_command("CLEARSEQLOG")
+
+    def save_message_log(self, fileName):
+        """ Saves the Anritsu Message logs to file
+
+        Args:
+          fileName: log file name
+
+        Returns:
+            None
+        """
+        cmd = 'SAVEMSGLOG "{}"'.format(fileName)
+        self.send_command(cmd)
+
+    def clear_message_log(self):
+        """ Clears the Anritsu Message logs
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self.send_command("CLEARMSGLOG")
+
+    def save_trace_log(self, fileName, fileType, overwrite, start, end):
+        """ Saves the Anritsu Trace logs
+
+        Args:
+          fileName: log file name
+          fileType: file type (BINARY, TEXT, H245,PACKET, CPLABE)
+          overwrite: whether to over write
+          start: starting trace number
+          end: ending trace number
+
+        Returns:
+            None
+        """
+        cmd = 'SAVETRACELOG "{}",{},{},{},{}'.format(fileName, fileType,
+                                                     overwrite, start, end)
+        self.send_command(cmd)
+
+    def send_cmas_lte_wcdma(self, serialNo, messageID, warningMessage):
+        """ Sends a CMAS message
+
+        Args:
+          serialNo: serial number of CMAS message
+          messageID: CMAS message ID
+          warningMessage:  CMAS Warning message
+
+        Returns:
+            None
+        """
+        cmd = ('PWSSENDWM 3GPP,"btsno=1&warningsystem=CMAS&serialno={}'
+               '&MessageID={}&wm={}"').format(serialNo, messageID,
+                                              warningMessage)
+        self.send_command(cmd)
+
+    def send_etws_lte_wcdma(self, serialNo, messageID, warningType, warningMessage,
+                  userAlertenable, popUpEnable):
+        """ Sends a ETWS message
+
+        Args:
+          serialNo: serial number of CMAS message
+          messageID: CMAS message ID
+          warningMessage:  CMAS Warning message
+
+        Returns:
+            None
+        """
+        cmd = ('PWSSENDWM 3GPP,"btsno=1&warningsystem=ETWS&serialno={}&'
+               'primary=ON&PrimaryMessageID={}&Secondary=ON&SecondaryMessageID={}'
+               '&warningtype={}&wm={}&useralert={}&popup={}&languagecode=en"').format(
+               serialNo, messageID, messageID, warningType, warningMessage,
+               userAlertenable, popUpEnable)
+        self.send_command(cmd)
+
+    def send_cmas_etws_cdma1x(self, message_id, service_category, alert_ext,
+            response_type, severity, urgency, certainty):
+        """ Sends a CMAS/ETWS message on CDMA 1X
+
+        Args:
+          serviceCategory: service category of alert
+          messageID: message ID
+          alertText: Warning message
+
+        Returns:
+            None
+        """
+        param = ('\"btsno=1&ServiceCategory={}&MessageID={}&AlertText={}&CharSet=ASCII'
+                '&ResponseType={}&Severity={}&Urgency={}&Certainty={}\"').format(
+                service_category, message_id, alert_ext, response_type, severity,
+                urgency, certainty)
+        cmd = ('PWSSENDWM 3GPP2,{}').format(param)
+        self.send_command(cmd)
+
+    @property
+    def csfb_type(self):
+        """ Gets the current CSFB type
+
+        Args:
+            None
+
+        Returns:
+            current CSFB type
+        """
+        return self.send_query("SIMMODELEX? CSFB")
+
+    @csfb_type.setter
+    def csfb_type(self, csfb_type):
+        """ sets the CSFB type
+        Args:
+            csfb_type: CSFB type
+
+        Returns:
+            None
+        """
+        if not isinstance(csfb_type, CsfbType):
+            raise ValueError('The parameter should be of type "CsfbType" ')
+        cmd = "SIMMODELEX CSFB," + csfb_type.value
+        self.send_command(cmd)
+
+    @property
+    def csfb_return_to_eutran(self):
+        """ Gets the current return to EUTRAN status
+
+        Args:
+            None
+
+        Returns:
+            current return to EUTRAN status
+        """
+        return self.send_query("SIMMODELEX? RETEUTRAN")
+
+    @csfb_return_to_eutran.setter
+    def csfb_return_to_eutran(self, enable):
+        """ sets the return to EUTRAN feature
+        Args:
+            enable: enable/disable return to EUTRAN feature
+
+        Returns:
+            None
+        """
+        if not isinstance(enable, ReturnToEUTRAN):
+            raise ValueError('The parameter should be of type "ReturnToEUTRAN"')
+        cmd = "SIMMODELEX RETEUTRAN," + enable.value
+        self.send_command(cmd)
+
+    def set_packet_preservation(self):
+        """ Set packet state to Preservation
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "OPERATEPACKET PRESERVATION"
+        self.send_command(cmd)
+
+    def set_packet_dormant(self):
+        """ Set packet state to Dormant
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "OPERATEPACKET DORMANT"
+        self.send_command(cmd)
+
+    def get_ue_identity(self, identity_type):
+        """ Get the UE identity IMSI, IMEI, IMEISV
+
+        Args:
+            identity_type : IMSI/IMEI/IMEISV
+
+        Returns:
+            IMSI/IMEI/IMEISV value
+        """
+        bts, rat = self.get_camping_cell()
+        if rat == BtsTechnology.LTE.value:
+            identity_request = TriggerMessageIDs.IDENTITY_REQUEST_LTE.value
+            if identity_type == UEIdentityType.IMSI:
+                userdata = IMSI_READ_USERDATA_LTE
+            elif identity_type == UEIdentityType.IMEI:
+                userdata = IMEI_READ_USERDATA_LTE
+            elif identity_type == UEIdentityType.IMEISV:
+                userdata = IMEISV_READ_USERDATA_LTE
+            else:
+                return None
+        elif rat == BtsTechnology.WCDMA.value:
+            identity_request = TriggerMessageIDs.IDENTITY_REQUEST_WCDMA.value
+            if identity_type == UEIdentityType.IMSI:
+                userdata = IMSI_READ_USERDATA_WCDMA
+            elif identity_type == UEIdentityType.IMEI:
+                userdata = IMEI_READ_USERDATA_WCDMA
+            elif identity_type == UEIdentityType.IMEISV:
+                userdata = IMEISV_READ_USERDATA_WCDMA
+            else:
+                return None
+        elif rat == BtsTechnology.GSM.value:
+            identity_request = TriggerMessageIDs.IDENTITY_REQUEST_GSM.value
+            if identity_type == UEIdentityType.IMSI:
+                userdata = IMSI_READ_USERDATA_GSM
+            elif identity_type == UEIdentityType.IMEI:
+                userdata = IMEI_READ_USERDATA_GSM
+            elif identity_type == UEIdentityType.IMEISV:
+                userdata = IMEISV_READ_USERDATA_GSM
+            else:
+                return None
+        else:
+            return None
+
+        self.send_command("TMMESSAGEMODE {},USERDATA".format(identity_request))
+        time.sleep(SETTLING_TIME)
+        self.send_command("TMUSERDATA {}, {}, {}".format(identity_request,
+                                              userdata, IDENTITY_REQ_DATA_LEN))
+        time.sleep(SETTLING_TIME)
+        self.send_command("TMSENDUSERMSG {}".format(identity_request))
+        time.sleep(WAIT_TIME_IDENTITY_RESPONSE)
+        # Go through sequence log and find the identity response message
+        target = '"{}"'.format(identity_type.value)
+        seqlog = self.send_query("SEQLOG?").split(",")
+        while (target not in seqlog):
+            index = int(seqlog[0]) - 1
+            if index < SEQ_LOG_MESSAGE_START_INDEX:
+                self.log.error("Can not find "+ target)
+                return None
+            seqlog = self.send_query("SEQLOG? %d" % index).split(",")
+        return (seqlog[-1])
+
+class _AnritsuTestCases:
+    '''Class to interact with the MD8475 supported test procedures '''
+
+    def __init__(self, anritsu):
+        self._anritsu = anritsu
+
+    @property
+    def procedure(self):
+        """ Gets the current Test Procedure type
+
+        Args:
+            None
+
+        Returns:
+            One of TestProcedure type values
+        """
+        return self._anritsu.send_query("TESTPROCEDURE?")
+
+    @procedure.setter
+    def procedure(self, procedure):
+        """ sets the Test Procedure type
+        Args:
+            procedure: One of TestProcedure type values
+
+        Returns:
+            None
+        """
+        if not isinstance(procedure, TestProcedure):
+            raise ValueError('The parameter should be of type "TestProcedure" ')
+        cmd = "TESTPROCEDURE " + procedure.value
+        self._anritsu.send_command(cmd)
+
+    @property
+    def bts_direction(self):
+        """ Gets the current Test direction
+
+         Args:
+            None
+
+        Returns:
+            Current Test direction eg:BTS2,BTS1
+        """
+        return self._anritsu.send_query("TESTBTSDIRECTION?")
+
+    @bts_direction.setter
+    def bts_direction(self, direction):
+        """ sets the Test direction  eg: BTS1 to BTS2 '''
+
+        Args:
+            direction: tuple (from-bts,to_bts) of type BtsNumber
+
+        Returns:
+            None
+        """
+        if not isinstance(direction, tuple) or len(direction) is not 2:
+            raise ValueError("Pass a tuple with two items")
+        from_bts, to_bts = direction
+        if (isinstance(from_bts, BtsNumber) and isinstance(to_bts, BtsNumber)):
+            cmd = "TESTBTSDIRECTION {},{}".format(from_bts.value, to_bts.value)
+            self._anritsu.send_command(cmd)
+        else:
+            raise ValueError(' The parameters should be of type "BtsNumber" ')
+
+    @property
+    def registration_timeout(self):
+        """ Gets the current Test registration timeout
+
+        Args:
+            None
+
+        Returns:
+            Current test registration timeout value
+        """
+        return self._anritsu.send_query("TESTREGISTRATIONTIMEOUT?")
+
+    @registration_timeout.setter
+    def registration_timeout(self, timeout_value):
+        """ sets the Test registration timeout value
+        Args:
+            timeout_value: test registration timeout value
+
+        Returns:
+            None
+        """
+        cmd = "TESTREGISTRATIONTIMEOUT " + str(timeout_value)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def power_control(self):
+        """ Gets the power control enabled/disabled status for test case
+
+        Args:
+            None
+
+        Returns:
+            current power control enabled/disabled status
+        """
+        return self._anritsu.send_query("TESTPOWERCONTROL?")
+
+    @power_control.setter
+    def power_control(self, enable):
+        """ Sets the power control enabled/disabled status for test case
+
+        Args:
+            enable:  enabled/disabled
+
+        Returns:
+            None
+        """
+        if not isinstance(enable, TestPowerControl):
+            raise ValueError(' The parameter should be of type'
+                             ' "TestPowerControl" ')
+        cmd = "TESTPOWERCONTROL " + enable.value
+        self._anritsu.send_command(cmd)
+
+    @property
+    def measurement_LTE(self):
+        """ Checks measurement status for LTE test case
+
+        Args:
+            None
+
+        Returns:
+            Enabled/Disabled
+        """
+        return self._anritsu.send_query("TESTMEASUREMENT? LTE")
+
+    @measurement_LTE.setter
+    def measurement_LTE(self, enable):
+        """ Sets the measurement enabled/disabled status for LTE test case
+
+        Args:
+            enable:  enabled/disabled
+
+        Returns:
+            None
+        """
+        if not isinstance(enable, TestMeasurement):
+            raise ValueError(' The parameter should be of type'
+                             ' "TestMeasurement" ')
+        cmd = "TESTMEASUREMENT LTE," + enable.value
+        self._anritsu.send_command(cmd)
+
+    @property
+    def measurement_WCDMA(self):
+        """ Checks measurement status for WCDMA test case
+
+        Args:
+            None
+
+        Returns:
+            Enabled/Disabled
+        """
+        return self._anritsu.send_query("TESTMEASUREMENT? WCDMA")
+
+    @measurement_WCDMA.setter
+    def measurement_WCDMA(self, enable):
+        """ Sets the measurement enabled/disabled status for WCDMA test case
+
+        Args:
+            enable:  enabled/disabled
+
+        Returns:
+            None
+        """
+        if not isinstance(enable, TestMeasurement):
+            raise ValueError(' The parameter should be of type'
+                             ' "TestMeasurement" ')
+        cmd = "TESTMEASUREMENT WCDMA," + enable.value
+        self._anritsu.send_command(cmd)
+
+    @property
+    def measurement_TDSCDMA(self):
+        """ Checks measurement status for TDSCDMA test case
+
+        Args:
+            None
+
+        Returns:
+            Enabled/Disabled
+        """
+        return self._anritsu.send_query("TESTMEASUREMENT? TDSCDMA")
+
+    @measurement_TDSCDMA.setter
+    def measurement_WCDMA(self, enable):
+        """ Sets the measurement enabled/disabled status for TDSCDMA test case
+
+        Args:
+            enable:  enabled/disabled
+
+        Returns:
+            None
+        """
+        if not isinstance(enable, TestMeasurement):
+            raise ValueError(' The parameter should be of type'
+                             ' "TestMeasurement" ')
+        cmd = "TESTMEASUREMENT TDSCDMA," + enable.value
+        self._anritsu.send_command(cmd)
+
+    def set_pdn_targeteps(self, pdn_order, pdn_number=1):
+        """ Sets PDN to connect as a target when performing the
+           test case for packet handover
+
+        Args:
+            pdn_order:  PRIORITY/USER
+            pdn_number: Target PDN number
+
+        Returns:
+            None
+        """
+        cmd = "TESTPDNTARGETEPS " + pdn_order
+        if pdn_order == "USER":
+            cmd = cmd + "," + str(pdn_number)
+        self._anritsu.send_command(cmd)
+
+
+class _BaseTransceiverStation:
+    '''Class to interact different BTS supported by MD8475 '''
+    def __init__(self, anritsu, btsnumber):
+        if not isinstance(btsnumber, BtsNumber):
+            raise ValueError(' The parameter should be of type "BtsNumber" ')
+        self._bts_number = btsnumber.value
+        self._anritsu = anritsu
+
+    @property
+    def output_level(self):
+        """ Gets the Downlink power of the cell
+
+        Args:
+            None
+
+        Returns:
+            DL Power level
+        """
+        cmd = "OLVL? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @output_level.setter
+    def output_level(self, level):
+        """ Sets the Downlink power of the cell
+
+        Args:
+            level: Power level
+
+        Returns:
+            None
+        """
+        cmd = "OLVL {},{}".format(level, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def input_level(self):
+        """ Gets the reference power of the cell
+
+        Args:
+            None
+
+        Returns:
+            Reference Power level
+        """
+        cmd = "RFLVL? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @input_level.setter
+    def input_level(self, level):
+        """ Sets the reference power of the cell
+
+        Args:
+            level: Power level
+
+        Returns:
+            None
+        """
+        cmd = "RFLVL {},{}".format(level, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def band(self):
+        """ Gets the Band of the cell
+
+        Args:
+            None
+
+        Returns:
+            Cell band
+        """
+        cmd = "BAND? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @band.setter
+    def band(self, band):
+        """ Sets the Band of the cell
+
+        Args:
+            band: Band of the cell
+
+        Returns:
+            None
+        """
+        cmd = "BAND {},{}".format(band, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def bandwidth(self):
+        """ Gets the channel bandwidth of the cell
+
+        Args:
+            None
+
+        Returns:
+            channel bandwidth
+        """
+        cmd = "BANDWIDTH? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @bandwidth.setter
+    def bandwidth(self, bandwidth):
+        """ Sets the channel bandwidth of the cell
+
+        Args:
+            bandwidth: channel bandwidth  of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(bandwidth, BtsBandwidth):
+            raise ValueError(' The parameter should be of type "BtsBandwidth"')
+        cmd = "BANDWIDTH {},{}".format(bandwidth.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def dl_bandwidth(self):
+        """ Gets the downlink bandwidth of the cell
+
+        Args:
+            None
+
+        Returns:
+            downlink bandwidth
+        """
+        cmd = "DLBANDWIDTH? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @dl_bandwidth.setter
+    def dl_bandwidth(self, bandwidth):
+        """ Sets the downlink bandwidth of the cell
+
+        Args:
+            bandwidth: downlink bandwidth of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(bandwidth, BtsBandwidth):
+            raise ValueError(' The parameter should be of type "BtsBandwidth"')
+        cmd = "DLBANDWIDTH {},{}".format(bandwidth.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def ul_bandwidth(self):
+        """ Gets the uplink bandwidth of the cell
+
+        Args:
+            None
+
+        Returns:
+            uplink bandwidth
+        """
+        cmd = "ULBANDWIDTH? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @ul_bandwidth.setter
+    def ul_bandwidth(self, bandwidth):
+        """ Sets the uplink bandwidth of the cell
+
+        Args:
+            bandwidth: uplink bandwidth of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(bandwidth, BtsBandwidth):
+            raise ValueError(' The parameter should be of type "BtsBandwidth" ')
+        cmd = "ULBANDWIDTH {},{}".format(bandwidth.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def packet_rate(self):
+        """ Gets the packet rate of the cell
+
+        Args:
+            None
+
+        Returns:
+            packet rate
+        """
+        cmd = "PACKETRATE? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @packet_rate.setter
+    def packet_rate(self, packetrate):
+        """ Sets the packet rate of the cell
+
+        Args:
+            packetrate: packet rate of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(packetrate, BtsPacketRate):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsPacketRate" ')
+        cmd = "PACKETRATE {},{}".format(packetrate.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def ul_windowsize(self):
+        """ Gets the uplink window size of the cell
+
+        Args:
+            None
+
+        Returns:
+            uplink window size
+        """
+        cmd = "ULWINSIZE? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @ul_windowsize.setter
+    def ul_windowsize(self, windowsize):
+        """ Sets the uplink window size of the cell
+
+        Args:
+            windowsize: uplink window size of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(windowsize, BtsPacketWindowSize):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsPacketWindowSize" ')
+        cmd = "ULWINSIZE {},{}".format(windowsize.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def dl_windowsize(self):
+        """ Gets the downlink window size of the cell
+
+        Args:
+            None
+
+        Returns:
+            downlink window size
+        """
+        cmd = "DLWINSIZE? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @dl_windowsize.setter
+    def dl_windowsize(self, windowsize):
+        """ Sets the downlink window size of the cell
+
+        Args:
+            windowsize: downlink window size of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(windowsize, BtsPacketWindowSize):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsPacketWindowSize" ')
+        cmd = "DLWINSIZE {},{}".format(windowsize.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def service_state(self):
+        """ Gets the service state of BTS
+
+        Args:
+            None
+
+        Returns:
+            service state IN/OUT
+        """
+        cmd = "OUTOFSERVICE? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @service_state.setter
+    def service_state(self, service_state):
+        """ Sets the service state of BTS
+
+        Args:
+            service_state: service state of BTS , IN/OUT
+
+        Returns:
+            None
+        """
+        if not isinstance(service_state, BtsServiceState):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsServiceState" ')
+        cmd = "OUTOFSERVICE {},{}".format(service_state.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def cell_barred(self):
+        """ Gets the Cell Barred state of the cell
+
+        Args:
+            None
+
+        Returns:
+            one of BtsCellBarred value
+        """
+        cmd = "CELLBARRED?" + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @cell_barred.setter
+    def cell_barred(self, barred_option):
+        """ Sets the Cell Barred state of the cell
+
+        Args:
+            barred_option: Cell Barred state of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(barred_option, BtsCellBarred):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsCellBarred" ')
+        cmd = "CELLBARRED {},{}".format(barred_option.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def accessclass_barred(self):
+        """ Gets the Access Class Barred state of the cell
+
+        Args:
+            None
+
+        Returns:
+            one of BtsAccessClassBarred value
+        """
+        cmd = "ACBARRED? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @accessclass_barred.setter
+    def accessclass_barred(self, barred_option):
+        """ Sets the Access Class Barred state of the cell
+
+        Args:
+            barred_option: Access Class Barred state of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(barred_option, BtsAccessClassBarred):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsAccessClassBarred" ')
+        cmd = "ACBARRED {},{}".format(barred_option.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def lteemergency_ac_barred(self):
+        """ Gets the LTE emergency Access Class Barred state of the cell
+
+        Args:
+            None
+
+        Returns:
+            one of BtsLteEmergencyAccessClassBarred value
+        """
+        cmd = "LTEEMERGENCYACBARRED? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @lteemergency_ac_barred.setter
+    def lteemergency_ac_barred(self, barred_option):
+        """ Sets the LTE emergency Access Class Barred state of the cell
+
+        Args:
+            barred_option: Access Class Barred state of the cell
+
+        Returns:
+            None
+        """
+        if not isinstance(barred_option, BtsLteEmergencyAccessClassBarred):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsLteEmergencyAccessClassBarred" ')
+        cmd = "LTEEMERGENCYACBARRED {},{}".format(barred_option.value,
+                                                  self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def mcc(self):
+        """ Gets the MCC of the cell
+
+        Args:
+            None
+
+        Returns:
+            MCC of the cell
+        """
+        cmd = "MCC? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @mcc.setter
+    def mcc(self, mcc_code):
+        """ Sets the MCC of the cell
+
+        Args:
+            mcc_code: MCC of the cell
+
+        Returns:
+            None
+        """
+        cmd = "MCC {},{}".format(mcc_code, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def mnc(self):
+        """ Gets the MNC of the cell
+
+        Args:
+            None
+
+        Returns:
+            MNC of the cell
+        """
+        cmd = "MNC? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @mnc.setter
+    def mnc(self, mnc_code):
+        """ Sets the MNC of the cell
+
+        Args:
+            mnc_code: MNC of the cell
+
+        Returns:
+            None
+        """
+        cmd = "MNC {},{}".format(mnc_code, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def nw_fullname_enable(self):
+        """ Gets the network full name enable status
+
+        Args:
+            None
+
+        Returns:
+            one of BtsNwNameEnable value
+        """
+        cmd = "NWFNAMEON? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @nw_fullname_enable.setter
+    def nw_fullname_enable(self, enable):
+        """ Sets the network full name enable status
+
+        Args:
+            enable: network full name enable status
+
+        Returns:
+            None
+        """
+        if not isinstance(enable, BtsNwNameEnable):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsNwNameEnable" ')
+        cmd = "NWFNAMEON {},{}".format(enable.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def nw_fullname(self):
+        """ Gets the network full name
+
+        Args:
+            None
+
+        Returns:
+            Network fulll name
+        """
+        cmd = "NWFNAME? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @nw_fullname.setter
+    def nw_fullname(self, fullname):
+        """ Sets the network full name
+
+        Args:
+            fullname: network full name
+
+        Returns:
+            None
+        """
+        cmd = "NWFNAME {},{}".format(fullname, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def nw_shortname_enable(self):
+        """ Gets the network short name enable status
+
+        Args:
+            None
+
+        Returns:
+            one of BtsNwNameEnable value
+        """
+        cmd = "NWSNAMEON? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @nw_shortname_enable.setter
+    def nw_shortname_enable(self, enable):
+        """ Sets the network short name enable status
+
+        Args:
+            enable: network short name enable status
+
+        Returns:
+            None
+        """
+        if not isinstance(enable, BtsNwNameEnable):
+            raise ValueError(' The parameter should be of type'
+                             ' "BtsNwNameEnable" ')
+        cmd = "NWSNAMEON {},{}".format(enable.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def nw_shortname(self):
+        """ Gets the network short name
+
+        Args:
+            None
+
+        Returns:
+            Network short name
+        """
+        cmd = "NWSNAME? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @nw_shortname.setter
+    def nw_shortname(self, shortname):
+        """ Sets the network short name
+
+        Args:
+            shortname: network short name
+
+        Returns:
+            None
+        """
+        cmd = "NWSNAME {},{}".format(shortname, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    def apply_parameter_changes(self):
+        """ apply the parameter changes at run time
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "APPLYPARAM"
+        self._anritsu.send_command(cmd)
+
+    @property
+    def wcdma_ctch(self):
+        """ Gets the WCDMA CTCH enable/disable status
+
+        Args:
+            None
+
+        Returns:
+            one of CTCHSetup values
+        """
+        cmd = "CTCHPARAMSETUP? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @wcdma_ctch.setter
+    def wcdma_ctch(self, enable):
+        """ Sets the WCDMA CTCH enable/disable status
+
+        Args:
+            enable: WCDMA CTCH enable/disable status
+
+        Returns:
+            None
+        """
+        cmd = "CTCHPARAMSETUP {},{}".format(enable.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def lac(self):
+        """ Gets the Location Area Code of the cell
+
+        Args:
+            None
+
+        Returns:
+            LAC value
+        """
+        cmd = "LAC? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @lac.setter
+    def lac(self, lac):
+        """ Sets the Location Area Code of the cell
+
+        Args:
+            lac: Location Area Code of the cell
+
+        Returns:
+            None
+        """
+        cmd = "LAC {},{}".format(lac, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def rac(self):
+        """ Gets the Routing Area Code of the cell
+
+        Args:
+            None
+
+        Returns:
+            RAC value
+        """
+        cmd = "RAC? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @rac.setter
+    def rac(self, rac):
+        """ Sets the Routing Area Code of the cell
+
+        Args:
+            rac: Routing Area Code of the cell
+
+        Returns:
+            None
+        """
+        cmd = "RAC {},{}".format(rac, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def dl_channel(self):
+        """ Gets the downlink channel number of the cell
+
+        Args:
+            None
+
+        Returns:
+            RAC value
+        """
+        cmd = "DLCHAN? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @dl_channel.setter
+    def dl_channel(self, channel):
+        """ Sets the downlink channel number of the cell
+
+        Args:
+            channel: downlink channel number of the cell
+
+        Returns:
+            None
+        """
+        cmd = "DLCHAN {},{}".format(channel, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def sector1_mcc(self):
+        """ Gets the sector 1 MCC of the CDMA cell
+
+        Args:
+            None
+
+        Returns:
+            sector 1 mcc
+        """
+        cmd = "S1MCC? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @sector1_mcc.setter
+    def sector1_mcc(self, mcc):
+        """ Sets the sector 1 MCC of the CDMA cell
+
+        Args:
+            mcc: sector 1 MCC of the CDMA cell
+
+        Returns:
+            None
+        """
+        cmd = "S1MCC {},{}".format(mcc, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def sector1_sid(self):
+        """ Gets the sector 1 system ID of the CDMA cell
+
+        Args:
+            None
+
+        Returns:
+            sector 1 system Id
+        """
+        cmd = "S1SID? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @sector1_sid.setter
+    def sector1_sid(self, sid):
+        """ Sets the sector 1 system ID of the CDMA cell
+
+        Args:
+            sid: sector 1 system ID of the CDMA cell
+
+        Returns:
+            None
+        """
+        cmd = "S1SID {},{}".format(sid, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def sector1_nid(self):
+        """ Gets the sector 1 network ID of the CDMA cell
+
+        Args:
+            None
+
+        Returns:
+            sector 1 network Id
+        """
+        cmd = "S1NID? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @sector1_nid.setter
+    def sector1_nid(self, nid):
+        """ Sets the sector 1 network ID of the CDMA cell
+
+        Args:
+            nid: sector 1 network ID of the CDMA cell
+
+        Returns:
+            None
+        """
+        cmd = "S1NID {},{}".format(nid, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def sector1_baseid(self):
+        """ Gets the sector 1 Base ID of the CDMA cell
+
+        Args:
+            None
+
+        Returns:
+            sector 1 Base Id
+        """
+        cmd = "S1BASEID? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @sector1_baseid.setter
+    def sector1_baseid(self, baseid):
+        """ Sets the sector 1 Base ID of the CDMA cell
+
+        Args:
+            baseid: sector 1 Base ID of the CDMA cell
+
+        Returns:
+            None
+        """
+        cmd = "S1BASEID {},{}".format(baseid, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def sector1_latitude(self):
+        """ Gets the sector 1 latitude of the CDMA cell
+
+        Args:
+            None
+
+        Returns:
+            sector 1 latitude
+        """
+        cmd = "S1LATITUDE? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @sector1_latitude.setter
+    def sector1_latitude(self, latitude):
+        """ Sets the sector 1 latitude of the CDMA cell
+
+        Args:
+            latitude: sector 1 latitude of the CDMA cell
+
+        Returns:
+            None
+        """
+        cmd = "S1LATITUDE {},{}".format(latitude, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def sector1_longitude(self):
+        """ Gets the sector 1 longitude of the CDMA cell
+
+        Args:
+            None
+
+        Returns:
+            sector 1 longitude
+        """
+        cmd = "S1LONGITUDE? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @sector1_longitude.setter
+    def sector1_longitude(self, longitude):
+        """ Sets the sector 1 longitude of the CDMA cell
+
+        Args:
+            longitude: sector 1 longitude of the CDMA cell
+
+        Returns:
+            None
+        """
+        cmd = "S1LONGITUDE {},{}".format(longitude, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def cell_id(self):
+        """ Gets the cell identity of the cell
+
+        Args:
+            None
+
+        Returns:
+            cell identity
+        """
+        cmd = "CELLID? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @cell_id.setter
+    def cell_id(self, cell_id):
+        """ Sets the cell identity of the cell
+
+        Args:
+            cell_id: cell identity of the cell
+
+        Returns:
+            None
+        """
+        cmd = "CELLID {},{}".format(cell_id, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def physical_cellid(self):
+        """ Gets the physical cell id of the cell
+
+        Args:
+            None
+
+        Returns:
+            physical cell id
+        """
+        cmd = "PHYCELLID? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @physical_cellid.setter
+    def physical_cellid(self, physical_cellid):
+        """ Sets the physical cell id of the cell
+
+        Args:
+            physical_cellid: physical cell id of the cell
+
+        Returns:
+            None
+        """
+        cmd = "PHYCELLID {},{}".format(physical_cellid, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def gsm_mcs_dl(self):
+        """ Gets the Modulation and Coding scheme (DL) of the GSM cell
+
+        Args:
+            None
+
+        Returns:
+            DL MCS
+        """
+        cmd = "DLMCS? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @gsm_mcs_dl.setter
+    def gsm_mcs_dl(self, mcs_dl):
+        """ Sets the Modulation and Coding scheme (DL) of the GSM cell
+
+        Args:
+            mcs_dl: Modulation and Coding scheme (DL) of the GSM cell
+
+        Returns:
+            None
+        """
+        cmd = "DLMCS {},{}".format(mcs_dl, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def gsm_mcs_ul(self):
+        """ Gets the Modulation and Coding scheme (UL) of the GSM cell
+
+        Args:
+            None
+
+        Returns:
+            UL MCS
+        """
+        cmd = "ULMCS? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @gsm_mcs_ul.setter
+    def gsm_mcs_ul(self, mcs_ul):
+        """ Sets the Modulation and Coding scheme (UL) of the GSM cell
+
+        Args:
+            mcs_ul:Modulation and Coding scheme (UL) of the GSM cell
+
+        Returns:
+            None
+        """
+        cmd = "ULMCS {},{}".format(mcs_ul, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def lte_mcs_dl(self):
+        """ Gets the Modulation and Coding scheme (DL) of the LTE cell
+
+        Args:
+            None
+
+        Returns:
+            DL MCS
+        """
+        cmd = "DLIMCS? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @lte_mcs_dl.setter
+    def lte_mcs_dl(self, mcs_dl):
+        """ Sets the Modulation and Coding scheme (DL) of the LTE cell
+
+        Args:
+            mcs_dl: Modulation and Coding scheme (DL) of the LTE cell
+
+        Returns:
+            None
+        """
+        cmd = "DLIMCS {},{}".format(mcs_dl, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def lte_mcs_ul(self):
+        """ Gets the Modulation and Coding scheme (UL) of the LTE cell
+
+        Args:
+            None
+
+        Returns:
+            UL MCS
+        """
+        cmd = "ULIMCS? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @lte_mcs_ul.setter
+    def lte_mcs_ul(self, mcs_ul):
+        """ Sets the Modulation and Coding scheme (UL) of the LTE cell
+
+        Args:
+            mcs_ul: Modulation and Coding scheme (UL) of the LTE cell
+
+        Returns:
+            None
+        """
+        cmd = "ULIMCS {},{}".format(mcs_ul, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def nrb_dl(self):
+        """ Gets the Downlink N Resource Block of the cell
+
+        Args:
+            None
+
+        Returns:
+            Downlink NRB
+        """
+        cmd = "DLNRB? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @nrb_dl.setter
+    def nrb_dl(self, blocks):
+        """ Sets the Downlink N Resource Block of the cell
+
+        Args:
+            blocks: Downlink N Resource Block of the cell
+
+        Returns:
+            None
+        """
+        cmd = "DLNRB {},{}".format(blocks, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def nrb_ul(self):
+        """ Gets the uplink N Resource Block of the cell
+
+        Args:
+            None
+
+        Returns:
+            uplink NRB
+        """
+        cmd = "ULNRB? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @nrb_ul.setter
+    def nrb_ul(self, blocks):
+        """ Sets the uplink N Resource Block of the cell
+
+        Args:
+            blocks: uplink N Resource Block of the cell
+
+        Returns:
+            None
+        """
+        cmd = "ULNRB {},{}".format(blocks, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def neighbor_cell_mode(self):
+        """ Gets the neighbor cell mode
+
+        Args:
+            None
+
+        Returns:
+            current neighbor cell mode
+        """
+        cmd = "NCLIST? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @neighbor_cell_mode.setter
+    def neighbor_cell_mode(self, mode):
+        """ Sets the neighbor cell mode
+
+        Args:
+            mode: neighbor cell mode , DEFAULT/ USERDATA
+
+        Returns:
+            None
+        """
+        cmd = "NCLIST {},{}".format(mode, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+
+    def get_neighbor_cell_type(self, system, index):
+        """ Gets the neighbor cell type
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell type
+        """
+        cmd = "NCTYPE? {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+
+    def set_neighbor_cell_type(self, system, index, cell_type):
+        """ Sets the neighbor cell type
+
+        Args:
+            system: simulation model of neighbor cell
+                   LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+            cell_type: cell type
+                BTS1, BTS2, BTS3, BTS4,CELLNAME, DISABLE
+
+        Returns:
+            None
+        """
+        cmd = "NCTYPE {},{},{},{}".format(system, index, cell_type, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    def get_neighbor_cell_name(self, system, index):
+        """ Gets the neighbor cell name
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell name
+        """
+        cmd = "NCCELLNAME? {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+
+    def set_neighbor_cell_name(self, system, index, name):
+        """ Sets the neighbor cell name
+
+        Args:
+            system: simulation model of neighbor cell
+                   LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+            name: cell name
+
+        Returns:
+            None
+        """
+        cmd = "NCCELLNAME {},{},{},{}".format(system, index, name, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+
+    def get_neighbor_cell_mcc(self, system, index):
+        """ Gets the neighbor cell mcc
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell mcc
+        """
+        cmd = "NCMCC? {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_mnc(self, system, index):
+        """ Gets the neighbor cell mnc
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell mnc
+        """
+        cmd = "NCMNC? {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_id(self, system, index):
+        """ Gets the neighbor cell id
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell id
+        """
+        cmd = "NCCELLID? {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_tac(self, system, index):
+        """ Gets the neighbor cell tracking area code
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell tracking area code
+        """
+        cmd = "NCTAC? {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_dl_channel(self, system, index):
+        """ Gets the neighbor cell downlink channel
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell tracking downlink channel
+        """
+        cmd = "NCDLCHAN? {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_dl_bandwidth(self, system, index):
+        """ Gets the neighbor cell downlink bandwidth
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell tracking downlink bandwidth
+        """
+        cmd = "NCDLBANDWIDTH {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_pcid(self, system, index):
+        """ Gets the neighbor cell physical cell id
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell physical cell id
+        """
+        cmd = "NCPHYCELLID {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_lac(self, system, index):
+        """ Gets the neighbor cell location area code
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell location area code
+        """
+        cmd = "NCLAC {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    def get_neighbor_cell_rac(self, system, index):
+        """ Gets the neighbor cell routing area code
+
+        Args:
+            system: simulation model of neighbor cell
+                    LTE, WCDMA, TDSCDMA, GSM, CDMA1X,EVDO
+            index: Index of neighbor cell
+
+        Returns:
+            neighbor cell routing area code
+        """
+        cmd = "NCRAC {},{},{}".format(system, index, self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    @property
+    def primary_scrambling_code(self):
+        """ Gets the primary scrambling code for WCDMA cell
+
+        Args:
+            None
+
+        Returns:
+            primary scrambling code
+        """
+        cmd = "PRISCRCODE? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @primary_scrambling_code.setter
+    def primary_scrambling_code(self, psc):
+        """ Sets the primary scrambling code for WCDMA cell
+
+        Args:
+            psc: primary scrambling code
+
+        Returns:
+            None
+        """
+        cmd = "PRISCRCODE {},{}".format(psc, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def tac(self):
+        """ Gets the Tracking Area Code of the LTE cell
+
+        Args:
+            None
+
+        Returns:
+            Tracking Area Code of the LTE cell
+        """
+        cmd = "TAC? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @tac.setter
+    def tac(self, tac):
+        """ Sets the Tracking Area Code of the LTE cell
+
+        Args:
+            tac: Tracking Area Code of the LTE cell
+
+        Returns:
+            None
+        """
+        cmd = "TAC {},{}".format(tac, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def cell(self):
+        """ Gets the current cell for BTS
+
+        Args:
+            None
+
+        Returns:
+            current cell for BTS
+        """
+        cmd = "CELLSEL? {}".format(self._bts_number)
+        return self._anritsu.send_query(cmd)
+
+    @cell.setter
+    def cell(self, cell_name):
+        """ sets the  cell for BTS
+        Args:
+            cell_name: cell name
+
+        Returns:
+            None
+        """
+        cmd = "CELLSEL {},{}".format(self._bts_number, cell_name)
+        return self._anritsu.send_command(cmd)
+
+    @property
+    def gsm_cbch(self):
+        """ Gets the GSM CBCH enable/disable status
+
+        Args:
+            None
+
+        Returns:
+            one of CBCHSetup values
+        """
+        cmd = "CBCHPARAMSETUP? " + self._bts_number
+        return self._anritsu.send_query(cmd)
+
+    @gsm_cbch.setter
+    def gsm_cbch(self, enable):
+        """ Sets the GSM CBCH enable/disable status
+
+        Args:
+            enable: GSM CBCH enable/disable status
+
+        Returns:
+            None
+        """
+        cmd = "CBCHPARAMSETUP {},{}".format(enable.value, self._bts_number)
+        self._anritsu.send_command(cmd)
+
+class _VirtualPhone:
+    '''Class to interact with virtual phone supported by MD8475 '''
+    def __init__(self, anritsu):
+        self._anritsu = anritsu
+
+    @property
+    def id(self):
+        """ Gets the virtual phone ID
+
+        Args:
+            None
+
+        Returns:
+            virtual phone ID
+        """
+        cmd = "VPID? "
+        return self._anritsu.send_query(cmd)
+
+    @id.setter
+    def id(self, phonenumber):
+        """ Sets the virtual phone ID
+
+        Args:
+            phonenumber: virtual phone ID
+
+        Returns:
+            None
+        """
+        cmd = "VPID {}".format(phonenumber)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def id_c2k(self):
+        """ Gets the virtual phone ID for CDMA 1x
+
+        Args:
+            None
+
+        Returns:
+            virtual phone ID
+        """
+        cmd = "VPIDC2K? "
+        return self._anritsu.send_query(cmd)
+
+    @id_c2k.setter
+    def id_c2k(self, phonenumber):
+        """ Sets the virtual phone ID for CDMA 1x
+
+        Args:
+            phonenumber: virtual phone ID
+
+        Returns:
+            None
+        """
+        cmd = "VPIDC2K {}".format(phonenumber)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def auto_answer(self):
+        """ Gets the auto answer status of virtual phone
+
+        Args:
+            None
+
+        Returns:
+            auto answer status, ON/OFF
+        """
+        cmd = "VPAUTOANSWER? "
+        return self._anritsu.send_query(cmd)
+
+    @auto_answer.setter
+    def auto_answer(self, option):
+        """ Sets the auto answer feature
+
+        Args:
+            option: tuple with two items for turning on Auto Answer
+                    (OFF or (ON, timetowait))
+
+        Returns:
+            None
+        """
+        enable = "OFF"
+        time = 5
+
+        try:
+            enable, time = option
+        except ValueError:
+            if enable != "OFF":
+                raise ValueError("Pass a tuple with two items for"
+                                 " Turning on Auto Answer")
+        cmd = "VPAUTOANSWER {},{}".format(enable.value, time)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def calling_mode(self):
+        """ Gets the calling mode of virtual phone
+
+        Args:
+            None
+
+        Returns:
+            calling mode of virtual phone
+        """
+        cmd = "VPCALLINGMODE? "
+        return self._anritsu.send_query(cmd)
+
+    @calling_mode.setter
+    def calling_mode(self, calling_mode):
+        """ Sets the calling mode of virtual phone
+
+        Args:
+            calling_mode: calling mode of virtual phone
+
+        Returns:
+            None
+        """
+        cmd = "VPCALLINGMODE {}".format(calling_mode)
+        self._anritsu.send_command(cmd)
+
+    def set_voice_off_hook(self):
+        """ Set the virtual phone operating mode to Voice Off Hook
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "OPERATEVPHONE 0"
+        return self._anritsu.send_command(cmd)
+
+    def set_voice_on_hook(self):
+        """ Set the virtual phone operating mode to Voice On Hook
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "OPERATEVPHONE 1"
+        return self._anritsu.send_command(cmd)
+
+    def set_video_off_hook(self):
+        """ Set the virtual phone operating mode to Video Off Hook
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "OPERATEVPHONE 2"
+        return self._anritsu.send_command(cmd)
+
+    def set_video_on_hook(self):
+        """ Set the virtual phone operating mode to Video On Hook
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "OPERATEVPHONE 3"
+        return self._anritsu.send_command(cmd)
+
+    def set_call_waiting(self):
+        """ Set the virtual phone operating mode to Call waiting
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        cmd = "OPERATEVPHONE 4"
+        return self._anritsu.send_command(cmd)
+
+    @property
+    def status(self):
+        """ Gets the virtual phone status
+
+        Args:
+            None
+
+        Returns:
+            virtual phone status
+        """
+        cmd = "VPSTAT?"
+        status = self._anritsu.send_query(cmd)
+        return _VP_STATUS[status]
+
+    def sendSms(self, phoneNumber, message):
+        """ Sends the SMS data from Anritsu to UE
+
+        Args:
+            phoneNumber: sender of SMS
+            message: message text
+
+        Returns:
+            None
+        """
+        cmd = ("SENDSMS /?PhoneNumber=001122334455&Sender={}&Text={}"
+               "&DCS=00").format(phoneNumber, AnritsuUtils.gsm_encode(message))
+        return self._anritsu.send_command(cmd)
+
+    def sendSms_c2k(self, phoneNumber, message):
+        """ Sends the SMS data from Anritsu to UE (in CDMA)
+
+        Args:
+            phoneNumber: sender of SMS
+            message: message text
+
+        Returns:
+            None
+        """
+        cmd = ("C2KSENDSMS System=CDMA\&Originating_Address={}\&UserData={}"
+              ).format(phoneNumber, AnritsuUtils.cdma_encode(message))
+        return self._anritsu.send_command(cmd)
+
+    def receiveSms(self):
+        """ Receives SMS messages sent by the UE in an external application
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        return self._anritsu.send_query("RECEIVESMS?")
+
+    def receiveSms_c2k(self):
+        """ Receives SMS messages sent by the UE(in CDMA) in an external application
+
+        Args:
+            None
+
+        Returns:
+            None
+        """
+        return self._anritsu.send_query("C2KRECEIVESMS?")
+
+    def setSmsStatusReport(self, status):
+        """ Set the Status Report value of the SMS
+
+        Args:
+            status: status code
+
+        Returns:
+            None
+        """
+        cmd = "SMSSTATUSREPORT {}".format(status)
+        return self._anritsu.send_command(cmd)
+
+
+class _PacketDataNetwork:
+    '''Class to configure PDN parameters'''
+    def __init__(self, anritsu, pdnnumber):
+        self._pdn_number = pdnnumber
+        self._anritsu = anritsu
+
+    @property
+    def ue_address_iptype(self):
+        """ Gets IP type of UE for particular PDN
+
+        Args:
+            None
+
+        Returns:
+            IP type of UE for particular PDN
+        """
+        cmd = "PDNIPTYPE? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @ue_address_iptype.setter
+    def ue_address_iptype(self, ip_type):
+        """ Set IP type of UE for particular PDN
+
+        Args:
+            ip_type: IP type of UE
+
+        Returns:
+            None
+        """
+        if not isinstance(ip_type, IPAddressType):
+            raise ValueError(' The parameter should be of type "IPAddressType"')
+        cmd = "PDNIPTYPE {},{}".format(self._pdn_number, ip_type.value)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def ue_address_ipv4(self):
+        """ Gets UE IPv4 address
+
+        Args:
+            None
+
+        Returns:
+            UE IPv4 address
+        """
+        cmd = "PDNIPV4? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @ue_address_ipv4.setter
+    def ue_address_ipv4(self, ip_address):
+        """ Set UE IPv4 address
+
+        Args:
+            ip_address: UE IPv4 address
+
+        Returns:
+            None
+        """
+        cmd = "PDNIPV4 {},{}".format(self._pdn_number, ip_address)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def ue_address_ipv6(self):
+        """ Gets UE IPv6 address
+
+        Args:
+            None
+
+        Returns:
+            UE IPv6 address
+        """
+        cmd = "PDNIPV6? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @ue_address_ipv6.setter
+    def ue_address_ipv6(self, ip_address):
+        """ Set UE IPv6 address
+
+        Args:
+            ip_address: UE IPv6 address
+
+        Returns:
+            None
+        """
+        cmd = "PDNIPV6 {},{}".format(self._pdn_number, ip_address)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def primary_dns_address_ipv4(self):
+        """ Gets Primary DNS server IPv4 address
+
+        Args:
+            None
+
+        Returns:
+            Primary DNS server IPv4 address
+        """
+        cmd = "PDNDNSIPV4PRI? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @primary_dns_address_ipv4.setter
+    def primary_dns_address_ipv4(self, ip_address):
+        """ Set Primary DNS server IPv4 address
+
+        Args:
+            ip_address: Primary DNS server IPv4 address
+
+        Returns:
+            None
+        """
+        cmd = "PDNDNSIPV4PRI {},{}".format(self._pdn_number, ip_address)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def secondary_dns_address_ipv4(self):
+        """ Gets secondary DNS server IPv4 address
+
+        Args:
+            None
+
+        Returns:
+            secondary DNS server IPv4 address
+        """
+        cmd = "PDNDNSIPV4SEC? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @secondary_dns_address_ipv4.setter
+    def secondary_dns_address_ipv4(self, ip_address):
+        """ Set secondary DNS server IPv4 address
+
+        Args:
+            ip_address: secondary DNS server IPv4 address
+
+        Returns:
+            None
+        """
+        cmd = "PDNDNSIPV4SEC {},{}".format(self._pdn_number, ip_address)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def dns_address_ipv6(self):
+        """ Gets DNS server IPv6 address
+
+        Args:
+            None
+
+        Returns:
+            DNS server IPv6 address
+        """
+        cmd = "PDNDNSIPV6? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @dns_address_ipv6.setter
+    def dns_address_ipv6(self, ip_address):
+        """ Set DNS server IPv6 address
+
+        Args:
+            ip_address: DNS server IPv6 address
+
+        Returns:
+            None
+        """
+        cmd = "PDNDNSIPV6 {},{}".format(self._pdn_number, ip_address)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def cscf_address_ipv4(self):
+        """ Gets Secondary P-CSCF IPv4 address
+
+        Args:
+            None
+
+        Returns:
+            Secondary P-CSCF IPv4 address
+        """
+        cmd = "PDNPCSCFIPV4? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @cscf_address_ipv4.setter
+    def cscf_address_ipv4(self, ip_address):
+        """ Set Secondary P-CSCF IPv4 address
+
+        Args:
+            ip_address: Secondary P-CSCF IPv4 address
+
+        Returns:
+            None
+        """
+        cmd = "PDNPCSCFIPV4 {},{}".format(self._pdn_number, ip_address)
+        self._anritsu.send_command(cmd)
+
+    @property
+    def cscf_address_ipv6(self):
+        """ Gets P-CSCF IPv6 address
+
+        Args:
+            None
+
+        Returns:
+            P-CSCF IPv6 address
+        """
+        cmd = "PDNPCSCFIPV6? " + self._pdn_number
+        return self._anritsu.send_query(cmd)
+
+    @cscf_address_ipv6.setter
+    def cscf_address_ipv6(self, ip_address):
+        """ Set P-CSCF IPv6 address
+
+        Args:
+            ip_address: P-CSCF IPv6 address
+
+        Returns:
+            None
+        """
+        cmd = "PDNPCSCFIPV6 {},{}".format(self._pdn_number, ip_address)
+        self._anritsu.send_command(cmd)
+
+
+class _TriggerMessage:
+    '''Class to interact with trigger message handling supported by MD8475 '''
+    def __init__(self, anritsu):
+        self._anritsu = anritsu
+
+    def set_reply_type(self, message_id, reply_type):
+        """ Sets the reply type of the trigger information
+
+        Args:
+            message_id: trigger information message Id
+            reply_type: reply type of the trigger information
+
+        Returns:
+            None
+        """
+        if not isinstance(message_id, TriggerMessageIDs):
+            raise ValueError(' The parameter should be of type'
+                             ' "TriggerMessageIDs"')
+        if not isinstance(reply_type, TriggerMessageReply):
+            raise ValueError(' The parameter should be of type'
+                             ' "TriggerMessageReply"')
+
+        cmd = "REJECTTYPE {},{}".format(message_id.value, reply_type.value)
+        self._anritsu.send_command(cmd)
+
+    def set_reject_cause(self, message_id, cause):
+        """ Sets the reject cause of the trigger information
+
+        Args:
+            message_id: trigger information message Id
+            cause: cause for reject
+
+        Returns:
+            None
+        """
+        if not isinstance(message_id, TriggerMessageIDs):
+            raise ValueError(' The parameter should be of type'
+                             ' "TriggerMessageIDs"')
+
+        cmd = "REJECTCAUSE {},{}".format(message_id.value, cause)
+        self._anritsu.send_command(cmd)
diff --git a/acts/framework/acts/controllers/tel/mg3710a.py b/acts/framework/acts/controllers/tel/mg3710a.py
new file mode 100644
index 0000000..31383b2
--- /dev/null
+++ b/acts/framework/acts/controllers/tel/mg3710a.py
@@ -0,0 +1,699 @@
+#!/usr/bin/python3.4
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+#   Copyright 2014- 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.
+
+"""
+Controller interface for Anritsu Signal Generator MG3710A.
+"""
+
+import time
+import socket
+from enum import Enum
+from enum import IntEnum
+
+from . _anritsu_utils import *
+
+TERMINATOR = "\n"
+
+class MG3710A():
+    """Class to communicate with Anritsu Signal Generator MG3710A.
+       This uses GPIB command to interface with Anritsu MG3710A """
+
+    def __init__(self, ip_address, log_handle):
+        self._ipaddr = ip_address
+        self.log = log_handle
+
+        # Open socket connection to Signaling Tester
+        self.log.info("Opening Socket Connection with "
+              "Signal Generator MG3710A ({}) ".format(self._ipaddr))
+        try:
+            self._sock = socket.create_connection((self._ipaddr, 49158),
+                                                  timeout=30)
+            self.send_query("*IDN?", 60)
+            self.log.info("Communication Signal Generator MG3710A OK.")
+            self.log.info("Opened Socket connection to ({})"
+                  "with handle ({})".format(self._ipaddr, self._sock))
+        except socket.timeout:
+            raise AnritsuError("Timeout happened while conencting to"
+                               " Anritsu MG3710A")
+        except socket.error:
+            raise AnritsuError("Socket creation error")
+
+    def disconnect(self):
+        """ Disconnect Signal Generator MG3710A
+
+        Args:
+          None
+
+        Returns:
+            None
+        """
+        self._sock.close()
+
+    def send_query(self, query, sock_timeout=10):
+        """ Sends a Query message to Anritsu MG3710A and return response
+
+        Args:
+            query - Query string
+
+        Returns:
+            query response
+        """
+        self.log.info("--> {}".format(query))
+        querytoSend = (query + TERMINATOR).encode('utf-8')
+        self._sock.settimeout(sock_timeout)
+        try:
+            self._sock.send(querytoSend)
+            result = self._sock.recv(256).rstrip(TERMINATOR.encode('utf-8'))
+            response = result.decode('utf-8')
+            self.log.info('<-- {}'.format(response))
+            return response
+        except socket.timeout:
+            raise AnritsuError("Timeout: Response from Anritsu")
+        except socket.error:
+            raise AnritsuError("Socket Error")
+
+    def send_command(self, command, sock_timeout=30):
+        """ Sends a Command message to Anritsu MG3710A
+
+        Args:
+            command - command string
+
+        Returns:
+            None
+        """
+        self.log.info("--> {}".format(command))
+        cmdToSend = (command + TERMINATOR).encode('utf-8')
+        self._sock.settimeout(sock_timeout)
+        try:
+            self._sock.send(cmdToSend)
+            # check operation status
+            status = self.send_query("*OPC?")
+            if int(status) != OPERATION_COMPLETE:
+                raise AnritsuError("Operation not completed")
+        except socket.timeout:
+            raise AnritsuError("Timeout for Command Response from Anritsu")
+        except socket.error:
+            raise AnritsuError("Socket Error for Anritsu command")
+        return
+
+    @property
+    def sg(self):
+        """ Gets current selected signal generator(SG)
+
+        Args:
+            None
+
+        Returns:
+            selected signal generatr number
+        """
+        return self.send_query("PORT?")
+
+    @sg.setter
+    def sg(self, sg_number):
+        """ Selects the signal generator to be controlled
+
+        Args:
+            sg_number: sg number 1 | 2
+
+        Returns:
+            None
+        """
+        cmd = "PORT {}".format(sg_number)
+        self.send_command(cmd)
+
+    def get_modulation_state(self, sg=1):
+        """ Gets the RF signal modulation state (ON/OFF) of signal generator
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            modulation state . 0 (OFF) | 1(ON)
+        """
+        return self.send_query("OUTP{}:MOD?".format(sg))
+
+    def set_modulation_state(self, state, sg=1):
+        """ Sets the RF signal modulation state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            state : ON/OFF
+
+        Returns:
+            None
+        """
+        cmd = "OUTP{}:MOD {}".format(sg, state)
+        self.send_command(cmd)
+
+    def get_rf_output_state(self, sg=1):
+        """ Gets RF signal output state (ON/OFF) of signal generator
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            RF signal output state . 0 (OFF) | 1(ON)
+        """
+        return self.send_query("OUTP{}?".format(sg))
+
+    def set_rf_output_state(self, state, sg=1):
+        """ Sets the RF signal output state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            state : ON/OFF
+
+        Returns:
+            None
+        """
+        cmd = "OUTP{} {}".format(sg, state)
+        self.send_command(cmd)
+
+    def get_frequency(self, sg=1):
+        """ Gets the selected frequency of signal generator
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            selected frequency
+        """
+        return self.send_query("SOUR{}:FREQ?".format(sg))
+
+    def set_frequency(self, freq, sg=1):
+        """ Sets the frequency of signal generator
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            freq : frequency
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ {}".format(sg, freq)
+        self.send_command(cmd)
+
+    def get_frequency_offset_state(self, sg=1):
+        """ Gets the Frequency Offset enable state (ON/OFF) of signal generator
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            Frequency Offset enable state . 0 (OFF) | 1(ON)
+        """
+        return self.send_query("SOUR{}:FREQ:OFFS:STAT?".format(sg))
+
+    def set_frequency_offset_state(self, state, sg=1):
+        """ Sets the Frequency Offset enable state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            state : enable state, ON/OFF
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:OFFS:STAT {}".format(sg, state)
+        self.send_command(cmd)
+
+    def get_frequency_offset(self, sg=1):
+        """ Gets the current frequency offset value
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            current frequency offset value
+        """
+        return self.send_query("SOUR{}:FREQ:OFFS?".format(sg))
+
+    def set_frequency_offset(self, offset, sg=1):
+        """ Sets the frequency offset value
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            offset : frequency offset value
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:OFFS {}".format(sg, offset)
+        self.send_command(cmd)
+
+    def get_frequency_offset_multiplier_state(self, sg=1):
+        """ Gets the Frequency Offset multiplier enable state (ON/OFF) of
+            signal generator
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            Frequency Offset  multiplier enable state . 0 (OFF) | 1(ON)
+        """
+        return self.send_query("SOUR{}:FREQ:MULT:STAT?".format(sg))
+
+    def set_frequency_offset_multiplier_state(self, state, sg=1):
+        """ Sets the  Frequency Offset multiplier enable state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            state : enable state, ON/OFF
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:MULT:STAT {}".format(sg, state)
+        self.send_command(cmd)
+
+    def get_frequency_offset_multiplier(self, sg=1):
+        """ Gets the current frequency offset multiplier value
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            frequency offset multiplier value
+        """
+        return self.send_query("SOUR{}:FREQ:MULT?".format(sg))
+
+    def set_frequency_offset_multiplier(self, multiplier, sg=1):
+        """ Sets the frequency offset multiplier value
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            multiplier : frequency offset multiplier value
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:MULT {}".format(sg, multiplier)
+        self.send_command(cmd)
+
+    def get_channel(self, sg=1):
+        """ Gets the current channel number
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            current channel number
+        """
+        return self.send_query("SOUR{}:FREQ:CHAN:NUMB?".format(sg))
+
+    def set_channel(self, channel, sg=1):
+        """ Sets the channel number
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            channel : channel number
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:CHAN:NUMB {}".format(sg, channel)
+        self.send_command(cmd)
+
+    def get_channel_group(self, sg=1):
+        """ Gets the current channel group number
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            current channel group number
+        """
+        return self.send_query("SOUR{}:FREQ:CHAN:GRO?".format(sg))
+
+    def set_channel_group(self, group, sg=1):
+        """ Sets the channel group number
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            group : channel group number
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:CHAN:GRO {}".format(sg, group)
+        self.send_command(cmd)
+
+    def get_rf_output_level(self, sg=1):
+        """ Gets the current RF output level
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            current RF output level
+        """
+        return self.send_query("SOUR{}:POW:CURR?".format(sg))
+
+    def get_output_level_unit(self, sg=1):
+        """ Gets the current RF output level unit
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            current RF output level unit
+        """
+        return self.send_query("UNIT{}:POW?".format(sg))
+
+    def set_output_level_unit(self, unit, sg=1):
+        """ Sets the RF output level unit
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            unit : Output level unit
+
+        Returns:
+            None
+        """
+        cmd = "UNIT{}:POW {}".format(sg, unit)
+        self.send_command(cmd)
+
+    def get_output_level(self, sg=1):
+        """ Gets the Output level
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            Output level
+        """
+        return self.send_query("SOUR{}:POW?".format(sg))
+
+    def set_output_level(self, level, sg=1):
+        """ Sets the Output level
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            level : Output level
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:POW {}".format(sg, level)
+        self.send_command(cmd)
+
+    def get_arb_state(self, sg=1):
+        """ Gets the ARB function state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            ARB function state . 0 (OFF) | 1(ON)
+        """
+        return self.send_query("SOUR{}:RAD:ARB?".format(sg))
+
+    def set_arb_state(self, state, sg=1):
+        """ Sets the ARB function state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            state : enable state (ON/OFF)
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB {}".format(sg, state)
+        self.send_command(cmd)
+
+    def restart_arb_waveform_pattern(self, sg=1):
+        """ playback the waveform pattern from the beginning.
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB:WAV:REST".format(sg)
+        self.send_command(cmd)
+
+    def load_waveform(self, package_name, pattern_name, memory, sg=1):
+        """ loads the waveform from HDD to specified memory
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            package_name : Package name of signal
+            pattern_name : Pattern name of signal
+            memory: memory for the signal - "A" or "B"
+
+        Returns:
+            None
+        """
+        cmd = "MMEM{}:LOAD:WAV:WM{} '{}','{}'".format(sg, memory, package_name,
+                                                      pattern_name )
+        self.send_command(cmd)
+
+    def select_waveform(self, package_name, pattern_name, memory, sg=1):
+        """ Selects the waveform to output on specified memory
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            package_name : Package name of signal
+            pattern_name : Pattern name of signal
+            memory: memory for the signal - "A" or "B"
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB:WM{}:WAV '{}','{}'".format(sg, memory, package_name,
+                                                         pattern_name )
+        self.send_command(cmd)
+
+    def get_freq_relative_display_status(self, sg=1):
+        """ Gets the frequency relative display status
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            frequency relative display status.   0 (OFF) | 1(ON)
+        """
+        return self.send_query("SOUR{}:FREQ:REF:STAT?".format(sg))
+
+    def set_freq_relative_display_status(self, enable, sg=1):
+        """ Sets frequency relative display status
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            enable : enable type (ON/OFF)
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:REF:STAT {}".format(sg, enable)
+        self.send_command(cmd)
+
+    def get_freq_channel_display_type(self, sg=1):
+        """ Gets the selected type(frequency/channel) for input display
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            selected type(frequecy/channel) for input display
+        """
+        return self.send_query("SOUR{}:FREQ:TYPE?".format(sg))
+
+    def set_freq_channel_display_type(self, freq_channel, sg=1):
+        """ Sets thes type(frequency/channel) for input display
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            freq_channel : display type (frequency/channel)
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:FREQ:TYPE {}".format(sg, freq_channel)
+        self.send_command(cmd)
+
+    def get_arb_combination_mode(self, sg=1):
+        """ Gets the current mode to generate the pattern
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            current mode to generate the pattern
+        """
+        return self.send_query("SOUR{}:RAD:ARB:PCOM?".format(sg))
+
+    def set_arb_combination_mode(self, mode, sg=1):
+        """ Sets the mode to generate the pattern
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            mode : pattern generation mode
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB:PCOM {}".format(sg, mode)
+        self.send_command(cmd)
+
+    def get_arb_pattern_aorb_state(self, a_or_b, sg=1):
+        """ Gets the Pattern A/B output state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            a_or_b : Patten A or Pattern B( "A" or "B")
+
+        Returns:
+            Pattern A/B output state . 0(OFF) | 1(ON)
+        """
+        return self.send_query("SOUR{}:RAD:ARB:WM{}:OUTP?".format(a_or_b, sg))
+
+    def set_arb_pattern_aorb_state(self, a_or_b, state, sg=1):
+        """ Sets the Pattern A/B output state
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            a_or_b : Patten A or Pattern B( "A" or "B")
+            state : output state
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB:WM{}:OUTP {}".format(sg, a_or_b, state)
+        self.send_command(cmd)
+
+    def get_arb_level_aorb(self, a_or_b, sg=1):
+        """ Gets the Pattern A/B output level
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            a_or_b : Patten A or Pattern B( "A" or "B")
+
+        Returns:
+             Pattern A/B output level
+        """
+        return self.send_query("SOUR{}:RAD:ARB:WM{}:POW?".format(sg, a_or_b))
+
+    def set_arb_level_aorb(self, a_or_b, level, sg=1):
+        """ Sets the Pattern A/B output level
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            a_or_b : Patten A or Pattern B( "A" or "B")
+            level : output level
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB:WM{}:POW {}".format(sg, a_or_b, level)
+        self.send_command(cmd)
+
+    def get_arb_freq_offset(self, sg=1):
+        """ Gets the frequency offset between Pattern A and Patten B
+            when CenterSignal is A or B.
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            frequency offset between Pattern A and Patten B
+        """
+        return self.send_query("SOUR{}:RAD:ARB:FREQ:OFFS?".format(sg))
+
+    def set_arb_freq_offset(self,  offset, sg=1):
+        """ Sets the frequency offset between Pattern A and Patten B when
+            CenterSignal is A or B.
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            offset : frequency offset
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB:FREQ:OFFS {}".format(sg, offset)
+        self.send_command(cmd)
+
+    def get_arb_freq_offset_aorb(self, sg=1):
+        """ Gets the frequency offset of Pattern A/Pattern B based on Baseband
+            center frequency
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+
+        Returns:
+            frequency offset
+        """
+        return self.send_query("SOUR{}:RAD:ARB:WM{}:FREQ:OFFS?".format(sg, a_or_b))
+
+    def set_arb_freq_offset_aorb(self, a_or_b, offset, sg=1):
+        """ Sets the frequency offset of Pattern A/Pattern B based on Baseband
+            center frequency
+
+        Args:
+            sg: signal generator number.
+                Default is 1
+            a_or_b : Patten A or Pattern B( "A" or "B")
+            offset : frequency offset
+
+        Returns:
+            None
+        """
+        cmd = "SOUR{}:RAD:ARB:WM{}:FREQ:OFFS {}".format(sg, a_or_b, offset)
+        self.send_command(cmd)
diff --git a/acts/framework/acts/event_dispatcher.py b/acts/framework/acts/event_dispatcher.py
new file mode 100644
index 0000000..ca9f8b4
--- /dev/null
+++ b/acts/framework/acts/event_dispatcher.py
@@ -0,0 +1,423 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014- 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(60000)
+            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/acts/framework/acts/jsonrpc.py b/acts/framework/acts/jsonrpc.py
new file mode 100644
index 0000000..5c0ddc9
--- /dev/null
+++ b/acts/framework/acts/jsonrpc.py
@@ -0,0 +1,119 @@
+#!/usr/bin/python3.4
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+#   Copyright 2014- 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.
+
+"""
+A simple JSON RPC client.
+"""
+import json
+import time
+from urllib import request
+
+class HTTPError(Exception):
+    pass
+
+class RemoteError(Exception):
+    pass
+
+def JSONCounter():
+    """A counter that generates JSON RPC call IDs.
+
+    Follows the increasing integer sequence. Every time this function is
+    called, the next number in the sequence is returned.
+    """
+    i = 0
+    while True:
+        yield i
+        i += 1
+
+class JSONRPCClient:
+    COUNTER = JSONCounter()
+    headers = {'content-type': 'application/json'}
+    def __init__(self, baseurl):
+        self._baseurl = baseurl
+
+    def call(self, path, methodname=None, *args):
+        """Wrapper for the internal _call method.
+
+        A retry is performed if the initial call fails to compensate for
+        unstable networks.
+
+        Params:
+            path: Path of the rpc service to be appended to the base url.
+            methodname: Method name of the RPC call.
+            args: A tuple of arguments for the RPC call.
+
+        Returns:
+            The returned message of the JSON RPC call from the server.
+        """
+        try:
+            return self._call(path, methodname, *args)
+        except:
+            # Take five and try again
+            time.sleep(5)
+            return self._call(path, methodname, *args)
+
+    def _post_json(self, url, payload):
+        """Performs an HTTP POST request with a JSON payload.
+
+        Params:
+            url: The full URL to post the payload to.
+            payload: A JSON string to be posted to server.
+
+        Returns:
+            The HTTP response code and text.
+        """
+        req = request.Request(url)
+        req.add_header('Content-Type', 'application/json')
+        resp = request.urlopen(req, data=payload.encode("utf-8"))
+        txt = resp.read()
+        return resp.code, txt.decode('utf-8')
+
+    def _call(self, path, methodname=None, *args):
+        """Performs a JSON RPC call and return the response.
+
+        Params:
+            path: Path of the rpc service to be appended to the base url.
+            methodname: Method name of the RPC call.
+            args: A tuple of arguments for the RPC call.
+
+        Returns:
+            The returned message of the JSON RPC call from the server.
+
+        Raises:
+            HTTPError: Raised if the http post return code is not 200.
+            RemoteError: Raised if server returned an error.
+        """
+        jsonid = next(JSONRPCClient.COUNTER)
+        payload = json.dumps({"method": methodname,
+                              "params": args,
+                              "id": jsonid})
+        url = self._baseurl + path
+        status_code, text = self._post_json(url, payload)
+        if status_code != 200:
+            raise HTTPError(text)
+        r = json.loads(text)
+        if r['error']:
+            raise RemoteError(r['error'])
+        return r['result']
+
+    def sys(self, *args):
+        return self.call("sys", *args)
+
+    def __getattr__(self, name):
+        def rpc_call(*args):
+            return self.call('uci', name, *args)
+        return rpc_call
\ No newline at end of file
diff --git a/acts/framework/acts/keys.py b/acts/framework/acts/keys.py
new file mode 100644
index 0000000..698a9f3
--- /dev/null
+++ b/acts/framework/acts/keys.py
@@ -0,0 +1,104 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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 enum
+
+"""This module has the global key values that are used across framework
+modules.
+"""
+class Config(enum.Enum):
+    """Enum values for test config related lookups.
+    """
+    # Keys used to look up values from test config files.
+    # These keys define the wording of test configs and their internal
+    # references.
+    key_log_path = "logpath"
+    key_testbed = "testbed"
+    key_testbed_name = "name"
+    key_test_paths = "testpaths"
+    key_android_device = "AndroidDevice"
+    key_access_point = "AP"
+    key_attenuator = "Attenuator"
+    key_port = "Port"
+    key_address = "Address"
+    key_iperf_server = "IPerfServer"
+    key_monsoon = "Monsoon"
+    key_adb_log_time_offset = "adb_log_time_offset"
+    key_adb_logcat_param = "adb_logcat_param"
+    # Internal keys, used internally, not exposed to user's config files.
+    ikey_lock = "lock"
+    ikey_user_param = "user_params"
+    ikey_android_device = "android_devices"
+    ikey_access_point = "access_points"
+    ikey_attenuator = "attenuators"
+    ikey_testbed_name = "testbed_name"
+    ikey_logger = "log"
+    ikey_logpath = "log_path"
+    ikey_monsoon = "monsoons"
+    ikey_reporter = "reporter"
+    ikey_adb_log_path = "adb_logcat_path"
+    ikey_adb_log_files = "adb_logcat_files"
+    ikey_iperf_server = "iperf_servers"
+    ikey_cli_args = "cli_args"
+    # module name of controllers
+    m_key_monsoon = "monsoon"
+    m_key_android_device = "android_device"
+    m_key_access_point = "access_point"
+    m_key_attenuator = "attenuator"
+    m_key_iperf_server = "iperf_server"
+
+    # A list of keys whose values in configs should not be passed to test
+    # classes without unpacking first.
+    reserved_keys = (key_testbed, key_log_path, key_test_paths)
+
+    controller_names = [
+        key_android_device,
+        key_access_point,
+        key_attenuator,
+        key_iperf_server,
+        key_monsoon
+    ]
+    tb_config_reserved_keys = controller_names + [key_testbed_name]
+
+def get_name_by_value(value):
+    for name, member in Config.__members__.items():
+        if member.value == value:
+            return name
+    return None
+
+def get_internal_value(external_value):
+    """Translates the value of an external key to the value of its
+    corresponding internal key.
+    """
+    return value_to_value(external_value, "i%s")
+
+def get_module_name(name_in_config):
+    """Translates the name of a controller in config file to its module name.
+    """
+    return value_to_value(name_in_config, "m_%s")
+
+def value_to_value(ref_value, pattern):
+    """Translates the value of a key to the value of its corresponding key. The
+    corresponding key is chosen based on the variable name pattern.
+    """
+    ref_key_name = get_name_by_value(ref_value)
+    if not ref_key_name:
+        return None
+    target_key_name = pattern % ref_key_name
+    try:
+        return getattr(Config, target_key_name).value
+    except AttributeError:
+        return None
\ No newline at end of file
diff --git a/acts/framework/acts/logger.py b/acts/framework/acts/logger.py
new file mode 100755
index 0000000..00d828f
--- /dev/null
+++ b/acts/framework/acts/logger.py
@@ -0,0 +1,253 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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 datetime
+import logging
+import os
+import re
+import sys
+
+from acts.utils import create_dir
+
+log_line_format = "%(asctime)s.%(msecs).03d %(levelname)s %(message)s"
+# The micro seconds are added by the format string above,
+# so the time format does not include ms.
+log_line_time_format = "%m-%d %H:%M:%S"
+log_line_timestamp_len = 18
+
+logline_timestamp_re = re.compile("\d\d-\d\d \d\d:\d\d:\d\d.\d\d\d")
+
+
+def _parse_logline_timestamp(t):
+    """Parses a logline timestamp into a tuple.
+
+    Args:
+        t: Timestamp in logline format.
+
+    Returns:
+        An iterable of date and time elements in the order of month, day, hour,
+        minute, second, microsecond.
+    """
+    date, time = t.split(' ')
+    month, day = date.split('-')
+    h, m, s = time.split(':')
+    s, ms = s.split('.')
+    return (month, day, h, m, s, ms)
+
+def is_valid_logline_timestamp(timestamp):
+    if len(timestamp) == log_line_timestamp_len:
+        if logline_timestamp_re.match(timestamp):
+            return True
+    return False
+
+def logline_timestamp_comparator(t1, t2):
+    """Comparator for timestamps in logline format.
+
+    Args:
+        t1: Timestamp in logline format.
+        t2: Timestamp in logline format.
+
+    Returns:
+        -1 if t1 < t2; 1 if t1 > t2; 0 if t1 == t2.
+    """
+    dt1 = _parse_logline_timestamp(t1)
+    dt2 = _parse_logline_timestamp(t2)
+    for u1, u2 in zip(dt1, dt2):
+        if u1 < u2:
+            return -1
+        elif u1 > u2:
+            return 1
+    return 0
+
+def _get_timestamp(time_format, delta=None):
+    t = datetime.datetime.now()
+    if delta:
+        t = t + datetime.timedelta(seconds=delta)
+    return t.strftime(time_format)[:-3]
+
+def epoch_to_log_line_timestamp(epoch_time):
+    d = datetime.datetime.fromtimestamp(epoch_time / 1000)
+    return d.strftime("%m-%d %H:%M:%S.%f")[:-3]
+
+def get_log_line_timestamp(delta=None):
+    """Returns a timestamp in the format used by log lines.
+
+    Default is current time. If a delta is set, the return value will be
+    the current time offset by delta seconds.
+
+    Args:
+        delta: Number of seconds to offset from current time; can be negative.
+
+    Returns:
+        A timestamp in log line format with an offset.
+    """
+    return _get_timestamp("%m-%d %H:%M:%S.%f", delta)
+
+def get_log_file_timestamp(delta=None):
+    """Returns a timestamp in the format used for log file names.
+
+    Default is current time. If a delta is set, the return value will be
+    the current time offset by delta seconds.
+
+    Args:
+        delta: Number of seconds to offset from current time; can be negative.
+
+    Returns:
+        A timestamp in log filen name format with an offset.
+    """
+    return _get_timestamp("%m-%d-%Y_%H-%M-%S-%f", delta)
+
+def get_test_logger(log_path, TAG, prefix=None, filename=None):
+    """Returns a logger object used for tests.
+
+    The logger object has a stream handler and a file handler. The stream
+    handler logs INFO level to the terminal, the file handler logs DEBUG
+    level to files.
+
+    Args:
+        log_path: Location of the log file.
+        TAG: Name of the logger's owner.
+        prefix: A prefix for each log line in terminal.
+        filename: Name of the log file. The default is the time the logger
+            is requested.
+
+    Returns:
+        A logger configured with one stream handler and one file handler
+    """
+    log = logging.getLogger(TAG)
+    if log.handlers:
+        # This logger has been requested before.
+        return log
+    log.propagate = False
+    log.setLevel(logging.DEBUG)
+    # Log info to stream
+    terminal_format = log_line_format
+    if prefix:
+        terminal_format = "[{}] {}".format(prefix, log_line_format)
+    c_formatter = logging.Formatter(terminal_format, log_line_time_format)
+    ch = logging.StreamHandler(sys.stdout)
+    ch.setFormatter(c_formatter)
+    ch.setLevel(logging.INFO)
+    # Log everything to file
+    f_formatter = logging.Formatter(log_line_format, log_line_time_format)
+    # All the logs of this test class go into one directory
+    if filename is None:
+        filename = get_log_file_timestamp()
+        create_dir(log_path)
+    fh = logging.FileHandler(os.path.join(log_path, 'test_run_details.txt'))
+    fh.setFormatter(f_formatter)
+    fh.setLevel(logging.DEBUG)
+    log.addHandler(ch)
+    log.addHandler(fh)
+    return log
+
+def kill_test_logger(logger):
+    """Cleans up a test logger object created by get_test_logger.
+
+    Args:
+        logger: The logging object to clean up.
+    """
+    for h in list(logger.handlers):
+        logger.removeHandler(h)
+        if isinstance(h, logging.FileHandler):
+            h.close()
+
+def get_test_reporter(log_path):
+    """Returns a file object used for reports.
+
+    Args:
+        log_path: Location of the report file.
+
+    Returns:
+        A file object.
+    """
+    create_dir(log_path)
+    f = open(os.path.join(log_path, 'test_run_summary.txt'), 'w')
+    return f
+
+def kill_test_reporter(reporter):
+    """Cleans up a test reporter object created by get_test_reporter.
+
+    Args:
+        reporter: The reporter file object to clean up.
+    """
+    reporter.close()
+
+def create_latest_log_alias(actual_path):
+    """Creates a symlink to the latest test run logs.
+
+    Args:
+        actual_path: The source directory where the latest test run's logs are.
+    """
+    link_path = os.path.join(os.path.dirname(actual_path), "latest")
+    if os.path.islink(link_path):
+        os.remove(link_path)
+    os.symlink(actual_path, link_path)
+
+def get_test_logger_and_reporter(log_path, TAG, prefix=None, filename=None):
+    """Returns a logger and a reporter of the same name.
+
+    Args:
+        log_path: Location of the report file.
+        TAG: Name of the logger's owner.
+        prefix: A prefix for each log line in terminal.
+        filename: Name of the files. The default is the time the objects
+            are requested.
+
+    Returns:
+        A log object and a reporter object.
+    """
+    if filename is None:
+        filename = get_log_file_timestamp()
+    create_dir(log_path)
+    logger = get_test_logger(log_path, TAG, prefix, filename)
+    reporter = get_test_reporter(log_path)
+    create_latest_log_alias(log_path)
+    return logger, reporter, filename
+
+def normalize_log_line_timestamp(log_line_timestamp):
+    """Replace special characters in log line timestamp with normal characters.
+
+    Args:
+        log_line_timestamp: A string in the log line timestamp format. Obtained
+            with get_log_line_timestamp.
+
+    Returns:
+        A string representing the same time as input timestamp, but without
+        special characters.
+    """
+    norm_tp = log_line_timestamp.replace(' ', '_')
+    norm_tp = norm_tp.replace(':', '-')
+    return norm_tp
+
+class LoggerProxy(object):
+    """This class is for situations where a logger may or may not exist.
+
+    e.g. In controller classes, sometimes we don't have a logger to pass in,
+    like during a quick try in python console. In these cases, we don't want to
+    crash on the log lines because logger is None, so we should set self.log to
+    an object of this class in the controller classes, instead of the actual
+    logger object.
+    """
+    def __init__(self, logger=None):
+        self.log = logger
+
+    def __getattr__(self, name):
+        def log_call(*args):
+            if self.log:
+                return getattr(self.log, name)(*args)
+            print(*args)
+        return log_call
\ No newline at end of file
diff --git a/acts/framework/acts/monsoon.py b/acts/framework/acts/monsoon.py
new file mode 100755
index 0000000..6896648
--- /dev/null
+++ b/acts/framework/acts/monsoon.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2015 - 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.
+
+"""Interface for a USB-connected Monsoon power meter
+(http://msoon.com/LabEquipment/PowerMonitor/).
+"""
+
+_Python3_author_ = 'angli@google.com (Ang Li)'
+_author_ = 'kens@google.com (Ken Shirriff)'
+
+import argparse
+import sys
+import time
+import collections
+
+from acts.controllers.monsoon import Monsoon
+
+def main(FLAGS):
+    """Simple command-line interface for Monsoon."""
+    if FLAGS.avg and FLAGS.avg < 0:
+        print("--avg must be greater than 0")
+        return
+
+    mon = Monsoon(serial=int(FLAGS.serialno[0]))
+
+    if FLAGS.voltage is not None:
+        mon.set_voltage(FLAGS.voltage)
+
+    if FLAGS.current is not None:
+        mon.set_max_current(FLAGS.current)
+
+    if FLAGS.status:
+        items = sorted(mon.status.items())
+        print("\n".join(["%s: %s" % item for item in items]))
+
+    if FLAGS.usbpassthrough:
+        mon.usb(FLAGS.usbpassthrough)
+
+    if FLAGS.startcurrent is not None:
+         mon.set_max_init_current(FLAGS.startcurrent)
+
+    if FLAGS.samples:
+        # Have to sleep a bit here for monsoon to be ready to lower the rate of
+        # socket read timeout.
+        time.sleep(1)
+        result = mon.take_samples(FLAGS.hz, FLAGS.samples,
+            sample_offset=FLAGS.offset, live=True)
+        print(repr(result))
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(description=("This is a python utility "
+                 "tool to control monsoon power measurement boxes."))
+    parser.add_argument("--status", action="store_true",
+        help="Print power meter status.")
+    parser.add_argument("-avg", "--avg", type=int, default=0,
+        help="Also report average over last n data points.")
+    parser.add_argument("-v", "--voltage", type=float,
+        help="Set output voltage (0 for off)")
+    parser.add_argument("-c", "--current", type=float,
+        help="Set max output current.")
+    parser.add_argument("-sc", "--startcurrent", type=float,
+        help="Set max power-up/inital current.")
+    parser.add_argument("-usb", "--usbpassthrough", choices=("on", "off",
+        "auto"), help="USB control (on, off, auto).")
+    parser.add_argument("-sp", "--samples", type=int,
+        help="Collect and print this many samples")
+    parser.add_argument("-hz", "--hz", type=int,
+        help="Sample this many times per second.")
+    parser.add_argument("-d", "--device", help="Use this /dev/ttyACM... file.")
+    parser.add_argument("-sn", "--serialno", type=int, nargs=1, required=True,
+        help="The serial number of the Monsoon to use.")
+    parser.add_argument("--offset", type=int, nargs='?', default=0,
+        help="The number of samples to discard when calculating average.")
+    parser.add_argument("-r", "--ramp", action="store_true", help=("Gradually "
+        "increase voltage to prevent tripping Monsoon overvoltage"))
+    args = parser.parse_args()
+    main(args)
diff --git a/acts/framework/acts/records.py b/acts/framework/acts/records.py
new file mode 100644
index 0000000..b750e63
--- /dev/null
+++ b/acts/framework/acts/records.py
@@ -0,0 +1,294 @@
+#!/usr/bin/python3.4
+#
+# Copyright 2015 - 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.
+
+
+"""This module is where all the record definitions and record containers live.
+"""
+
+import json
+import pprint
+
+from acts.signals import TestSignal
+from acts.utils import epoch_to_human_time
+from acts.utils import get_current_epoch_time
+
+
+class TestResultEnums(object):
+    """Enums used for TestResultRecord class.
+
+    Includes the tokens to mark test result with, and the string names for each
+    field in TestResultRecord.
+    """
+
+    RECORD_NAME = "Test Name"
+    RECORD_BEGIN_TIME = "Begin Time"
+    RECORD_END_TIME = "End Time"
+    RECORD_RESULT = "Result"
+    RECORD_UID = "UID"
+    RECORD_EXTRAS = "Extras"
+    RECORD_DETAILS = "Details"
+    TEST_RESULT_PASS = "PASS"
+    TEST_RESULT_FAIL = "FAIL"
+    TEST_RESULT_SKIP = "SKIP"
+
+class TestResultRecord(object):
+    """A record that holds the information of a test case execution.
+
+    Attributes:
+        test_name: A string representing the name of the test case.
+        begin_time: Epoch timestamp of when the test case started.
+        end_time: Epoch timestamp of when the test case ended.
+        self.uid: Unique identifier of a test case.
+        self.result: Test result, PASS/FAIL/SKIP.
+        self.extras: User defined extra information of the test result.
+        self.details: A string explaining the details of the test case.
+    """
+
+    def __init__(self, t_name):
+        self.test_name = t_name
+        self.begin_time = None
+        self.end_time = None
+        self.uid = None
+        self.result = None
+        self.extras = None
+        self.details = None
+
+    def test_begin(self):
+        """Call this when the test case it records begins execution.
+
+        Sets the begin_time of this record.
+        """
+        self.begin_time = get_current_epoch_time()
+
+    def _test_end(self, result, e):
+        """Class internal function to signal the end of a test case execution.
+
+        Args:
+            result: One of the TEST_RESULT enums in TestResultEnums.
+            e: A test termination signal (usually an exception object). It can
+                be any exception instance or of any subclass of
+                base_test._TestSignal.
+        """
+        self.end_time = get_current_epoch_time()
+        self.result = result
+        if isinstance(e, TestSignal):
+            self.details = e.details
+            self.extras = e.extras
+        else:
+            self.details = str(e)
+
+    def test_pass(self, e=None):
+        """To mark the test as passed in this record.
+
+        Args:
+            e: An instance of acts.signals.TestPass.
+        """
+        self._test_end(TestResultEnums.TEST_RESULT_PASS, e)
+
+    def test_fail(self, e=None):
+        """To mark the test as failed in this record.
+
+        Only test_fail does instance check because we want "assert xxx" to also
+        fail the test same way assert_true does.
+
+        Args:
+            e: An exception object. It can be an instance of AssertionError or
+                acts.base_test.TestFailure.
+        """
+        self._test_end(TestResultEnums.TEST_RESULT_FAIL, e)
+
+    def test_skip(self, e=None):
+        """To mark the test as skipped in this record.
+
+        Args:
+            e: An instance of acts.signals.TestSkip.
+        """
+        self._test_end(TestResultEnums.TEST_RESULT_SKIP, e)
+
+    def __str__(self):
+        d = self.to_dict()
+        l = ["%s = %s" % (k, v) for k, v in d.items()]
+        s = ', '.join(l)
+        return s
+
+    def __repr__(self):
+        """This returns a short string representation of the test record."""
+        t = epoch_to_human_time(self.begin_time)
+        return "%s %s %s" % (t, self.test_name, self.result)
+
+    def to_dict(self):
+        """Gets a dictionary representating the content of this class.
+
+        Returns:
+            A dictionary representating the content of this class.
+        """
+        d = {}
+        d[TestResultEnums.RECORD_NAME] = self.test_name
+        d[TestResultEnums.RECORD_BEGIN_TIME] = self.begin_time
+        d[TestResultEnums.RECORD_END_TIME] = self.end_time
+        d[TestResultEnums.RECORD_RESULT] = self.result
+        d[TestResultEnums.RECORD_UID] = self.uid
+        d[TestResultEnums.RECORD_EXTRAS] = self.extras
+        d[TestResultEnums.RECORD_DETAILS] = self.details
+        return d
+
+    def json_str(self):
+        """Converts this test record to a string in json format.
+
+        Format of the json string is:
+            {
+                'Test Name': <test name>,
+                'Begin Time': <epoch timestamp>,
+                'Details': <details>,
+                ...
+            }
+
+        Returns:
+            A json-format string representing the test record.
+        """
+        return json.dumps(self.to_dict())
+
+class TestResult(object):
+    """A class that contains metrics of a test run.
+
+    This class is essentially a container of TestResultRecord objects.
+
+    Attributes:
+        self.requested: A list of strings, each is the name of a test requested
+            by user.
+        self.failed: A list of records for tests failed.
+        self.executed: A list of records for tests that were actually executed.
+        self.passed: A list of records for tests passed.
+        self.skipped: A list of records for tests skipped.
+        self.unknown: A list of records for tests with unknown result token.
+    """
+
+    def __init__(self):
+        self.requested = []
+        self.failed = []
+        self.executed = []
+        self.passed = []
+        self.skipped = []
+        self.unknown = []
+
+    def __add__(self, r):
+        """Overrides '+' operator for TestResult class.
+
+        The add operator merges two TestResult objects by concatenating all of
+        their lists together.
+
+        Args:
+            r: another instance of TestResult to be added
+
+        Returns:
+            A TestResult instance that's the sum of two TestResult instances.
+        """
+        assert isinstance(r, TestResult)
+        sum_result = TestResult()
+        for name in sum_result.__dict__:
+            l_value = list(getattr(self, name))
+            r_value = list(getattr(r, name))
+            setattr(sum_result, name, l_value + r_value)
+        return sum_result
+
+    def add_record(self, record):
+        """Adds a test record to test result.
+
+        A record is considered executed once it's added to the test result.
+
+        Args:
+            record: A test record object to add.
+        """
+        self.executed.append(record)
+        if record.result == TestResultEnums.TEST_RESULT_FAIL:
+            self.failed.append(record)
+        elif record.result == TestResultEnums.TEST_RESULT_SKIP:
+            self.skipped.append(record)
+        elif record.result == TestResultEnums.TEST_RESULT_PASS:
+            self.passed.append(record)
+        else:
+            self.unknown.append(record)
+
+    def skip_all(self, e=None):
+        """Marks every requested test in this result obj as skipped.
+
+        Args:
+            e: An instance of acts.signals.TestSkip specifying the reason for
+                skipping.
+        """
+        for t_name in self.requested:
+            tr = TestResultRecord(t_name)
+            tr.test_begin()
+            tr.test_skip(e)
+            self.add_record(tr)
+
+    def json_str(self):
+        """Converts this test result to a string in json format.
+
+        Format of the json string is:
+            {
+                "Results": [
+                    {<executed test record 1>},
+                    {<executed test record 2>},
+                    ...
+                ],
+                "Summary": <summary dict>
+            }
+
+        Returns:
+            A json-format string representing the test results.
+        """
+        d = {}
+        executed = [record.to_dict() for record in self.executed]
+        d["Results"] = executed
+        d["Summary"] = self.summary_dict()
+        json_str = json.dumps(d, indent=4, sort_keys=True)
+        return json_str
+
+    def summary_str(self):
+        """Gets a string that summarizes the stats of this test result.
+
+        The summary rovides the counts of how many test cases fall into each
+        category, like "Passed", "Failed" etc.
+
+        Format of the string is:
+            Requested <int>, Executed <int>, ...
+
+        Returns:
+            A summary string of this test result.
+        """
+        l = ["%s %d" % (k, v) for k, v in self.summary_dict().items()]
+        # Sort the list so the order is the same every time.
+        msg = ", ".join(sorted(l))
+        return msg
+
+    def summary_dict(self):
+        """Gets a dictionary that summarizes the stats of this test result.
+
+        The summary rovides the counts of how many test cases fall into each
+        category, like "Passed", "Failed" etc.
+
+        Returns:
+            A dictionary with the stats of this test result.
+        """
+        d = {}
+        d["Requested"] = len(self.requested)
+        d["Executed"] = len(self.executed)
+        d["Passed"] = len(self.passed)
+        d["Failed"] = len(self.failed)
+        d["Skipped"] = len(self.skipped)
+        d["Unknown"] = len(self.unknown)
+        return d
diff --git a/acts/framework/acts/signals.py b/acts/framework/acts/signals.py
new file mode 100644
index 0000000..a937296
--- /dev/null
+++ b/acts/framework/acts/signals.py
@@ -0,0 +1,76 @@
+#!/usr/bin/python3.4
+#
+# Copyright 2015 - 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.
+
+"""This module is where all the test signal classes and related utilities live.
+"""
+
+import functools
+import json
+
+def generated_test(func):
+    """A decorator used to suppress result reporting for the test case that
+    kicks off a group of generated test cases.
+
+    Returns:
+        What the decorated function returns.
+    """
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        func(*args, **kwargs)
+        raise TestSilent(
+            "Result reporting for %s is suppressed" % func.__name__)
+    return wrapper
+
+class TestSignalError(Exception):
+    """Raised when an error occurs inside a test signal."""
+
+class TestSignal(Exception):
+    """Base class for all test result control signals."""
+    def __init__(self, details, extras=None):
+        if not isinstance(details, str):
+            raise TestSignalError("Message has to be a string.")
+        self.details = details
+        try:
+            json.dumps(extras)
+            self.extras = extras
+        except TypeError:
+            raise TestSignalError(("Extras must be json serializable. %s "
+                                   "is not.") % extras)
+
+class TestFailure(TestSignal):
+    """Raised when a test has failed."""
+
+class TestPass(TestSignal):
+    """Raised when a test has passed."""
+
+class TestSkip(TestSignal):
+    """Raised when a test has been skipped."""
+
+class TestSilent(TestSignal):
+    """Raised when a test should not be reported. This should only be used for
+    generated test cases.
+    """
+
+class TestAbortClass(TestSignal):
+    """Raised when all subsequent test cases within the same test class should
+    be aborted.
+    """
+
+class TestAbortAll(TestSignal):
+    """Raised when all subsequent test cases should be aborted."""
+
+class ControllerError(Exception):
+    """Raised when an error occured in controller classes."""
\ No newline at end of file
diff --git a/acts/framework/acts/test_runner.py b/acts/framework/acts/test_runner.py
new file mode 100644
index 0000000..3f12f5e
--- /dev/null
+++ b/acts/framework/acts/test_runner.py
@@ -0,0 +1,328 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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 argparse
+import functools
+import importlib
+import inspect
+import os
+import pkgutil
+import sys
+from urllib.error import URLError
+
+import acts.logger as logger
+
+from acts.keys import Config
+from acts.keys import get_internal_value
+from acts.keys import get_module_name
+from acts.records import TestResult
+from acts.signals import TestAbortAll
+from acts.utils import create_dir
+from acts.utils import find_files
+from acts.utils import load_config
+from acts.utils import start_standing_subprocess
+from acts.utils import stop_standing_subprocess
+
+adb_logcat_tag = "adb_logcat"
+
+class USERError(Exception):
+    """Raised when a problem is caused by user mistake, e.g. wrong command,
+    misformatted config, test info, wrong test paths etc.
+    """
+
+class TestRunner(object):
+    """The class that instantiates test classes, executes test cases, and
+    report results.
+        Attrubutes:
+        self.configs: A dictionary containing the configurations for this test
+            run. This is populated during instantiation.
+        self.procs: A dictionary keeping track of processes started by this
+            test run.
+        self.id: A string that is the unique identifier of this test run.
+        self.log_path: A string representing the path of the dir under which
+            all logs from this test run should be written.
+        self.log: The logger object used throughout this test run.
+        self.reporter: A file to write result summary to.
+        self.controller_destructors: A dictionary that holds the controller
+            distructors. Keys are controllers' names.
+        self.test_classes: A dictionary where we can look up the test classes
+            by name to instantiate.
+        self.run_list: A list of tuples specifying what tests to run.
+        self.results: The test result object used to record the results of
+            this test run.
+        self.running: A boolean signifies whether this test run is ongoing or
+            not.
+    """
+    def __init__(self, test_configs, run_list):
+        self.configs = {}
+        self.procs = {}
+        tb = test_configs[Config.key_testbed.value]
+        self.testbed_name = tb[Config.key_testbed_name.value]
+        start_time = logger.get_log_file_timestamp()
+        self.id = "{}@{}".format(self.testbed_name, start_time)
+        # log_path should be set before parsing configs.
+        l_path = os.path.join(test_configs[Config.key_log_path.value],
+            self.testbed_name, start_time)
+        self.log_path = os.path.abspath(l_path)
+        (self.log,
+         self.reporter,
+         self.log_name) = logger.get_test_logger_and_reporter(
+            self.log_path,
+            self.id,
+            self.testbed_name)
+        self.controller_destructors = {}
+        self.run_list = run_list
+        self.parse_config(test_configs)
+        t_configs = test_configs[Config.key_test_paths.value]
+        self.test_classes = self.import_test_modules(t_configs)
+        self.set_test_util_logs()
+        self.results = TestResult()
+        self.running = False
+
+    def import_test_modules(self, test_paths):
+        """Imports test classes from test scripts.
+
+        1. Locate all .py files under test paths.
+        2. Import the .py files as modules.
+        3. Find the module members that are test classes.
+        4. Categorize the test classes by name.
+
+        Args:
+            test_paths: A list of directory paths where the test files reside.
+
+        Returns:
+            A dictionary where keys are test class name strings, values are actual
+            test classes that can be instantiated.
+        """
+        def is_testfile_name(name, ext):
+            if ext == ".py":
+                if name.endswith("Test") or name.endswith("_test"):
+                    return True
+            return False
+        file_list = find_files(test_paths, is_testfile_name)
+        test_classes = {}
+        for path, name, _ in file_list:
+            sys.path.append(path)
+            try:
+                module = importlib.import_module(name)
+            except ImportError:
+                for test_cls_name, _ in self.run_list:
+                    # Only block if a test class on the run list causes an import
+                    # error.
+                    if name == test_cls_name:
+                        raise USERError(("Encountered error importing test class "
+                            "%s, abort.") % test_cls_name)
+                continue
+            for member_name in dir(module):
+                if not member_name.startswith("__"):
+                    if member_name.endswith("Test"):
+                        test_class = getattr(module, member_name)
+                        if inspect.isclass(test_class):
+                            test_classes[member_name] = test_class
+        return test_classes
+
+    def parse_config(self, test_configs):
+        """Parses the test configuration and unpacks objects and parameters
+        into a dictionary to be passed to test classes.
+
+        Args:
+            test_configs: A json object representing the test configurations.
+        """
+        data = test_configs[Config.key_testbed.value]
+        testbed_configs = data[Config.key_testbed_name.value]
+        self.configs[Config.ikey_testbed_name.value] = testbed_configs
+        # Unpack controllers
+        for ctrl_name in Config.controller_names.value:
+            if ctrl_name in data:
+                module_name = get_module_name(ctrl_name)
+                module = importlib.import_module("acts.controllers.%s" %
+                    module_name)
+                # Create controller objects.
+                create = getattr(module, "create")
+                try:
+                    objects = create(data[ctrl_name], self.log)
+                    controller_var_name = get_internal_value(ctrl_name)
+                    self.configs[controller_var_name] = objects
+                    self.log.debug("Found %d objects for controller %s" %
+                        (len(objects), module_name))
+                    # Bind controller objects to their destructors.
+                    destroy_func = getattr(module, "destroy")
+                    self.controller_destructors[controller_var_name] = destroy_func
+                except:
+                    msg = ("Failed to initialize objects for controller {}, "
+                        "abort!").format(module_name)
+                    self.log.error(msg)
+                    self.clean_up()
+                    raise
+        test_runner_keys = (Config.key_adb_logcat_param.value,)
+        for key in test_runner_keys:
+            if key in test_configs:
+                setattr(self, key, test_configs[key])
+        # Unpack other params.
+        self.configs[Config.ikey_logpath.value] = self.log_path
+        self.configs[Config.ikey_logger.value] = self.log
+        self.configs[Config.ikey_reporter.value] = self.reporter
+        cli_args = test_configs[Config.ikey_cli_args.value]
+        self.configs[Config.ikey_cli_args.value] = cli_args
+        user_param_pairs = []
+        for item in test_configs.items():
+            if item[0] not in Config.reserved_keys.value:
+                user_param_pairs.append(item)
+        self.configs[Config.ikey_user_param.value] = dict(user_param_pairs)
+
+    def set_test_util_logs(self, module=None):
+        """Sets the log object to each test util module.
+
+        This recursively include all modules under acts.test_utils and sets the
+        main test logger to each module.
+
+        Args:
+            module: A module under acts.test_utils.
+        """
+        # Initial condition of recursion.
+        if not module:
+            module = importlib.import_module("acts.test_utils")
+        # Somehow pkgutil.walk_packages is not working for me.
+        # Using iter_modules for now.
+        pkg_iter = pkgutil.iter_modules(module.__path__, module.__name__ + '.')
+        for _, module_name, ispkg in pkg_iter:
+            m = importlib.import_module(module_name)
+            if ispkg:
+                self.set_test_util_logs(module=m)
+            else:
+                msg = "Setting logger to test util module %s" % module_name
+                self.log.debug(msg)
+                setattr(m, "log", self.log)
+
+    def run_test_class(self, test_cls_name, test_cases=None):
+        """Instantiates and executes a test class.
+
+        If test_cases is None, the test cases listed by self.tests will be
+        executed instead. If self.tests is empty as well, no test case in this
+        test class will be executed.
+
+        Args:
+            test_cls_name: Name of the test class to execute.
+            test_cases: List of test case names to execute within the class.
+
+        Returns:
+            A tuple, with the number of cases passed at index 0, and the total
+            number of test cases at index 1.
+        """
+        try:
+            test_cls = self.test_classes[test_cls_name]
+        except KeyError:
+            raise USERError(("Unable to locate class %s in any of the test "
+                "paths specified.") % test_cls_name)
+
+        with test_cls(self.configs) as test_cls_instance:
+            try:
+                cls_result = test_cls_instance.run(test_cases)
+                self.results += cls_result
+            except TestAbortAll as e:
+                self.results += e.results
+                raise e
+
+    def run(self):
+        if not self.running:
+            # Only do these if this is the first iteration.
+            self.start_adb_logcat()
+            self.running = True
+        self.log.debug("Executing run list {}.".format(self.run_list))
+        for test_cls_name, test_case_names in self.run_list:
+            if not self.running:
+                break
+            if test_case_names:
+                self.log.debug(("Executing test cases {} in test class {}."
+                                ).format(test_case_names, test_cls_name))
+            else:
+                self.log.debug("Executing test class {}".format(
+                    test_cls_name))
+            try:
+                self.run_test_class(test_cls_name, test_case_names)
+            except TestAbortAll as e:
+                msg = "Abort all subsequent test classes. Reason: %s" % str(e)
+                self.log.warning(msg)
+                raise
+
+    def stop(self):
+        """Releases resources from test run. Should be called right after run()
+        finishes.
+        """
+        if self.running:
+            msg = "\nSummary for test run %s: %s\n" % (self.id,
+                self.results.summary_str())
+            self.reporter.write(msg)
+            self._write_results_json_str()
+            self.log.info(msg.strip())
+            self.clean_up()
+            logger.kill_test_logger(self.log)
+            logger.kill_test_reporter(self.reporter)
+            self.stop_adb_logcat()
+            self.running = False
+
+    def clean_up(self):
+        for name, destroy in self.controller_destructors.items():
+            try:
+                self.log.debug("Destroying %s." % name)
+                destroy(self.configs[name])
+            except:
+                self.log.exception("Exception occurred destroying %s." % name)
+
+    def start_adb_logcat(self):
+        """Starts adb logcat for each device in separate subprocesses and save
+        the logs in files.
+        """
+        if Config.ikey_android_device.value not in self.configs:
+            self.log.debug("No android device available, skipping adb logcat.")
+            return
+        devices = self.configs[Config.ikey_android_device.value]
+        file_list = []
+        for d in devices:
+            # Disable adb log spam filter.
+            d.adb.shell("logpersist.start")
+            serial = d.serial
+            extra_param = ""
+            f_name = "adblog,{},{}.txt".format(d.model, serial)
+            if hasattr(self, Config.key_adb_logcat_param.value):
+                extra_param = getattr(self, Config.key_adb_logcat_param.value)
+            cmd = "adb -s {} logcat -v threadtime {} > {}".format(
+                serial, extra_param, os.path.join(self.log_path, f_name))
+            p = start_standing_subprocess(cmd)
+            self.procs[serial + adb_logcat_tag] = p
+            file_list.append(f_name)
+        if file_list:
+            self.configs[Config.ikey_adb_log_path.value] = self.log_path
+            self.configs[Config.ikey_adb_log_files.value] = file_list
+
+    def stop_adb_logcat(self):
+        """Stops all adb logcat subprocesses.
+        """
+        for k, p in self.procs.items():
+            if k[-len(adb_logcat_tag):] == adb_logcat_tag:
+                stop_standing_subprocess(p)
+
+    def _write_results_json_str(self):
+        """Writes out a json file with the test result info for easy parsing.
+
+        TODO(angli): This should be replaced by standard log record mechanism.
+        """
+        path = os.path.join(self.log_path, "test_run_summary.json")
+        with open(path, 'w') as f:
+            f.write(self.results.json_str())
+
+if __name__ == "__main__":
+    pass
diff --git a/acts/framework/acts/test_utils/__init__.py b/acts/framework/acts/test_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/test_utils/__init__.py
diff --git a/acts/framework/acts/test_utils/bt/BleEnum.py b/acts/framework/acts/test_utils/bt/BleEnum.py
new file mode 100644
index 0000000..67e017f
--- /dev/null
+++ b/acts/framework/acts/test_utils/bt/BleEnum.py
@@ -0,0 +1,162 @@
+# python3.4
+# Copyright (C) 2014 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 enum import Enum
+
+
+class ScanSettingsCallbackType(Enum):
+  CALLBACK_TYPE_ALL_MATCHES = 1
+  CALLBACK_TYPE_FIRST_MATCH = 2
+  CALLBACK_TYPE_MATCH_LOST = 4
+  CALLBACK_TYPE_FOUND_AND_LOST = 6
+
+
+class ScanSettingsMatchMode(Enum):
+  AGGRESIVE = 1
+  STICKY = 2
+
+
+class ScanSettingsMatchNum(Enum):
+  MATCH_NUM_ONE_ADVERTISEMENT = 1
+  MATCH_NUM_FEW_ADVERTISEMENT = 2
+  MATCH_NUM_MAX_ADVERTISEMENT = 3
+
+
+class ScanSettingsScanResultType(Enum):
+  SCAN_RESULT_TYPE_FULL = 0
+  SCAN_RESULT_TYPE_ABBREVIATED = 1
+
+
+class ScanSettingsScanMode(Enum):
+  SCAN_MODE_OPPORTUNISTIC = -1
+  SCAN_MODE_LOW_POWER = 0
+  SCAN_MODE_BALANCED = 1
+  SCAN_MODE_LOW_LATENCY = 2
+
+
+class ScanSettingsReportDelaySeconds(Enum):
+  MIN = 0
+  MAX = 9223372036854775807
+
+
+class AdvertiseSettingsAdvertiseType(Enum):
+  ADVERTISE_TYPE_NON_CONNECTABLE = 0
+  ADVERTISE_TYPE_CONNECTABLE = 1
+
+
+class AdvertiseSettingsAdvertiseMode(Enum):
+  ADVERTISE_MODE_LOW_POWER = 0
+  ADVERTISE_MODE_BALANCED = 1
+  ADVERTISE_MODE_LOW_LATENCY = 2
+
+
+class AdvertiseSettingsAdvertiseTxPower(Enum):
+  ADVERTISE_TX_POWER_ULTRA_LOW = 0
+  ADVERTISE_TX_POWER_LOW = 1
+  ADVERTISE_TX_POWER_MEDIUM = 2
+  ADVERTISE_TX_POWER_HIGH = 3
+
+
+class JavaInteger(Enum):
+  MIN = -2147483648
+  MAX = 2147483647
+
+
+class Uuids(Enum):
+  P_Service = "0000feef-0000-1000-8000-00805f9b34fb"
+  HR_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb"
+
+
+class GattConnectionState(Enum):
+  STATE_DISCONNECTED = 0
+  STATE_CONNECTING = 1
+  STATE_CONNECTED = 2
+  STATE_DISCONNECTING = 3
+
+
+class BluetoothGattCharacteristic(Enum):
+  PROPERTY_BROADCAST = 0x01
+  PROPERTY_READ = 0x02
+  PROPERTY_WRITE_NO_RESPONSE = 0x04
+  PROPERTY_WRITE = 0x08
+  PROPERTY_NOTIFY = 0x10
+  PROPERTY_INDICATE = 0x20
+  PROPERTY_SIGNED_WRITE = 0x40
+  PROPERTY_EXTENDED_PROPS = 0x80
+  PERMISSION_READ = 0x01
+  PERMISSION_READ_ENCRYPTED = 0x02
+  PERMISSION_READ_ENCRYPTED_MITM = 0x04
+  PERMISSION_WRITE = 0x10
+  PERMISSION_WRITE_ENCRYPTED = 0x20
+  PERMISSION_WRITE_ENCRYPTED_MITM = 0x40
+  PERMISSION_WRITE_SIGNED = 0x80
+  PERMISSION_WRITE_SIGNED_MITM = 0x100
+  WRITE_TYPE_DEFAULT = 0x02
+  WRITE_TYPE_NO_RESPONSE = 0x01
+  WRITE_TYPE_SIGNED = 0x04
+  FORMAT_UINT8 = 0x11
+  FORMAT_UINT16 = 0x12
+  FORMAT_UINT32 = 0x14
+  FORMAT_SINT8 = 0x21
+  FORMAT_SINT16 = 0x22
+  FORMAT_SINT32 = 0x24
+  FORMAT_SFLOAT = 0x32
+  FORMAT_FLOAT = 0x34
+
+class BluetoothGattDescriptor(Enum):
+  ENABLE_NOTIFICATION_VALUE = [0x01, 0x00]
+  ENABLE_INDICATION_VALUE = [0x02, 0x00]
+  DISABLE_NOTIFICATION_VALUE = [0x00, 0x00]
+  PERMISSION_READ = 0x01
+  PERMISSION_READ_ENCRYPTED = 0x02
+  PERMISSION_READ_ENCRYPTED_MITM = 0x04
+  PERMISSION_WRITE = 0x10
+  PERMISSION_WRITE_ENCRYPTED = 0x20
+  PERMISSION_WRITE_ENCRYPTED_MITM = 0x40
+  PERMISSION_WRITE_SIGNED = 0x80
+  PERMISSION_WRITE_SIGNED_MITM = 0x100
+
+class BluetoothGattService(Enum):
+  SERVICE_TYPE_PRIMARY = 0
+  SERVICE_TYPE_SECONDARY = 1
+
+class BluetoothGattConnectionPriority(Enum):
+  CONNECTION_PRIORITY_BALANCED = 0
+  CONNECTION_PRIORITY_HIGH = 1
+  CONNECTION_PRIORITY_LOW_POWER = 2
+
+class BluetoothGatt(Enum):
+  GATT_SUCCESS = 0
+  GATT_FAILURE = 0x101
+
+class AdvertiseErrorCode(Enum):
+  DATA_TOO_LARGE = 1
+  TOO_MANY_ADVERTISERS = 2
+  ADVERTISE_ALREADY_STARTED = 3
+  BLUETOOTH_INTERNAL_FAILURE = 4
+  FEATURE_NOT_SUPPORTED = 5
+
+class BluetoothAdapterState(Enum):
+  STATE_OFF = 10
+  STATE_TURNING_ON = 11
+  STATE_ON = 12
+  STATE_TURNING_OFF = 13
+  STATE_BLE_TURNING_ON = 14
+  STATE_BLE_ON = 15
+  STATE_BLE_TURNING_OFF = 16
+
+class BluetoothMtuSize(Enum):
+  MIN = 23
+  MAX = 217
\ No newline at end of file
diff --git a/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py b/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
new file mode 100644
index 0000000..e337181
--- /dev/null
+++ b/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
@@ -0,0 +1,71 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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.
+"""
+    Base Class for Defining Common Bluetooth Test Functionality
+"""
+
+import time
+from acts.base_test import BaseTestClass
+from acts.test_utils.bt.bt_test_utils import (log_energy_info,
+                                              reset_bluetooth,
+                                              setup_multiple_devices_for_bt_test,
+                                              take_btsnoop_logs)
+
+class BluetoothBaseTest(BaseTestClass):
+
+    def __init__(self, controllers):
+        BaseTestClass.__init__(self, controllers)
+
+    # Use for logging in the test cases to facilitate
+    # faster log lookup and reduce ambiguity in logging.
+    def bt_test_wrap(fn):
+        def _safe_wrap_test_case(self, *args, **kwargs):
+            test_id = "{}:{}:{}".format(
+                self.__class__.__name__,
+                fn.__name__,
+                time.time())
+            log_string = "[Test ID] {}".format(test_id)
+            self.log.info(log_string)
+            return fn(self, *args, **kwargs)
+        return _safe_wrap_test_case
+
+    def setup_class(self):
+        return setup_multiple_devices_for_bt_test(self.droids, self.eds)
+
+    def setup_test(self):
+        self.log.debug(log_energy_info(self.droids, "Start"))
+        for e in self.eds:
+            e.clear_all_events()
+        return True
+
+    def teardown_test(self):
+        self.log.debug(log_energy_info(self.droids, "End"))
+        return True
+
+    def on_fail(self, test_name, begin_time):
+        self.log.debug("Test {} failed. Gathering bugreport and btsnoop logs".
+                       format(test_name))
+        take_btsnoop_logs(self.droids, self, test_name)
+        reset_bluetooth(self.droids, self.eds)
+
+        if "no_bug_report_on_fail" not in self.user_params:
+            try:
+                self.take_bug_reports(
+                    test_name, begin_time, self.android_devices)
+            except:
+                self.log.error("Failed to take a bug report for {}"
+                               .format(test_name))
+
diff --git a/acts/framework/acts/test_utils/bt/BtEnum.py b/acts/framework/acts/test_utils/bt/BtEnum.py
new file mode 100644
index 0000000..18de47b
--- /dev/null
+++ b/acts/framework/acts/test_utils/bt/BtEnum.py
@@ -0,0 +1,22 @@
+# python3.4
+# Copyright (C) 2014 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 enum import Enum
+
+class BluetoothScanModeType(Enum):
+  STATE_OFF = -1
+  SCAN_MODE_NONE = 0
+  SCAN_MODE_CONNECTABLE = 1
+  SCAN_MODE_CONNECTABLE_DISCOVERABLE = 3
diff --git a/acts/framework/acts/test_utils/bt/__init__.py b/acts/framework/acts/test_utils/bt/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/test_utils/bt/__init__.py
diff --git a/acts/framework/acts/test_utils/bt/bt_test_utils.py b/acts/framework/acts/test_utils/bt/bt_test_utils.py
new file mode 100644
index 0000000..4cea2e7
--- /dev/null
+++ b/acts/framework/acts/test_utils/bt/bt_test_utils.py
@@ -0,0 +1,693 @@
+# python3.4
+# Copyright (C) 2014 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 random
+import pprint
+import string
+import queue
+import threading
+import time
+
+from contextlib import suppress
+
+from acts.logger import LoggerProxy
+from acts.test_utils.bt.BleEnum import *
+from acts.test_utils.bt.BtEnum import *
+from acts.utils import exe_cmd
+
+default_timeout = 10
+# bt discovery timeout
+default_discovery_timeout = 3
+log = LoggerProxy()
+
+# Callback strings
+characteristic_write_request = "GattServer{}onCharacteristicWriteRequest"
+characteristic_write = "GattConnect{}onCharacteristicWrite"
+descriptor_write_request = "GattServer{}onDescriptorWriteRequest"
+descriptor_write = "GattConnect{}onDescriptorWrite"
+read_remote_rssi = "GattConnect{}onReadRemoteRssi"
+gatt_services_discovered = "GattConnect{}onServicesDiscovered"
+scan_result = "BleScan{}onScanResults"
+scan_failed = "BleScan{}onScanFailed"
+service_added = "GattServer{}onServiceAdded"
+batch_scan_result = "BleScan{}onBatchScanResult"
+adv_fail = "BleAdvertise{}onFailure"
+adv_succ = "BleAdvertise{}onSuccess"
+bluetooth_off = "BluetoothStateChangedOff"
+bluetooth_on = "BluetoothStateChangedOn"
+mtu_changed = "GattConnect{}onMtuChanged"
+
+# rfcomm test uuids
+rfcomm_secure_uuid = "fa87c0d0-afac-11de-8a39-0800200c9a66"
+rfcomm_insecure_uuid = "8ce255c0-200a-11e0-ac64-0800200c9a66"
+
+advertisements_to_devices = {
+    "Nexus 4": 0,
+    "Nexus 5": 0,
+    "Nexus 5X":15,
+    "Nexus 7": 0,
+    "Nexus Player": 1,
+    "Nexus 6": 4,
+    "Nexus 6P": 4,
+    "AOSP on Shamu": 4,
+    "Nexus 9": 4,
+    "Sprout": 10,
+    "Micromax AQ4501": 10,
+    "4560MMX": 10,
+    "G Watch R": 1,
+    "Gear Live": 1,
+    "SmartWatch 3": 1,
+    "Zenwatch": 1,
+    "AOSP on Shamu": 4,
+    "MSM8992 for arm64": 9,
+    "LG Watch Urbane": 1,
+    "Pixel C":4,
+    "angler": 4,
+    "bullhead": 15,
+}
+
+batch_scan_supported_list = {
+    "Nexus 4": False,
+    "Nexus 5": False,
+    "Nexus 7": False,
+    "Nexus Player": True,
+    "Nexus 6": True,
+    "Nexus 6P": True,
+    "Nexus 5X": True,
+    "AOSP on Shamu": True,
+    "Nexus 9": True,
+    "Sprout": True,
+    "Micromax AQ4501": True,
+    "4560MMX": True,
+    "Pixel C":True,
+    "G Watch R": True,
+    "Gear Live": True,
+    "SmartWatch 3": True,
+    "Zenwatch": True,
+    "AOSP on Shamu": True,
+    "MSM8992 for arm64": True,
+    "LG Watch Urbane": True,
+    "angler": True,
+    "bullhead": True,
+}
+
+
+def generate_ble_scan_objects(droid):
+    filter_list = droid.bleGenFilterList()
+    scan_settings = droid.bleBuildScanSetting()
+    scan_callback = droid.bleGenScanCallback()
+    return filter_list, scan_settings, scan_callback
+
+
+def generate_ble_advertise_objects(droid):
+    advertise_callback = droid.bleGenBleAdvertiseCallback()
+    advertise_data = droid.bleBuildAdvertiseData()
+    advertise_settings = droid.bleBuildAdvertiseSettings()
+    return advertise_callback, advertise_data, advertise_settings
+
+
+def extract_string_from_byte_array(string_list):
+    """Extract the string from array of string list
+    """
+    start = 1
+    end = len(string_list) - 1
+    extract_string = string_list[start:end]
+    return extract_string
+
+
+def extract_uuidlist_from_record(uuid_string_list):
+    """Extract uuid from Service UUID List
+    """
+    start = 1
+    end = len(uuid_string_list) - 1
+    uuid_length = 36
+    uuidlist = []
+    while start < end:
+        uuid = uuid_string_list[start:(start + uuid_length)]
+        start += uuid_length + 1
+        uuidlist.append(uuid)
+    return uuidlist
+
+
+def build_advertise_settings(droid, mode, txpower, type):
+    """Build Advertise Settings
+    """
+    droid.bleSetAdvertiseSettingsAdvertiseMode(mode)
+    droid.bleSetAdvertiseSettingsTxPowerLevel(txpower)
+    droid.bleSetAdvertiseSettingsIsConnectable(type)
+    settings = droid.bleBuildAdvertiseSettings()
+    return settings
+
+def setup_multiple_devices_for_bt_test(droids, eds):
+    log.info("Setting up Android Devices")
+    threads = []
+    for i in range(len(droids)):
+        thread = threading.Thread(target=reset_bluetooth,
+                                      args=([droids[i]],[eds[i]]))
+        threads.append(thread)
+        thread.start()
+    for t in threads:
+        t.join()
+
+    for d in droids:
+        setup_result = d.bluetoothSetLocalName(generate_id_by_size(4))
+        if not setup_result:
+            return setup_result
+        d.bluetoothDisableBLE()
+        bonded_devices = d.bluetoothGetBondedDevices()
+        for b in bonded_devices:
+            d.bluetoothUnbond(b['address'])
+    for x in range(len(droids)):
+        droid, ed = droids[x], eds[x]
+        setup_result = droid.bluetoothConfigHciSnoopLog(True)
+        if not setup_result:
+            return setup_result
+    return setup_result
+
+
+def reset_bluetooth(droids, eds):
+    """Resets bluetooth on the list of android devices passed into the function.
+    :param android_devices: list of android devices
+    :return: bool
+    """
+    for x in range(len(droids)):
+        droid, ed = droids[x], eds[x]
+        log.info(
+            "Reset state of bluetooth on device: {}".format(
+                droid.getBuildSerial()))
+        if droid.bluetoothCheckState() is True:
+            droid.bluetoothToggleState(False)
+            expected_bluetooth_off_event_name = bluetooth_off
+            try:
+                ed.pop_event(
+                    expected_bluetooth_off_event_name, default_timeout)
+            except Exception:
+                log.info("Failed to toggle Bluetooth off.")
+                return False
+        # temp sleep for b/17723234
+        time.sleep(3)
+        droid.bluetoothToggleState(True)
+        expected_bluetooth_on_event_name = bluetooth_on
+        try:
+            ed.pop_event(expected_bluetooth_on_event_name, default_timeout)
+        except Exception:
+            log.info("Failed to toggle Bluetooth on.")
+            return False
+    return True
+
+
+def get_advanced_droid_list(droids, eds):
+    droid_list = []
+    for i in range(len(droids)):
+        d = droids[i]
+        e = eds[i]
+        model = d.getBuildModel()
+        print (model)
+        max_advertisements = 0
+        batch_scan_supported = True
+        if model in advertisements_to_devices.keys():
+            max_advertisements = advertisements_to_devices[model]
+        if model in batch_scan_supported_list.keys():
+            batch_scan_supported = batch_scan_supported_list[model]
+        role = {
+            'droid': d,
+            'ed': e,
+            'max_advertisements': max_advertisements,
+            'batch_scan_supported': batch_scan_supported
+        }
+        droid_list.append(role)
+    return droid_list
+
+
+def generate_id_by_size(size,
+                        chars=(string.ascii_lowercase +
+                               string.ascii_uppercase + string.digits)):
+    return ''.join(random.choice(chars) for _ in range(size))
+
+
+def cleanup_scanners_and_advertisers(scan_droid, scan_ed, scan_callback_list,
+                                     adv_droid, adv_ed, adv_callback_list):
+    """
+    Try to gracefully stop all scanning and advertising instances.
+    """
+    try:
+        for scan_callback in scan_callback_list:
+            scan_droid.bleStopBleScan(scan_callback)
+    except Exception:
+        reset_bluetooth([scan_droid], [scan_ed])
+    try:
+        for adv_callback in adv_callback_list:
+            adv_droid.bleStopBleAdvertising(adv_callback)
+    except Exception:
+        reset_bluetooth([adv_droid], [adv_ed])
+
+
+def setup_gatt_characteristics(droid, input):
+    characteristic_list = []
+    for item in input:
+        index = droid.gattServerCreateBluetoothGattCharacteristic(
+            item['uuid'],
+            item['property'],
+            item['permission'])
+        characteristic_list.append(index)
+    return characteristic_list
+
+
+def setup_gatt_descriptors(droid, input):
+    descriptor_list = []
+    for item in input:
+        index = droid.gattServerCreateBluetoothGattDescriptor(
+            item['uuid'],
+            item['property'],
+        )
+        descriptor_list.append(index)
+    log.info("setup descriptor list: {}".format(descriptor_list))
+    return descriptor_list
+
+
+def get_mac_address_of_generic_advertisement(scan_droid, scan_ed, adv_droid,
+                                             adv_ed):
+    adv_droid.bleSetAdvertiseDataIncludeDeviceName(True)
+    adv_droid.bleSetAdvertiseSettingsAdvertiseMode(
+        AdvertiseSettingsAdvertiseMode.ADVERTISE_MODE_LOW_LATENCY.value)
+    adv_droid.bleSetAdvertiseSettingsIsConnectable(True)
+    adv_droid.bleSetAdvertiseSettingsTxPowerLevel(
+        AdvertiseSettingsAdvertiseTxPower.ADVERTISE_TX_POWER_HIGH.value)
+    advertise_callback, advertise_data, advertise_settings = (
+        generate_ble_advertise_objects(adv_droid))
+    adv_droid.bleStartBleAdvertising(
+        advertise_callback, advertise_data, advertise_settings)
+    adv_ed.pop_event("BleAdvertise{}onSuccess".format(
+        advertise_callback), default_timeout)
+    filter_list = scan_droid.bleGenFilterList()
+    scan_settings = scan_droid.bleBuildScanSetting()
+    scan_callback = scan_droid.bleGenScanCallback()
+    scan_droid.bleSetScanFilterDeviceName(adv_droid.bluetoothGetLocalName())
+    scan_droid.bleBuildScanFilter(filter_list)
+    scan_droid.bleStartBleScan(filter_list, scan_settings, scan_callback)
+    event = scan_ed.pop_event(
+        "BleScan{}onScanResults".format(scan_callback), default_timeout)
+    mac_address = event['data']['Result']['deviceInfo']['address']
+    scan_droid.bleStopBleScan(scan_callback)
+    return mac_address, advertise_callback
+
+
+def setup_gatt_connection(cen_droid, cen_ed, mac_address, autoconnect):
+    test_result = True
+    gatt_callback = cen_droid.gattCreateGattCallback()
+    log.info("Gatt Connect to mac address {}.".format(mac_address))
+    bluetooth_gatt = cen_droid.gattClientConnectGatt(
+        gatt_callback, mac_address,
+        autoconnect)
+    event = cen_ed.pop_event(
+        "GattConnect{}onConnectionStateChange".format(gatt_callback),
+        default_timeout)
+    if event['data']['State'] != GattConnectionState.STATE_CONNECTED.value:
+        log.info("Could not establish a connection to peripheral. Event "
+                 "Details:".format(pprint.pformat(event)))
+        test_result = False
+    # To avoid race condition of quick connect/disconnect
+    time.sleep(1)
+    return test_result, bluetooth_gatt, gatt_callback
+
+
+def disconnect_gatt_connection(cen_droid, cen_ed, bluetooth_gatt, gatt_callback):
+    cen_droid.gattClientDisconnect(bluetooth_gatt)
+    event = cen_ed.pop_event(
+        "GattConnect{}onConnectionStateChange".format(gatt_callback),
+        default_timeout)
+    if event['data']['State'] != GattConnectionState.STATE_DISCONNECTED.value:
+        return False
+    return True
+
+
+def orchestrate_gatt_connection(cen_droid, cen_ed, per_droid, per_ed, le=True,
+                                mac_address=None):
+    adv_callback = None
+    if mac_address is None:
+        if le:
+            mac_address, adv_callback = (
+                get_mac_address_of_generic_advertisement(cen_droid, cen_ed,
+                                                         per_droid, per_ed))
+        else:
+            mac_address = get_bt_mac_address(cen_droid, per_droid, le)
+            adv_callback = None
+    autoconnect = False
+    test_result, bluetooth_gatt, gatt_callback = setup_gatt_connection(
+        cen_droid, cen_ed, mac_address, autoconnect)
+    if not test_result:
+        log.info("Could not connect to peripheral.")
+        return False
+    return bluetooth_gatt, gatt_callback, adv_callback
+
+
+def run_continuous_write_descriptor(
+        cen_droid, cen_ed, per_droid, per_ed, gatt_server, gatt_server_callback,
+        bluetooth_gatt, services_count, discovered_services_index):
+    log.info("starting continuous write")
+    bt_device_id = 0
+    status = 1
+    offset = 1
+    test_value = "1,2,3,4,5,6,7"
+    test_value_return = "1,2,3"
+    from contextlib import suppress
+    with suppress(Exception):
+        for x in range(100000):
+            for i in range(services_count):
+                characteristic_uuids = (
+                    cen_droid.gattClientGetDiscoveredCharacteristicUuids(
+                        discovered_services_index, i))
+                log.info(characteristic_uuids)
+                for characteristic in characteristic_uuids:
+                    descriptor_uuids = (
+                        cen_droid.gattClientGetDiscoveredDescriptorUuids(
+                            discovered_services_index, i, characteristic))
+                    log.info(descriptor_uuids)
+                    for descriptor in descriptor_uuids:
+                        log.info(
+                            "descriptor to be written {}".format(descriptor))
+                        cen_droid.gattClientDescriptorSetValue(
+                            bluetooth_gatt, discovered_services_index,
+                            i, characteristic, descriptor, test_value)
+                        cen_droid.gattClientWriteDescriptor(
+                            bluetooth_gatt, discovered_services_index,
+                            i, characteristic, descriptor)
+                        event = per_ed.pop_event(
+                            descriptor_write_request.format(
+                                gatt_server_callback), default_timeout)
+                        log.info(
+                            "onDescriptorWriteRequest event found: {}".format(
+                                event))
+                        request_id = event['data']['requestId']
+                        found_value = event['data']['value']
+                        if found_value != test_value:
+                            log.info(
+                                "Values didn't match. Found: {}, Expected: "
+                                "{}".format(found_value, test_value))
+                        per_droid.gattServerSendResponse(
+                            gatt_server, bt_device_id, request_id, status,
+                            offset, test_value_return)
+                        log.info("onDescriptorWrite event found: {}".format(
+                            cen_ed.pop_event(
+                                descriptor_write.format(bluetooth_gatt),
+                                default_timeout)))
+
+
+def setup_characteristics_and_descriptors(droid):
+    characteristic_input = [
+        {
+            'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+            'property': BluetoothGattCharacteristic.PROPERTY_WRITE.value |
+                    BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE.value,
+            'permission': BluetoothGattCharacteristic.PROPERTY_WRITE.value
+        },
+        {
+            'uuid': "21c0a0bf-ad51-4a2d-8124-b74003e4e8c8",
+            'property': BluetoothGattCharacteristic.PROPERTY_NOTIFY.value |
+                    BluetoothGattCharacteristic.PROPERTY_READ.value,
+            'permission': BluetoothGattCharacteristic.PERMISSION_READ.value
+        },
+        {
+            'uuid': "6774191f-6ec3-4aa2-b8a8-cf830e41fda6",
+            'property': BluetoothGattCharacteristic.PROPERTY_NOTIFY.value |
+                    BluetoothGattCharacteristic.PROPERTY_READ.value,
+            'permission': BluetoothGattCharacteristic.PERMISSION_READ.value
+        },
+    ]
+    descriptor_input = [
+        {
+            'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+            'property': BluetoothGattDescriptor.PERMISSION_READ.value |
+                    BluetoothGattDescriptor.PERMISSION_WRITE.value,
+        },
+        {
+            'uuid': "76d5ed92-ca81-4edb-bb6b-9f019665fb32",
+            'property': BluetoothGattDescriptor.PERMISSION_READ.value |
+                    BluetoothGattCharacteristic.PERMISSION_WRITE.value,
+        }
+    ]
+    characteristic_list = setup_gatt_characteristics(
+        droid, characteristic_input)
+    descriptor_list = setup_gatt_descriptors(droid, descriptor_input)
+    return characteristic_list, descriptor_list
+
+
+def setup_multiple_services(per_droid, per_ed):
+    gatt_server_callback = per_droid.gattServerCreateGattServerCallback()
+    gatt_server = per_droid.gattServerOpenGattServer(gatt_server_callback)
+    characteristic_list, descriptor_list = (
+        setup_characteristics_and_descriptors(per_droid))
+    per_droid.gattServerCharacteristicAddDescriptor(
+        characteristic_list[1], descriptor_list[0])
+    per_droid.gattServerCharacteristicAddDescriptor(
+        characteristic_list[2], descriptor_list[1])
+    gattService = per_droid.gattServerCreateService(
+        "00000000-0000-1000-8000-00805f9b34fb",
+        BluetoothGattService.SERVICE_TYPE_PRIMARY.value)
+    gattService2 = per_droid.gattServerCreateService(
+        "FFFFFFFF-0000-1000-8000-00805f9b34fb",
+        BluetoothGattService.SERVICE_TYPE_PRIMARY.value)
+    gattService3 = per_droid.gattServerCreateService(
+        "3846D7A0-69C8-11E4-BA00-0002A5D5C51B",
+        BluetoothGattService.SERVICE_TYPE_PRIMARY.value)
+    for characteristic in characteristic_list:
+        per_droid.gattServerAddCharacteristicToService(
+            gattService, characteristic)
+    per_droid.gattServerAddService(gatt_server, gattService)
+    per_ed.pop_event(service_added.format(gatt_server_callback),
+                     default_timeout)
+    for characteristic in characteristic_list:
+        per_droid.gattServerAddCharacteristicToService(
+            gattService2, characteristic)
+    per_droid.gattServerAddService(gatt_server, gattService2)
+    per_ed.pop_event(service_added.format(gatt_server_callback),
+                     default_timeout)
+    for characteristic in characteristic_list:
+        per_droid.gattServerAddCharacteristicToService(gattService3,
+                                                       characteristic)
+    per_droid.gattServerAddService(gatt_server, gattService3)
+    per_ed.pop_event(service_added.format(gatt_server_callback),
+                     default_timeout)
+    return gatt_server_callback, gatt_server
+
+
+def setup_characteristics_and_descriptors(droid):
+    characteristic_input = [
+        {
+            'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+            'property': BluetoothGattCharacteristic.PROPERTY_WRITE.value |
+            BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE.value,
+            'permission': BluetoothGattCharacteristic.PROPERTY_WRITE.value
+        },
+        {
+            'uuid': "21c0a0bf-ad51-4a2d-8124-b74003e4e8c8",
+            'property': BluetoothGattCharacteristic.PROPERTY_NOTIFY.value |
+            BluetoothGattCharacteristic.PROPERTY_READ.value,
+            'permission': BluetoothGattCharacteristic.PERMISSION_READ.value
+        },
+        {
+            'uuid': "6774191f-6ec3-4aa2-b8a8-cf830e41fda6",
+            'property': BluetoothGattCharacteristic.PROPERTY_NOTIFY.value |
+            BluetoothGattCharacteristic.PROPERTY_READ.value,
+            'permission': BluetoothGattCharacteristic.PERMISSION_READ.value
+        },
+    ]
+    descriptor_input = [
+        {
+            'uuid': "aa7edd5a-4d1d-4f0e-883a-d145616a1630",
+            'property': BluetoothGattDescriptor.PERMISSION_READ.value |
+            BluetoothGattDescriptor.PERMISSION_WRITE.value,
+        },
+        {
+            'uuid': "76d5ed92-ca81-4edb-bb6b-9f019665fb32",
+            'property': BluetoothGattDescriptor.PERMISSION_READ.value |
+            BluetoothGattCharacteristic.PERMISSION_WRITE.value,
+        }
+    ]
+    characteristic_list = setup_gatt_characteristics(
+        droid, characteristic_input)
+    descriptor_list = setup_gatt_descriptors(droid, descriptor_input)
+    return characteristic_list, descriptor_list
+
+
+def get_device_local_info(droid):
+    local_info_dict = {}
+    local_info_dict['name'] = droid.bluetoothGetLocalName()
+    local_info_dict['uuids'] = droid.bluetoothGetLocalUuids()
+    return local_info_dict
+
+
+def enable_bluetooth(droid, ed):
+    if droid.bluetoothCheckState() is False:
+        droid.bluetoothToggleState(True)
+        if droid.bluetoothCheckState() is False:
+            return False
+    return True
+
+
+def disable_bluetooth(droid, ed):
+    if droid.bluetoothCheckState() is True:
+        droid.bluetoothToggleState(False)
+        if droid.bluetoothCheckState() is True:
+            return False
+    return True
+
+
+def set_bt_scan_mode(droid, ed, scan_mode_value):
+    if scan_mode_value == BluetoothScanModeType.STATE_OFF.value:
+        disable_bluetooth(droid, ed)
+        scan_mode = droid.bluetoothGetScanMode()
+        reset_bluetooth([droid], [ed])
+        if scan_mode != scan_mode_value:
+            return False
+    elif scan_mode_value == BluetoothScanModeType.SCAN_MODE_NONE.value:
+        droid.bluetoothMakeUndiscoverable()
+        scan_mode = droid.bluetoothGetScanMode()
+        if scan_mode != scan_mode_value:
+            return False
+    elif scan_mode_value == BluetoothScanModeType.SCAN_MODE_CONNECTABLE.value:
+        droid.bluetoothMakeUndiscoverable()
+        droid.bluetoothMakeConnectable()
+        scan_mode = droid.bluetoothGetScanMode()
+        if scan_mode != scan_mode_value:
+            return False
+    elif (scan_mode_value ==
+              BluetoothScanModeType.SCAN_MODE_CONNECTABLE_DISCOVERABLE.value):
+        droid.bluetoothMakeDiscoverable()
+        scan_mode = droid.bluetoothGetScanMode()
+        if scan_mode != scan_mode_value:
+            return False
+    else:
+        # invalid scan mode
+        return False
+    return True
+
+
+def set_device_name(droid, name):
+    droid.bluetoothSetLocalName(name)
+    # temporary (todo:tturney fix)
+    time.sleep(2)
+    droid_name = droid.bluetoothGetLocalName()
+    if droid_name != name:
+        return False
+    return True
+
+
+def check_device_supported_profiles(droid):
+    profile_dict = {}
+    profile_dict['hid'] = droid.bluetoothHidIsReady()
+    profile_dict['hsp'] = droid.bluetoothHspIsReady()
+    profile_dict['a2dp'] = droid.bluetoothA2dpIsReady()
+    profile_dict['avrcp'] = droid.bluetoothAvrcpIsReady()
+    return profile_dict
+
+
+def log_energy_info(droids, state):
+    return_string = "{} Energy info collection:\n".format(state)
+    for d in droids:
+        with suppress(Exception):
+            if (d.getBuildModel() == "Nexus 6" or d.getBuildModel() == "Nexus 9"
+                or d.getBuildModel() == "Nexus 6P"
+                or d.getBuildModel() == "Nexus5X"):
+
+                description = ("Device: {}\tEnergyStatus: {}\n".format(
+                    d.getBuildSerial(),
+                    d.bluetoothGetControllerActivityEnergyInfo(1)))
+                return_string = return_string + description
+    return return_string
+
+
+def pair_pri_to_sec(pri_droid, sec_droid):
+    sec_droid.bluetoothMakeDiscoverable(default_timeout)
+    pri_droid.bluetoothStartDiscovery()
+    target_name = sec_droid.bluetoothGetLocalName()
+    time.sleep(default_discovery_timeout)
+    discovered_devices = pri_droid.bluetoothGetDiscoveredDevices()
+    discovered = False
+    for device in discovered_devices:
+        log.info(device)
+        if 'name' in device and target_name == device['name']:
+            discovered = True
+            continue
+    if not discovered:
+        return False
+    pri_droid.bluetoothStartPairingHelper()
+    sec_droid.bluetoothStartPairingHelper()
+    result = pri_droid.bluetoothDiscoverAndBond(target_name)
+    return result
+
+
+def get_bt_mac_address(droid, droid1, make_undisocverable=True):
+    droid1.bluetoothMakeDiscoverable(default_timeout)
+    droid.bluetoothStartDiscovery()
+    mac = ""
+    target_name = droid1.bluetoothGetLocalName()
+    time.sleep(default_discovery_timeout)
+    discovered_devices = droid.bluetoothGetDiscoveredDevices()
+    for device in discovered_devices:
+        if 'name' in device.keys() and target_name == device['name']:
+            mac = device['address']
+            continue
+    if make_undisocverable:
+        droid1.bluetoothMakeUndiscoverable()
+    droid.bluetoothCancelDiscovery()
+    if mac == "":
+        return False
+    return mac
+
+
+def get_client_server_bt_mac_address(droid, droid1):
+    return get_bt_mac_address(droid, droid1), get_bt_mac_address(droid1, droid)
+
+
+def take_btsnoop_logs(droids, testcase, testname):
+    for d in droids:
+        take_btsnoop_log(d, testcase, testname)
+
+# TODO (tturney): Fix this.
+
+
+def take_btsnoop_log(droid, testcase, test_name):
+    """Grabs the btsnoop_hci log on a device and stores it in the log directory
+    of the test class.
+
+    If you want grab the btsnoop_hci log, call this function with android_device
+    objects in on_fail. Bug report takes a relative long time to take, so use
+    this cautiously.
+
+    Params:
+      test_name: Name of the test case that triggered this bug report.
+      android_device: The android_device instance to take bugreport on.
+    """
+    test_name = "".join(x for x in test_name if x.isalnum())
+    with suppress(Exception):
+        serial = droid.getBuildSerial()
+        device_model = droid.getBuildModel()
+        device_model = device_model.replace(" ", "")
+        out_name = ','.join((test_name, device_model, serial))
+        cmd = ''.join(("adb -s ", serial, " pull /sdcard/btsnoop_hci.log > ",
+                       testcase.log_path + "/" + out_name,
+                       ".btsnoop_hci.log"))
+        testcase.log.info("Test failed, grabbing the bt_snoop logs on {} {}."
+                          .format(device_model, serial))
+        exe_cmd(cmd)
+
+
+def rfcomm_connect(droid, device_address):
+    droid.bluetoothRfcommConnect(device_address)
+
+
+def rfcomm_accept(droid):
+    droid.bluetoothRfcommAccept()
diff --git a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
new file mode 100644
index 0000000..2286c9b
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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.
+"""
+    Base Class for Defining Common Telephony Test Functionality
+"""
+
+import time
+from acts.base_test import BaseTestClass
+from acts.signals import TestSignal
+
+from .tel_test_utils import ensure_phones_default_state
+from .tel_test_utils import get_sub_ids_for_sim_slots
+from .tel_test_utils import set_phone_screen_on
+from .tel_test_utils import set_phone_silent_mode
+from .tel_test_utils import setup_droid_properties
+from .tel_defines import PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND
+from .tel_defines import PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING
+from .tel_defines import PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND
+from .tel_defines import WIFI_VERBOSE_LOGGING_ENABLED
+from .tel_defines import WIFI_VERBOSE_LOGGING_DISABLED
+
+
+class TelephonyBaseTest(BaseTestClass):
+
+    def __init__(self, controllers):
+        BaseTestClass.__init__(self, controllers)
+
+    # Use for logging in the test cases to facilitate
+    # faster log lookup and reduce ambiguity in logging.
+    def tel_test_wrap(fn):
+        def _safe_wrap_test_case(self, *args, **kwargs):
+            test_id = "{}:{}:{}".format(
+                self.__class__.__name__,
+                fn.__name__,
+                time.time())
+            log_string = "[Test ID] {}".format(test_id)
+            self.log.info(log_string)
+            try:
+                for ad in self.android_devices:
+                    ad.droid.logI("Started "+log_string)
+                # TODO: start QXDM Logging b/19002120
+                return fn(self, *args, **kwargs)
+            except TestSignal:
+                raise
+            except Exception as e:
+                self.log.error(str(e))
+                return False
+            finally:
+                # TODO: stop QXDM Logging b/19002120
+                for ad in self.android_devices:
+                    try:
+                        ad.adb.wait_for_device()
+                        ad.droid.logI("Finished "+log_string)
+                    except Exception as e:
+                        self.log.error(str(e))
+        return _safe_wrap_test_case
+
+    def setup_class(self):
+        for ad in self.android_devices:
+            setup_droid_properties(
+                self.log, ad, self.user_params["sim_conf_file"])
+            if not set_phone_screen_on(self.log, ad):
+                self.info.error("Failed to set phone screen-on time.")
+                return False
+            if not set_phone_silent_mode(self.log, ad):
+                self.info.error("Failed to set phone silent mode.")
+                return False
+
+            ad.droid.phoneAdjustPreciseCallStateListenLevel(
+                PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND, True)
+            ad.droid.phoneAdjustPreciseCallStateListenLevel(
+                PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING, True)
+            ad.droid.phoneAdjustPreciseCallStateListenLevel(
+                PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND, True)
+
+            if "enable_wifi_verbose_logging" in self.user_params:
+                ad.droid.wifiEnableVerboseLogging(WIFI_VERBOSE_LOGGING_ENABLED)
+
+        setattr(self, 'sim_sub_ids',
+                get_sub_ids_for_sim_slots(self.log,
+                                          self.android_devices))
+        return True
+
+    def teardown_class(self):
+        ensure_phones_default_state(self.log, self.android_devices)
+
+        for ad in self.android_devices:
+            ad.droid.phoneAdjustPreciseCallStateListenLevel(
+                PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND, False)
+            ad.droid.phoneAdjustPreciseCallStateListenLevel(
+                PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING, False)
+            ad.droid.phoneAdjustPreciseCallStateListenLevel(
+                PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND, False)
+            if "enable_wifi_verbose_logging" in self.user_params:
+                ad.droid.wifiEnableVerboseLogging(WIFI_VERBOSE_LOGGING_DISABLED)
+        return True
+
+    def setup_test(self):
+        return ensure_phones_default_state(self.log, self.android_devices)
+
+    def teardown_test(self):
+        return True
+
+    def on_fail(self, test_name, begin_time):
+        return True
+
+    def on_exception(self, test_name, begin_time):
+        # Since it's a debug flag, as long as it's "set" we consider it valid
+        if "no_bug_report_on_fail" not in self.user_params:
+            # magical sleep to ensure the runtime restart or reboot begins
+            time.sleep(1)
+            for ad in self.android_devices:
+                try:
+                    ad.adb.wait_for_device()
+                    ad.take_bug_reports(
+                        test_name, begin_time, self.android_devices)
+                    # FIXME(nharold): rename tombstone files correctly
+                    # TODO(nharold): make support generic and move to
+                    # base_test and utils respectively
+                    ad.adb.pull('/data/tombstones/', self.log_path)
+                except:
+                    ad.log.error("Failed to take a bug report for {}, {}"
+                                 .format(ad.serial, test_name))
diff --git a/acts/framework/acts/test_utils/tel/__init__.py b/acts/framework/acts/test_utils/tel/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/__init__.py
diff --git a/acts/framework/acts/test_utils/tel/tel_atten_utils.py b/acts/framework/acts/test_utils/tel/tel_atten_utils.py
new file mode 100644
index 0000000..7e9b649
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_atten_utils.py
@@ -0,0 +1,114 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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 time
+import math
+from .tel_defines import *
+
+def get_atten(log, atten_obj):
+    """Get attenuator current attenuation value.
+
+    Args:
+        log: log object.
+        atten_obj: attenuator object.
+    Returns:
+        Current attenuation value.
+    """
+    return atten_obj.get_atten()
+
+def set_atten(log, atten_obj, target_atten, step_size=0, time_per_step=0):
+    """Set attenuator attenuation value.
+
+    Args:
+        log: log object.
+        atten_obj: attenuator object.
+        target_atten: target attenuation value.
+        step_size: step size (in unit of dBm) for 'attenuation value setting'.
+            This is optional. Default value is 0. If step_size is 0, it means
+            the setting will be done in only one step.
+        time_per_step: delay time (in unit of second) per step when setting
+            the attenuation value.
+            This is optional. Default value is 0.
+    Returns:
+        True is no error happened. Otherwise false.
+    """
+    try:
+        print_name = atten_obj.path
+    except AttributeError:
+        print_name = str(atten_obj)
+
+    current_atten = get_atten(log, atten_obj)
+    info = "set_atten {} from {} to {}".format(print_name, current_atten,
+            target_atten)
+    if step_size>0:
+        info +=", step size {}, time per step {}s.".format(step_size, time_per_step)
+    log.info(info)
+    try:
+        delta = target_atten - current_atten
+        if step_size>0:
+            number_of_steps = int(abs(delta)/step_size)
+            while number_of_steps > 0:
+                number_of_steps -=1
+                current_atten += math.copysign(step_size, (target_atten - current_atten))
+                atten_obj.set_atten(current_atten)
+                time.sleep(time_per_step)
+        atten_obj.set_atten(target_atten)
+    except Exception as e:
+        log.error("set_atten error happened: {}".format(e))
+        return False
+    return True
+
+def set_rssi(log, atten_obj, calibration_rssi, target_rssi, step_size=0,
+             time_per_step=0):
+    """Set RSSI value by changing attenuation.
+
+    Args:
+        log: log object.
+        atten_obj: attenuator object.
+        calibration_rssi: RSSI calibration information.
+        target_rssi: target RSSI value.
+        step_size: step size (in unit of dBm) for 'RSSI value setting'.
+            This is optional. Default value is 0. If step_size is 0, it means
+            the setting will be done in only one step.
+        time_per_step: delay time (in unit of second) per step when setting
+            the attenuation value.
+            This is optional. Default value is 0.
+    Returns:
+        True is no error happened. Otherwise false.
+    """
+    try:
+        print_name = atten_obj.path
+    except AttributeError:
+        print_name = str(atten_obj)
+
+    if target_rssi == MAX_RSSI_RESERVED_VALUE:
+        target_atten = ATTEN_MIN_VALUE
+    elif target_rssi == MIN_RSSI_RESERVED_VALUE:
+        target_atten = ATTEN_MAX_VALUE
+    else:
+        log.info("set_rssi {} to {}.".
+                 format(print_name, target_rssi))
+        target_atten = calibration_rssi - target_rssi
+
+    if target_atten < 0:
+        log.info("set_rssi: WARNING - you are setting an unreachable RSSI.")
+        log.info("max RSSI value on {} is {}. Setting attenuation to 0.".
+            format(wifi_or_cell, calibration_rssi))
+        target_atten = 0
+    if not set_atten(log, atten_obj, target_atten, step_size, time_per_step):
+        log.error("set_rssi to {}failed".format(target_rssi))
+        return False
+    return True
\ No newline at end of file
diff --git a/acts/framework/acts/test_utils/tel/tel_data_utils.py b/acts/framework/acts/test_utils/tel/tel_data_utils.py
new file mode 100644
index 0000000..bed714d
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_data_utils.py
@@ -0,0 +1,401 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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 random
+import string
+import time
+import warnings
+
+from acts.utils import rand_ascii_str
+from acts.test_utils.tel.tel_defines import *
+from acts.test_utils.tel.tel_test_utils import *
+
+def wifi_tethering_cleanup(log, provider, client_list):
+    """Clean up steps for WiFi Tethering.
+
+    Make sure provider turn off tethering.
+    Make sure clients reset WiFi and turn on cellular data.
+
+    Args:
+        log: log object.
+        provider: android object provide WiFi tethering.
+        client_list: a list of clients using tethered WiFi.
+
+    Returns:
+        True if no error happened. False otherwise.
+    """
+    for client in client_list:
+        client.droid.toggleDataConnection(True)
+        if not WifiUtils.wifi_reset(log, client):
+            log.error("Reset client WiFi failed. {}".format(client.serial))
+            return False
+    if not provider.droid.wifiIsApEnabled():
+        log.error("Provider WiFi tethering stopped.")
+        return False
+    if not WifiUtils.stop_wifi_tethering(log, provider):
+        log.error("Provider strop WiFi tethering failed.")
+        return False
+    return True
+
+def wifi_tethering_setup_teardown(log, provider, client_list,
+                                  ap_band=WifiUtils.WIFI_CONFIG_APBAND_2G,
+                                  check_interval=30, check_iteration=4,
+                                  do_cleanup=True,
+                                  ssid=None, password=None):
+    """Test WiFi Tethering.
+
+    Turn off WiFi on provider and clients.
+    Turn off data and reset WiFi on clients.
+    Verify no Internet access on clients.
+    Turn on WiFi tethering on provider.
+    Clients connect to provider's WiFI.
+    Verify Internet on provider and clients.
+    Tear down WiFi tethering setup and clean up.
+
+    Args:
+        log: log object.
+        provider: android object provide WiFi tethering.
+        client_list: a list of clients using tethered WiFi.
+        ap_band: setup WiFi tethering on 2G or 5G.
+            This is optional, default value is WifiUtils.WIFI_CONFIG_APBAND_2G
+        check_interval: delay time between each around of Internet connection check.
+            This is optional, default value is 30 (seconds).
+        check_iteration: check Internet connection for how many times in total.
+            This is optional, default value is 4 (4 times).
+        do_cleanup: after WiFi tethering test, do clean up to tear down tethering
+            setup or not. This is optional, default value is True.
+        ssid: use this string as WiFi SSID to setup tethered WiFi network.
+            This is optional. Default value is None.
+            If it's None, a random string will be generated.
+        password: use this string as WiFi password to setup tethered WiFi network.
+            This is optional. Default value is None.
+            If it's None, a random string will be generated.
+
+    Returns:
+        True if no error happened. False otherwise.
+    """
+    log.info("--->Start wifi_tethering_setup_teardown<---")
+    log.info("Provider: {}".format(provider.serial))
+    WifiUtils.wifi_toggle_state(log, provider, False)
+
+    if ssid is None:
+        ssid = rand_ascii_str(10)
+    if password is None:
+        password = rand_ascii_str(8)
+
+    # No password
+    if password == "":
+        password = None
+
+    try:
+        for client in client_list:
+            log.info("Client: {}".format(client.serial))
+            WifiUtils.wifi_toggle_state(log, client, False)
+            client.droid.toggleDataConnection(False)
+        log.info("WiFI Tethering: Verify client have no Internet access.")
+        for client in client_list:
+            if verify_http_connection(log, client):
+                log.error("Turn off Data on client fail. {}".format(client.serial))
+                return False
+
+        log.info("WiFI Tethering: Turn on WiFi tethering on {}. SSID: {}, password: {}".
+                 format(provider.serial, ssid, password))
+
+        if not WifiUtils.start_wifi_tethering(log, provider, ssid, password, ap_band):
+            log.error("Provider start WiFi tethering failed.")
+            return False
+        time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+
+        log.info("Provider {} check Internet connection.".
+                 format(provider.serial))
+        if not verify_http_connection(log, provider):
+            return False
+        for client in client_list:
+            log.info("WiFI Tethering: {} connect to WiFi and verify AP band correct.".
+                     format(client.serial))
+            if not ensure_wifi_connected(log, client, ssid, password):
+                log.error("Client connect to WiFi failed.")
+                return False
+
+            wifi_info = client.droid.wifiGetConnectionInfo()
+            if ap_band == WifiUtils.WIFI_CONFIG_APBAND_5G:
+                if wifi_info["is_24ghz"]:
+                    log.error("Expected 5g network. WiFi Info: {}".
+                              format(wifi_info))
+                    return False
+            else:
+                if wifi_info["is_5ghz"]:
+                    log.error("Expected 2g network. WiFi Info: {}".
+                              format(wifi_info))
+                    return False
+
+            log.info("Client{} check Internet connection.".
+                     format(client.serial))
+            if (not wait_for_wifi_data_connection(log, client, True) or not
+                    verify_http_connection(log, client)):
+                log.error("No WiFi Data on client: {}.".format(client.serial))
+                return False
+
+        if not tethering_check_internet_connection(log, provider, client_list,
+                                                   check_interval,
+                                                   check_iteration):
+            return False
+
+    finally:
+        if (do_cleanup and
+            (not wifi_tethering_cleanup(log, provider, client_list))):
+            return False
+    return True
+
+def tethering_check_internet_connection(log, provider, client_list,
+                                        check_interval, check_iteration):
+    """During tethering test, check client(s) and provider Internet connection.
+
+    Do the following for <check_iteration> times:
+        Delay <check_interval> seconds.
+        Check Tethering provider's Internet connection.
+        Check each client's Internet connection.
+
+    Args:
+        log: log object.
+        provider: android object provide WiFi tethering.
+        client_list: a list of clients using tethered WiFi.
+        check_interval: delay time between each around of Internet connection check.
+        check_iteration: check Internet connection for how many times in total.
+
+    Returns:
+        True if no error happened. False otherwise.
+    """
+    for i in range(1, check_iteration):
+        time.sleep(check_interval)
+        log.info("Provider {} check Internet connection after {} seconds.".
+                      format(provider.serial, check_interval*i))
+        if not verify_http_connection(log, provider):
+            return False
+        for client in client_list:
+            log.info("Client {} check Internet connection after {} seconds.".
+                          format(client.serial, check_interval*i))
+            if not verify_http_connection(log, client):
+                return False
+    return True
+
+def wifi_cell_switching(log, ad, wifi_network_ssid, wifi_network_pass,
+                        nw_type=None):
+    """Test data connection network switching when phone camped on <nw_type>.
+
+    Ensure phone is camped on <nw_type>
+    Ensure WiFi can connect to live network,
+    Airplane mode is off, data connection is on, WiFi is on.
+    Turn off WiFi, verify data is on cell and browse to google.com is OK.
+    Turn on WiFi, verify data is on WiFi and browse to google.com is OK.
+    Turn off WiFi, verify data is on cell and browse to google.com is OK.
+
+    Args:
+        log: log object.
+        ad: android object.
+        wifi_network_ssid: ssid for live wifi network.
+        wifi_network_pass: password for live wifi network.
+        nw_type: network rat the phone should be camped on.
+
+    Returns:
+        True if pass.
+    """
+    # TODO: take wifi_cell_switching out of tel_data_utils.py
+    # b/23354769
+
+    try:
+        if not ensure_network_rat(log, ad, nw_type, WAIT_TIME_NW_SELECTION,
+                                  NETWORK_SERVICE_DATA):
+            log.error("Device failed to register in {}".format(nw_type))
+            return False
+
+        # Temporary hack to give phone enough time to register.
+        # TODO: Proper check using SL4A API.
+        time.sleep(WAIT_TIME_BETWEEN_REG_AND_CALL)
+
+        # Ensure WiFi can connect to live network
+        log.info("Make sure phone can connect to live network by WIFI")
+        if not ensure_wifi_connected(log, ad,
+                                     wifi_network_ssid, wifi_network_pass):
+            log.error("WiFi connect fail.")
+            return False
+        log.info("Phone connected to WIFI.")
+
+        log.info("Step1 Airplane Off, WiFi On, Data On.")
+        toggle_airplane_mode(log, ad, False)
+        WifiUtils.wifi_toggle_state(log, ad, True)
+        ad.droid.toggleDataConnection(True)
+        #TODO: Add a check to ensure data routes through wifi here
+
+        log.info("Step2 WiFi is Off, Data is on Cell.")
+        WifiUtils.wifi_toggle_state(log, ad, False)
+        if (not wait_for_cell_data_connection(log, ad, True) or not
+                verify_http_connection(log, ad)):
+            log.error("Data did not return to cell")
+            return False
+
+        log.info("Step3 WiFi is On, Data is on WiFi.")
+        WifiUtils.wifi_toggle_state(log, ad, True)
+        if (not wait_for_wifi_data_connection(log, ad, True) or not
+                verify_http_connection(log, ad)):
+            log.error("Data did not return to WiFi")
+            return False
+
+        log.info("Step4 WiFi is Off, Data is on Cell.")
+        WifiUtils.wifi_toggle_state(log, ad, False)
+        if (not wait_for_cell_data_connection(log, ad, True) or not
+                verify_http_connection(log, ad)):
+            log.error("Data did not return to cell")
+            return False
+        return True
+
+    finally:
+        WifiUtils.wifi_toggle_state(log, ad, False)
+
+def airplane_mode_test(log, ad):
+    """ Test airplane mode basic on Phone and Live SIM.
+
+    Ensure phone attach, data on, WiFi off and verify Internet.
+    Turn on airplane mode to make sure detach.
+    Turn off airplane mode to make sure attach.
+    Verify Internet connection.
+
+    Args:
+        log: log object.
+        ad: android object.
+
+    Returns:
+        True if pass; False if fail.
+    """
+    if not ensure_phones_idle(log, [ad]):
+        log.error("Failed to return phones to idle.")
+        return False
+
+    try:
+        ad.droid.toggleDataConnection(True)
+        WifiUtils.wifi_toggle_state(log, ad, False)
+
+        log.info("Step1: ensure attach")
+        if not toggle_airplane_mode(log, ad, False):
+            log.error("Failed initial attach")
+            return False
+        if not verify_http_connection(log, ad):
+            log.error("Data not available on cell.")
+            return False
+
+        log.info("Step2: enable airplane mode and ensure detach")
+        if not toggle_airplane_mode(log, ad, True):
+            log.error("Failed to enable Airplane Mode")
+            return False
+        if not wait_for_cell_data_connection(log, ad, False):
+            log.error("Failed to disable cell data connection")
+            return False
+        if verify_http_connection(log, ad):
+            log.error("Data available in airplane mode.")
+            return False
+
+        log.info("Step3: disable airplane mode and ensure attach")
+        if not toggle_airplane_mode(log, ad, False):
+            log.error("Failed to disable Airplane Mode")
+            return False
+
+        if not wait_for_cell_data_connection(log, ad, True):
+            log.error("Failed to enable cell data connection")
+            return False
+
+        time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+
+        log.info("Step4 verify internet")
+        return verify_http_connection(log, ad)
+    finally:
+        toggle_airplane_mode(log, ad, False)
+
+def data_connectivity_single_bearer(log, ad, nw_gen):
+    """Test data connection: single-bearer (no voice).
+
+    Turn off airplane mode, enable Cellular Data.
+    Ensure phone data generation is expected.
+    Verify Internet.
+    Disable Cellular Data, verify Internet is inaccessible.
+    Enable Cellular Data, verify Internet.
+
+    Args:
+        log: log object.
+        ad: android object.
+        nw_gen: network generation the phone should on.
+
+    Returns:
+        True if success.
+        False if failed.
+    """
+    ensure_phones_idle(log, [ad])
+
+    if not ensure_network_generation(log, ad, nw_gen,
+            WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_DATA):
+
+        log.error("Device failed to reselect in {}s.".format(
+            WAIT_TIME_NW_SELECTION))
+        return False
+
+    # Temporary hack to give phone enough time to register.
+    # TODO: Proper check using SL4A API.
+    time.sleep(5)
+
+    try:
+        log.info("Step1 Airplane Off, Data On.")
+        toggle_airplane_mode(log, ad, False)
+        ad.droid.toggleDataConnection(True)
+        if not wait_for_cell_data_connection(log, ad, True):
+            log.error("Failed to enable data connection.")
+            return False
+
+        log.info("Step2 Verify internet")
+        if not verify_http_connection(log, ad):
+            log.error("Data not available on cell.")
+            return False
+
+        log.info("Step3 Turn off data and verify not connected.")
+        ad.droid.toggleDataConnection(False)
+        if not wait_for_cell_data_connection(log, ad, False):
+            log.error("Step3 Failed to disable data connection.")
+            return False
+
+        if verify_http_connection(log, ad):
+            log.error("Step3 Data still available when disabled.")
+            return False
+
+        log.info("Step4 Re-enable data.")
+        ad.droid.toggleDataConnection(True)
+        if not wait_for_cell_data_connection(log, ad, True):
+            log.error("Step4 failed to re-enable data.")
+            return False
+        if not verify_http_connection(log, ad):
+            log.error("Data not available on cell.")
+            return False
+
+        if not is_droid_in_network_generation(
+                log, ad, nw_gen,
+                NETWORK_SERVICE_DATA):
+            log.error("Failed: droid is no longer on correct network")
+            log.info("Expected:{}, Current:{}".format(nw_gen,
+                rat_generation_from_type(
+                    get_network_rat_for_subscription(log, ad,
+                    ad.droid.subscriptionGetDefaultSubId(),
+                    NETWORK_SERVICE_DATA))))
+            return False
+        return True
+    finally:
+        ad.droid.toggleDataConnection(True)
diff --git a/acts/framework/acts/test_utils/tel/tel_defines.py b/acts/framework/acts/test_utils/tel/tel_defines.py
new file mode 100644
index 0000000..e012dd2
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_defines.py
@@ -0,0 +1,538 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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.
+
+###############################################
+# TIMERS
+###############################################
+# Max time to wait for phone data/network connection state update
+WAIT_TIME_CONNECTION_STATE_UPDATE = 20
+
+# Max time to wait for network reselection
+WAIT_TIME_NW_SELECTION = 120
+
+# Wait time for call drop
+WAIT_TIME_CALL_DROP = 60
+
+# Time to wait after call setup before declaring
+# that the call is actually successful
+WAIT_TIME_IN_CALL = 15
+
+# Time to wait after phone receive incoming call before phone answer this call.
+WAIT_TIME_ANSWER_CALL = 2
+
+# Time to wait after phone receive incoming call before phone reject this call.
+WAIT_TIME_REJECT_CALL = WAIT_TIME_ANSWER_CALL
+
+# Time to wait after phone receive incoming video call before phone answer this call.
+WAIT_TIME_ANSWER_VIDEO_CALL = WAIT_TIME_ANSWER_CALL
+
+# Time to wait after caller make a call and before
+# callee start ringing
+WAIT_TIME_CALLEE_RINGING = 30
+
+# Time to leave a voice message after callee reject the incoming call
+WAIT_TIME_TO_LEAVE_VOICE_MAIL = 30
+
+# Time to wait after caller make a call and before
+# callee start ringing
+WAIT_TIME_ACCEPT_CALL_TO_OFFHOOK_EVENT = 30
+
+# Time to wait after ad end a call and before get
+# "onCallStatehangedIdle" event
+WAIT_TIME_HANGUP_TO_IDLE_EVENT = 30
+
+# Time to wait after toggle airplane mode and before
+# get expected event
+WAIT_TIME_AIRPLANEMODE_EVENT = 90
+
+# Time to wait after device sent an SMS and before
+# get "onSmsSentSuccess" event
+WAIT_TIME_SMS_SENT_SUCCESS = 60
+
+# Time to wait after MT SMS was sent and before device
+# actually receive this MT SMS.
+WAIT_TIME_SMS_RECEIVE = 120
+
+# (For IMS, e.g. VoLTE-VoLTE, WFC-WFC, VoLTE-WFC test only)
+# Time to wait after call setup before declaring
+# that the call is actually successful
+WAIT_TIME_IN_CALL_FOR_IMS = 30
+
+# Time delay to ensure user actions are performed in
+# 'human' time rather than at the speed of the script
+WAIT_TIME_ANDROID_STATE_SETTLING = 1
+
+# Time to wait after registration to ensure the phone
+# has sufficient time to reconfigure based on new network
+WAIT_TIME_BETWEEN_REG_AND_CALL = 5
+
+# Time to wait for IMS registration
+WAIT_TIME_IMS_REGISTRATION = 120
+
+# Max time to wait after initiating a call for telecom to report in-call
+WAIT_TIME_CALL_INITIATION = 15
+
+# Time to wait for VZW phone in phone setup function
+VZW_WAIT_TIME_IN_PHONE_SETUP_FUNC = 30
+
+# FIXME : This timer should only be used for wait after IMS Registration
+# Max time to wait for VoLTE enabled flag to be True
+WAIT_TIME_VOLTE_ENABLED = WAIT_TIME_IMS_REGISTRATION + 20
+
+# FIXME : This timer should only be used for wait after IMS Registration
+# Max time to wait for WFC enabled flag to be True
+WAIT_TIME_WFC_ENABLED = WAIT_TIME_IMS_REGISTRATION + 50
+
+# Maximum Wait for WiFi Manager to Connect to an AP
+WAIT_TIME_WIFI_CONNECTION = 30
+
+# During wifi tethering, wait time for data status change.
+WAIT_TIME_FOR_DATA_STATUS_CHANGE_DURING_WIFI_TETHERING = 30
+
+# Maximum Wait time for Video Session Modify Messaging
+WAIT_TIME_VIDEO_SESSION_EVENT = 10
+
+# Max time to wait after a network connection for ConnectivityManager to
+# report a working user plane data connection
+WAIT_TIME_USER_PLANE_DATA = 20
+
+# Timeout value (second) for tethering entitlement check
+TETHERING_ENTITLEMENT_CHECK_TIMEOUT = 15
+
+# invalid SIM slot index
+INVALID_SIM_SLOT_INDEX = -1
+
+# WiFI RSSI is -127 if WiFi is not connected
+INVALID_WIFI_RSSI = -127
+
+# MAX and MIN value for attenuator settings
+ATTEN_MAX_VALUE = 90
+ATTEN_MIN_VALUE = 0
+
+MAX_RSSI_RESERVED_VALUE = 100
+MIN_RSSI_RESERVED_VALUE = -200
+
+# cellular weak RSSI value
+CELL_WEAK_RSSI_VALUE = -120
+# cellular strong RSSI value
+CELL_STRONG_RSSI_VALUE = -70
+# WiFi weak RSSI value
+WIFI_WEAK_RSSI_VALUE = -80
+
+# Wait time for rssi calibration.
+# This is the delay between <WiFi Connected> and <Turn on Screen to get RSSI>.
+WAIT_TIME_FOR_WIFI_RSSI_CALIBRATION_WIFI_CONNECTED = 10
+# This is the delay between <Turn on Screen> and <Call API to get WiFi RSSI>.
+WAIT_TIME_FOR_WIFI_RSSI_CALIBRATION_SCREEN_ON = 2
+
+# These are used in phone_number_formatter
+PHONE_NUMBER_STRING_FORMAT_7_DIGIT = 7
+PHONE_NUMBER_STRING_FORMAT_10_DIGIT = 10
+PHONE_NUMBER_STRING_FORMAT_11_DIGIT = 11
+PHONE_NUMBER_STRING_FORMAT_12_DIGIT = 12
+
+# MAX screen-on time during test (in unit of second)
+MAX_SCREEN_ON_TIME = 1800
+
+# In Voice Mail box, press this digit to delete one message.
+VOICEMAIL_DELETE_DIGIT = '7'
+# MAX number of saved voice mail in voice mail box.
+MAX_SAVED_VOICE_MAIL = 25
+# Time to wait for each operation on voice mail box.
+VOICE_MAIL_SERVER_RESPONSE_DELAY = 10
+# Time to wait for voice mail count report correct result.
+MAX_WAIT_TIME_FOR_VOICE_MAIL_COUNT = 30
+
+# Time to wait after registration to ensure the phone
+# has sufficient time to reconfigure based on new network in Anritsu
+WAIT_TIME_ANRITSU_REG_AND_CALL = 10
+
+# Time to wait after registration before sending a command to Anritsu
+# to ensure the phone has sufficient time to reconfigure based on new
+# network in Anritsu
+WAIT_TIME_ANRITSU_REG_AND_OPER = 10
+
+# SIM1 slot index
+SIM1_SLOT_INDEX = 0
+
+# SIM2 slot index
+SIM2_SLOT_INDEX = 1
+
+# Data SIM change time
+WAIT_TIME_DATA_SUB_CHANGE = 150
+
+# Wait time for radio to up and running after reboot
+WAIT_TIME_AFTER_REBOOT = 10
+
+# Wait time for tethering test after reboot
+WAIT_TIME_FOR_TETHERING_AFTER_REBOOT = 10
+
+# invalid Subscription ID
+INVALID_SUB_ID = -1
+
+AOSP_PREFIX = "aosp_"
+
+INCALL_UI_DISPLAY_FOREGROUND = "foreground"
+INCALL_UI_DISPLAY_BACKGROUND = "background"
+INCALL_UI_DISPLAY_DEFAULT = "default"
+
+NETWORK_CONNECTION_TYPE_WIFI = 'wifi'
+NETWORK_CONNECTION_TYPE_CELL = 'cell'
+NETWORK_CONNECTION_TYPE_MMS = 'mms'
+NETWORK_CONNECTION_TYPE_HIPRI = 'hipri'
+NETWORK_CONNECTION_TYPE_UNKNOWN = 'unknown'
+
+TETHERING_MODE_WIFI = 'wifi'
+
+NETWORK_SERVICE_VOICE = 'voice'
+NETWORK_SERVICE_DATA = 'data'
+
+CARRIER_VZW = 'vzw'
+CARRIER_ATT = 'att'
+CARRIER_TMO = 'tmo'
+CARRIER_SPT = 'spt'
+CARRIER_UNKNOWN = 'unknown'
+
+RAT_FAMILY_CDMA = 'cdma'
+RAT_FAMILY_CDMA2000 = 'cdma2000'
+RAT_FAMILY_IDEN = 'iden'
+RAT_FAMILY_GSM = 'gsm'
+RAT_FAMILY_UMTS = 'umts'
+RAT_FAMILY_WLAN = 'wlan'
+RAT_FAMILY_LTE = 'lte'
+RAT_FAMILY_UNKNOWN = 'unknown'
+
+CAPABILITY_PHONE = 'phone'
+CAPABILITY_VOLTE = 'volte'
+CAPABILITY_VT = 'vt'
+CAPABILITY_WFC = 'wfc'
+CAPABILITY_MSIM = 'msim'
+CAPABILITY_OMADM = 'omadm'
+
+# Constant for operation direction
+MOBILE_ORIGINATED = "MO"
+MOBILE_TERMINATED = "MT"
+
+# Constant for call teardown side
+CALL_TEARDOWN_PHONE = "PHONE"
+CALL_TEARDOWN_REMOTE = "REMOTE"
+
+WIFI_VERBOSE_LOGGING_ENABLED = 1
+WIFI_VERBOSE_LOGGING_DISABLED = 0
+
+"""
+Begin shared constant define for both Python and Java
+"""
+
+# Constant for WiFi Calling WFC mode
+WFC_MODE_WIFI_ONLY = "WIFI_ONLY"
+WFC_MODE_CELLULAR_PREFERRED = "CELLULAR_PREFERRED"
+WFC_MODE_WIFI_PREFERRED = "WIFI_PREFERRED"
+WFC_MODE_DISABLED = "DISABLED"
+WFC_MODE_UNKNOWN = "UNKNOWN"
+
+# Constant for Video Telephony VT state
+VT_STATE_AUDIO_ONLY = "AUDIO_ONLY"
+VT_STATE_TX_ENABLED = "TX_ENABLED"
+VT_STATE_RX_ENABLED = "RX_ENABLED"
+VT_STATE_BIDIRECTIONAL = "BIDIRECTIONAL"
+VT_STATE_TX_PAUSED = "TX_PAUSED"
+VT_STATE_RX_PAUSED = "RX_PAUSED"
+VT_STATE_BIDIRECTIONAL_PAUSED = "BIDIRECTIONAL_PAUSED"
+VT_STATE_STATE_INVALID = "INVALID"
+
+# Constant for Video Telephony Video quality
+VT_VIDEO_QUALITY_DEFAULT = "DEFAULT"
+VT_VIDEO_QUALITY_UNKNOWN = "UNKNOWN"
+VT_VIDEO_QUALITY_HIGH = "HIGH"
+VT_VIDEO_QUALITY_MEDIUM = "MEDIUM"
+VT_VIDEO_QUALITY_LOW = "LOW"
+VT_VIDEO_QUALITY_INVALID = "INVALID"
+
+# Constant for Call State (for call object)
+CALL_STATE_ACTIVE = "ACTIVE"
+CALL_STATE_NEW = "NEW"
+CALL_STATE_DIALING = "DIALING"
+CALL_STATE_RINGING = "RINGING"
+CALL_STATE_HOLDING = "HOLDING"
+CALL_STATE_DISCONNECTED = "DISCONNECTED"
+CALL_STATE_PRE_DIAL_WAIT = "PRE_DIAL_WAIT"
+CALL_STATE_CONNECTING = "CONNECTING"
+CALL_STATE_DISCONNECTING = "DISCONNECTING"
+CALL_STATE_UNKNOWN = "UNKNOWN"
+CALL_STATE_INVALID = "INVALID"
+
+# Constant for PRECISE Call State (for call object)
+PRECISE_CALL_STATE_ACTIVE = "ACTIVE"
+PRECISE_CALL_STATE_ALERTING = "ALERTING"
+PRECISE_CALL_STATE_DIALING = "DIALING"
+PRECISE_CALL_STATE_INCOMING = "INCOMING"
+PRECISE_CALL_STATE_HOLDING = "HOLDING"
+PRECISE_CALL_STATE_DISCONNECTED = "DISCONNECTED"
+PRECISE_CALL_STATE_WAITING = "WAITING"
+PRECISE_CALL_STATE_DISCONNECTING = "DISCONNECTING"
+PRECISE_CALL_STATE_IDLE = "IDLE"
+PRECISE_CALL_STATE_UNKNOWN = "UNKNOWN"
+PRECISE_CALL_STATE_INVALID = "INVALID"
+
+# Constant for DC POWER STATE
+DC_POWER_STATE_LOW = "LOW"
+DC_POWER_STATE_HIGH = "HIGH"
+DC_POWER_STATE_MEDIUM = "MEDIUM"
+DC_POWER_STATE_UNKNOWN = "UNKNOWN"
+
+# Constant for Audio Route
+AUDIO_ROUTE_EARPIECE = "EARPIECE"
+AUDIO_ROUTE_BLUETOOTH = "BLUETOOTH"
+AUDIO_ROUTE_SPEAKER = "SPEAKER"
+AUDIO_ROUTE_WIRED_HEADSET = "WIRED_HEADSET"
+AUDIO_ROUTE_WIRED_OR_EARPIECE = "WIRED_OR_EARPIECE"
+
+# Constant for Call Capability
+CALL_CAPABILITY_HOLD = "HOLD"
+CALL_CAPABILITY_SUPPORT_HOLD = "SUPPORT_HOLD"
+CALL_CAPABILITY_MERGE_CONFERENCE = "MERGE_CONFERENCE"
+CALL_CAPABILITY_SWAP_CONFERENCE = "SWAP_CONFERENCE"
+CALL_CAPABILITY_UNUSED_1 = "UNUSED_1"
+CALL_CAPABILITY_RESPOND_VIA_TEXT = "RESPOND_VIA_TEXT"
+CALL_CAPABILITY_MUTE = "MUTE"
+CALL_CAPABILITY_MANAGE_CONFERENCE = "MANAGE_CONFERENCE"
+CALL_CAPABILITY_SUPPORTS_VT_LOCAL_RX = "SUPPORTS_VT_LOCAL_RX"
+CALL_CAPABILITY_SUPPORTS_VT_LOCAL_TX = "SUPPORTS_VT_LOCAL_TX"
+CALL_CAPABILITY_SUPPORTS_VT_LOCAL_BIDIRECTIONAL = "SUPPORTS_VT_LOCAL_BIDIRECTIONAL"
+CALL_CAPABILITY_SUPPORTS_VT_REMOTE_RX = "SUPPORTS_VT_REMOTE_RX"
+CALL_CAPABILITY_SUPPORTS_VT_REMOTE_TX = "SUPPORTS_VT_REMOTE_TX"
+CALL_CAPABILITY_SUPPORTS_VT_REMOTE_BIDIRECTIONAL = "SUPPORTS_VT_REMOTE_BIDIRECTIONAL"
+CALL_CAPABILITY_SEPARATE_FROM_CONFERENCE = "SEPARATE_FROM_CONFERENCE"
+CALL_CAPABILITY_DISCONNECT_FROM_CONFERENCE = "DISCONNECT_FROM_CONFERENCE"
+CALL_CAPABILITY_SPEED_UP_MT_AUDIO = "SPEED_UP_MT_AUDIO"
+CALL_CAPABILITY_CAN_UPGRADE_TO_VIDEO = "CAN_UPGRADE_TO_VIDEO"
+CALL_CAPABILITY_CAN_PAUSE_VIDEO = "CAN_PAUSE_VIDEO"
+CALL_CAPABILITY_UNKOWN = "UNKOWN"
+
+# Constant for Call Property
+CALL_PROPERTY_HIGH_DEF_AUDIO = "HIGH_DEF_AUDIO"
+CALL_PROPERTY_CONFERENCE = "CONFERENCE"
+CALL_PROPERTY_GENERIC_CONFERENCE = "GENERIC_CONFERENCE"
+CALL_PROPERTY_WIFI = "WIFI"
+CALL_PROPERTY_EMERGENCY_CALLBACK_MODE = "EMERGENCY_CALLBACK_MODE"
+CALL_PROPERTY_UNKNOWN = "UNKNOWN"
+
+# Constant for Call Presentation
+CALL_PRESENTATION_ALLOWED = "ALLOWED"
+CALL_PRESENTATION_RESTRICTED = "RESTRICTED"
+CALL_PRESENTATION_PAYPHONE = "PAYPHONE"
+CALL_PRESENTATION_UNKNOWN = "UNKNOWN"
+
+
+# Constant for Network RAT
+RAT_IWLAN = "IWLAN"
+RAT_LTE = "LTE"
+RAT_4G = "4G"
+RAT_3G = "3G"
+RAT_2G = "2G"
+RAT_WCDMA = "WCDMA"
+RAT_UMTS = "UMTS"
+RAT_1XRTT = "1XRTT"
+RAT_EDGE = "EDGE"
+RAT_GPRS = "GPRS"
+RAT_HSDPA = "HSDPA"
+RAT_HSUPA = "HSUPA"
+RAT_CDMA = "CDMA"
+RAT_EVDO = "EVDO"
+RAT_EVDO_0 = "EVDO_0"
+RAT_EVDO_A = "EVDO_A"
+RAT_EVDO_B = "EVDO_B"
+RAT_IDEN = "IDEN"
+RAT_EHRPD = "EHRPD"
+RAT_HSPA = "HSPA"
+RAT_HSPAP = "HSPAP"
+RAT_GSM = "GSM"
+RAT_TD_SCDMA = "TD_SCDMA"
+RAT_GLOBAL = "GLOBAL"
+RAT_UNKNOWN = "UNKNOWN"
+
+# NETWORK_MODE_* See ril.h RIL_REQUEST_SET_PREFERRED_NETWORK_TYPE
+NETWORK_MODE_WCDMA_PREF     = 0 # GSM/WCDMA (WCDMA preferred)
+NETWORK_MODE_GSM_ONLY       = 1 # GSM only
+NETWORK_MODE_WCDMA_ONLY     = 2 # WCDMA only
+NETWORK_MODE_GSM_UMTS       = 3 # GSM/WCDMA (auto mode, according to PRL)
+                                #     AVAILABLE Application Settings menu
+NETWORK_MODE_CDMA           = 4 # CDMA and EvDo (auto mode, according to PRL)
+                                #    AVAILABLE Application Settings menu
+NETWORK_MODE_CDMA_NO_EVDO   = 5 # CDMA only
+NETWORK_MODE_EVDO_NO_CDMA   = 6 # EvDo only
+NETWORK_MODE_GLOBAL         = 7 # GSM/WCDMA, CDMA, and EvDo
+                                #    (auto mode, according to PRL)
+                                #     AVAILABLE Application Settings menu
+NETWORK_MODE_LTE_CDMA_EVDO  = 8 # LTE, CDMA and EvDo
+NETWORK_MODE_LTE_GSM_WCDMA  = 9 # LTE, GSM/WCDMA
+NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA = 10 # LTE, CDMA, EvDo, GSM/WCDMA
+NETWORK_MODE_LTE_ONLY       = 11 # LTE Only mode
+NETWORK_MODE_LTE_WCDMA      = 12 # LTE/WCDMA
+NETWORK_MODE_TDSCDMA_ONLY            = 13 # TD-SCDMA only
+NETWORK_MODE_TDSCDMA_WCDMA           = 14 # TD-SCDMA and WCDMA
+NETWORK_MODE_LTE_TDSCDMA             = 15 # TD-SCDMA and LTE
+NETWORK_MODE_TDSCDMA_GSM             = 16 # TD-SCDMA and GSM
+NETWORK_MODE_LTE_TDSCDMA_GSM         = 17 # TD-SCDMA,GSM and LTE
+NETWORK_MODE_TDSCDMA_GSM_WCDMA       = 18 # TD-SCDMA, GSM/WCDMA
+NETWORK_MODE_LTE_TDSCDMA_WCDMA       = 19 # TD-SCDMA, WCDMA and LTE
+NETWORK_MODE_LTE_TDSCDMA_GSM_WCDMA   = 20 # TD-SCDMA, GSM/WCDMA and LTE
+NETWORK_MODE_TDSCDMA_CDMA_EVDO_GSM_WCDMA  = 21 # TD-SCDMA,EvDo,CDMA,GSM/WCDMA
+NETWORK_MODE_LTE_TDSCDMA_CDMA_EVDO_GSM_WCDMA = 22 # TD-SCDMA/LTE/GSM/WCDMA,
+                                                  #    CDMA, and EvDo
+
+# Constant for Phone Type
+PHONE_TYPE_GSM = "GSM"
+PHONE_TYPE_NONE = "NONE"
+PHONE_TYPE_CDMA = "CDMA"
+PHONE_TYPE_SIP = "SIP"
+
+# Constant for SIM State
+SIM_STATE_READY = "READY"
+SIM_STATE_UNKNOWN = "UNKNOWN"
+SIM_STATE_ABSENT = "ABSENT"
+SIM_STATE_PUK_REQUIRED = "PUK_REQUIRED"
+SIM_STATE_PIN_REQUIRED = "PIN_REQUIRED"
+SIM_STATE_NETWORK_LOCKED = "NETWORK_LOCKED"
+SIM_STATE_NOT_READY = "NOT_READY"
+SIM_STATE_PERM_DISABLED = "PERM_DISABLED"
+SIM_STATE_CARD_IO_ERROR = "CARD_IO_ERROR"
+
+# Constant for Data Connection State
+DATA_STATE_CONNECTED = "CONNECTED"
+DATA_STATE_DISCONNECTED = "DISCONNECTED"
+DATA_STATE_CONNECTING = "CONNECTING"
+DATA_STATE_SUSPENDED = "SUSPENDED"
+DATA_STATE_UNKNOWN = "UNKNOWN"
+
+# Constant for Telephony Manager Call State
+TELEPHONY_STATE_RINGING = "RINGING"
+TELEPHONY_STATE_IDLE = "IDLE"
+TELEPHONY_STATE_OFFHOOK = "OFFHOOK"
+TELEPHONY_STATE_UNKNOWN = "UNKNOWN"
+
+# Constant for TTY Mode
+TTY_MODE_FULL = "FULL"
+TTY_MODE_HCO = "HCO"
+TTY_MODE_OFF = "OFF"
+TTY_MODE_VCO ="VCO"
+
+# Constant for Service State
+SERVICE_STATE_EMERGENCY_ONLY = "EMERGENCY_ONLY"
+SERVICE_STATE_IN_SERVICE = "IN_SERVICE"
+SERVICE_STATE_OUT_OF_SERVICE = "OUT_OF_SERVICE"
+SERVICE_STATE_POWER_OFF = "POWER_OFF"
+SERVICE_STATE_UNKNOWN = "UNKNOWN"
+
+# Constant for VoLTE Hand-over Service State
+VOLTE_SERVICE_STATE_HANDOVER_STARTED = "STARTED"
+VOLTE_SERVICE_STATE_HANDOVER_COMPLETED = "COMPLETED"
+VOLTE_SERVICE_STATE_HANDOVER_FAILED = "FAILED"
+VOLTE_SERVICE_STATE_HANDOVER_CANCELED = "CANCELED"
+VOLTE_SERVICE_STATE_HANDOVER_UNKNOWN = "UNKNOWN"
+
+# Constant for precise call state state listen level
+PRECISE_CALL_STATE_LISTEN_LEVEL_FOREGROUND = "FOREGROUND"
+PRECISE_CALL_STATE_LISTEN_LEVEL_RINGING = "RINGING"
+PRECISE_CALL_STATE_LISTEN_LEVEL_BACKGROUND = "BACKGROUND"
+
+# Constant for Messaging Event Name
+EventSmsDeliverSuccess = "SmsDeliverSuccess"
+EventSmsDeliverFailure = "SmsDeliverFailure"
+EventSmsSentSuccess = "SmsSentSuccess"
+EventSmsSentFailure = "SmsSentFailure"
+EventSmsReceived = "SmsReceived"
+EventMmsSentSuccess = "MmsSentSuccess"
+EventMmsSentFailure = "MmsSentFailure"
+EventMmsDownloaded = "MmsDownloaded"
+EventWapPushReceived = "WapPushReceived"
+EventDataSmsReceived = "DataSmsReceived"
+EventCmasReceived = "CmasReceived"
+EventEtwsReceived = "EtwsReceived"
+
+# Constant for Telecom Call Event Name
+EventTelecomCallStateChanged = "TelecomCallStateChanged"
+EventTelecomCallParentChanged = "TelecomCallParentChanged"
+EventTelecomCallChildrenChanged = "TelecomCallChildrenChanged"
+EventTelecomCallDetailsChanged = "TelecomCallDetailsChanged"
+EventTelecomCallCannedTextResponsesLoaded = "TelecomCallCannedTextResponsesLoaded"
+EventTelecomCallPostDialWait = "TelecomCallPostDialWait"
+EventTelecomCallVideoCallChanged = "TelecomCallVideoCallChanged"
+EventTelecomCallDestroyed = "TelecomCallDestroyed"
+EventTelecomCallConferenceableCallsChanged = "TelecomCallConferenceableCallsChanged"
+
+# Constant for Video Call Event Name
+EventTelecomVideoCallSessionModifyRequestReceived = "TelecomVideoCallSessionModifyRequestReceived"
+EventTelecomVideoCallSessionModifyResponseReceived = "TelecomVideoCallSessionModifyResponseReceived"
+EventTelecomVideoCallSessionEvent = "TelecomVideoCallSessionEvent"
+EventTelecomVideoCallPeerDimensionsChanged = "TelecomVideoCallPeerDimensionsChanged"
+EventTelecomVideoCallVideoQualityChanged = "TelecomVideoCallVideoQualityChanged"
+EventTelecomVideoCallDataUsageChanged = "TelecomVideoCallDataUsageChanged"
+EventTelecomVideoCallCameraCapabilities = "TelecomVideoCallCameraCapabilities"
+
+# Constant for Video Call Call-Back Event Name
+EventSessionModifyRequestRceived = "SessionModifyRequestRceived"
+EventSessionModifyResponsetRceived = "SessionModifyResponsetRceived"
+EventSessionEvent = "SessionEvent"
+EventPeerDimensionsChanged = "PeerDimensionsChanged"
+EventVideoQualityChanged = "VideoQualityChanged"
+EventDataUsageChanged = "DataUsageChanged"
+EventCameraCapabilitiesChanged = "CameraCapabilitiesChanged"
+EventInvalid = "Invalid"
+
+# Constant for Video Call Session Event Name
+SessionEventRxPause = "SessionEventRxPause"
+SessionEventRxResume = "SessionEventRxResume"
+SessionEventTxStart = "SessionEventTxStart"
+SessionEventTxStop = "SessionEventTxStop"
+SessionEventCameraFailure = "SessionEventCameraFailure"
+SessionEventCameraReady = "SessionEventCameraReady"
+SessionEventUnknown = "SessionEventUnknown"
+
+# Constant for Other Event Name
+EventCallStateChanged = "CallStateChanged"
+EventPreciseStateChanged = "PreciseStateChanged"
+EventDataConnectionRealTimeInfoChanged = "DataConnectionRealTimeInfoChanged"
+EventDataConnectionStateChanged = "DataConnectionStateChanged"
+EventServiceStateChanged = "ServiceStateChanged"
+EventVolteServiceStateChanged = "VolteServiceStateChanged"
+EventMessageWaitingIndicatorChanged = "MessageWaitingIndicatorChanged"
+EventConnectivityChanged = "ConnectivityChanged"
+
+# Constant for Packet Keep Alive Call Back
+PacketKeepaliveCallBack = "PacketKeepliveCallBack"
+PacketKeepaliveCallBackStarted = "Started"
+PacketKeepaliveCallBackStopped = "Stopped"
+PacketKeepaliveCallBackError = "Error"
+PacketKeepaliveCallBackInvalid = "Invalid"
+
+# Constant for Network Call Back
+NetworkCallBack = "NetworkCallBack"
+NetworkCallBackPreCheck = "PreCheck"
+NetworkCallBackAvailable = "Available"
+NetworkCallBackLosing = "Losing"
+NetworkCallBackLost = "Lost"
+NetworkCallBackUnavailable = "Unavailable"
+NetworkCallBackCapabilitiesChanged = "CapabilitiesChanged"
+NetworkCallBackSuspended = "Suspended"
+NetworkCallBackResumed = "Resumed"
+NetworkCallBackLinkPropertiesChanged = "LinkPropertiesChanged"
+NetworkCallBackInvalid = "Invalid"
+
+"""
+End shared constant define for both Python and Java
+"""
diff --git a/acts/framework/acts/test_utils/tel/tel_lookup_tables.py b/acts/framework/acts/test_utils/tel/tel_lookup_tables.py
new file mode 100644
index 0000000..93dce95
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_lookup_tables.py
@@ -0,0 +1,416 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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 acts.utils import NexusModelNames
+from acts.test_utils.tel import tel_defines
+
+# TODO: rename to remove the word "type"
+# rat_family_from_rat()
+def rat_family_from_type(rat_type):
+    return _TelTables.technology_tbl[rat_type]['rat_family']
+
+# TODO: rename to remove the word "type"
+# rat_generation_from_rat()
+def rat_generation_from_type(rat_type):
+    return _TelTables.technology_tbl[rat_type]['generation']
+
+def operator_name_from_plmn_id(plmn_id):
+    return _TelTables.operator_id_to_name[plmn_id]
+
+def is_valid_rat(rat_type):
+    return True if rat_type in _TelTables.technology_tbl else False
+
+def is_valid_generation(gen):
+    return True if gen in _TelTables.technology_gen_tbl else False
+
+def is_rat_svd_capable(rat):
+    return _TelTables.technology_tbl[rat]["simultaneous_voice_data"]
+
+def network_mode_by_operator_generation(operator, generation):
+    try:
+        return _TelTables.operator_network_mode_preference_tbl[operator][generation]
+    except KeyError:
+        return None
+
+# TODO: Create a networking lookup tables file and move
+def connection_type_from_type_string(input_string):
+    if input_string in _ConnectionTables.connection_type_tbl:
+        return _ConnectionTables.connection_type_tbl[input_string]
+    return tel_defines.NETWORK_CONNECTION_TYPE_UNKNOWN
+
+def is_user_plane_data_type(connection_type):
+    if connection_type in _ConnectionTables.user_plane_data_type:
+      return _ConnectionTables.user_plane_data_type[connection_type]
+    return False
+
+# For TMO, to check if voice mail count is correct after leaving a new voice message.
+def check_tmo_voice_mail_count(voice_mail_count_before, voice_mail_count_after):
+    return (voice_mail_count_after == -1)
+
+# For ATT, to check if voice mail count is correct after leaving a new voice message.
+def check_att_voice_mail_count(voice_mail_count_before, voice_mail_count_after):
+    return (voice_mail_count_after == (voice_mail_count_before + 1))
+
+# For SPT, to check if voice mail count is correct after leaving a new voice message.
+def check_spt_voice_mail_count(voice_mail_count_before, voice_mail_count_after):
+    return (voice_mail_count_after == (voice_mail_count_before + 1))
+
+# For TMO, get the voice mail number
+def get_tmo_voice_mail_number():
+    return "123"
+
+# For ATT, get the voice mail number
+def get_att_voice_mail_number():
+    return None
+
+# For SPT, get the voice mail number
+def get_spt_voice_mail_number():
+    return None
+
+def get_voice_mail_number_function(operator):
+    return _TelTables.voice_mail_number_get_function_tbl[operator]
+
+def get_voice_mail_count_check_function(operator):
+    return _TelTables.voice_mail_count_check_function_tbl[operator]
+
+class _ConnectionTables():
+    connection_type_tbl = {
+        'WIFI': tel_defines.NETWORK_CONNECTION_TYPE_WIFI,
+        'WIFI_P2P': tel_defines.NETWORK_CONNECTION_TYPE_WIFI,
+        'MOBILE': tel_defines.NETWORK_CONNECTION_TYPE_CELL,
+        'MOBILE_DUN': tel_defines.NETWORK_CONNECTION_TYPE_CELL,
+        'MOBILE_HIPRI': tel_defines.NETWORK_CONNECTION_TYPE_HIPRI,
+        # TODO: add support for 'MOBILE_SUPL', 'MOBILE_HIPRI', 'MOBILE_FOTA',
+        # 'MOBILE_IMS', 'MOBILE_CBS', 'MOBILE_IA', 'MOBILE_EMERGENCY'
+        'MOBILE_MMS': tel_defines.NETWORK_CONNECTION_TYPE_MMS
+    }
+
+    user_plane_data_type = {
+        tel_defines.NETWORK_CONNECTION_TYPE_WIFI: True,
+        tel_defines.NETWORK_CONNECTION_TYPE_CELL: False,
+        tel_defines.NETWORK_CONNECTION_TYPE_MMS: False,
+        tel_defines.NETWORK_CONNECTION_TYPE_UNKNOWN: False
+    }
+
+
+class _TelTables():
+    # Operator id mapping to operator name
+    # Reference: Pages 43-50 in
+    # https://www.itu.int/dms_pub/itu-t/opb/sp/T-SP-E.212B-2013-PDF-E.pdf [2013]
+
+    operator_id_to_name = {
+
+        #VZW (Verizon Wireless)
+        '310010': tel_defines.CARRIER_VZW,
+        '310012': tel_defines.CARRIER_VZW,
+        '310013': tel_defines.CARRIER_VZW,
+        '310590': tel_defines.CARRIER_VZW,
+        '310890': tel_defines.CARRIER_VZW,
+        '310910': tel_defines.CARRIER_VZW,
+        '310110': tel_defines.CARRIER_VZW,
+        '311270': tel_defines.CARRIER_VZW,
+        '311271': tel_defines.CARRIER_VZW,
+        '311272': tel_defines.CARRIER_VZW,
+        '311273': tel_defines.CARRIER_VZW,
+        '311274': tel_defines.CARRIER_VZW,
+        '311275': tel_defines.CARRIER_VZW,
+        '311276': tel_defines.CARRIER_VZW,
+        '311277': tel_defines.CARRIER_VZW,
+        '311278': tel_defines.CARRIER_VZW,
+        '311279': tel_defines.CARRIER_VZW,
+        '311280': tel_defines.CARRIER_VZW,
+        '311281': tel_defines.CARRIER_VZW,
+        '311282': tel_defines.CARRIER_VZW,
+        '311283': tel_defines.CARRIER_VZW,
+        '311284': tel_defines.CARRIER_VZW,
+        '311285': tel_defines.CARRIER_VZW,
+        '311286': tel_defines.CARRIER_VZW,
+        '311287': tel_defines.CARRIER_VZW,
+        '311288': tel_defines.CARRIER_VZW,
+        '311289': tel_defines.CARRIER_VZW,
+        '311390': tel_defines.CARRIER_VZW,
+        '311480': tel_defines.CARRIER_VZW,
+        '311481': tel_defines.CARRIER_VZW,
+        '311482': tel_defines.CARRIER_VZW,
+        '311483': tel_defines.CARRIER_VZW,
+        '311484': tel_defines.CARRIER_VZW,
+        '311485': tel_defines.CARRIER_VZW,
+        '311486': tel_defines.CARRIER_VZW,
+        '311487': tel_defines.CARRIER_VZW,
+        '311488': tel_defines.CARRIER_VZW,
+        '311489': tel_defines.CARRIER_VZW,
+
+        #TMO (T-Mobile USA)
+        '310160': tel_defines.CARRIER_TMO,
+        '310200': tel_defines.CARRIER_TMO,
+        '310210': tel_defines.CARRIER_TMO,
+        '310220': tel_defines.CARRIER_TMO,
+        '310230': tel_defines.CARRIER_TMO,
+        '310240': tel_defines.CARRIER_TMO,
+        '310250': tel_defines.CARRIER_TMO,
+        '310260': tel_defines.CARRIER_TMO,
+        '310270': tel_defines.CARRIER_TMO,
+        '310310': tel_defines.CARRIER_TMO,
+        '310490': tel_defines.CARRIER_TMO,
+        '310660': tel_defines.CARRIER_TMO,
+        '310800': tel_defines.CARRIER_TMO,
+
+        #ATT (AT&T and Cingular)
+        '310070': tel_defines.CARRIER_ATT,
+        '310560': tel_defines.CARRIER_ATT,
+        '310670': tel_defines.CARRIER_ATT,
+        '310680': tel_defines.CARRIER_ATT,
+        '310150': tel_defines.CARRIER_ATT,   #Cingular
+        '310170': tel_defines.CARRIER_ATT,   #Cingular
+        '310410': tel_defines.CARRIER_ATT,   #Cingular
+        '311180': tel_defines.CARRIER_ATT,   #Cingular Licensee Pacific Telesis Mobile Services, LLC
+
+        #Sprint (and Sprint-Nextel)
+        '310120': tel_defines.CARRIER_SPT,
+        '311490': tel_defines.CARRIER_SPT,
+        '311870': tel_defines.CARRIER_SPT,
+        '311880': tel_defines.CARRIER_SPT,
+        '312190': tel_defines.CARRIER_SPT,   #Sprint-Nextel Communications Inc
+        '316010': tel_defines.CARRIER_SPT    #Sprint-Nextel Communications Inc
+    }
+
+    technology_gen_tbl = [tel_defines.RAT_2G, tel_defines.RAT_3G,
+                          tel_defines.RAT_4G]
+
+    technology_tbl = {
+        tel_defines.RAT_1XRTT: {
+            'is_voice_rat': True,
+            'is_data_rat': False,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_CDMA2000
+        },
+        tel_defines.RAT_EDGE: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_2G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_GSM
+        },
+        tel_defines.RAT_GPRS: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_2G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_GSM
+        },
+        tel_defines.RAT_GSM: {
+            'is_voice_rat': True,
+            'is_data_rat': False,
+            'generation': tel_defines.RAT_2G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_GSM
+        },
+        tel_defines.RAT_UMTS: {
+            'is_voice_rat': True,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': True,
+            'rat_family': tel_defines.RAT_FAMILY_UMTS
+        },
+        tel_defines.RAT_WCDMA: {
+            'is_voice_rat': True,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': True,
+            'rat_family': tel_defines.RAT_FAMILY_UMTS
+        },
+        tel_defines.RAT_HSDPA: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_UMTS
+        },
+        tel_defines.RAT_HSUPA: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_UMTS
+        },
+        # TODO: Confirm whether this is for IS-95
+        tel_defines.RAT_CDMA: {
+            'is_voice_rat': True,
+            'is_data_rat': False,
+            'generation': tel_defines.RAT_2G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_CDMA
+        },
+        tel_defines.RAT_EVDO: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_CDMA2000
+        },
+        tel_defines.RAT_EVDO_0: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_CDMA2000
+        },
+        tel_defines.RAT_EVDO_A: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_CDMA2000
+        },
+        tel_defines.RAT_EVDO_B: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_CDMA2000
+        },
+        tel_defines.RAT_IDEN: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_2G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_IDEN
+        },
+        tel_defines.RAT_LTE: {
+            'is_voice_rat': True,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_4G,
+            'simultaneous_voice_data': True,
+            'rat_family': tel_defines.RAT_FAMILY_LTE
+        },
+        tel_defines.RAT_EHRPD: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_CDMA2000
+        },
+        tel_defines.RAT_HSPA: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': True,
+            'rat_family': tel_defines.RAT_FAMILY_UMTS
+        },
+        tel_defines.RAT_HSPAP: {
+            'is_voice_rat': False,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': True,
+            'rat_family': tel_defines.RAT_FAMILY_UMTS
+        },
+        tel_defines.RAT_IWLAN: {
+            'is_voice_rat': True,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_4G,
+            'simultaneous_voice_data': True,
+            'rat_family': tel_defines.RAT_FAMILY_WLAN
+        },
+        tel_defines.RAT_TD_SCDMA: {
+            'is_voice_rat': True,
+            'is_data_rat': True,
+            'generation': tel_defines.RAT_3G,
+            'simultaneous_voice_data': True,
+            'rat_family': tel_defines.RAT_FAMILY_UMTS
+        },
+        tel_defines.RAT_UNKNOWN: {
+            'is_voice_rat': False,
+            'is_data_rat': False,
+            'generation': tel_defines.RAT_UNKNOWN,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_UNKNOWN
+        },
+        tel_defines.RAT_GLOBAL: {
+            'is_voice_rat': False,
+            'is_data_rat': False,
+            'generation': tel_defines.RAT_UNKNOWN,
+            'simultaneous_voice_data': False,
+            'rat_family': tel_defines.RAT_FAMILY_UNKNOWN
+        }
+
+    }
+
+    voice_mail_number_get_function_tbl = {
+        tel_defines.CARRIER_TMO: get_tmo_voice_mail_number,
+        tel_defines.CARRIER_ATT: get_att_voice_mail_number,
+        tel_defines.CARRIER_SPT: get_spt_voice_mail_number
+    }
+
+    voice_mail_count_check_function_tbl = {
+        tel_defines.CARRIER_TMO: check_tmo_voice_mail_count,
+        tel_defines.CARRIER_ATT: check_att_voice_mail_count,
+        tel_defines.CARRIER_SPT: check_spt_voice_mail_count
+    }
+
+    _operator_na_cdma_tbl = {
+        tel_defines.RAT_2G: tel_defines.NETWORK_MODE_CDMA_NO_EVDO,
+        tel_defines.RAT_1XRTT: tel_defines.NETWORK_MODE_CDMA_NO_EVDO,
+        tel_defines.RAT_3G: tel_defines.NETWORK_MODE_CDMA,
+        tel_defines.RAT_EVDO: tel_defines.NETWORK_MODE_CDMA,
+        tel_defines.RAT_4G: tel_defines.NETWORK_MODE_LTE_CDMA_EVDO,
+        tel_defines.RAT_LTE: tel_defines.NETWORK_MODE_LTE_CDMA_EVDO,
+        tel_defines.RAT_GLOBAL: tel_defines.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA
+    }
+
+    _operator_na_umts_tbl = {
+        tel_defines.RAT_2G: tel_defines.NETWORK_MODE_GSM_ONLY,
+        tel_defines.RAT_GSM: tel_defines.NETWORK_MODE_GSM_ONLY,
+        tel_defines.RAT_3G: tel_defines.NETWORK_MODE_WCDMA_PREF,
+        tel_defines.RAT_WCDMA: tel_defines.NETWORK_MODE_WCDMA_PREF,
+        tel_defines.RAT_4G: tel_defines.NETWORK_MODE_LTE_GSM_WCDMA,
+        tel_defines.RAT_LTE: tel_defines.NETWORK_MODE_LTE_GSM_WCDMA,
+        tel_defines.RAT_GLOBAL: tel_defines.NETWORK_MODE_LTE_GSM_WCDMA
+    }
+
+    operator_network_mode_preference_tbl = {
+        tel_defines.CARRIER_VZW: _operator_na_cdma_tbl,
+        tel_defines.CARRIER_SPT: _operator_na_cdma_tbl,
+        tel_defines.CARRIER_ATT: _operator_na_umts_tbl,
+        tel_defines.CARRIER_TMO: _operator_na_umts_tbl,
+    }
+
+device_capabilities = {
+    NexusModelNames.ONE:
+        [tel_defines.CAPABILITY_PHONE, tel_defines.CAPABILITY_MSIM],
+    NexusModelNames.N5:
+        [tel_defines.CAPABILITY_PHONE],
+    NexusModelNames.N5v2:
+        [tel_defines.CAPABILITY_PHONE, tel_defines.CAPABILITY_VOLTE],
+    NexusModelNames.N6:
+        [tel_defines.CAPABILITY_PHONE, tel_defines.CAPABILITY_OMADM,
+         tel_defines.CAPABILITY_VOLTE, tel_defines.CAPABILITY_WFC,
+         tel_defines.CAPABILITY_VT],
+    NexusModelNames.N6v2:
+        [tel_defines.CAPABILITY_PHONE, tel_defines.CAPABILITY_VOLTE],
+}
+
+operator_capabilities = {
+    tel_defines.CARRIER_VZW:
+        [tel_defines.CAPABILITY_PHONE, tel_defines.CAPABILITY_OMADM,
+         tel_defines.CAPABILITY_VOLTE, tel_defines.CAPABILITY_VT],
+    tel_defines.CARRIER_ATT:
+        [tel_defines.CAPABILITY_PHONE],
+    tel_defines.CARRIER_TMO:
+        [tel_defines.CAPABILITY_PHONE, tel_defines.CAPABILITY_VOLTE,
+         tel_defines.CAPABILITY_WFC],
+    tel_defines.CARRIER_SPT:
+        [tel_defines.CAPABILITY_PHONE],
+}
diff --git a/acts/framework/acts/test_utils/tel/tel_test_anritsu_utils.py b/acts/framework/acts/test_utils/tel/tel_test_anritsu_utils.py
new file mode 100644
index 0000000..8d8d824
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_test_anritsu_utils.py
@@ -0,0 +1,1413 @@
+#!/usr/bin/python3.4
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+# Copyright (C) 2014- 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 time
+from queue import Empty
+from datetime import datetime
+
+from acts.controllers.tel.md8475a import BtsNumber
+from acts.controllers.tel.md8475a import BtsTechnology
+from acts.controllers.tel.md8475a import BtsNwNameEnable
+from acts.controllers.tel.md8475a import CsfbType
+from acts.controllers.tel.md8475a import ReturnToEUTRAN
+from acts.controllers.tel.md8475a import MD8475A
+from acts.controllers.tel.md8475a import BtsServiceState
+from acts.controllers.tel.md8475a import VirtualPhoneStatus
+from . tel_test_utils import *
+from acts.controllers.tel._anritsu_utils import AnritsuUtils
+
+# Test PLMN information
+TEST_PLMN_LTE_NAME = "MD8475A_LTE"
+TEST_PLMN_WCDMA_NAME = "MD8475A_WCDMA"
+TEST_PLMN_GSM_NAME = "MD8475A_GSM"
+TEST_PLMN_1X_NAME = "MD8475A_1X"
+TEST_PLMN_1_MCC = "001"
+TEST_PLMN_1_MNC = "01"
+DEFAULT_MCC = "001"
+DEFAULT_MNC = "01"
+DEFAULT_RAC = 1
+DEFAULT_LAC = 1
+
+# IP address information for internet sharing
+GATEWAY_IPV4_ADDRESS = "192.168.137.1"
+UE_IPV4_ADDRESS_1 = "192.168.137.2"
+UE_IPV4_ADDRESS_2 = "192.168.137.3"
+DNS_IPV4_ADDRESS = "192.168.137.1"
+CSCF_IPV4_ADDRESS = "192.168.137.1"
+
+# preferred network modes
+NETWORK_MODE_WCDMA_PREF = 0
+NETWORK_MODE_GSM_ONLY = 1
+NETWORK_MODE_WCDMA_ONLY = 2
+NETWORK_MODE_GSM_UMTS = 3
+NETWORK_MODE_CDMA = 4
+NETWORK_MODE_CDMA_NO_EVDO = 5
+NETWORK_MODE_EVDO_NO_CDMA = 6
+NETWORK_MODE_GLOBAL = 7
+NETWORK_MODE_LTE_CDMA_EVDO = 8
+NETWORK_MODE_LTE_GSM_WCDMA = 9
+NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA = 10
+NETWORK_MODE_LTE_ONLY = 11
+NETWORK_MODE_LTE_WCDMA = 12
+
+# LTE BAND constants
+LTE_BAND_1 = 1
+LTE_BAND_2 = 2
+LTE_BAND_3 = 3
+LTE_BAND_4 = 4
+LTE_BAND_5 = 5
+LTE_BAND_7 = 7
+LTE_BAND_12 = 12
+LTE_BAND_13 = 13
+
+# WCDMA BAND constants
+WCDMA_BAND_1 = 1
+WCDMA_BAND_2 = 2
+WCDMA_BAND_4 = 4
+WCDMA_BAND_5 = 5
+WCDMA_BAND_8 = 8
+
+
+# GSM BAND constants
+GSM_BAND_GSM450 = "GSM450"
+GSM_BAND_GSM480 = "GSM480"
+GSM_BAND_GSM850 = "GSM850"
+GSM_BAND_PGSM900 = "P-GSM900"
+GSM_BAND_EGSM900 = "E-GSM900"
+GSM_BAND_RGSM900 = "R-GSM900"
+GSM_BAND_DCS1800 = "DCS1800"
+GSM_BAND_PCS1900 = "PCS1900"
+
+# CDMA 1X BAND constants
+CDMA_1X_BAND_0 = 0
+CDMA_1X_BAND_1 = 1
+
+# CDMA 1X DL Channel constants
+CDMA1X_CHANNEL_356 = 356
+
+# CDMA 1X SID constants
+CDMA1X_SID_0 = 0
+
+# CDMA 1X NID constants
+CDMA1X_NID_65535 = 65535
+
+
+# BANDWIDTH constants
+CDMA1X_NID_65535 = 65535
+
+# CMAS Message IDs
+CMAS_MESSAGE_PRESIDENTIAL_ALERT = hex(0x1112)
+CMAS_MESSAGE_EXTREME_IMMEDIATE_OBSERVED = hex(0x1113)
+CMAS_MESSAGE_EXTREME_IMMEDIATE_LIKELY = hex(0x1114)
+CMAS_MESSAGE_EXTREME_EXPECTED_OBSERVED = hex(0x1115)
+CMAS_MESSAGE_EXTREME_EXPECTED_LIKELY = hex(0x1116)
+CMAS_MESSAGE_SEVERE_IMMEDIATE_OBSERVED = hex(0x1117)
+CMAS_MESSAGE_SEVERE_IMMEDIATE_LIKELY = hex(0x1118)
+CMAS_MESSAGE_SEVERE_EXPECTED_OBSERVED = hex(0x1119)
+CMAS_MESSAGE_SEVERE_EXPECTED_LIKELY = hex(0x111A)
+CMAS_MESSAGE_CHILD_ABDUCTION_EMERGENCY = hex(0x111B)
+CMAS_MESSAGE_MONTHLY_TEST = hex(0x111C)
+CMAS_MESSAGE_CMAS_EXECERCISE = hex(0x111D)
+
+# ETWS Message IDs
+ETWS_WARNING_EARTHQUAKE = hex(0x1100)
+ETWS_WARNING_TSUNAMI = hex(0x1101)
+ETWS_WARNING_EARTHQUAKETSUNAMI = hex(0x1102)
+ETWS_WARNING_TEST_MESSAGE = hex(0x1103)
+ETWS_WARNING_OTHER_EMERGENCY = hex(0x1104)
+
+# C2K CMAS Message Constants
+CMAS_C2K_CATEGORY_PRESIDENTIAL = "Presidential"
+CMAS_C2K_CATEGORY_EXTREME = "Extreme"
+CMAS_C2K_CATEGORY_SEVERE = "Severe"
+CMAS_C2K_CATEGORY_AMBER = "AMBER"
+CMAS_C2K_CATEGORY_CMASTEST = "CMASTest"
+
+CMAS_C2K_PRIORITY_NORMAL = "Normal"
+CMAS_C2K_PRIORITY_INTERACTIVE = "Interactive"
+CMAS_C2K_PRIORITY_URGENT = "Urgent"
+CMAS_C2K_PRIORITY_EMERGENCY = "Emergency"
+
+CMAS_C2K_RESPONSETYPE_SHELTER = "Shelter"
+CMAS_C2K_RESPONSETYPE_EVACUATE = "Evacuate"
+CMAS_C2K_RESPONSETYPE_PREPARE = "Prepare"
+CMAS_C2K_RESPONSETYPE_EXECUTE = "Execute"
+CMAS_C2K_RESPONSETYPE_MONITOR = "Monitor"
+CMAS_C2K_RESPONSETYPE_AVOID = "Avoid"
+CMAS_C2K_RESPONSETYPE_ASSESS = "Assess"
+CMAS_C2K_RESPONSETYPE_NONE = "None"
+
+CMAS_C2K_SEVERITY_EXTREME = "Extreme"
+CMAS_C2K_SEVERITY_SEVERE = "Severe"
+
+CMAS_C2K_URGENCY_IMMEDIATE = "Immediate"
+CMAS_C2K_URGENCY_EXPECTED = "Expected"
+
+CMAS_C2K_CERTIANTY_OBSERVED = "Observed"
+CMAS_C2K_CERTIANTY_LIKELY = "Likely"
+
+
+#PDN Numbers
+PDN_NO_1 = 1
+
+#Cell Numbers
+CELL_1 = 1
+CELL_2 = 2
+
+def cb_serial_number():
+    """ CMAS/ETWS serial number generator """
+    i = 0x3000
+    while True:
+        yield i
+        i += 1
+
+def save_anritsu_log_files(anritsu_handle, test_name, user_params):
+    """ saves the anritsu smart studio log files
+        The logs should be saved in Anritsu system. Need to provide
+        log folder path in Anritsu system
+
+    Args:
+        anritsu_handle: anritusu device object.
+        test_name: test case name
+        user_params : user supplied parameters list
+
+    Returns:
+        None
+    """
+    md8475a_log_folder =  user_params["anritsu_log_file_path"]
+    file_name = getfilenamewithtimestamp(test_name)
+    seq_logfile = "{}\\{}_seq.csv".format(md8475a_log_folder, file_name)
+    msg_logfile = "{}\\{}_msg.csv".format(md8475a_log_folder, file_name)
+    trace_logfile = "{}\\{}_trace.lgex".format(md8475a_log_folder, file_name)
+    anritsu_handle.save_sequence_log(seq_logfile)
+    anritsu_handle.save_message_log(msg_logfile)
+    anritsu_handle.save_trace_log(trace_logfile, "BINARY", 1, 0, 0)
+    anritsu_handle.clear_sequence_log()
+    anritsu_handle.clear_message_log()
+
+
+def getfilenamewithtimestamp(test_name):
+    """ Gets the test name appended with current time
+
+    Args:
+        test_name : test case name
+
+    Returns:
+        string of test name appended with current time
+    """
+    time_stamp = datetime.now().strftime("%m-%d-%Y_%H-%M-%S")
+    return "{}_{}".format(test_name, time_stamp)
+
+
+def _init_lte_bts(bts, user_params, cell_no):
+    """ initializes the LTE BTS
+        All BTS parameters should be set here
+
+    Args:
+        bts: BTS object.
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        None
+    """
+    bts.nw_fullname_enable = BtsNwNameEnable.NAME_ENABLE
+    bts.nw_fullname = TEST_PLMN_LTE_NAME
+    bts.mcc = get_lte_mcc(user_params, cell_no)
+    bts.mnc = get_lte_mnc(user_params, cell_no)
+    bts.band = get_lte_band(user_params, cell_no)
+
+
+def _init_wcdma_bts(bts, user_params, cell_no):
+    """ initializes the WCDMA BTS
+        All BTS parameters should be set here
+
+    Args:
+        bts: BTS object.
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        None
+    """
+    bts.nw_fullname_enable = BtsNwNameEnable.NAME_ENABLE
+    bts.nw_fullname = TEST_PLMN_WCDMA_NAME
+    bts.mcc = get_lte_mcc(user_params, cell_no)
+    bts.mnc = get_lte_mnc(user_params, cell_no)
+    bts.band = get_wcdma_band(user_params, cell_no)
+    bts.rac = get_wcdma_rac(user_params, cell_no)
+    bts.lac = get_wcdma_lac(user_params, cell_no)
+
+
+def _init_gsm_bts(bts, user_params, cell_no):
+    """ initializes the GSM BTS
+        All BTS parameters should be set here
+
+    Args:
+        bts: BTS object.
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        None
+    """
+    bts.nw_fullname_enable = BtsNwNameEnable.NAME_ENABLE
+    bts.nw_fullname = TEST_PLMN_GSM_NAME
+    bts.mcc = get_lte_mcc(user_params, cell_no)
+    bts.mnc = get_lte_mnc(user_params, cell_no)
+    bts.band = get_gsm_band(user_params, cell_no)
+    bts.rac = get_gsm_rac(user_params, cell_no)
+    bts.lac = get_gsm_lac(user_params, cell_no)
+
+
+def _init_1x_bts(bts, user_params, cell_no):
+    """ initializes the 1X BTS
+        All BTS parameters should be set here
+
+    Args:
+        bts: BTS object.
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        None
+    """
+    bts.sector1_mcc = get_1x_mcc(user_params, cell_no)
+    bts.band = get_1x_band(user_params, cell_no)
+    bts.dl_channel = get_1x_channel(user_params, cell_no)
+    bts.sector1_sid = get_1x_sid(user_params, cell_no)
+    bts.sector1_nid = get_1x_nid(user_params, cell_no)
+
+
+def _init_evdo_bts(bts, user_params, cell_no):
+    """ initializes the EVDO BTS
+        All BTS parameters should be set here
+
+    Args:
+        bts: BTS object.
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        None
+    """
+    #TODO
+    pass
+
+
+def _init_PDN(anritsu_handle, pdn, ip_address):
+    """ initializes the PDN parameters
+        All PDN parameters should be set here
+
+    Args:
+        anritsu_handle: anritusu device object.
+        pdn: pdn object
+        ip_address : UE IP address
+
+    Returns:
+        None
+    """
+    # Setting IP address for internet connection sharing
+    anritsu_handle.gateway_ipv4addr = GATEWAY_IPV4_ADDRESS
+    pdn.ue_address_ipv4 = ip_address
+    pdn.primary_dns_address_ipv4 = DNS_IPV4_ADDRESS
+    pdn.secondary_dns_address_ipv4 = DNS_IPV4_ADDRESS
+    pdn.cscf_address_ipv4 = CSCF_IPV4_ADDRESS
+
+def set_system_model_lte_lte(anritsu_handle, user_params):
+    """ Configures Anritsu system for LTE and LTE simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Lte and Wcdma BTS objects
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.LTE,
+                                       BtsTechnology.LTE)
+    # setting BTS parameters
+    lte1_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    lte2_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_lte_bts(lte1_bts, user_params, CELL_1)
+    _init_lte_bts(lte2_bts, user_params, CELL_2)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [lte1_bts, lte2_bts]
+
+def set_system_model_wcdma_wcdma(anritsu_handle, user_params):
+    """ Configures Anritsu system for WCDMA and WCDMA simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Lte and Wcdma BTS objects
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.WCDMA,
+                                       BtsTechnology.WCDMA)
+    # setting BTS parameters
+    wcdma1_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    wcdma2_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_wcdma_bts(wcdma1_bts, user_params, CELL_1)
+    _init_wcdma_bts(wcdma2_bts, user_params, CELL_2)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [wcdma1_bts, wcdma2_bts]
+
+def set_system_model_lte_wcdma(anritsu_handle, user_params):
+    """ Configures Anritsu system for LTE and WCDMA simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Lte and Wcdma BTS objects
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.LTE,
+                                       BtsTechnology.WCDMA)
+    # setting BTS parameters
+    lte_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    wcdma_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_lte_bts(lte_bts, user_params, CELL_1)
+    _init_wcdma_bts(wcdma_bts, user_params, CELL_2)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [lte_bts, wcdma_bts]
+
+def set_system_model_lte_gsm(anritsu_handle, user_params):
+    """ Configures Anritsu system for LTE and GSM simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Lte and Wcdma BTS objects
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.LTE,
+                                       BtsTechnology.GSM)
+    # setting BTS parameters
+    lte_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    gsm_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_lte_bts(lte_bts, user_params, CELL_1)
+    _init_gsm_bts(gsm_bts, user_params, CELL_2)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [lte_bts, gsm_bts]
+
+def set_system_model_wcdma_gsm(anritsu_handle, user_params):
+    """ Configures Anritsu system for WCDMA and GSM simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Wcdma and Gsm BTS objects
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.WCDMA, BtsTechnology.GSM)
+    # setting BTS parameters
+    wcdma_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    gsm_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_wcdma_bts(wcdma_bts, user_params, CELL_1)
+    _init_gsm_bts(gsm_bts, user_params, CELL_2)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [wcdma_bts, gsm_bts]
+
+def set_system_model_gsm_gsm(anritsu_handle, user_params):
+    """ Configures Anritsu system for GSM and GSM simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Wcdma and Gsm BTS objects
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.GSM, BtsTechnology.GSM)
+    # setting BTS parameters
+    gsm1_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    gsm2_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_gsm_bts(gsm1_bts, user_params, CELL_1)
+    _init_gsm_bts(gsm2_bts, user_params, CELL_2)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [gsm1_bts, gsm2_bts]
+
+def set_system_model_lte(anritsu_handle, user_params):
+    """ Configures Anritsu system for LTE simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Lte BTS object
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.LTE)
+    # setting BTS parameters
+    lte_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    _init_lte_bts(lte_bts, user_params, CELL_1)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [lte_bts]
+
+
+def set_system_model_wcdma(anritsu_handle, user_params):
+    """ Configures Anritsu system for WCDMA simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Wcdma BTS object
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.WCDMA)
+    # setting BTS parameters
+    wcdma_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    _init_wcdma_bts(wcdma_bts, user_params, CELL_1)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [wcdma_bts]
+
+
+def set_system_model_gsm(anritsu_handle, user_params):
+    """ Configures Anritsu system for GSM simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Gsm BTS object
+    """
+    anritsu_handle.set_simulation_model(BtsTechnology.GSM)
+    # setting BTS parameters
+    gsm_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    _init_gsm_bts(gsm_bts, user_params, CELL_1)
+    pdn1 = anritsu_handle.get_PDN(PDN_NO_1)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [gsm_bts]
+
+
+def set_system_model_1x(anritsu_handle, user_params):
+    """ Configures Anritsu system for CDMA 1X simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Cdma 1x BTS object
+    """
+    PDN_ONE = 1
+    anritsu_handle.set_simulation_model(BtsTechnology.CDMA1X)
+    # setting BTS parameters
+    cdma1x_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    _init_1x_bts(cdma1x_bts, user_params, CELL_1)
+    pdn1 = anritsu_handle.get_PDN(PDN_ONE)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [cdma1x_bts]
+
+
+def set_system_model_1x_evdo(anritsu_handle, user_params):
+    """ Configures Anritsu system for CDMA 1X simulation
+
+    Args:
+        anritsu_handle: anritusu device object.
+        user_params: pointer to user supplied parameters
+
+    Returns:
+        Cdma 1x BTS object
+    """
+    PDN_ONE = 1
+    anritsu_handle.set_simulation_model(BtsTechnology.CDMA1X,
+                                        BtsTechnology.EVDO)
+    # setting BTS parameters
+    cdma1x_bts = anritsu_handle.get_BTS(BtsNumber.BTS1)
+    evdo_bts = anritsu_handle.get_BTS(BtsNumber.BTS2)
+    _init_1x_bts(cdma1x_bts, user_params, CELL_1)
+    _init_evdo_bts(evdo_bts, user_params, CELL_1)
+    pdn1 = anritsu_handle.get_PDN(PDN_ONE)
+    # Initialize PDN IP address for internet connection sharing
+    _init_PDN(anritsu_handle, pdn1, UE_IPV4_ADDRESS_1)
+    return [cdma1x_bts]
+
+
+def wait_for_bts_state(log, btsnumber, state, timeout=30):
+    """ Waits for BTS to be in the specified state ("IN" or "OUT")
+
+    Args:
+        btsnumber: BTS number.
+        state: expected state
+
+    Returns:
+        True for success False for failure
+    """
+    #  state value are "IN" and "OUT"
+    status = False
+    sleep_interval = 1
+    wait_time = timeout
+
+    if state is "IN":
+        service_state = BtsServiceState.SERVICE_STATE_IN
+    elif state is "OUT":
+        service_state = BtsServiceState.SERVICE_STATE_OUT
+    else:
+       log.info("wrong state value")
+       return status
+
+    if btsnumber.service_state is service_state:
+        log.info("BTS state is already in {}".format(state))
+        return True
+
+    # set to desired service state
+    btsnumber.service_state = service_state
+
+    while wait_time > 0:
+        if service_state == btsnumber.service_state:
+            status = True
+            break
+        time.sleep(sleep_interval)
+        wait_time = wait_time - sleep_interval
+
+    if not status:
+        log.info("Timeout: Expected BTS state is not received.")
+    return status
+
+
+def call_mo_setup_teardown(log, ad, virtual_phone_handle, callee_number,
+        teardown_side=CALL_TEARDOWN_PHONE, emergency=False):
+    """ Makes a MO call and tear down the call
+
+    Args:
+        ad: Android device object.
+        virtual_phone_handle: Anritus virtual phone handle
+        callee_number = Number to be called
+        teardown_side = specifiy the side to end the call (Phone or remote)
+        emergency : specify the call is emergency
+
+    Returns:
+        True for success False for failure
+    """
+    log.info("Making Call to " + callee_number)
+
+    try:
+        if not wait_for_virtualphone_state(log, virtual_phone_handle,
+                                           VirtualPhoneStatus.STATUS_IDLE):
+            raise Exception("Virtual Phone is not in a state to start call")
+
+        if not initiate_call(log, ad, callee_number, emergency):
+            raise Exception("Initiate call failed.")
+
+        # check Virtual phone answered the call
+        if not wait_for_virtualphone_state(log, virtual_phone_handle,
+                     VirtualPhoneStatus.STATUS_VOICECALL_INPROGRESS):
+            raise Exception("Virtual Phone did not answer the call.")
+
+        time.sleep(WAIT_TIME_IN_CALL)
+
+        if not ad.droid.telecomIsInCall():
+            raise Exception(
+                "Call ended before delay_in_call.")
+    except Exception:
+        return False
+
+    if ad.droid.telecomIsInCall():
+        if teardown_side is CALL_TEARDOWN_REMOTE:
+            log.info("Disconnecting the call from Remote")
+            virtual_phone_handle.set_voice_on_hook()
+        else:
+            log.info("Disconnecting the call from Phone")
+            ad.droid.telecomEndCall()
+
+    wait_for_virtualphone_state(log, virtual_phone_handle,
+                                VirtualPhoneStatus.STATUS_IDLE)
+    ensure_phone_idle(log, ad)
+    return True
+
+
+def call_mt_setup_teardown(log, ad, virtual_phone_handle, caller_number=None,
+        teardown_side=CALL_TEARDOWN_PHONE, rat=""):
+    """ Makes a call from Anritsu Virtual phone to device and tear down the call
+
+    Args:
+        ad: Android device object.
+        virtual_phone_handle: Anritus virtual phone handle
+        caller_number =  Caller number
+        teardown_side = specifiy the side to end the call (Phone or remote)
+
+    Returns:
+        True for success False for failure
+    """
+    log.info("Receive MT Call - Making a call to the phone from remote")
+    try:
+        if not wait_for_virtualphone_state(log, virtual_phone_handle,
+                                             VirtualPhoneStatus.STATUS_IDLE):
+            raise Exception("Virtual Phone is not in a state to start call")
+        if caller_number is not None:
+            if rat == RAT_1XRTT:
+                virtual_phone_handle.id_c2k = caller_number
+            else:
+                virtual_phone_handle.id = caller_number
+        virtual_phone_handle.set_voice_off_hook()
+
+        if not wait_and_answer_call(log, ad, caller_number):
+            raise Exception("Answer call Fail")
+
+        time.sleep(WAIT_TIME_IN_CALL)
+
+        if not ad.droid.telecomIsInCall():
+            raise Exception(
+                "Call ended before delay_in_call.")
+    except Exception:
+        return False
+
+    if ad.droid.telecomIsInCall():
+        if teardown_side is CALL_TEARDOWN_REMOTE:
+            log.info("Disconnecting the call from Remote")
+            virtual_phone_handle.set_voice_on_hook()
+        else:
+            log.info("Disconnecting the call from Phone")
+            ad.droid.telecomEndCall()
+
+    wait_for_virtualphone_state(log, virtual_phone_handle,
+                                VirtualPhoneStatus.STATUS_IDLE)
+    ensure_phone_idle(log, ad)
+
+    return True
+
+def wait_for_sms_deliver_success(log, ad, time_to_wait=60):
+    sms_deliver_event = EventSmsDeliverSuccess
+    sleep_interval = 2
+    status = False
+    event = None
+
+    try:
+        event = ad.ed.pop_event(sms_deliver_event, time_to_wait)
+        status = True
+    except Empty:
+        log.info("Timeout: Expected event is not received.")
+    return status
+
+def wait_for_sms_sent_success(log, ad, time_to_wait=60):
+    sms_sent_event = EventSmsSentSuccess
+    sleep_interval = 2
+    status = False
+    event = None
+
+    try:
+        event = ad.ed.pop_event(sms_sent_event, time_to_wait)
+        log.info(event)
+        status = True
+    except Empty:
+        log.info("Timeout: Expected event is not received.")
+    return status
+
+def wait_for_incoming_sms(log, ad, time_to_wait=60):
+    sms_received_event = EventSmsReceived
+    sleep_interval = 2
+    status = False
+    event = None
+
+    try:
+        event = ad.ed.pop_event(sms_received_event, time_to_wait)
+        log.info(event)
+        status = True
+    except Empty:
+        log.info("Timeout: Expected event is not received.")
+    return status, event
+
+def verify_anritsu_received_sms(log, vp_handle, receiver_number, message, rat):
+    if rat == RAT_1XRTT:
+        receive_sms = vp_handle.receiveSms_c2k()
+    else:
+        receive_sms = vp_handle.receiveSms()
+
+    if receive_sms == "NONE":
+        return False
+    split = receive_sms.split('&')
+    text = ""
+    if rat == RAT_1XRTT:
+        #TODO There is some problem when retreiving message with é from Anritsu.
+        #Anritsu receives the message with é.
+        return True
+    for i in range(len(split)):
+        if split[i].startswith('Text='):
+            text = split[i][5:]
+            text = AnritsuUtils.gsm_decode(text)
+            break
+    # TODO Verify Phone number
+    if text != message:
+        log.error("Wrong message received")
+        return False
+    return True
+
+def sms_mo_send(log, ad, vp_handle, receiver_number, message, rat=""):
+    try:
+        if not wait_for_virtualphone_state(log, vp_handle,
+                                           VirtualPhoneStatus.STATUS_IDLE):
+            raise Exception("Virtual Phone is not in a state to receive SMS")
+        log.info("Sending SMS to " + receiver_number)
+        ad.droid.smsSendTextMessage(receiver_number, message, False)
+        log.info("Waiting for SMS sent event")
+        test_status = wait_for_sms_sent_success(log, ad)
+        if not test_status:
+            raise Exception("Failed to send SMS")
+        if not verify_anritsu_received_sms(log, vp_handle,receiver_number, message, rat):
+            raise Exception("Anritsu didn't receive message")
+    except Exception as e:
+        log.error("Exception :" + str(e))
+        return False
+    return True
+
+def sms_mt_receive_verify(log, ad, vp_handle, sender_number, message, rat=""):
+    ad.droid.smsStartTrackingIncomingMessage()
+    try:
+        if not wait_for_virtualphone_state(log, vp_handle,
+                                           VirtualPhoneStatus.STATUS_IDLE):
+            raise Exception("Virtual Phone is not in a state to receive SMS")
+        log.info("Waiting for Incoming SMS from " + sender_number)
+        if rat == RAT_1XRTT:
+            vp_handle.sendSms_c2k(sender_number,  message)
+        else:
+            vp_handle.sendSms(sender_number,  message)
+        test_status, event = wait_for_incoming_sms(log, ad)
+        if not test_status:
+            raise Exception("Failed to receive SMS")
+        log.info("Incoming SMS: Sender " + event['data']['Sender'])
+        log.info("Incoming SMS: Message " + event['data']['Text'])
+        if event['data']['Sender'] != sender_number:
+            raise Exception("Wrong sender Number")
+        if event['data']['Text'] != message:
+            raise Exception("Wrong message")
+    except Exception as e:
+        log.error("exception: " + str(e))
+        return False
+    finally:
+        ad.droid.smsStopTrackingIncomingMessage()
+    return True
+
+def wait_for_virtualphone_state(log, vp_handle, state, timeout=30):
+    """ Waits for Anritsu Virtual phone to be in expected state
+
+    Args:
+        ad: Android device object.
+        vp_handle: Anritus virtual phone handle
+        state =  expected state
+
+    Returns:
+        True for success False for failure
+    """
+    status = False
+    sleep_interval = 1
+    wait_time = timeout
+    while wait_time > 0:
+        if vp_handle.status == state:
+            log.info(vp_handle.status)
+            status = True
+            break
+        time.sleep(sleep_interval)
+        wait_time = wait_time - sleep_interval
+
+    if not status:
+        log.info("Timeout: Expected state is not received.")
+    return status
+
+# There is a difference between CMAS/ETWS message formation in LTE/WCDMA and CDMA 1X
+# LTE and CDMA : 3GPP
+# CDMA 1X: 3GPP2
+# hence different functions
+def cmas_receive_verify_message_lte_wcdma(log, ad, anritsu_handle, serial_number,
+        message_id, warning_message):
+    """ Makes Anritsu to send a CMAS message and phone and verifies phone
+        receives the message on LTE/WCDMA
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritus device object
+        serial_number =  serial number of CMAS message
+        message_id =  CMAS message ID
+        warning_message =  CMAS warning message
+
+    Returns:
+        True for success False for failure
+    """
+    status = False
+    event = None
+    ad.droid.smsStartTrackingGsmEmergencyCBMessage()
+    anritsu_handle.send_cmas_lte_wcdma(hex(serial_number), message_id,
+                                       warning_message)
+    try:
+        log.info("Waiting for CMAS Message")
+        event = ad.ed.pop_event(EventCmasReceived, 60)
+        status = True
+        log.info(event)
+        if warning_message != event['data']['message']:
+            log.info("Wrong warning messgae received")
+            status = False
+        if message_id != hex(event['data']['serviceCategory']):
+            log.info("Wrong warning messgae received")
+            status = False
+    except Empty:
+        log.info("Timeout: Expected event is not received.")
+
+    ad.droid.smsStopTrackingGsmEmergencyCBMessage()
+    return status
+
+
+def cmas_receive_verify_message_cdma1x(log, ad, anritsu_handle,  message_id,
+        service_category, alert_text,
+        response_type=CMAS_C2K_RESPONSETYPE_SHELTER,
+        severity=CMAS_C2K_SEVERITY_EXTREME,
+        urgency=CMAS_C2K_URGENCY_IMMEDIATE,
+        certainty=CMAS_C2K_CERTIANTY_OBSERVED):
+    """ Makes Anritsu to send a CMAS message and phone and verifies phone
+        receives the message on CDMA 1X
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritus device object
+        serial_number =  serial number of CMAS message
+        message_id =  CMAS message ID
+        warning_message =  CMAS warning message
+
+    Returns:
+        True for success False for failure
+    """
+    status = False
+    event = None
+    ad.droid.smsStartTrackingCdmaEmergencyCBMessage()
+    anritsu_handle.send_cmas_etws_cdma1x(message_id, service_category, alert_text,
+        response_type, severity, urgency, certainty)
+    try:
+        log.info("Waiting for CMAS Message")
+        event = ad.ed.pop_event(EventCmasReceived, 60)
+        status = True
+        log.info(event)
+        if alert_text != event['data']['message']:
+            log.info("Wrong alert messgae received")
+            status = False
+
+        if event['data']['cmasResponseType'].lower() != response_type.lower():
+            log.info("Wrong response type received")
+            status = False
+
+        if event['data']['cmasUrgency'].lower() != urgency.lower():
+            log.info("Wrong cmasUrgency received")
+            status = False
+
+        if event['data']['cmasSeverity'].lower() != severity.lower():
+            Log.info("Wrong cmasSeverity received")
+            status = False
+    except Empty:
+        log.info("Timeout: Expected event is not received.")
+
+    ad.droid.smsStopTrackingCdmaEmergencyCBMessage()
+    return status
+
+
+def etws_receive_verify_message_lte_wcdma(log, ad, anritsu_handle,  serial_number,
+        message_id, warning_message):
+    """ Makes Anritsu to send a ETWS message and phone and verifies phone
+        receives the message on LTE/WCDMA
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritus device object
+        serial_number =  serial number of ETWS message
+        message_id =  ETWS message ID
+        warning_message =  ETWS warning message
+
+    Returns:
+        True for success False for failure
+    """
+    status = False
+    event = None
+    if message_id == ETWS_WARNING_EARTHQUAKE:
+        warning_type = "Earthquake"
+    elif message_id == ETWS_WARNING_EARTHQUAKETSUNAMI:
+        warning_type = "EarthquakeandTsunami"
+    elif message_id == ETWS_WARNING_TSUNAMI:
+        warning_type = "Tsunami"
+    elif message_id == ETWS_WARNING_TEST_MESSAGE:
+        warning_type = "test"
+    elif message_id == ETWS_WARNING_OTHER_EMERGENCY:
+        warning_type = "other"
+    ad.droid.smsStartTrackingGsmEmergencyCBMessage()
+    anritsu_handle.send_etws_lte_wcdma(hex(serial_number), message_id,
+                                       warning_type,
+                                       warning_message, "ON", "ON")
+    try:
+        log.info("Waiting for ETWS Message")
+        event = ad.ed.pop_event(EventEtwsReceived, 60)
+        status = True
+        log.info(event)
+        #TODO Event data verification
+    except Empty:
+        log.info("Timeout: Expected event is not received.")
+
+    ad.droid.smsStopTrackingGsmEmergencyCBMessage()
+    return status
+
+
+def etws_receive_verify_message_cdma1x(log, ad, anritsu_handle,  serial_number,
+        message_id, warning_message):
+    """ Makes Anritsu to send a ETWS message and phone and verifies phone
+        receives the message on CDMA1X
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritus device object
+        serial_number =  serial number of ETWS message
+        message_id =  ETWS message ID
+        warning_message =  ETWS warning message
+
+    Returns:
+        True for success False for failure
+    """
+    status = False
+    event = None
+    #TODO
+    return status
+
+def read_ue_identity(log, ad, anritsu_handle, identity_type):
+    """ Get the UE identity IMSI, IMEI, IMEISV
+
+    Args:
+        ad: Android device object.
+        anritsu_handle: Anritus device object
+        identity_type: Identity type(IMSI/IMEI/IMEISV)
+
+    Returns:
+        Requested Identity value
+    """
+    return anritsu_handle.get_ue_identity(identity_type)
+
+def get_lte_band(user_params, cell_no):
+    """ Returns the LTE BAND to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        LTE BAND to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_lte_band".format(cell_no)
+    try:
+        lte_band = user_params[key]
+    except KeyError:
+        lte_band = LTE_BAND_2
+    return lte_band
+
+
+def get_wcdma_band(user_params, cell_no):
+    """ Returns the WCDMA BAND to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        WCDMA BAND to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_wcdma_band".format(cell_no)
+    try:
+        wcdma_band = user_params[key]
+    except KeyError:
+        wcdma_band = WCDMA_BAND_1
+    return wcdma_band
+
+
+def get_gsm_band(user_params, cell_no):
+    """ Returns the GSM BAND to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        GSM BAND to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_gsm_band".format(cell_no)
+    try:
+        gsm_band = user_params[key]
+    except KeyError:
+        gsm_band = GSM_BAND_GSM850
+    return gsm_band
+
+
+def get_1x_band(user_params, cell_no):
+    """ Returns the 1X BAND to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        1X BAND to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_1x_band".format(cell_no)
+    try:
+        cdma_1x_band = user_params[key]
+    except KeyError:
+        cdma_1x_band = CDMA_1X_BAND_0
+    return cdma_1x_band
+
+def get_wcdma_rac(user_params, cell_no):
+    """ Returns the WCDMA RAC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        WCDMA RAC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_wcdma_rac".format(cell_no)
+    try:
+        wcdma_rac = user_params[key]
+    except KeyError:
+        wcdma_rac = DEFAULT_RAC
+    return wcdma_rac
+
+
+def get_gsm_rac(user_params, cell_no):
+    """ Returns the GSM RAC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        GSM RAC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_gsm_rac".format(cell_no)
+    try:
+        gsm_rac = user_params[key]
+    except KeyError:
+        gsm_rac = DEFAULT_RAC
+    return gsm_rac
+
+
+def get_wcdma_lac(user_params, cell_no):
+    """ Returns the WCDMA LAC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        WCDMA LAC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_wcdma_lac".format(cell_no)
+    try:
+        wcdma_lac = user_params[key]
+    except KeyError:
+        wcdma_lac = DEFAULT_LAC
+    return wcdma_lac
+
+
+def get_gsm_lac(user_params, cell_no):
+    """ Returns the GSM LAC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        GSM LAC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_gsm_lac".format(cell_no)
+    try:
+        gsm_lac = user_params[key]
+    except KeyError:
+        gsm_lac = DEFAULT_LAC
+    return gsm_lac
+
+
+def get_lte_mcc(user_params, cell_no):
+    """ Returns the LTE MCC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        LTE MCC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_lte_mcc".format(cell_no)
+    try:
+        lte_mcc = user_params[key]
+    except KeyError:
+        lte_mcc = DEFAULT_MCC
+    return lte_mcc
+
+
+def get_lte_mnc(user_params, cell_no):
+    """ Returns the LTE MNC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        LTE MNC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_lte_mnc".format(cell_no)
+    try:
+        lte_mnc = user_params[key]
+    except KeyError:
+        lte_mnc = DEFAULT_MNC
+    return lte_mnc
+
+
+def get_wcdma_mcc(user_params, cell_no):
+    """ Returns the WCDMA MCC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        WCDMA MCC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_wcdma_mcc".format(cell_no)
+    try:
+        wcdma_mcc = user_params[key]
+    except KeyError:
+        wcdma_mcc = DEFAULT_MCC
+    return wcdma_mcc
+
+
+def get_wcdma_mnc(user_params, cell_no):
+    """ Returns the WCDMA MNC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        WCDMA MNC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_wcdma_mnc".format(cell_no)
+    try:
+        wcdma_mnc = user_params[key]
+    except KeyError:
+        wcdma_mnc = DEFAULT_MNC
+    return wcdma_mnc
+
+
+def get_gsm_mcc(user_params, cell_no):
+    """ Returns the GSM MCC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        GSM MCC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_gsm_mcc".format(cell_no)
+    try:
+        gsm_mcc = user_params[key]
+    except KeyError:
+        gsm_mcc = DEFAULT_MCC
+    return gsm_mcc
+
+
+def get_gsm_mnc(user_params, cell_no):
+    """ Returns the GSM MNC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        GSM MNC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_gsm_mnc".format(cell_no)
+    try:
+        gsm_mnc = user_params[key]
+    except KeyError:
+        gsm_mnc = DEFAULT_MNC
+    return gsm_mnc
+
+
+def get_1x_mcc(user_params, cell_no):
+    """ Returns the 1X MCC to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        1X MCC to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_1x_mcc".format(cell_no)
+    try:
+        cdma_1x_mcc = user_params[key]
+    except KeyError:
+        cdma_1x_mcc = DEFAULT_MCC
+    return cdma_1x_mcc
+
+def get_1x_channel(user_params, cell_no):
+    """ Returns the 1X Channel to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        1X Channel to be used
+    """
+    key = "cell{}_1x_channel".format(cell_no)
+    try:
+        cdma_1x_channel = user_params[key]
+    except KeyError:
+        cdma_1x_channel = CDMA1X_CHANNEL_356
+    return cdma_1x_channel
+
+
+def get_1x_sid(user_params, cell_no):
+    """ Returns the 1X SID to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        1X SID to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_1x_sid".format(cell_no)
+    try:
+        cdma_1x_sid = user_params[key]
+    except KeyError:
+        cdma_1x_sid = CDMA1X_SID_0
+    return cdma_1x_sid
+
+
+def get_1x_nid(user_params, cell_no):
+    """ Returns the 1X NID to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        1X NID to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    key = "cell{}_1x_nid".format(cell_no)
+    try:
+        cdma_1x_nid = user_params[key]
+    except KeyError:
+        cdma_1x_nid = CDMA1X_NID_65535
+    return cdma_1x_nid
+
+
+def get_csfb_type(user_params):
+    """ Returns the CSFB Type to be used from the user specified parameters
+        or default value
+
+    Args:
+        user_params: pointer to user supplied parameters
+        cell_no: specify the cell number this BTS is configured
+        Anritsu supports two cells. so cell_1 or cell_2
+
+    Returns:
+        CSFB Type to be used
+    """
+    #TODO : Need to re look in the logic of passing parameters to test case
+    try:
+        csfb_type = user_params["csfb_type"]
+    except KeyError:
+        csfb_type = CsfbType.CSFB_TYPE_REDIRECTION
+    return csfb_type
diff --git a/acts/framework/acts/test_utils/tel/tel_test_utils.py b/acts/framework/acts/test_utils/tel/tel_test_utils.py
new file mode 100644
index 0000000..d884478
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_test_utils.py
@@ -0,0 +1,3004 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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 concurrent.futures
+import random
+import string
+import urllib.parse
+import time
+import warnings
+
+from queue import Empty
+from .tel_defines import *
+from .tel_lookup_tables import *
+from acts.event_dispatcher import EventDispatcher
+from acts.utils import load_config
+from acts.controllers.android_device import AndroidDevice
+from acts.logger import LoggerProxy
+log = LoggerProxy()
+
+class TelTestUtilsError(Exception):
+    pass
+
+def setup_droid_properties(log, ad, sim_filename):
+
+    # Check to see if droid already has this property
+    if hasattr(ad, 'cfg'):
+        return
+
+    device_props = {}
+    device_props['subscription'] = {}
+
+    try:
+        sim_data = load_config(sim_filename)
+    except Exception:
+        log.warning("Failed to load {}!".format(sim_filename))
+        sim_data = None
+    sub_info_list = ad.droid.subscriptionGetAllSubInfoList()
+    found_sims = 0
+    for sub_info in sub_info_list:
+        sub_id = sub_info['subscriptionId']
+        if sub_info['simSlotIndex'] is not INVALID_SIM_SLOT_INDEX:
+            found_sims += 1
+            sim_record = {}
+            try:
+                sim_serial = ad.droid.getSimSerialNumberForSubscription(sub_id)
+                if not sim_serial:
+                    log.error("Unable to find ICC-ID for SIM on {}!".format(
+                        ad.serial))
+                if sim_data is not None:
+                    number = sim_data[sim_serial]["phone_num"]
+                else:
+                    raise KeyError("No file to load phone number info!")
+            except KeyError:
+                number = ad.droid.getLine1NumberForSubscription(sub_id)
+            if not number or number == "":
+                raise TelTestUtilsError(
+                    "Failed to find valid phone number for {}"
+                    .format(ad.serial))
+
+            sim_record['phone_num'] = number
+            sim_record['operator'] = get_operator_name(log, ad, sub_id)
+            device_props['subscription'][sub_id] = sim_record
+            log.info("phone_info: <{}:{}>, <subId:{}> {} <{}>, ICC-ID:<{}>".
+                     format(ad.model, ad.serial, sub_id, number,
+                            get_operator_name(log, ad, sub_id),
+                            ad.droid.getSimSerialNumberForSubscription(sub_id)))
+
+    if found_sims == 0:
+        log.warning("No Valid SIMs found in device {}".format(ad.serial))
+
+    setattr(ad, 'cfg', device_props)
+
+def get_subid_from_slot_index(log, ad, sim_slot_index):
+    """ Get the subscription ID for a SIM at a particular slot
+
+    Args:
+        ad: android_device object.
+
+    Returns:
+        result: Subscription ID
+    """
+    subInfo = ad.droid.subscriptionGetAllSubInfoList()
+    for info in subInfo:
+        if info['simSlotIndex'] == sim_slot_index:
+            return info['subscriptionId']
+    return INVALID_SUB_ID
+
+def get_num_active_sims(log, ad):
+    """ Get the number of active SIM cards by counting slots
+
+    Args:
+        ad: android_device object.
+
+    Returns:
+        result: The number of loaded (physical) SIM cards
+    """
+    # using a dictionary as a cheap way to prevent double counting
+    # in the situation where multiple subscriptions are on the same SIM.
+    # yes, this is a corner corner case.
+    valid_sims = {}
+    subInfo = ad.droid.subscriptionGetAllSubInfoList()
+    for info in subInfo:
+        ssidx = info['simSlotIndex']
+        if ssidx == INVALID_SIM_SLOT_INDEX:
+            continue
+        valid_sims[ssidx] = True
+    return len(valid_sims.keys())
+
+def toggle_airplane_mode(log, ad, new_state=None):
+    """ Toggle the state of airplane mode.
+
+    Args:
+        ad: android_device object.
+        new_state: Airplane mode state to set to.
+            If None, opposite of the current state.
+
+    Returns:
+        result: True if operation succeed. False if error happens.
+    """
+    return toggle_airplane_mode_msim(log, ad, new_state)
+
+def is_expected_event(event_to_check, events_list):
+    """ check whether event is present in the event list
+
+    Args:
+        event_to_check: event to be checked.
+        events_list: list of events
+    Returns:
+        result: True if event present in the list. False if not.
+    """
+    for event in events_list:
+        if event in event_to_check['name']:
+            return True
+    return False
+
+def is_sim_ready(log, ad, sim_slot_id=None):
+    """ check whether SIM is ready.
+
+    Args:
+        ad: android_device object.
+        sim_slot_id: check the SIM status for sim_slot_id
+            This is optional. If this is None, check default SIM.
+
+    Returns:
+        result: True if all SIMs are ready. False if not.
+    """
+    if sim_slot_id is None:
+        status = ad.droid.getSimState()
+    else:
+        status = ad.droid.getSimStateForSlotId(sim_slot_id)
+    if status != SIM_STATE_READY:
+       log.info("Sim not ready")
+       return False
+    return True
+
+def _is_expecting_event(event_recv_list):
+    """ check for more event is expected in event list
+
+    Args:
+        event_recv_list: list of events
+    Returns:
+        result: True if more events are expected. False if not.
+    """
+    for state in event_recv_list:
+        if state is False:
+           return True
+    return False
+
+def _set_event_list(event_recv_list , sub_id_list, sub_id, value):
+    """ set received event in expected event list
+
+    Args:
+        event_recv_list: list of received events
+        sub_id_list: subscription ID list
+        sub_id: subscription id of current event
+        value: True or False
+    Returns:
+        None.
+    """
+    for i in range(len(sub_id_list)):
+        if sub_id_list[i] == sub_id:
+            event_recv_list[i] = value
+
+def toggle_airplane_mode_msim(log, ad, new_state=None):
+    """ Toggle the state of airplane mode.
+
+    Args:
+        ad: android_device object.
+        new_state: Airplane mode state to set to.
+            If None, opposite of the current state.
+
+    Returns:
+        result: True if operation succeed. False if error happens.
+    """
+    serial_number = ad.serial
+
+    ad.ed.clear_all_events()
+    sub_id_list = []
+
+    active_sub_info = ad.droid.subscriptionGetAllSubInfoList()
+    for info in active_sub_info :
+        sub_id_list.append(info['subscriptionId'])
+
+    cur_state = ad.droid.connectivityCheckAirplaneMode()
+    if cur_state == new_state:
+        log.info("Airplane mode already <{}> on {}".format(new_state,
+                                                            serial_number))
+        return True
+    elif new_state is None:
+        log.info("Current State {} New state {}".format(cur_state, new_state))
+
+    if new_state is None:
+        new_state = not cur_state
+
+    sub_event_name_list = []
+    if new_state:
+        sub_event_name_list.append(SERVICE_STATE_POWER_OFF)
+        log.info("Turn on airplane mode: " + serial_number)
+
+    else:
+        # If either one of these 3 events show up, it should be OK.
+        # Normal SIM, phone in service
+        sub_event_name_list.append(SERVICE_STATE_IN_SERVICE)
+        # NO SIM, or Dead SIM, or no Roaming coverage.
+        sub_event_name_list.append(SERVICE_STATE_OUT_OF_SERVICE)
+        sub_event_name_list.append(SERVICE_STATE_EMERGENCY_ONLY)
+        log.info("Turn off airplane mode: " + serial_number)
+
+    for sub_id in sub_id_list:
+        ad.droid.phoneStartTrackingServiceStateChangeForSubscription(sub_id)
+    ad.droid.connectivityToggleAirplaneMode(new_state)
+
+    event = None
+
+    try:
+        try:
+            event = ad.ed.wait_for_event(EventServiceStateChanged,
+                is_sub_event_match_for_sub_event_list,
+                timeout=WAIT_TIME_AIRPLANEMODE_EVENT,
+                sub_event_list=sub_event_name_list)
+        except Empty:
+            pass
+        if event is None:
+            log.error("Did not get expected sub_event {}".
+                format(sub_event_name_list))
+        log.info("Received event: {}".format(event))
+    finally:
+        for sub_id in sub_id_list:
+            ad.droid.phoneStopTrackingServiceStateChangeForSubscription(sub_id)
+
+    if new_state:
+        if (not ad.droid.connectivityCheckAirplaneMode() or
+                ad.droid.wifiCheckState() or
+                ad.droid.bluetoothCheckState()):
+            log.error("Airplane mode ON fail on {}".format(ad.serial))
+            return False
+    else:
+        if ad.droid.connectivityCheckAirplaneMode():
+            log.error("Airplane mode OFF fail on {}".format(ad.serial))
+            return False
+    return True
+
+def wait_and_answer_call(log, ad, incoming_number=None,
+                         delay_answer=WAIT_TIME_ANSWER_CALL,
+                         incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """Wait for an incoming call on default voice subscription and
+       accepts the call.
+
+    Args:
+        ad: android device object.
+        incoming_number: Expected incoming number.
+            Optional. Default is None
+        delay_answer: time to wait before answering the call
+            Optional. Default is 1
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True: if incoming call is received and answered successfully.
+        False: for errors
+        """
+    return wait_and_answer_call_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId(),
+        incoming_number, delay_answer, incall_ui_display)
+
+def wait_for_ringing_event(log, ad, wait_time):
+    """Wait for ringing event.
+
+    Args:
+        log: log object.
+        ad: android device object.
+        wait_time: max time to wait for ringing event.
+
+    Returns:
+        event_ringing if received ringing event.
+        otherwise return None.
+    """
+    log.info("Wait for ringing.")
+    start_time = time.time()
+    remaining_time = wait_time
+    event_iter_timeout = 4
+    event_ringing = None
+
+    while remaining_time > 0:
+        try:
+            event_ringing =  ad.ed.wait_for_event(EventCallStateChanged,
+                             is_sub_event_match,
+                             timeout=event_iter_timeout,
+                             sub_event=TELEPHONY_STATE_RINGING)
+        except Empty:
+            if ad.droid.telecomIsRinging():
+                log.error("No Ringing event. But Callee in Ringing state.")
+                log.error("Test framework dropped event.")
+                return None
+        remaining_time = start_time + wait_time - time.time()
+        if event_ringing is not None:
+            break
+    if event_ringing is None:
+        log.error("No Ringing Event, Callee not in ringing state.")
+        log.error("No incoming call.")
+        return None
+
+    return event_ringing
+
+def wait_and_answer_call_for_subscription(log, ad, sub_id, incoming_number=None,
+                                          delay_answer=WAIT_TIME_ANSWER_CALL,
+                                          incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """Wait for an incoming call on specified subscription and
+       accepts the call.
+
+    Args:
+        ad: android device object.
+        sub_id: subscription ID
+        incoming_number: Expected incoming number.
+            Optional. Default is None
+        delay_answer: time to wait before answering the call
+            Optional. Default is 1
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True: if incoming call is received and answered successfully.
+        False: for errors
+    """
+    ad.ed.clear_all_events()
+    ad.droid.phoneStartTrackingCallStateForSubscription(sub_id)
+    if (not ad.droid.telecomIsRinging() and
+            ad.droid.getCallStateForSubscription(sub_id) != TELEPHONY_STATE_RINGING):
+        try:
+            event_ringing = wait_for_ringing_event(log, ad,
+                                                   WAIT_TIME_CALLEE_RINGING)
+            if event_ringing is None:
+                log.error("No Ringing Event.")
+                return False
+        finally:
+            ad.droid.phoneStopTrackingCallStateChangeForSubscription(sub_id)
+
+        if not incoming_number:
+            result = True
+        else:
+            result = check_phone_number_match(event_ringing['data']['incomingNumber'],
+                                              incoming_number)
+
+        if not result:
+            log.error("Incoming Number not match")
+            log.error("Expected number:{}, actual number:{}".
+                      format(incoming_number,
+                             event_ringing['data']['incomingNumber']))
+            return False
+
+    ad.ed.clear_all_events()
+    ad.droid.phoneStartTrackingCallStateForSubscription(sub_id)
+    # Delay between ringing and answer.
+    time.sleep(delay_answer)
+
+    log.info("Accept on callee.")
+    ad.droid.telecomAcceptRingingCall()
+    try:
+        ad.ed.wait_for_event(EventCallStateChanged,
+                             is_sub_event_match,
+                             timeout=WAIT_TIME_ACCEPT_CALL_TO_OFFHOOK_EVENT,
+                             sub_event=TELEPHONY_STATE_OFFHOOK)
+    except Empty:
+        if not ad.droid.telecomIsInCall():
+            log.error("Accept call failed.")
+            return False
+    finally:
+        ad.droid.phoneStopTrackingCallStateChangeForSubscription(sub_id)
+    if incall_ui_display == INCALL_UI_DISPLAY_FOREGROUND:
+        ad.droid.telecomShowInCallScreen()
+    elif incall_ui_display == INCALL_UI_DISPLAY_BACKGROUND:
+        ad.droid.showHomeScreen()
+    return True
+
+def wait_and_reject_call(log, ad, incoming_number=None,
+                         delay_reject=WAIT_TIME_REJECT_CALL):
+    """Wait for an incoming call on default voice subscription and
+       reject the call.
+
+    Args:
+        ad: android device object.
+        incoming_number: Expected incoming number.
+            Optional. Default is None
+        delay_reject: time to wait before rejecting the call
+            Optional. Default is WAIT_TIME_REJECT_CALL
+
+    Returns:
+        True: if incoming call is received and reject successfully.
+        False: for errors
+    """
+    return wait_and_reject_call_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId(),
+        incoming_number, delay_reject)
+
+def wait_and_reject_call_for_subscription(log, ad, sub_id, incoming_number=None,
+                                          delay_reject=WAIT_TIME_REJECT_CALL):
+    """Wait for an incoming call on specific subscription and
+       reject the call.
+
+    Args:
+        ad: android device object.
+        sub_id: subscription ID
+        incoming_number: Expected incoming number.
+            Optional. Default is None
+        delay_reject: time to wait before rejecting the call
+            Optional. Default is WAIT_TIME_REJECT_CALL
+
+    Returns:
+        True: if incoming call is received and reject successfully.
+        False: for errors
+    """
+    ad.ed.clear_all_events()
+    ad.droid.phoneStartTrackingCallStateForSubscription(sub_id)
+    if (not ad.droid.telecomIsRinging() and
+            ad.droid.getCallStateForSubscription(sub_id) != TELEPHONY_STATE_RINGING):
+        try:
+            event_ringing = wait_for_ringing_event(log, ad,
+                                                   WAIT_TIME_CALLEE_RINGING)
+            if event_ringing is None:
+                log.error("No Ringing Event.")
+                return False
+        finally:
+            ad.droid.phoneStopTrackingCallStateChangeForSubscription(sub_id)
+
+        if not incoming_number:
+            result = True
+        else:
+            result = check_phone_number_match(event_ringing['data']['incomingNumber'],
+                                              incoming_number)
+
+        if not result:
+            log.error("Incoming Number not match")
+            log.error("Expected number:{}, actual number:{}".
+                      format(incoming_number,
+                             event_ringing['data']['incomingNumber']))
+            return False
+
+    ad.ed.clear_all_events()
+    ad.droid.phoneStartTrackingCallStateForSubscription(sub_id)
+    # Delay between ringing and reject.
+    time.sleep(delay_reject)
+
+    log.info("Reject on callee.")
+    ad.droid.telecomEndCall()
+    try:
+        ad.ed.wait_for_event(EventCallStateChanged,
+                             is_sub_event_match,
+                             timeout=WAIT_TIME_HANGUP_TO_IDLE_EVENT,
+                             sub_event=TELEPHONY_STATE_IDLE)
+    except Empty:
+        log.error("No onCallStateChangedIdle event received.")
+        return False
+    finally:
+        ad.droid.phoneStopTrackingCallStateChangeForSubscription(sub_id)
+    return True
+
+
+def hangup_call(log, ad):
+    """Hang up ongoing active call.
+    """
+    ad.ed.clear_all_events()
+    ad.droid.phoneStartTrackingCallState()
+    log.info("Hangup call.")
+    ad.droid.telecomEndCall()
+
+    try:
+        ad.ed.wait_for_event(EventCallStateChanged,
+                             is_sub_event_match,
+                             timeout=WAIT_TIME_HANGUP_TO_IDLE_EVENT,
+                             sub_event=TELEPHONY_STATE_IDLE)
+    except Empty:
+        if ad.droid.telecomIsInCall():
+            log.error("Hangup call failed.")
+            return False
+    finally:
+        ad.droid.phoneStopTrackingCallStateChange()
+    return True
+
+def disconnect_call_by_id(log, ad, call_id):
+    """Disconnect call by call id.
+    """
+    ad.droid.telecomCallDisconnect(call_id)
+    return True
+
+def check_phone_number_match(number1, number2):
+    """Check whether two input phone numbers match or not.
+
+    Compare the two input phone numbers.
+    If they match, return True; otherwise, return False.
+    Currently only handle phone number with the following formats:
+        (US phone number format)
+        +1abcxxxyyyy
+        1abcxxxyyyy
+        abcxxxyyyy
+        abc xxx yyyy
+        abc.xxx.yyyy
+        abc-xxx-yyyy
+
+    Args:
+        number1: 1st phone number to be compared.
+        number2: 2nd phone number to be compared.
+
+    Returns:
+        True if two phone numbers match. Otherwise False.
+    """
+    # Remove "1"  or "+1"from front
+    if number1[0] == "1":
+        number1 = number1[1:]
+    elif number1[0:2] == "+1":
+        number1 = number1[2:]
+    if number2[0] == "1":
+        number2 = number2[1:]
+    elif number2[0:2] == "+1":
+        number2 = number2[2:]
+    # Remove white spaces, dashes, dots
+    number1 = number1.replace(" ", "").replace("-", "").replace(".", "")
+    number2 = number2.replace(" ", "").replace("-", "").replace(".", "")
+    return number1 == number2
+
+
+def initiate_call(log, ad_caller, callee_number, emergency=False):
+    """Make phone call from caller to callee.
+
+    Args:
+        ad_caller: Caller android device object.
+        callee_number: Callee phone number.
+        emergency : specify the call is emergency.
+            Optional. Default value is False.
+
+    Returns:
+        result: if phone call is placed successfully.
+    """
+    ad_caller.ed.clear_all_events()
+    sub_id = ad_caller.droid.subscriptionGetDefaultVoiceSubId()
+    ad_caller.droid.phoneStartTrackingCallStateForSubscription(sub_id)
+
+    wait_time_for_incall_state = WAIT_TIME_CALL_INITIATION
+
+    try:
+        # Make a Call
+        if emergency:
+            ad_caller.droid.phoneCallEmergencyNumber(callee_number)
+        else:
+            ad_caller.droid.phoneCallNumber(callee_number)
+
+        # Verify OFFHOOK event
+        if ad_caller.droid.getCallState() != TELEPHONY_STATE_OFFHOOK:
+            event_offhook =  ad_caller.ed.wait_for_event(EventCallStateChanged,
+                is_sub_event_match, timeout=wait_time_for_incall_state,
+                sub_event=TELEPHONY_STATE_OFFHOOK)
+    except Empty:
+        log.error("initiate_call did not receive Telephony OFFHOOK event.")
+        return False
+    finally:
+        ad_caller.droid.phoneStopTrackingCallStateChangeForSubscription(sub_id)
+
+    # Verify call state
+    while wait_time_for_incall_state > 0:
+        wait_time_for_incall_state -= 1
+        if (ad_caller.droid.telecomIsInCall() and
+            (ad_caller.droid.getCallState() == TELEPHONY_STATE_OFFHOOK) and
+            (ad_caller.droid.telecomGetCallState() == TELEPHONY_STATE_OFFHOOK)):
+            return True
+        time.sleep(1)
+    log.error("Make call fail. telecomIsInCall:{}, Telecom State:{},"
+        " Telephony State:{}".format(ad_caller.droid.telecomIsInCall(),
+        ad_caller.droid.getCallState(), ad_caller.droid.telecomGetCallState()))
+    return False
+
+def call_reject_leave_message(log, ad_caller, ad_callee,
+       verify_caller_func=None,
+       wait_time_in_call=WAIT_TIME_TO_LEAVE_VOICE_MAIL):
+    """On default voice subscription, Call from caller to callee,
+    reject on callee, caller leave a voice mail.
+
+    1. Caller call Callee.
+    2. Callee reject incoming call.
+    3. Caller leave a voice mail.
+    4. Verify callee received the voice mail notification.
+
+    Args:
+        ad_caller: caller android device object.
+        ad_callee: callee android device object.
+        verify_caller_func: function to verify caller is in correct state while in-call.
+            This is optional, default is None.
+        wait_time_in_call: time to wait when leaving a voice mail.
+            This is optional, default is WAIT_TIME_TO_LEAVE_VOICE_MAIL
+
+    Returns:
+        True: if voice message is received on callee successfully.
+        False: for errors
+    """
+    subid_caller = ad_caller.droid.subscriptionGetDefaultVoiceSubId()
+    subid_callee = ad_callee.droid.subscriptionGetDefaultVoiceSubId()
+    return call_reject_leave_message_for_subscription(log, ad_caller, ad_callee,
+                                                      subid_caller, subid_callee,
+                                                      verify_caller_func,
+                                                      wait_time_in_call)
+
+def call_reject_leave_message_for_subscription(log, ad_caller, ad_callee,
+                                               subid_caller, subid_callee,
+                                               verify_caller_func=None,
+                                               wait_time_in_call=WAIT_TIME_TO_LEAVE_VOICE_MAIL):
+    """On specific voice subscription, Call from caller to callee,
+    reject on callee, caller leave a voice mail.
+
+    1. Caller call Callee.
+    2. Callee reject incoming call.
+    3. Caller leave a voice mail.
+    4. Verify callee received the voice mail notification.
+
+    Args:
+        ad_caller: caller android device object.
+        ad_callee: callee android device object.
+        subid_caller: caller's subscription id.
+        subid_callee: callee's subscription id.
+        verify_caller_func: function to verify caller is in correct state while in-call.
+            This is optional, default is None.
+        wait_time_in_call: time to wait when leaving a voice mail.
+            This is optional, default is WAIT_TIME_TO_LEAVE_VOICE_MAIL
+
+    Returns:
+        True: if voice message is received on callee successfully.
+        False: for errors
+    """
+    class _CallSequenceException(Exception):
+        pass
+    # Currently this test utility only works for TMO and ATT and SPT.
+    # It does not work for VZW (see b/21559800)
+    # "with VVM TelephonyManager APIs won't work for vm"
+
+    caller_number = ad_caller.cfg['subscription'][subid_caller]['phone_num']
+    callee_number = ad_callee.cfg['subscription'][subid_callee]['phone_num']
+
+    log.info("Call from {} to {}".format(caller_number, callee_number))
+
+    try:
+
+        if not initiate_call(log, ad_caller, callee_number):
+            raise _CallSequenceException("Initiate call failed.")
+
+        if not wait_and_reject_call_for_subscription(
+                log, ad_callee, subid_callee, incoming_number=caller_number):
+            raise _CallSequenceException("Reject call fail.")
+
+        ad_callee.droid.phoneStartTrackingVoiceMailStateChangeForSubscription(
+            subid_callee)
+        voice_mail_count_before = ad_callee.droid.getVoiceMailCountForSubscription(
+            subid_callee)
+
+        # -1 means there are unread voice mail, but the count is unknown
+        # 0 means either this API not working (VZW) or no unread voice mail.
+        if voice_mail_count_before != 0:
+            log.warning("--Pending new Voice Mail, please clear on phone.--")
+
+        # ensure that all internal states are updated in telecom
+        time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+        ad_callee.ed.clear_all_events()
+
+        if verify_caller_func and not verify_caller_func(log, ad_caller):
+            raise _CallSequenceException(
+                "Caller not in correct state!")
+
+        # TODO: Need to play some sound to leave message. Otherwise carrier
+        # voice mail server have a big chance to drop this voice mail.
+
+        time.sleep(wait_time_in_call)
+
+        if not verify_caller_func:
+            caller_state_result = ad_caller.droid.telecomIsInCall()
+        else:
+            caller_state_result = verify_caller_func(log, ad_caller)
+        if not caller_state_result:
+            raise _CallSequenceException(
+                "Caller not in correct state after {} seconds".format(wait_time_in_call))
+
+        if not hangup_call(log, ad_caller):
+            raise _CallSequenceException(
+                "Error in Hanging-Up Call")
+
+        log.info("Wait for voice mail indicator on callee.")
+        try:
+            event = ad_callee.ed.wait_for_event(EventMessageWaitingIndicatorChanged,
+                                                _is_on_message_waiting_event_true)
+            log.info(event)
+        except Empty:
+            raise _CallSequenceException("No expected event {}.".
+                                         format(EventMessageWaitingIndicatorChanged))
+        voice_mail_count_after = ad_callee.droid.getVoiceMailCountForSubscription(
+            subid_callee)
+        log.info("getVoiceMailCount output - before: {}, after: {}".
+                 format(voice_mail_count_before, voice_mail_count_after))
+
+        # voice_mail_count_after should:
+        # either equals to (voice_mail_count_before + 1) [For ATT and SPT]
+        # or equals to -1 [For TMO]
+        # -1 means there are unread voice mail, but the count is unknown
+        if not check_voice_mail_count(log, ad_callee,
+                                      voice_mail_count_before,
+                                      voice_mail_count_after):
+            log.error("getVoiceMailCount output is incorrect.")
+            return False
+
+    except _CallSequenceException as e:
+        log.error(e)
+        return False
+    finally:
+        ad_callee.droid.phoneStopTrackingVoiceMailStateChangeForSubscription(
+            subid_callee)
+    return True
+
+def call_voicemail_erase_all_pending_voicemail(log, ad):
+    """Script for phone to erase all pending voice mail.
+    This script only works for TMO and ATT and SPT currently.
+    This script only works if phone have already set up voice mail options,
+    and phone should disable password protection for voice mail.
+
+    1. If phone don't have pending voice message, return True.
+    2. Dial voice mail number.
+        For TMO, the number is '123'
+        For ATT, the number is phone's number
+        For SPT, the number is phone's number
+    3. Wait for voice mail connection setup.
+    4. Wait for voice mail play pending voice message.
+    5. Send DTMF to delete one message.
+        The digit is '7'.
+    6. Repeat steps 4 and 5 until voice mail server drop this call.
+        (No pending message)
+    6. Check getVoiceMailCount result. it should be 0.
+
+    Args:
+        log: log object
+        ad: android device object
+    Returns:
+        False if error happens. True is succeed.
+    """
+    log.info("Erase all pending voice mail.")
+    if ad.droid.getVoiceMailCount() == 0:
+        log.info("No Pending voice mail.")
+        return True
+
+    voice_mail_number = get_voice_mail_number(log, ad)
+
+    if not initiate_call(log, ad, voice_mail_number):
+        log.error("Initiate call failed.")
+        return False
+    time.sleep(VOICE_MAIL_SERVER_RESPONSE_DELAY)
+    callId = ad.droid.telecomCallGetCallIds()[0]
+    time.sleep(VOICE_MAIL_SERVER_RESPONSE_DELAY)
+    count = MAX_SAVED_VOICE_MAIL
+    while(is_phone_in_call(log, ad) and (count > 0)):
+        log.info("Press 7 to delete voice mail.")
+        ad.droid.telecomCallPlayDtmfTone(callId, VOICEMAIL_DELETE_DIGIT)
+        ad.droid.telecomCallStopDtmfTone(callId)
+        time.sleep(VOICE_MAIL_SERVER_RESPONSE_DELAY)
+        count -= 1
+    log.info("Voice mail server dropped this call.")
+    # wait for getVoiceMailCount to update correct result
+    remaining_time = MAX_WAIT_TIME_FOR_VOICE_MAIL_COUNT
+    while((remaining_time > 0) and (ad.droid.getVoiceMailCount() != 0)):
+        time.sleep(1)
+        remaining_time -= 1
+    current_voice_mail_count = ad.droid.getVoiceMailCount()
+    log.info("getVoiceMailCount: {}".format(current_voice_mail_count))
+    return (current_voice_mail_count == 0)
+
+def _is_on_message_waiting_event_true(event):
+    """Private function to return if the received EventMessageWaitingIndicatorChanged
+    event 'MessageWaitingIndicator' field is True.
+    """
+    return event['data']['MessageWaitingIndicator']
+
+def call_setup_teardown(log, ad_caller, ad_callee, ad_hangup=None,
+       verify_caller_func=None, verify_callee_func=None,
+       wait_time_in_call=WAIT_TIME_IN_CALL,
+       incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """ Call process, including make a phone call from caller,
+    accept from callee, and hang up. The call is on default voice subscription
+
+    In call process, call from <droid_caller> to <droid_callee>,
+    after ringing, wait <delay_answer> to accept the call,
+    (optional)then hang up from <droid_hangup>.
+
+    Args:
+        ad_caller: Caller Android Device Object.
+        ad_callee: Callee Android Device Object.
+        ad_hangup: Android Device Object end the phone call.
+            Optional. Default value is None, and phone call will continue.
+        verify_call_mode_caller: func_ptr to verify caller in correct mode
+            Optional. Default is None
+        verify_call_mode_caller: func_ptr to verify caller in correct mode
+            Optional. Default is None
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True if call process without any error.
+        False if error happened.
+
+    """
+    subid_caller = ad_caller.droid.subscriptionGetDefaultVoiceSubId()
+    subid_callee = ad_callee.droid.subscriptionGetDefaultVoiceSubId()
+    return call_setup_teardown_for_subscription(log, ad_caller, ad_callee,
+                                                subid_caller, subid_callee,
+                                                ad_hangup, verify_caller_func,
+                                                verify_callee_func,
+                                                wait_time_in_call,
+                                                incall_ui_display)
+
+
+def call_setup_teardown_for_subscription(log, ad_caller, ad_callee, subid_caller,
+                                         subid_callee, ad_hangup=None,
+                                         verify_caller_func=None,
+                                         verify_callee_func=None,
+                                         wait_time_in_call=WAIT_TIME_IN_CALL,
+                                         incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """ Call process, including make a phone call from caller,
+    accept from callee, and hang up. The call is on specified subscription
+
+    In call process, call from <droid_caller> to <droid_callee>,
+    after ringing, wait <delay_answer> to accept the call,
+    (optional)then hang up from <droid_hangup>.
+
+    Args:
+        ad_caller: Caller Android Device Object.
+        ad_callee: Callee Android Device Object.
+        subid_caller: Caller subscription ID
+        subid_callee: Callee subscription ID
+        ad_hangup: Android Device Object end the phone call.
+            Optional. Default value is None, and phone call will continue.
+        verify_call_mode_caller: func_ptr to verify caller in correct mode
+            Optional. Default is None
+        verify_call_mode_caller: func_ptr to verify caller in correct mode
+            Optional. Default is None
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True if call process without any error.
+        False if error happened.
+
+    """
+    class _CallSequenceException(Exception):
+        pass
+
+    caller_number = ad_caller.cfg['subscription'][subid_caller]['phone_num']
+    callee_number = ad_callee.cfg['subscription'][subid_callee]['phone_num']
+
+    log.info("Call from {} to {}".format(caller_number, callee_number))
+
+    try:
+        if not initiate_call(log, ad_caller, callee_number):
+            raise _CallSequenceException("Initiate call failed.")
+
+        if not wait_and_answer_call_for_subscription(
+                log, ad_callee, subid_callee, incoming_number=caller_number,
+                incall_ui_display=incall_ui_display):
+            raise _CallSequenceException("Answer call fail.")
+
+        # ensure that all internal states are updated in telecom
+        time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+
+        if verify_caller_func and not verify_caller_func(log, ad_caller):
+            raise _CallSequenceException(
+                "Caller not in correct state!")
+        if verify_callee_func and not verify_callee_func(log, ad_callee):
+            raise _CallSequenceException(
+                "Callee not in correct state!")
+
+        time.sleep(wait_time_in_call)
+
+        if not verify_caller_func:
+            caller_state_result = ad_caller.droid.telecomIsInCall()
+        else:
+            caller_state_result = verify_caller_func(log, ad_caller)
+        if not caller_state_result:
+            raise _CallSequenceException(
+                "Caller not in correct state after {} seconds".format(wait_time_in_call))
+
+        if not verify_callee_func:
+            callee_state_result = ad_callee.droid.telecomIsInCall()
+        else:
+            callee_state_result = verify_callee_func(log, ad_callee)
+        if not callee_state_result:
+            raise _CallSequenceException(
+                "Callee not in correct state after {} seconds".format(wait_time_in_call))
+
+        if not ad_hangup:
+            return True
+
+        if not hangup_call(log, ad_hangup):
+            raise _CallSequenceException(
+                "Error in Hanging-Up Call")
+
+        return True
+
+    except _CallSequenceException as e:
+        log.error(e)
+        return False
+    finally:
+        if ad_hangup:
+            for ad in [ad_caller, ad_callee]:
+                try:
+                    if ad.droid.telecomIsInCall():
+                        ad.droid.telecomEndCall()
+                except Exception as e:
+                    log.error(str(e))
+
+def phone_number_formatter(input_string, format):
+    """Get expected format of input phone number string.
+
+    Args:
+        input_string: (string) input phone number.
+            The input could be 10/11/12 digital, with or without " "/"-"/"."
+        format: (int) expected format, this could be 7/10/11/12
+            if format is 7: output string would be 7 digital number.
+            if format is 10: output string would be 10 digital (standard) number.
+            if format is 11: output string would be "1" + 10 digital number.
+            if format is 12: output string would be "+1" + 10 digital number.
+
+    Returns:
+        If no error happen, return phone number in expected format.
+        Else, return None.
+    """
+    # make sure input_string is 10 digital
+    # Remove white spaces, dashes, dots
+    input_string = input_string.replace(" ", "").replace("-", "").replace(".", "")
+    # Remove "1"  or "+1"from front
+    if (len(input_string) == PHONE_NUMBER_STRING_FORMAT_11_DIGIT and
+        input_string[0] == "1"):
+        input_string = input_string[1:]
+    elif (len(input_string) == PHONE_NUMBER_STRING_FORMAT_12_DIGIT and
+          input_string[0:2] == "+1"):
+        input_string = input_string[2:]
+    elif (len(input_string) == PHONE_NUMBER_STRING_FORMAT_7_DIGIT and
+          format == PHONE_NUMBER_STRING_FORMAT_7_DIGIT):
+        return input_string
+    elif len(input_string) != PHONE_NUMBER_STRING_FORMAT_10_DIGIT:
+        return None
+    # change input_string according to format
+    if format == PHONE_NUMBER_STRING_FORMAT_12_DIGIT:
+        input_string = "+1"+input_string
+    elif format == PHONE_NUMBER_STRING_FORMAT_11_DIGIT:
+        input_string = "1"+input_string
+    elif format == PHONE_NUMBER_STRING_FORMAT_10_DIGIT:
+        input_string = input_string
+    elif format == PHONE_NUMBER_STRING_FORMAT_7_DIGIT:
+        input_string = input_string[3:]
+    else:
+        return None
+    return input_string
+
+def get_internet_connection_type(log, ad):
+    """Get current active connection type name.
+
+    Args:
+        log: Log object.
+        ad: Android Device Object.
+    Returns:
+        current active connection type name.
+    """
+    if not ad.droid.connectivityNetworkIsConnected():
+        return 'none'
+    return connection_type_from_type_string(
+        ad.droid.connectivityNetworkGetActiveConnectionTypeName())
+
+def verify_http_connection(log, ad, url="http://www.google.com/", retry=3, retry_interval=5):
+    """Make ping request and return status.
+
+    Args:
+        ad: Android Device Object.
+        url: Optional. The ping request will be made to this URL.
+            Default Value is "http://www.google.com/".
+
+    """
+    for i in range(0, retry+1):
+
+        try:
+            http_response = ad.droid.httpPing(url)
+        except:
+            http_response = None
+
+        # If httpPing failed, it may return {} (if phone just turn off APM) or
+        # None (regular fail)
+        # So here use "if http_response" to see if it pass or fail
+        if  http_response:
+            log.info("Verify Internet succeeded after {}s."
+                     .format(i*retry_interval) if i > 0 else
+                     "Verify Internet succeeded.")
+            return True
+        else:
+            if i < retry:
+                time.sleep(retry_interval)
+    log.info("Verify Internet retry failed after {}s"
+             .format(i*retry_interval))
+    return False
+
+
+def _connection_state_change(_event, target_state, connection_type):
+    if connection_type:
+        if 'TypeName' not in _event['data']:
+            return False
+        connection_type_string_in_event = _event['data']['TypeName']
+        cur_type = connection_type_from_type_string(connection_type_string_in_event)
+        if cur_type != connection_type:
+            log.info(
+                "_connection_state_change expect: {}, received: {} <type {}>".
+                format(connection_type, connection_type_string_in_event, cur_type))
+            return False
+
+    if 'isConnected' in _event['data'] and _event['data']['isConnected'] == target_state:
+        return True
+    return False
+
+
+def wait_for_cell_data_connection(log, ad, state,
+                                  timeout_value=EventDispatcher.DEFAULT_TIMEOUT):
+    """Wait for data connection status to be expected value for default
+       data subscription.
+
+    Wait for the data connection status to be DATA_STATE_CONNECTED
+        or DATA_STATE_DISCONNECTED.
+
+    Args:
+        log: Log object.
+        ad: Android Device Object.
+        state: Expected status: True or False.
+            If True, it will wait for status to be DATA_STATE_CONNECTED.
+            If False, it will wait for status ti be DATA_STATE_DISCONNECTED.
+        timeout_value: wait for cell data timeout value.
+            This is optional, default value is EventDispatcher.DEFAULT_TIMEOUT
+
+    Returns:
+        True if success.
+        False if failed.
+    """
+    sub_id = ad.droid.subscriptionGetDefaultDataSubId()
+    return wait_for_cell_data_connection_for_subscription(log, ad, sub_id, state,
+                                                          timeout_value)
+
+def _is_data_connection_state_match(log, ad, expected_data_connection_state):
+    return (expected_data_connection_state == ad.droid.getDataConnectionState())
+
+def _is_network_connected_state_match(log, ad, expected_network_connected_state):
+    return (expected_network_connected_state == ad.droid.connectivityNetworkIsConnected())
+
+def wait_for_cell_data_connection_for_subscription(log, ad, sub_id, state,
+                                  timeout_value=EventDispatcher.DEFAULT_TIMEOUT):
+    """Wait for data connection status to be expected value for specified
+       subscrption id.
+
+    Wait for the data connection status to be DATA_STATE_CONNECTED
+        or DATA_STATE_DISCONNECTED.
+
+    Args:
+        log: Log object.
+        ad: Android Device Object.
+        sub_id: subscription Id
+        state: Expected status: True or False.
+            If True, it will wait for status to be DATA_STATE_CONNECTED.
+            If False, it will wait for status ti be DATA_STATE_DISCONNECTED.
+        timeout_value: wait for cell data timeout value.
+            This is optional, default value is EventDispatcher.DEFAULT_TIMEOUT
+
+    Returns:
+        True if success.
+        False if failed.
+    """
+    state_str, tel_sub_event_name = {
+        True: (DATA_STATE_CONNECTED, DATA_STATE_CONNECTED),
+        False: (DATA_STATE_DISCONNECTED, DATA_STATE_DISCONNECTED)
+    }[state]
+
+    ad.ed.clear_all_events()
+    ad.droid.phoneStartTrackingDataConnectionStateChangeForSubscription(sub_id)
+    ad.droid.connectivityStartTrackingConnectivityStateChange()
+    try:
+        # TODO There is no framework API to get data connection state by sub id
+        data_state = ad.droid.getDataConnectionState()
+        if data_state == state_str:
+            return _wait_for_nw_data_connection(log, ad, state,
+                                                NETWORK_CONNECTION_TYPE_CELL,
+                                                timeout_value)
+
+        try:
+            event = ad.ed.wait_for_event(EventDataConnectionStateChanged,
+                                         is_sub_event_match,
+                                         timeout=timeout_value,
+                                         sub_event=tel_sub_event_name)
+        except Empty:
+            log.debug("No expected event EventDataConnectionStateChanged {}.".
+                      format(tel_sub_event_name))
+
+        # TODO: Wait for <WAIT_TIME_CONNECTION_STATE_UPDATE> seconds for
+        # data connection state.
+        # Otherwise, the network state will not be correct.
+        # The bug is tracked here: b/20921915
+        # will remove this sleep once bug fixed.
+
+        # FIXME: previously we use _is_data_connection_state_match
+        # but getDataConnectionState sometimes return wrong value. (b/22612607)
+        # Use _is_network_connected_state_match temporarily, until b/22612607 fixed.
+
+        if _wait_for_droid_in_state(log, ad,
+            WAIT_TIME_CONNECTION_STATE_UPDATE, _is_network_connected_state_match,
+            state):
+            return _wait_for_nw_data_connection(log, ad, state,
+                                                NETWORK_CONNECTION_TYPE_CELL,
+                                                timeout_value)
+        else:
+            return False
+
+    finally:
+        ad.droid.phoneStopTrackingDataConnectionStateChangeForSubscription(sub_id)
+
+
+def wait_for_wifi_data_connection(log, ad, state,
+                                  timeout_value=EventDispatcher.DEFAULT_TIMEOUT):
+    """Wait for data connection status to be expected value and connection is by WiFi.
+
+    Args:
+        log: Log object.
+        ad: Android Device Object.
+        state: Expected status: True or False.
+            If True, it will wait for status to be DATA_STATE_CONNECTED.
+            If False, it will wait for status ti be DATA_STATE_DISCONNECTED.
+        timeout_value: wait for network data timeout value.
+            This is optional, default value is EventDispatcher.DEFAULT_TIMEOUT
+
+    Returns:
+        True if success.
+        False if failed.
+    """
+    log.info("{} wait_for_wifi_data_connection".format(ad.serial))
+    return _wait_for_nw_data_connection(log, ad, state,
+                                        NETWORK_CONNECTION_TYPE_WIFI,
+                                        timeout_value)
+
+
+def wait_for_data_connection(log, ad, state,
+                             timeout_value=EventDispatcher.DEFAULT_TIMEOUT):
+    """Wait for data connection status to be expected value.
+
+    Wait for the data connection status to be DATA_STATE_CONNECTED
+        or DATA_STATE_DISCONNECTED.
+
+    Args:
+        log: Log object.
+        ad: Android Device Object.
+        state: Expected status: True or False.
+            If True, it will wait for status to be DATA_STATE_CONNECTED.
+            If False, it will wait for status ti be DATA_STATE_DISCONNECTED.
+        timeout_value: wait for network data timeout value.
+            This is optional, default value is EventDispatcher.DEFAULT_TIMEOUT
+
+    Returns:
+        True if success.
+        False if failed.
+    """
+    return _wait_for_nw_data_connection(log, ad, state, None, timeout_value)
+
+
+def _wait_for_nw_data_connection(log, ad, is_connected, connection_type=None,
+                                 timeout_value=EventDispatcher.DEFAULT_TIMEOUT):
+    """Wait for data connection status to be expected value.
+
+    Wait for the data connection status to be DATA_STATE_CONNECTED
+        or DATA_STATE_DISCONNECTED.
+
+    Args:
+        log: Log object.
+        ad: Android Device Object.
+        is_connected: Expected connection status: True or False.
+            If True, it will wait for status to be DATA_STATE_CONNECTED.
+            If False, it will wait for status ti be DATA_STATE_DISCONNECTED.
+        connection_type: expected connection type.
+            This is optional, if it is None, then any connection type will return True.
+        timeout_value: wait for network data timeout value.
+            This is optional, default value is EventDispatcher.DEFAULT_TIMEOUT
+
+    Returns:
+        True if success.
+        False if failed.
+    """
+    ad.ed.clear_all_events()
+    ad.droid.connectivityStartTrackingConnectivityStateChange()
+    try:
+        cur_data_connection_state = ad.droid.connectivityNetworkIsConnected()
+        if is_connected == cur_data_connection_state:
+            current_type = get_internet_connection_type(log, ad)
+            log.info("_wait_for_nw_data_connection: current connection type: {}".
+                     format(current_type))
+            if not connection_type:
+                return True
+            else:
+                if not is_connected and current_type != connection_type:
+                    log.info("wait_for_nw_data_connection success: {} data not on {}!".
+                        format(ad.serial, connection_type))
+                    return True
+                elif is_connected and current_type == connection_type:
+                    log.info("wait_for_nw_data_connection success: {} data on {}!".
+                        format(ad.serial, connection_type))
+                    return True
+        else:
+            log.info("{} current state: {} target: {}".
+                format(ad.serial, cur_data_connection_state, is_connected))
+
+        try:
+            event = ad.ed.wait_for_event(EventConnectivityChanged,
+                                         _connection_state_change,
+                                         timeout_value,
+                                         is_connected, connection_type)
+            log.info("_wait_for_nw_data_connection received event:{}".format(event))
+        except Empty:
+            pass
+
+        log.info("_wait_for_nw_data_connection: check connection after wait event.")
+        # TODO: Wait for <WAIT_TIME_CONNECTION_STATE_UPDATE> seconds for
+        # data connection state.
+        # Otherwise, the network state will not be correct.
+        # The bug is tracked here: b/20921915
+        # will remove this sleep once bug fixed.
+        if _wait_for_droid_in_state(log, ad,
+            WAIT_TIME_CONNECTION_STATE_UPDATE, _is_network_connected_state_match,
+            is_connected):
+            current_type = get_internet_connection_type(log, ad)
+            log.info("_wait_for_nw_data_connection: current connection type: {}".
+                     format(current_type))
+            if not connection_type:
+                return True
+            else:
+                if not is_connected and current_type != connection_type:
+                    log.info("wait_for_nw_data_connection after event wait, success: {} data not on {}!".
+                        format(ad.serial, connection_type))
+                    return True
+                elif is_connected and current_type == connection_type:
+                    log.info("wait_for_nw_data_connection after event wait, success: {} data on {}!".
+                        format(ad.serial, connection_type))
+                    return True
+                else:
+                    return False
+        else:
+            return False
+    except Exception as e:
+        log.error("tel_test_utils._wait_for_nw_data_connection threw Random exception {}".
+            format(str(e)))
+        return False
+    finally:
+        ad.droid.connectivityStopTrackingConnectivityStateChange()
+
+def verify_incall_state(log, ads, expected_status):
+    """Verify phones in incall state or not.
+
+    Verify if all phones in the array <ads> are in <expected_status>.
+
+    Args:
+        log: Log object.
+        ads: Array of Android Device Object. All droid in this array will be tested.
+        expected_status: If True, verify all Phones in incall state.
+            If False, verify all Phones not in incall state.
+
+    """
+    result = True
+    for ad in ads:
+        if ad.droid.telecomIsInCall() is not expected_status:
+            log.error("Verify_incall_state: {} status:{}, expected:{}".
+                format(ad.serial, ad.droid.telecomIsInCall(), expected_status))
+            result = False
+    return result
+
+def verify_active_call_number(log, ad, expected_number):
+    """Verify the number of current active call.
+
+    Verify if the number of current active call in <ad> is
+        equal to <expected_number>.
+
+    Args:
+        ad: Android Device Object.
+        expected_number: Expected active call number.
+    """
+    calls = ad.droid.telecomCallGetCallIds()
+    if calls is None:
+        actual_number = 0
+    else:
+        actual_number = len(calls)
+    if actual_number != expected_number:
+        log.error("Active Call number in {}".format(ad.serial))
+        log.error("Expected:{}, Actual:{}".format(expected_number, actual_number))
+        return False
+    return True
+
+
+def num_active_calls(log, ad):
+    """Get the count of current active calls.
+
+    Args:
+        log: Log object.
+        ad: Android Device Object.
+
+    Returns:
+        Count of current active calls.
+    """
+    calls = ad.droid.telecomCallGetCallIds()
+    return len(calls) if calls else 0
+
+
+def toggle_volte(log, ad, new_state=None):
+    """Toggle enable/disable VoLTE for default voice subscription.
+
+    Args:
+        ad: Android device object.
+        new_state: VoLTE mode state to set to.
+            True for enable, False for disable.
+            If None, opposite of the current state.
+
+    Raises:
+        TelTestUtilsError if platform does not support VoLTE.
+    """
+    return toggle_volte_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId(), new_state)
+
+
+def toggle_volte_for_subscription(log, ad, sub_id, new_state=None):
+    """Toggle enable/disable VoLTE for specified voice subscription.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription ID
+        new_state: VoLTE mode state to set to.
+            True for enable, False for disable.
+            If None, opposite of the current state.
+
+    Raises:
+        TelTestUtilsError if platform does not support VoLTE.
+    """
+    # TODO Need to take care of sub_id. No framework API support to get IMS
+    # setting for subscription
+    if not ad.droid.imsIsEnhanced4gLteModeSettingEnabledByPlatform():
+        raise TelTestUtilsError("VoLTE not supported by platform.")
+    current_state = ad.droid.imsIsEnhanced4gLteModeSettingEnabledByUser()
+    if new_state is None:
+        new_state = not current_state
+    if new_state != current_state:
+        ad.droid.imsSetEnhanced4gMode(new_state)
+    return True
+
+def set_wfc_mode(log, ad, wfc_mode):
+    """Set WFC enable/disable and mode.
+
+    Args:
+        log: Log object
+        ad: Android device object.
+        wfc_mode: WFC mode to set to.
+            Valid mode includes: WFC_MODE_WIFI_ONLY, WFC_MODE_CELLULAR_PREFERRED,
+            WFC_MODE_WIFI_PREFERRED, WFC_MODE_DISABLED.
+
+    Returns:
+        True if success. False if ad does not support WFC or error happened.
+    """
+    try:
+        log.info("{} set wfc mode to {}".format(ad.serial, wfc_mode))
+        if not ad.droid.imsIsWfcEnabledByPlatform():
+            if wfc_mode == WFC_MODE_DISABLED:
+                return True
+            else:
+                log.error("WFC not supported by platform.")
+                return False
+
+        ad.droid.imsSetWfcMode(wfc_mode)
+
+    except Exception as e:
+        log.error(e)
+        return False
+
+    return True
+
+
+def set_preferred_network_type(log, ad, network_type):
+    """Set preferred network type for default subscription.
+
+    Args:
+        ad: android_device object
+        network_type: Network type string. For example, "3G", "LTE", "2G".
+
+    Raises:
+        TelTestUtilsError if type is not supported.
+    """
+    return set_preferred_network_type_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), network_type)
+
+
+def set_preferred_network_type_for_subscription(log, ad, sub_id, network_type):
+    """Set preferred network type for specified subscription.
+
+    Args:
+        ad: android_device object
+        sub_id: subscription ID
+        network_type: Network type string. For example, "3G", "LTE", "2G".
+
+    Raises:
+        TelTestUtilsError if type is not supported.
+    """
+
+    operator = get_operator_name(log, ad, sub_id)
+    if operator is None:
+        log.error('Unknown operator for {}'.format(ad.serial))
+        return False
+
+    # Temporarily using the integer network mode setting due to b/24880020
+    # We should switch back to "safe" api once resolved
+    network_mode = network_mode_by_operator_generation(operator, network_type)
+    if network_mode is None:
+        log.error("Couldn't determine appropriate network mode for operator"
+                  .format(operator))
+        return False
+
+    ad.droid.setPreferredNetworkForSubscription(sub_id, network_mode)
+
+    return True
+
+def is_droid_in_network_generation(log, ad, nw_gen, voice_or_data):
+    """Checks if a droid in expected network generation ("2g", "3g" or "4g").
+
+    Args:
+        log: log object.
+        ad: android device.
+        nw_gen: expected generation "4g", "3g", "2g".
+        voice_or_data: check voice network generation or data network generation
+            This parameter is optional. If voice_or_data is None, then if
+            either voice or data in expected generation, function will return True.
+
+    Returns:
+        True if droid in expected network generation. Otherwise False.
+    """
+    return is_droid_in_network_generation_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), nw_gen, voice_or_data)
+
+def is_droid_in_network_generation_for_subscription(log, ad, sub_id, nw_gen, voice_or_data):
+    """Checks if a droid in expected network generation ("2g", "3g" or "4g").
+
+    Args:
+        log: log object.
+        ad: android device.
+        nw_gen: expected generation "4g", "3g", "2g".
+        voice_or_data: check voice network generation or data network generation
+            This parameter is optional. If voice_or_data is None, then if
+            either voice or data in expected generation, function will return True.
+
+    Returns:
+        True if droid in expected network generation. Otherwise False.
+    """
+    service_list = ["data", "voice"]
+
+    if voice_or_data:
+        service_list = [voice_or_data]
+
+    for service in service_list:
+        nw_rat = get_network_rat_for_subscription(log, ad, sub_id, service)
+
+        if nw_rat == RAT_UNKNOWN or not is_valid_rat(nw_rat):
+            continue
+
+        if rat_generation_from_type(nw_rat) == nw_gen:
+            return True
+        else:
+            return False
+
+    return False
+
+
+def is_droid_in_network_rat(log, ad, rat, voice_or_data=None):
+    """Checks if a droid in expected network rat for default subscription Id.
+
+    Args:
+        log: log object.
+        ad: android device.
+        rat: expected network rat
+        voice_or_data: check voice network rat or data network rat
+            This parameter is optional. If voice_or_data is None, then if
+            either voice or data in expected rat, function will return True.
+
+    Returns:
+        True if droid in expected network rat. Otherwise False.
+    """
+    return is_droid_in_network_rat_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), rat, voice_or_data)
+
+def is_droid_not_in_network_rat(log, ad, rat, voice_or_data=None):
+    """Checks if a droid not in network rat for default subscription Id.
+
+    Args:
+        log: log object.
+        ad: android device.
+        rat: network rat
+        voice_or_data: check voice network rat or data network rat
+            This parameter is optional. If voice_or_data is None, then if
+            either voice or data in expected rat, function will return True.
+
+    Returns:
+        True if droid in expected network rat. Otherwise False.
+    """
+    return is_droid_not_in_network_rat_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), rat, voice_or_data)
+
+
+def is_droid_in_network_rat_for_subscription(log, ad, sub_id, rat,
+                                             voice_or_data=None):
+    """Checks if a droid in expected network rat for specified subscription Id.
+
+    Args:
+        log: log object.
+        ad: android device.
+        sub_id: subscription Id
+        rat: expected network rat
+        voice_or_data: check voice network rat or data network rat
+            This parameter is optional. If voice_or_data is None, then if
+            either voice or data in expected rat, function will return True.
+
+    Returns:
+        True if droid in expected network rat. Otherwise False.
+    """
+    service_list = [NETWORK_SERVICE_DATA, NETWORK_SERVICE_VOICE]
+
+    if voice_or_data:
+        service_list = [voice_or_data]
+
+    if not is_valid_rat(rat):
+        raise TelTestUtilsError("Invalid RAT {}".format(rat))
+
+    for service in service_list:
+        current_rat = get_network_rat_for_subscription(log, ad, sub_id, service)
+        if rat_family_from_type(rat) == rat_family_from_type(current_rat):
+            return True
+
+    return False
+
+def is_droid_not_in_network_rat_for_subscription(log, ad, sub_id, rat,
+                                                 voice_or_data=None):
+    """Checks if a droid not in expected network rat for specified subscription Id.
+
+    Args:
+        log: log object.
+        ad: android device.
+        sub_id: subscription Id
+        rat: expected network rat
+        voice_or_data: check voice network rat or data network rat
+            This parameter is optional. If voice_or_data is None, then if
+            either voice or data in expected rat, function will return True.
+
+    Returns:
+        True if droid not in expected network rat. Otherwise False.
+    """
+    return not is_droid_in_network_rat_for_subscription(
+        log, ad, sub_id, rat, voice_or_data)
+
+def _wait_for_droid_in_state(log, ad, max_time, state_check_func, *args, **kwargs):
+    while max_time > 0:
+        if state_check_func(log, ad, *args, **kwargs):
+            return True
+
+        time.sleep(1)
+        max_time -= 1
+
+    return False
+
+
+def _wait_for_droid_in_state_for_subscription(log, ad, sub_id, max_time,
+                                              state_check_func, *args, **kwargs):
+    while max_time > 0:
+        if state_check_func(log, ad, sub_id, *args, **kwargs):
+            return True
+
+        time.sleep(1)
+        max_time -= 1
+
+    return False
+
+
+def _wait_for_droids_in_state(log, ads, max_time, state_check_func, *args, **kwargs):
+    while max_time > 0:
+        success = True
+        for ad in ads:
+            if not state_check_func(log, ad, *args, **kwargs):
+                success = False
+                break
+        if success:
+            return True
+
+        time.sleep(1)
+        max_time -= 1
+
+    return False
+
+def is_phone_in_call(log, ad):
+    """Return True if phone in call.
+
+    Args:
+        log: log object.
+        ad:  android device.
+    """
+    return ad.droid.telecomIsInCall()
+
+def is_phone_not_in_call(log, ad):
+    """Return True if phone not in call.
+
+    Args:
+        log: log object.
+        ad:  android device.
+    """
+    return not ad.droid.telecomIsInCall()
+
+def wait_for_droid_in_call(log, ad, max_time):
+    """Wait for android to be in call state.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        If phone become in call state within max_time, return True.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, is_phone_in_call)
+
+def wait_for_droid_not_in_call(log, ad, max_time):
+    """Wait for android to be not in call state.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        If phone become not in call state within max_time, return True.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, is_phone_not_in_call)
+
+def wait_for_droid_in_network_generation(
+        log, ad, network_type, max_time, voice_or_data=None):
+    """Wait for droid to be in certain connection mode (e.g. lte, 3g).
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: max number of seconds to wait (each droid in the droids list).
+        network_type: expected connection network type. e.g. lte, 3g, 2g.
+        voice_or_data: check droid's voice network type or data network type.
+            Optional, default value is None.
+
+    """
+    return _wait_for_droid_in_state(
+        log, ad, max_time,
+        is_droid_in_network_generation, network_type, voice_or_data)
+
+
+def wait_for_droid_in_network_generation_for_subscription(
+        log, ad, sub_id, network_type, max_time, voice_or_data=None):
+    """Wait for droid to be in certain connection mode (e.g. lte, 3g).
+
+    Args:
+        log: log object.
+        ad:  android device.
+        sub_id: subscription Id
+        max_time: max number of seconds to wait (each droid in the droids list).
+        network_type: expected connection network type. e.g. lte, 3g, 2g.
+        voice_or_data: check droid's voice network type or data network type.
+            Optional, default value is None.
+
+    """
+    return _wait_for_droid_in_state_for_subscription(
+        log, ad, sub_id, max_time, is_droid_in_network_generation_for_subscription,
+        network_type, voice_or_data)
+
+
+def wait_for_droid_in_network_rat(
+        log, ad, network_type, max_time, voice_or_data=None):
+    """Wait for droid to be in certain connection mode (e.g. lte, 3g).
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: max number of seconds to wait (each droid in the droids list).
+        network_type: expected connection network type. e.g. lte, 3g, 2g.
+        voice_or_data: check droid's voice network type or data network type.
+            Optional, default value is None.
+
+    """
+
+    return _wait_for_droid_in_state(
+        log, ad, max_time,
+        is_droid_in_network_rat, network_type, voice_or_data)
+
+def wait_for_droid_not_in_network_rat(
+        log, ad, network_type, max_time, voice_or_data=None):
+    """Wait for droid to be not in certain connection mode (e.g. lte, 3g).
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: max number of seconds to wait (each droid in the droids list).
+        network_type: connection network type. e.g. lte, 3g, 2g.
+        voice_or_data: check droid's voice network type or data network type.
+            Optional, default value is None.
+
+    """
+
+    return _wait_for_droid_in_state(
+        log, ad, max_time,
+        is_droid_not_in_network_rat, network_type, voice_or_data)
+
+
+def wait_for_droid_in_network_rat_for_subscription(
+        log, ad, sub_id, network_type, max_time, voice_or_data=None):
+    """Wait for droid to be in certain connection mode (e.g. lte, 3g).
+
+    Args:
+        log: log object.
+        ad:  android device.
+        sub_id: subscription Id
+        max_time: max number of seconds to wait (each droid in the droids list).
+        network_type: expected connection network type. e.g. lte, 3g, 2g.
+        voice_or_data: check droid's voice network type or data network type.
+            Optional, default value is None.
+
+    """
+
+    return _wait_for_droid_in_state_for_subscription(
+        log, ad, sub_id, max_time,
+        is_droid_in_network_rat_for_subscription, network_type, voice_or_data)
+
+
+def wait_for_droids_in_network_generation(
+        log, ads, network_type, max_time, voice_or_data=None):
+    """Wait for droid to be in certain connection mode (e.g. lte, 3g).
+
+    Args:
+        log: log object.
+        ads: array of android device.
+        max_time: max number of seconds to wait (each droid in the droids list).
+        network_type: expected connection network type. e.g. lte, 3g, 2g.
+        voice_or_data: check droid's voice network type or data network type.
+            Optional, default value is None.
+
+    """
+    # TODO(yangxliu): replace loop time wait with SL4A event.
+
+    return _wait_for_droids_in_state(
+        log, ads, max_time,
+        is_droid_in_network_generation, network_type, voice_or_data)
+
+
+def wait_for_droids_in_network_rat(
+        log, ads, network_type, max_time, voice_or_data=None):
+    """Wait for droid to be in certain connection mode (e.g. lte, 3g).
+
+    Args:
+        log: log object.
+        ads: array of android device.
+        max_time: max number of seconds to wait (each droid in the droids list).
+        network_type: expected connection network type. e.g. lte, 3g, 2g.
+        voice_or_data: check droid's voice network type or data network type.
+            Optional, default value is None.
+
+    """
+
+    return _wait_for_droids_in_state(
+        log, ads, max_time,
+        is_droid_in_network_rat, network_type, voice_or_data)
+
+
+def _is_attached(log, ad, voice_or_data):
+    return _is_attached_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), voice_or_data)
+
+def _is_attached_for_subscription(log, ad, sub_id, voice_or_data):
+    if get_network_rat_for_subscription(log, ad, sub_id,
+                                         voice_or_data) != RAT_UNKNOWN:
+        return True
+    else:
+        return False
+
+
+def wait_for_voice_attach(log, ad, max_time):
+    """Wait for android device to attach on voice.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device attach voice within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, _is_attached,
+                                    NETWORK_SERVICE_VOICE)
+
+def wait_for_voice_attach_for_subscription(log, ad, sub_id, max_time):
+    """Wait for android device to attach on voice in subscription id.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        sub_id: subscription id.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device attach voice within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state_for_subscription(
+        log, ad, sub_id, max_time, _is_attached_for_subscription,
+        NETWORK_SERVICE_VOICE)
+
+def wait_for_data_attach(log, ad, max_time):
+    """Wait for android device to attach on data.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device attach data within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, _is_attached,
+                                    NETWORK_SERVICE_DATA)
+
+def wait_for_data_attach_for_subscription(log, ad, sub_id, max_time):
+    """Wait for android device to attach on data in subscription id.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        sub_id: subscription id.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device attach data within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state_for_subscription(
+        log, ad, sub_id, max_time, _is_attached_for_subscription,
+        NETWORK_SERVICE_DATA)
+
+def _is_ims_registered(log, ad):
+    return ad.droid.isImsRegistered()
+
+def wait_for_ims_registered(log, ad, max_time):
+    """Wait for android device to register on ims.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device register ims successfully within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, _is_ims_registered)
+
+def _is_volte_enabled(log, ad):
+    return ad.droid.isVolteAvailable()
+
+def _is_video_enabled(log, ad):
+    return ad.droid.isVideoCallingAvailable()
+
+def wait_for_volte_enabled(log, ad, max_time):
+    """Wait for android device to report VoLTE enabled bit true.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device report VoLTE enabled bit true within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, _is_volte_enabled)
+
+def wait_for_video_enabled(log, ad, max_time):
+    """Wait for android device to report Video Telephony enabled bit true.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device report Video Telephony enabled bit true within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, _is_video_enabled)
+
+def is_wfc_enabled(log, ad):
+    """Return True if WiFi Calling feature bit is True.
+
+    Args:
+        log: log object.
+        ad: android device.
+
+    Returns:
+        Return True if WiFi Calling feature bit is True.
+        Return False if WiFi Calling feature bit is False.
+    """
+    return ad.droid.isWifiCallingAvailable()
+
+def wait_for_wfc_enabled(log, ad, max_time):
+    """Wait for android device to report WiFi Calling enabled bit true.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device report WiFi Calling enabled bit true within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time, is_wfc_enabled)
+
+def wait_for_wfc_disabled(log, ad, max_time):
+    """Wait for android device to report WiFi Calling enabled bit false.
+
+    Args:
+        log: log object.
+        ad:  android device.
+        max_time: maximal wait time.
+
+    Returns:
+        Return True if device report WiFi Calling enabled bit false within max_time.
+        Return False if timeout.
+    """
+    return _wait_for_droid_in_state(log, ad, max_time,
+        lambda log, ad : not is_wfc_enabled(log, ad))
+
+def get_phone_number(log, ad):
+    """Get phone number for default subscription
+
+    Args:
+        log: log object.
+        ad: Android device object.
+
+    Returns:
+        Phone number.
+    """
+    return get_phone_number_for_subscription(log, ad,
+        ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def get_phone_number_for_subscription(log, ad, subid):
+    """Get phone number for subscription
+
+    Args:
+        log: log object.
+        ad: Android device object.
+        subid: subscription id.
+
+    Returns:
+        Phone number.
+    """
+    number = None
+    try:
+        number = ad.cfg['subscription'][subid]['phone_num']
+    except KeyError:
+        number = ad.droid.getLine1NumberForSubscription(subid)
+    return number
+
+def set_phone_number(log, ad, phone_num):
+    """Set phone number for default subscription
+
+    Args:
+        log: log object.
+        ad: Android device object.
+        phone_num: phone number string.
+
+    Returns:
+        True if success.
+    """
+    return set_phone_number_for_subscription(log, ad,
+        ad.droid.subscriptionGetDefaultVoiceSubId(), phone_num)
+
+def set_phone_number_for_subscription(log, ad, subid, phone_num):
+    """Set phone number for subscription
+
+    Args:
+        log: log object.
+        ad: Android device object.
+        subid: subscription id.
+        phone_num: phone number string.
+
+    Returns:
+        True if success.
+    """
+    try:
+        ad.cfg['subscription'][subid]['phone_num'] = phone_num
+    except Exception:
+        return False
+    return True
+
+def get_operator_name(log, ad, subId=None):
+    """Get operator name (e.g. vzw, tmo) of droid.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription ID
+            Optional, default is None
+
+    Returns:
+        Operator name.
+    """
+    try:
+        if subId is not None:
+            result = operator_name_from_plmn_id(
+                ad.droid.getSimOperatorForSubscription(subId))
+        else:
+            result = operator_name_from_plmn_id(
+                ad.droid.getSimOperator())
+    except KeyError:
+        result = CARRIER_UNKNOWN
+    return result
+
+def is_sms_match(event, phonenumber_tx, text):
+    """Return True if 'text' equals to event['data']['Text']
+        and phone number match.
+
+    Args:
+        event: Event object to verify.
+        phonenumber_tx: phone number for sender.
+        text: text string to verify.
+
+    Returns:
+        Return True if 'text' equals to event['data']['Text']
+            and phone number match.
+    """
+    return (check_phone_number_match(event['data']['Sender'], phonenumber_tx) and
+            event['data']['Text'] == text)
+
+def is_sms_partial_match(event, phonenumber_tx, text):
+    """Return True if 'text' starts with event['data']['Text']
+        and phone number match.
+
+    Args:
+        event: Event object to verify.
+        phonenumber_tx: phone number for sender.
+        text: text string to verify.
+
+    Returns:
+        Return True if 'text' starts with event['data']['Text']
+            and phone number match.
+    """
+    return (check_phone_number_match(event['data']['Sender'], phonenumber_tx) and
+            text.startswith(event['data']['Text']))
+
+def sms_send_receive_verify(log, ad_tx, ad_rx, array_message):
+    """Send SMS, receive SMS, and verify content and sender's number.
+
+        Send (several) SMS from droid_tx to droid_rx.
+        Verify SMS is sent, delivered and received.
+        Verify received content and sender's number are correct.
+
+    Args:
+        log: Log object.
+        ad_tx: Sender's Android Device Object
+        ad_rx: Receiver's Android Device Object
+        array_message: the array of message to send/receive
+    """
+    return sms_send_receive_verify_for_subscription(log, ad_tx, ad_rx,
+                     ad_tx.droid.subscriptionGetDefaultSmsSubId(),
+                     ad_rx.droid.subscriptionGetDefaultSmsSubId(),
+                     array_message)
+
+def wait_for_matching_sms(log, ad_rx, phonenumber_tx, text,
+        allow_multi_part_long_sms=True):
+    """Wait for matching incoming SMS.
+
+    Args:
+        log: Log object.
+        ad_rx: Receiver's Android Device Object
+        phonenumber_tx: Sender's phone number.
+        text: SMS content string.
+        allow_multi_part_long_sms: is long SMS allowed to be received as
+            multiple short SMS. This is optional, default value is True.
+
+    Returns:
+        True if matching incoming SMS is received.
+    """
+    if not allow_multi_part_long_sms:
+        try:
+            ad_rx.ed.wait_for_event(EventSmsReceived, is_sms_match,
+                                    WAIT_TIME_SMS_RECEIVE,
+                                    phonenumber_tx, text)
+            return True
+        except Empty:
+            log.error("No matched SMS received event.")
+            return False
+    else:
+        try:
+            received_sms = ''
+            while(text != ''):
+                event = ad_rx.ed.wait_for_event(EventSmsReceived,
+                                                is_sms_partial_match,
+                                                WAIT_TIME_SMS_RECEIVE,
+                                                phonenumber_tx, text)
+                text = text[len(event['data']['Text']):]
+                received_sms += event['data']['Text']
+            return True
+        except Empty:
+            log.error("No matched SMS received event.")
+            if received_sms != '':
+                log.error("Only received partial matched SMS: {}".
+                    format(received_sms))
+            return False
+
+def sms_send_receive_verify_for_subscription(log, ad_tx, ad_rx, subid_tx,
+        subid_rx, array_message):
+    """Send SMS, receive SMS, and verify content and sender's number.
+
+        Send (several) SMS from droid_tx to droid_rx.
+        Verify SMS is sent, delivered and received.
+        Verify received content and sender's number are correct.
+
+    Args:
+        log: Log object.
+        ad_tx: Sender's Android Device Object..
+        ad_rx: Receiver's Android Device Object.
+        subid_tx: Sender's subsciption ID to be used for SMS
+        subid_rx: Receiver's subsciption ID to be used for SMS
+        array_message: the array of message to send/receive
+    """
+
+    phonenumber_tx = ad_tx.cfg['subscription'][subid_tx]['phone_num']
+    phonenumber_rx = ad_rx.cfg['subscription'][subid_rx]['phone_num']
+    for text in array_message:
+        log.info("Sending SMS {} to {}, len: {}, content: {}.".
+                 format(phonenumber_tx, phonenumber_rx, len(text), text))
+        result = False
+        ad_rx.ed.clear_all_events()
+        ad_rx.droid.smsStartTrackingIncomingSmsMessage()
+        try:
+            ad_tx.droid.smsSendTextMessage(phonenumber_rx, text, True)
+
+            try:
+                ad_tx.ed.pop_event(EventSmsSentSuccess, WAIT_TIME_SMS_SENT_SUCCESS)
+            except Empty:
+                log.error("No sent_success event.")
+                return False
+
+            if not wait_for_matching_sms(log, ad_rx, phonenumber_tx, text,
+                                allow_multi_part_long_sms=True):
+                return False
+        finally:
+            ad_rx.droid.smsStopTrackingIncomingSmsMessage()
+    return True
+
+def mms_send_receive_verify(log, ad_tx, ad_rx, array_message):
+    """Send SMS, receive SMS, and verify content and sender's number.
+
+        Send (several) SMS from droid_tx to droid_rx.
+        Verify SMS is sent, delivered and received.
+        Verify received content and sender's number are correct.
+
+    Args:
+        log: Log object.
+        ad_tx: Sender's Android Device Object
+        ad_rx: Receiver's Android Device Object
+        array_message: the array of message to send/receive
+    """
+    return mms_send_receive_verify_for_subscription(log, ad_tx, ad_rx,
+                     ad_tx.droid.subscriptionGetDefaultSmsSubId(),
+                     ad_rx.droid.subscriptionGetDefaultSmsSubId(),
+                     array_message)
+
+
+#FIXME: This function is still a WIP and is disabled
+def mms_send_receive_verify_for_subscription(log, ad_tx, ad_rx, subid_tx,
+        subid_rx, array_payload):
+    """Send SMS, receive SMS, and verify content and sender's number.
+
+        Send (several) SMS from droid_tx to droid_rx.
+        Verify SMS is sent, delivered and received.
+        Verify received content and sender's number are correct.
+
+    Args:
+        log: Log object.
+        ad_tx: Sender's Android Device Object..
+        ad_rx: Receiver's Android Device Object.
+        subid_tx: Sender's subsciption ID to be used for SMS
+        subid_rx: Receiver's subsciption ID to be used for SMS
+        array_message: the array of message to send/receive
+    """
+
+    log.error("Function is non-working: b/21569494")
+    return False
+
+    phonenumber_tx = ad_tx.cfg['subscription'][subid_tx]['phone_num']
+    phonenumber_rx = ad_rx.cfg['subscription'][subid_rx]['phone_num']
+    for subject, message, filename in array_payload:
+        log.info("Sending MMS {} to {}, subject: {}, message: {}.".
+                 format(phonenumber_tx, phonenumber_rx, subject, message))
+        result = False
+        ad_rx.ed.clear_all_events()
+        ad_rx.droid.smsStartTrackingIncomingMmsMessage()
+        ad_rx.droid.smsStartTrackingIncomingSmsMessage()
+        try:
+            ad_tx.droid.smsSendMultimediaMessage(
+                phonenumber_rx,
+                subject,
+                message,
+                phonenumber_tx,
+                filename
+            )
+
+            ad_tx.ed.pop_event(EventMmsSentSuccess, WAIT_TIME_SMS_SENT_SUCCESS)
+
+            start_time = time.time()
+            remaining_time = WAIT_TIME_SMS_RECEIVE
+            while remaining_time > 0:
+                event = ad_rx.ed.pop_event(EventSmsReceived, remaining_time)
+                if check_phone_number_match(
+                        event['data']['Sender'],
+                        phonenumber_tx):
+                    log.debug("Received SMS Indication")
+                    while remaining_time > 0:
+                        event = ad_rx.ed.pop_event(
+                            EventDataSmsReceived, remaining_time)
+                        if check_phone_number_match(
+                                event['data']['Sender'],
+                                phonenumber_tx):
+                                result = True
+                                break
+                        remaining_time = time.time() - start_time
+                remaining_time = time.time() - start_time
+
+            if not result:
+                log.info("Expected sender:" + phonenumber_tx)
+                log.error("Received sender:" + event['data']['Sender'])
+                log.error("Failed in verify receiving MMS.")
+                return False
+        finally:
+            ad_rx.droid.smsStopTrackingIncomingSmsMessage()
+            ad_rx.droid.smsStopTrackingIncomingMmsMessage()
+    return True
+
+
+def get_preferred_network_rat(log, ad):
+    """Get preferred network type settings for default subscription
+
+    Args:
+        ad: Android Device Object
+
+    Returns:
+        Current voice/data network type.
+    """
+    return get_preferred_network_rat_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId())
+
+
+def get_preferred_network_rat_for_subscription(log, ad, sub_id):
+    """Get preferred network type settings for specified subscription
+
+    Args:
+        ad: Android Device Object
+        sub_id: subscription ID
+
+    Returns:
+        Current voice/data network type.
+    """
+
+    ret_val = ad.droid.phoneGetPreferredNetworkTypeForSubscription(sub_id)
+
+    if ret_val is None:
+        log.error("get_preferred_network_rat(): Unexpected null return value")
+        return RAT_UNKNOWN
+    else:
+        return ret_val
+
+
+def get_network_rat(log, ad, voice_or_data):
+    """Get current network type (Voice network type, or data network type)
+       for default subscription id
+
+    Args:
+        ad: Android Device Object
+        voice_or_data: Input parameter indicating to get voice network type or
+            data network type.
+
+    Returns:
+        Current voice/data network type.
+    """
+    return get_network_rat_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), voice_or_data)
+
+
+def get_network_rat_for_subscription(log, ad, sub_id, voice_or_data):
+    """Get current network type (Voice network type, or data network type)
+       for specified subscription id
+
+    Args:
+        ad: Android Device Object
+        sub_id: subscription ID
+        voice_or_data: Input parameter indicating to get voice network type or
+            data network type.
+
+    Returns:
+        Current voice/data network type.
+    """
+    if voice_or_data == NETWORK_SERVICE_VOICE:
+        ret_val = ad.droid.getVoiceNetworkTypeForSubscription(sub_id)
+    elif voice_or_data == NETWORK_SERVICE_DATA:
+        ret_val = ad.droid.getDataNetworkTypeForSubscription(sub_id)
+    else:
+        ret_val = ad.droid.getNetworkTypeForSubscription(sub_id)
+
+    if ret_val is None:
+        log.error("get_network_rat(): Unexpected null return value")
+        return RAT_UNKNOWN
+    else:
+        return ret_val
+
+
+def get_network_gen(log, ad, voice_or_data):
+    """Get current network generation string (Voice network type, or data network type)
+
+    Args:
+        ad: Android Device Object
+        voice_or_data: Input parameter indicating to get voice network generation
+            or data network generation.
+
+    Returns:
+        Current voice/data network generation.
+    """
+    return get_network_gen_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), voice_or_data)
+
+def get_network_gen_for_subscription(log, ad, sub_id, voice_or_data):
+    """Get current network generation string (Voice network type, or data network type)
+
+    Args:
+        ad: Android Device Object
+        voice_or_data: Input parameter indicating to get voice network generation
+            or data network generation.
+
+    Returns:
+        Current voice/data network generation.
+    """
+    try:
+        return rat_generation_from_type(get_network_rat_for_subscription(log, ad,
+                                                          sub_id, voice_or_data))
+    except KeyError:
+        log.error("KeyError happened in get_network_gen, ad:{}, d/v: {}, rat: {}".
+            format(ad.serial, voice_or_data, get_network_rat_for_subscription(log, ad,
+                                                           sub_id, voice_or_data)))
+        return RAT_UNKNOWN
+
+def ensure_network_generation(log, ad, generation, max_wait_time=120,
+                              voice_or_data=None, toggle_apm_after_setting=True):
+    """Ensure ad's network is <network generation> for default subscription ID.
+
+    Set preferred network generation to <generation>.
+    Toggle ON/OFF airplane mode if necessary.
+    Wait for ad in expected network type.
+    """
+    return ensure_network_generation_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(),
+        generation, max_wait_time, voice_or_data, toggle_apm_after_setting)
+
+def check_voice_mail_count(log, ad, voice_mail_count_before, voice_mail_count_after):
+    """function to check if voice mail count is correct after leaving a new voice message.
+    """
+    return get_voice_mail_count_check_function(get_operator_name(log, ad))(
+        voice_mail_count_before, voice_mail_count_after)
+
+def get_voice_mail_number(log, ad):
+    """function to get the voice mail number
+    """
+    voice_mail_number = get_voice_mail_number_function(get_operator_name(log, ad))()
+    if voice_mail_number is None:
+        return get_phone_number(log, ad)
+    return voice_mail_number
+
+
+def ensure_network_generation_for_subscription(log, ad, sub_id, generation,
+                                               max_wait_time=120,
+                                               voice_or_data=None,
+                                               toggle_apm_after_setting=True):
+    """Ensure ad's network is <network generation> for specified subscription ID.
+
+    Set preferred network generation to <generation>.
+    Toggle ON/OFF airplane mode if necessary.
+    Wait for ad in expected network type.
+    """
+
+    cur_pref_generation = rat_generation_from_type(
+        get_preferred_network_rat_for_subscription(log, ad, sub_id))
+
+    if cur_pref_generation != generation:
+        log.info("Set {} Gen {}, Cur Gen {}".format(
+            ad.serial, generation, cur_pref_generation))
+        set_preferred_network_type_for_subscription(log, ad, sub_id, generation)
+
+    if (get_network_gen_for_subscription(log, ad, sub_id, voice_or_data) == generation):
+            return True
+
+    log.info("NW Gen - {} current: {}, expected: {}".
+             format(ad.serial, get_network_gen_for_subscription(log, ad, sub_id,
+                                                    voice_or_data), generation))
+    if toggle_apm_after_setting:
+        toggle_airplane_mode(log, ad, True)
+        toggle_airplane_mode(log, ad, False)
+
+    result = wait_for_droids_in_network_generation(
+        log, [ad], generation, max_wait_time, voice_or_data)
+
+    log.info("End of ensure_network_generation NW Gen - {} current: {}, expected: {}".
+        format(ad.serial, get_network_gen_for_subscription(log, ad, sub_id,
+                                                           voice_or_data), generation))
+
+    return result
+
+
+def ensure_network_rat(
+        log, ad, network_rat, max_wait_time=120, voice_or_data=None):
+    """Ensure ad's network is <network_type_string> for default subscription ID.
+
+    Set preferred network to <network_type_string>.
+    Toggle ON/OFF airplane mode if necessary.
+    Wait for ad in expected network type.
+    """
+    return ensure_network_rat_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultSubId(), network_rat,
+        max_wait_time, voice_or_data)
+
+
+def ensure_network_rat_for_subscription(
+        log, ad, sub_id, network_rat, max_wait_time=120, voice_or_data=None):
+    """Ensure ad's network is <network_type_string> for specified subscription ID.
+
+    Set preferred network to <network_type_string>.
+    Toggle ON/OFF airplane mode if necessary.
+    Wait for ad in expected network type.
+    """
+
+    cur_pref_nw_type = get_preferred_network_rat_for_subscription(log, ad, sub_id)
+
+    if rat_family_from_type(cur_pref_nw_type) != rat_family_from_type(
+            network_rat):
+        log.info("Set {} Type {}, Cur Type {}".format(
+            ad.serial, network_rat, cur_pref_nw_type))
+        set_preferred_network_type_for_subscription(log, ad, sub_id, network_rat)
+
+    if (rat_family_from_type(get_network_rat_for_subscription(log, ad, sub_id, voice_or_data)) ==
+            rat_family_from_type(network_rat)):
+        return True
+
+    current_rat = get_network_rat(log, ad, voice_or_data)
+    log.info("NW RAT - {} current: {}(family: {}), expected: {}(family: {})".
+             format(ad.serial, current_rat, rat_family_from_type(current_rat),
+                    network_rat, rat_family_from_type(network_rat)))
+    toggle_airplane_mode(log, ad, True)
+    time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+    toggle_airplane_mode(log, ad, False)
+
+    result = wait_for_droid_in_network_rat_for_subscription(
+        log, ad, sub_id, network_rat, max_wait_time, voice_or_data)
+
+    current_rat = get_network_rat(log, ad, voice_or_data)
+    log.info("End of ensure_network_rat. NW RAT - {} current: {}(family: {}), "
+             "expected: {}(family: {})".
+             format(ad.serial, current_rat, rat_family_from_type(current_rat),
+                    network_rat, rat_family_from_type(network_rat)))
+
+    return result
+
+def ensure_phones_idle(log, ads,
+                       settling_time=WAIT_TIME_ANDROID_STATE_SETTLING):
+    """Ensure ads idle (not in call).
+    """
+    for ad in ads:
+        if ad.droid.telecomIsInCall():
+            ad.droid.telecomEndCall()
+    # Leave the delay time to make sure droid can recover to idle from ongoing call.
+    time.sleep(settling_time)
+    return True
+
+def ensure_phone_idle(log, ad,
+                      settling_time=WAIT_TIME_ANDROID_STATE_SETTLING):
+    """Ensure ad idle (not in call).
+    """
+    return ensure_phones_idle(log, [ad], settling_time)
+
+def ensure_phone_default_state(log, ad):
+    """Ensure ad in default state.
+    Phone not in call.
+    Phone have no stored WiFi network and WiFi disconnected.
+    Phone not in airplane mode.
+    """
+    result = True
+    if ad.droid.telecomIsInCall():
+        ad.droid.telecomEndCall()
+    set_wfc_mode(log, ad, WFC_MODE_DISABLED)
+
+    # FIXME: bug/23906084 We shouldn't force the device using modes unavailable
+    #        to a normal user
+    if is_droid_in_network_rat(log, ad, RAT_GSM, NETWORK_SERVICE_VOICE):
+        log.error("Device is stuck in GSM... Attempting to Un-stick")
+        ad.droid.setPreferredNetwork(NETWORK_MODE_LTE_ONLY)
+        if not wait_for_droid_in_network_rat(
+                log, ad, RAT_LTE, WAIT_TIME_NW_SELECTION):
+            if not wait_for_droid_in_network_rat(
+                    log, ad, RAT_WCDMA, WAIT_TIME_NW_SELECTION):
+                log.error("Device failed to un-stick from GSM."
+                            "Game over man, game over.")
+                result = False
+
+    if not wait_for_droid_not_in_network_rat(log, ad, RAT_IWLAN,
+                                             WAIT_TIME_NW_SELECTION,
+                                             NETWORK_SERVICE_DATA):
+        log.error("ensure_phones_default_state: wait_for_droid_not_in iwlan fail {}.".
+            format(ad.serial))
+        result = False
+    if ((not WifiUtils.wifi_reset(log, ad)) or
+        (not WifiUtils.wifi_toggle_state(log, ad, False))):
+        log.error("ensure_phones_default_state:reset WiFi fail {}.".
+            format(ad.serial))
+        result = False
+    if not toggle_airplane_mode(log, ad, False):
+        log.error("ensure_phones_default_state:turn off airplane mode fail {}.".
+            format(ad.serial))
+        result = False
+    # make sure phone data is on
+    ad.droid.toggleDataConnection(True)
+
+    # Leave the delay time to make sure droid can recover to idle from ongoing call.
+    time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+    return result
+
+def ensure_phones_default_state(log, ads):
+    """Ensure ads in default state.
+    Phone not in call.
+    Phone have no stored WiFi network and WiFi disconnected.
+    Phone not in airplane mode.
+    """
+    tasks = []
+    for ad in ads:
+        tasks.append((ensure_phone_default_state, (log, ad)))
+    if not multithread_func(log, tasks):
+        log.error("Ensure_phones_default_state Fail.")
+        return False
+    return True
+
+def ensure_wifi_connected(log, ad, wifi_ssid, wifi_pwd=None, retry=1):
+    """Ensure ad connected to wifi.
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        wifi_ssid: WiFi network SSID.
+        wifi_pwd: WiFi network password. This is optional.
+
+    """
+    while(retry >= 0):
+        WifiUtils.wifi_reset(log, ad)
+        WifiUtils.wifi_toggle_state(log, ad, False)
+        WifiUtils.wifi_toggle_state(log, ad, True)
+        if WifiUtils.wifi_connect(log, ad, wifi_ssid, wifi_pwd):
+            return True
+        else:
+            log.info("ensure_wifi_connected: Connect WiFi failed, retry + 1.")
+            retry -= 1
+    return False
+
+def task_wrapper(task):
+    """Task wrapper for multithread_func
+
+    Args:
+        task[0]: function to be wrapped.
+        task[1]: function args.
+
+    Returns:
+        Return value of wrapped function call.
+    """
+    func = task[0]
+    params = task[1]
+    return func(*params)
+
+def multithread_func(log, tasks):
+    """Multi-thread function wrapper.
+
+    Args:
+        log: log object.
+        tasks: tasks to be executed in parallel.
+
+    Returns:
+        True if all tasks return True.
+        False if any task return False.
+    """
+    MAX_NUMBER_OF_WORKERS = 4
+    number_of_workers = min(MAX_NUMBER_OF_WORKERS, len(tasks))
+    executor = concurrent.futures.ThreadPoolExecutor(max_workers=number_of_workers)
+    results = list(executor.map(task_wrapper, tasks))
+    executor.shutdown()
+    log.info("multithread_func result: {}".format(results))
+    for r in results:
+        if not r:
+            return False
+    return True
+
+def set_phone_screen_on(log, ad, screen_on_time=MAX_SCREEN_ON_TIME):
+    """Set phone screen on time.
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        screen_on_time: screen on time.
+            This is optional, default value is MAX_SCREEN_ON_TIME.
+    Returns:
+        True if set successfully.
+    """
+    ad.droid.setScreenTimeout(screen_on_time)
+    return screen_on_time == ad.droid.getScreenTimeout()
+
+def set_phone_silent_mode(log, ad, silent_mode=True):
+    """Set phone silent mode.
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        silent_mode: set phone silent or not.
+            This is optional, default value is True (silent mode on).
+    Returns:
+        True if set successfully.
+    """
+    ad.droid.toggleRingerSilentMode(silent_mode)
+    return silent_mode == ad.droid.checkRingerSilentMode()
+
+def set_preferred_subid_for_sms(log, ad, sub_id):
+    """set subscription id for SMS
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        sub_id :Subscription ID.
+
+    """
+    log.info("Setting subscription:{} as Message SIM for {}".format(
+             sub_id, ad.serial))
+    ad.droid.subscriptionSetDefaultSmsSubId(sub_id)
+    # Wait to make sure settings take effect
+    time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+    return sub_id == ad.droid.subscriptionGetDefaultSmsSubId()
+
+def set_preferred_subid_for_data(log, ad, sub_id):
+    """set subscription id for data
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        sub_id :Subscription ID.
+
+    """
+    log.info("Setting subscription:{} as Data SIM for {}".format(
+             sub_id, ad.serial))
+    ad.droid.subscriptionSetDefaultDataSubId(sub_id)
+    time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+    # Wait to make sure settings take effect
+    # Data SIM change takes around 1 min
+    # Check whether data has changed to selected sim
+    if not wait_for_data_connection(log, ad, True, WAIT_TIME_DATA_SUB_CHANGE):
+        log.error("Data Connection failed - Not able to switch Data SIM")
+        return False
+    return True
+
+def set_preferred_subid_for_voice(log, ad, sub_id):
+    """set subscription id for voice
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        sub_id :Subscription ID.
+
+    """
+    log.info("Setting subscription:{} as Voice SIM for {}".format(
+             sub_id, ad.serial))
+    ad.droid.subscriptionSetDefaultVoiceSubId(sub_id)
+    ad.droid.telecomSetUserSelectedOutgoingPhoneAccountBySubId(sub_id)
+    # Wait to make sure settings take effect
+    time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+    return True
+
+def set_call_state_listen_level(log, ad, value, sub_id):
+    """Set call state listen level for subscription id.
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        value: True or False
+        sub_id :Subscription ID.
+
+    Returns:
+        True or False
+    """
+    if sub_id == INVALID_SUB_ID:
+        log.error("Invalid Subscription ID")
+        return False
+    ad.droid.phoneAdjustPreciseCallStateListenLevelForSubscription(
+               "Foreground", value, sub_id)
+    ad.droid.phoneAdjustPreciseCallStateListenLevelForSubscription(
+               "Ringing", value, sub_id)
+    ad.droid.phoneAdjustPreciseCallStateListenLevelForSubscription(
+               "Background", value, sub_id)
+    return True
+
+def setup_sim(log, ad, sub_id, voice=False, sms=False, data=False):
+    """set subscription id for voice, sms and data
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        sub_id :Subscription ID.
+        voice: True if to set subscription as default voice subscription
+        sms: True if to set subscription as default sms subscription
+        data: True if to set subscription as default data subscription
+
+    """
+    if sub_id == INVALID_SUB_ID:
+        log.error("Invalid Subscription ID")
+        return False
+    else:
+        if voice:
+            if not set_preferred_subid_for_voice(log, ad, sub_id):
+                return False
+        if sms:
+            if not set_preferred_subid_for_sms(log, ad, sub_id):
+                return False
+        if data:
+            if not set_preferred_subid_for_data(log, ad,sub_id):
+                return False
+    return True
+
+def get_sub_ids_for_sim_slots(log, ads):
+    """get subscription id for each sim slots avaialble in all devices
+       in  ads
+
+    Args:
+        log: Log object.
+        ads: Android device object list.
+
+    Returns:
+        list of sub ids for all devices.
+        it is a matrix.
+        eg: sim_sub_ids[0][0] is sub id for sim1 in device 1
+    """
+    sim_sub_ids = []
+    for index in range(len(ads)):
+        sim_sub_ids.append([])
+        sim_count = ads[index].droid.getSimCount()
+        for count in range(sim_count):
+            subid = get_subid_from_slot_index(log, ads[index], count)
+            sim_sub_ids[index].append(subid)
+    return sim_sub_ids
+
+def is_sub_event_match(event, sub_event):
+    """Return if subEvent field in "event" match "sub_event" or not.
+
+    Args:
+        event: event to test. This event need to have 'subEvent' field.
+        sub_event: sub_event to match.
+
+    Returns:
+        True if subEvent field in "event" match "sub_event".
+        False otherwise.
+    """
+    return is_sub_event_match_for_sub_event_list(event, [sub_event])
+
+def is_sub_event_match_for_sub_event_list(event, sub_event_list):
+    """Return if subEvent field in "event" match any one of the sub_event
+        in "sub_event_list" or not.
+
+    Args:
+        event: event to test. This event need to have 'subEvent' field.
+        sub_event_list: a list of sub_event to match.
+
+    Returns:
+        True if subEvent field in "event" match one of the sub_event in "sub_event_list".
+        False otherwise.
+    """
+    try:
+        sub = event['data']['subEvent']
+    except KeyError:
+        return False
+    for sub_event in sub_event_list:
+        if sub == sub_event:
+            return True
+    return False
+
+def is_network_call_back_event_match(event, network_callback_id, sub_event):
+    try:
+        return ((network_callback_id == event['data']['id']) and
+                (sub_event == event['data']['subEvent']))
+    except KeyError:
+        return False
+
+def is_build_id(log, ad, build_id):
+    """Return if ad's build id is the same as input parameter build_id.
+
+    Args:
+        log: log object.
+        ad: android device object.
+        build_id: android build id.
+
+    Returns:
+        True if ad's build id is the same as input parameter build_id.
+        False otherwise.
+    """
+    actual_bid = ad.droid.getBuildID()
+
+    log.info("{} BUILD DISPLAY: {}"
+             .format(ad.serial, ad.droid.getBuildDisplay()))
+    #In case we want to log more stuff/more granularity...
+    #log.info("{} BUILD ID:{} ".format(ad.serial, ad.droid.getBuildID()))
+    #log.info("{} BUILD FINGERPRINT: {} "
+    # .format(ad.serial), ad.droid.getBuildFingerprint())
+    #log.info("{} BUILD TYPE: {} "
+    # .format(ad.serial), ad.droid.getBuildType())
+    #log.info("{} BUILD NUMBER: {} "
+    # .format(ad.serial), ad.droid.getBuildNumber())
+    if actual_bid.upper() != build_id.upper():
+        log.error("{}: Incorrect Build ID".format(ad.model))
+        return False
+    return True
+
+def is_uri_equivalent(uri1, uri2):
+    """Check whether two input uris match or not.
+
+    Compare Uris.
+        If Uris are tel URI, it will only take the digit part
+        and compare as phone number.
+        Else, it will just do string compare.
+
+    Args:
+        uri1: 1st uri to be compared.
+        uri2: 2nd uri to be compared.
+
+    Returns:
+        True if two uris match. Otherwise False.
+    """
+    if uri1.startswith('tel:') and uri2.startswith('tel:'):
+        uri1_number = ''.join(i for i in urllib.parse.unquote(uri1) if i.isdigit())
+        uri2_number = ''.join(i for i in urllib.parse.unquote(uri2) if i.isdigit())
+        return check_phone_number_match(uri1_number, uri2_number)
+    else:
+        return uri1 == uri2
+
+def get_call_uri(ad, call_id):
+    """Get call's uri field.
+
+    Get Uri for call_id in ad.
+
+    Args:
+        ad: android device object.
+        call_id: the call id to get Uri from.
+
+    Returns:
+        call's Uri if call is active and have uri field. None otherwise.
+    """
+    try:
+        call_detail = ad.droid.telecomCallGetDetails(call_id)
+        return call_detail["Handle"]["Uri"]
+    except:
+        return None
+
+# FIXME: Remove wrapper class once wifi_utils methods updated
+class WifiUtils():
+
+    from acts.test_utils.wifi_test_utils \
+        import reset_droid_wifi as _reset_droid_wifi
+    from acts.test_utils.wifi_test_utils \
+        import wifi_connect as _wifi_connect
+    from acts.test_utils.wifi_test_utils \
+        import wifi_toggle_state as _wifi_toggle_state
+    from acts.test_utils.wifi_test_utils \
+        import start_wifi_tethering as _start_wifi_tethering
+    from acts.test_utils.wifi_test_utils \
+        import stop_wifi_tethering as _stop_wifi_tethering
+    from acts.test_utils.wifi_test_utils \
+        import WifiEnums as _WifiEnums
+
+    WIFI_CONFIG_APBAND_2G = _WifiEnums.WIFI_CONFIG_APBAND_2G
+    WIFI_CONFIG_APBAND_5G = _WifiEnums.WIFI_CONFIG_APBAND_5G
+    SSID_KEY = _WifiEnums.SSID_KEY
+
+    @staticmethod
+    def wifi_toggle_state(log, ad, state):
+        try:
+            WifiUtils._wifi_toggle_state(ad.droid, ad.ed, state)
+        except Exception as e:
+            log.error("WifiUtils.wifi_toggle_state exception: {}".format(e))
+            return False
+        return True
+
+    @staticmethod
+    def wifi_reset(log, ad, disable_wifi=True):
+        try:
+            WifiUtils._reset_droid_wifi(ad.droid, ad.ed)
+        except Exception as e:
+            log.error("WifiUtils.wifi_reset exception: {}".format(e))
+            return False
+        finally:
+            if disable_wifi is True:
+                ad.droid.wifiToggleState(False)
+                # Ensure toggle state has human-time to take effect
+            time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+        return True
+
+    @staticmethod
+    def wifi_connect(log, ad, ssid, password=None):
+        if password == "":
+            password = None
+        try:
+            return WifiUtils._wifi_connect(ad, ssid, password)
+        except Empty:
+            # did not get event, then check connection info
+            try:
+                if ad.droid.wifiGetConnectionInfo()[WifiUtils.SSID_KEY] == ssid:
+                    return True
+                else:
+                    log.error("WifiUtils.wifi_connect not connected."
+                        "No event received. Expected SSID: {}, current SSID:{}".
+                        format(ssid,
+                            ad.droid.wifiGetConnectionInfo()[WifiUtils.SSID_KEY]))
+                    return False
+            except Exception as e:
+                log.error("WifiUtils.wifi_connect not connected, no event.")
+                return False
+        except Exception as e:
+            log.error("WifiUtils.wifi_connect exception: {}".format(e))
+            return False
+
+    @staticmethod
+    def start_wifi_tethering(log, ad, ssid, password, ap_band=None):
+        try:
+            return WifiUtils._start_wifi_tethering(ad, ssid, password, ap_band)
+        except Exception as e:
+            log.error("WifiUtils.start_wifi_tethering exception: {}".format(e))
+            return False
+
+    @staticmethod
+    def stop_wifi_tethering(log, ad):
+        try:
+            WifiUtils._stop_wifi_tethering(ad)
+            return True
+        except Exception as e:
+            log.error("WifiUtils.stop_wifi_tethering exception: {}".format(e))
+            return False
diff --git a/acts/framework/acts/test_utils/tel/tel_video_utils.py b/acts/framework/acts/test_utils/tel/tel_video_utils.py
new file mode 100644
index 0000000..d8895b1
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_video_utils.py
@@ -0,0 +1,837 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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 acts.test_utils.tel.tel_lookup_tables import *
+from acts.test_utils.tel.tel_test_utils import *
+from acts.test_utils.tel.tel_voice_utils import *
+
+def phone_setup_video(log, ad):
+    """Setup phone default sub_id to make video call
+
+    Args:
+        log: log object.
+        ad: android device object
+
+    Returns:
+        True if ad (default sub_id) is setup correctly and idle for video call.
+    """
+    return phone_setup_video_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_video_for_subscription(log, ad, sub_id):
+    """Setup phone sub_id to make video call
+
+    Args:
+        log: log object.
+        ad: android device object
+        sub_id: ad's sub id.
+
+    Returns:
+        True if ad (sub_id) is setup correctly and idle for video call.
+    """
+    toggle_airplane_mode(log, ad, False)
+    if not set_wfc_mode(log, ad, WFC_MODE_DISABLED):
+        log.error("{} Disable WFC failed.".format(ad.serial))
+        return False
+    toggle_volte_for_subscription(log, ad, sub_id, True)
+    if not ensure_network_rat_for_subscription(
+            log, ad, sub_id, RAT_LTE, WAIT_TIME_NW_SELECTION,
+            NETWORK_SERVICE_DATA):
+        log.error("{} failed to select LTE (data) within {}s.".
+                  format(ad.serial, WAIT_TIME_NW_SELECTION))
+        return False
+    return phone_idle_video_for_subscription(log, ad, sub_id)
+
+
+def phone_idle_video(log, ad):
+    """Return if phone (default sub_id) is idle for video call.
+
+    Args:
+        log: log object.
+        ad: android device object
+
+    Returns:
+        True if ad is idle for video call.
+    """
+    return phone_idle_video_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def phone_idle_video_for_subscription(log, ad, sub_id):
+    """Return if phone (sub_id) is idle for video call.
+
+    Args:
+        log: log object.
+        ad: android device object
+        sub_id: ad's sub id
+
+    Returns:
+        True if ad (sub_id) is idle for video call.
+    """
+    if not wait_for_droid_in_network_rat_for_subscription(
+            log, ad, sub_id, RAT_LTE, WAIT_TIME_NW_SELECTION,
+            NETWORK_SERVICE_VOICE):
+        log.error("{} voice not in LTE mode.".format(ad.serial))
+        return False
+    if not wait_for_video_enabled(log, ad, WAIT_TIME_VOLTE_ENABLED):
+        log.error("{} failed to <report volte enabled true> within {}s.".
+                  format(ad.serial, WAIT_TIME_VOLTE_ENABLED))
+        return False
+    return True
+
+def is_phone_in_call_video(log, ad):
+    """Return if ad is in a video call (in expected video state).
+
+    Args:
+        log: log object.
+        ad: android device object
+        video_state: Expected Video call state.
+            This is optional, if it's None,
+            then TX_ENABLED/RX_ENABLED/BIDIRECTIONAL video call state will
+            return True.
+
+    Returns:
+        True if ad (for sub_id) is in a video call (in expected video state).
+    """
+    return is_phone_in_call_video_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def is_phone_in_call_video_for_subscription(log, ad, sub_id, video_state=None):
+    """Return if ad (for sub_id) is in a video call (in expected video state).
+    Args:
+        log: log object.
+        ad: android device object
+        sub_id: device sub_id
+        video_state: Expected Video call state.
+            This is optional, if it's None,
+            then TX_ENABLED/RX_ENABLED/BIDIRECTIONAL video call state will
+            return True.
+
+    Returns:
+        True if ad is in a video call (in expected video state).
+    """
+
+    if video_state is None:
+        log.info(
+            "Verify if {}(subid {}) in video call.".format(ad.serial, sub_id))
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    call_list = ad.droid.telecomCallGetCallIds()
+    for call in call_list:
+        state = ad.droid.telecomCallVideoGetState(call)
+        if video_state is None:
+            if {
+                VT_STATE_AUDIO_ONLY: False,
+                VT_STATE_TX_ENABLED: True,
+                VT_STATE_TX_PAUSED: True,
+                VT_STATE_RX_ENABLED: True,
+                VT_STATE_RX_PAUSED: True,
+                VT_STATE_BIDIRECTIONAL: True,
+                VT_STATE_BIDIRECTIONAL_PAUSED: True,
+                VT_STATE_PAUSED: False,
+                VT_STATE_STATE_INVALID: False
+            }[state]:
+                return True
+        else:
+            if state == video_state:
+                return True
+        log.info("Non-Video-State: {}".format(state))
+    log.error("Phone not in video call. Call list: {}".format(call_list))
+    return False
+
+def is_phone_in_call_video_bidirectional(log, ad):
+    """Return if phone in bi-directional video call.
+
+    Args:
+        log: log object.
+        ad: android device object
+
+    Returns:
+        True if phone in bi-directional video call.
+    """
+    return is_phone_in_call_video_bidirectional_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def is_phone_in_call_video_bidirectional_for_subscription(log, ad, sub_id):
+    """Return if phone in bi-directional video call for subscription id.
+
+    Args:
+        log: log object.
+        ad: android device object
+        sub_id: subscription id.
+
+    Returns:
+        True if phone in bi-directional video call.
+    """
+    log.info("Verify if {}(subid {}) in bi-directional video call.".
+             format(ad.serial, sub_id))
+    return is_phone_in_call_video_for_subscription(log, ad, sub_id,
+                                                   VT_STATE_BIDIRECTIONAL)
+
+def is_phone_in_call_video_tx_enabled(log, ad):
+    """Return if phone in tx_enabled video call.
+
+    Args:
+        log: log object.
+        ad: android device object
+
+    Returns:
+        True if phone in tx_enabled video call.
+    """
+    return is_phone_in_call_video_tx_enabled_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def is_phone_in_call_video_tx_enabled_for_subscription(log, ad, sub_id):
+    """Return if phone in tx_enabled video call for subscription id.
+
+    Args:
+        log: log object.
+        ad: android device object
+        sub_id: subscription id.
+
+    Returns:
+        True if phone in tx_enabled video call.
+    """
+    log.info("Verify if {}(subid {}) in tx_enabled video call.".
+             format(ad.serial, sub_id))
+    return is_phone_in_call_video_for_subscription(log, ad, sub_id,
+                                                   VT_STATE_TX_ENABLED)
+
+def is_phone_in_call_video_rx_enabled(log, ad):
+    """Return if phone in rx_enabled video call.
+
+    Args:
+        log: log object.
+        ad: android device object
+
+    Returns:
+        True if phone in rx_enabled video call.
+    """
+    return is_phone_in_call_video_rx_enabled_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def is_phone_in_call_video_rx_enabled_for_subscription(log, ad, sub_id):
+    """Return if phone in rx_enabled video call for subscription id.
+
+    Args:
+        log: log object.
+        ad: android device object
+        sub_id: subscription id.
+
+    Returns:
+        True if phone in rx_enabled video call.
+    """
+    log.info("Verify if {}(subid {}) in rx_enabled video call.".
+             format(ad.serial, sub_id))
+    return is_phone_in_call_video_for_subscription(log, ad, sub_id,
+                                                   VT_STATE_RX_ENABLED)
+
+def is_phone_in_call_voice_hd(log, ad):
+    """Return if phone in hd voice call.
+
+    Args:
+        log: log object.
+        ad: android device object
+
+    Returns:
+        True if phone in hd voice call.
+    """
+    return is_phone_in_call_voice_hd_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def is_phone_in_call_voice_hd_for_subscription(log, ad, sub_id):
+    """Return if phone in hd voice call for subscription id.
+
+    Args:
+        log: log object.
+        ad: android device object
+        sub_id: subscription id.
+
+    Returns:
+        True if phone in hd voice call.
+    """
+    log.info("Verify if {}(subid {}) in hd voice call.".format(ad.serial, sub_id))
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    for call in ad.droid.telecomCallGetCallIds():
+        state = ad.droid.telecomCallVideoGetState(call)
+        if (state == VT_STATE_AUDIO_ONLY and is_call_hd(log, ad, call)):
+            return True
+        log.info("Non-HDAudio-State: {}, property: {}".
+                 format(state, ad.droid.telecomCallGetProperties(call)))
+    return False
+
+def initiate_video_call(log, ad_caller, callee_number):
+    """Make phone call from caller to callee.
+
+    Args:
+        ad_caller: Caller android device object.
+        callee_number: Callee phone number.
+        emergency : specify the call is emergency.
+            Optional. Default value is False.
+
+    Returns:
+        result: if phone call is placed successfully.
+    """
+
+    wait_time_for_incall_state = WAIT_TIME_CALL_INITIATION
+    ad_caller.droid.phoneCallNumber(callee_number, True)
+    while wait_time_for_incall_state > 0:
+        wait_time_for_incall_state -= 1
+        if ad_caller.droid.telecomIsInCall():
+            return True
+        time.sleep(1)
+    log.error("Make call fail.")
+    return False
+
+
+def wait_and_answer_video_call(
+        log, ad, incoming_number=None,
+        delay_answer=WAIT_TIME_ANSWER_VIDEO_CALL,
+        video_state=VT_STATE_BIDIRECTIONAL,
+        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """Wait for an incoming call on default voice subscription and
+       accepts the call.
+
+    Args:
+        ad: android device object.
+        incoming_number: Expected incoming number.
+            Optional. Default is None
+        delay_answer: time to wait before answering the call
+            Optional. Default is 1
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True: if incoming call is received and answered successfully.
+        False: for errors
+    """
+    return wait_and_answer_video_call_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId(),
+        incoming_number, delay_answer, video_state, incall_ui_display)
+
+
+def wait_and_answer_video_call_for_subscription(
+        log, ad, sub_id, incoming_number=None,
+        delay_answer=WAIT_TIME_ANSWER_VIDEO_CALL,
+        video_state=VT_STATE_BIDIRECTIONAL,
+        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """Wait for an incoming call on specified subscription and
+       accepts the call.
+
+    Args:
+        ad: android device object.
+        sub_id: subscription ID
+        incoming_number: Expected incoming number.
+            Optional. Default is None
+        delay_answer: time to wait befor answering the call
+            Optional. Default is 1
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True: if incoming call is received and answered successfully.
+        False: for errors
+    """
+    ad.ed.clear_all_events()
+    ad.droid.phoneStartTrackingCallStateForSubscription(sub_id)
+    if (not ad.droid.telecomIsRinging() and
+            ad.droid.getCallStateForSubscription(sub_id) != TELEPHONY_STATE_RINGING):
+        try:
+            event_ringing = wait_for_ringing_event(log, ad,
+                                                   WAIT_TIME_CALLEE_RINGING)
+            if event_ringing is None:
+                log.error("No Ringing Event.")
+                return False
+        finally:
+            ad.droid.phoneStopTrackingCallStateChangeForSubscription(sub_id)
+
+        if not incoming_number:
+            result = True
+        else:
+            result = check_phone_number_match(
+                event_ringing['data']['incomingNumber'], incoming_number)
+
+        if not result:
+            log.error("Incoming Number not match")
+            log.error("Expected number:{}, actual number:{}".
+                      format(incoming_number,
+                             event_ringing['data']['incomingNumber']))
+            return False
+
+    ad.ed.clear_all_events()
+
+    ringing_call_id = None
+
+    #FIXME: magic polling loop
+    for i in range(10):
+        calls = ad.droid.telecomCallGetCallIds()
+        for call in calls:
+            call_state = ad.droid.telecomCallGetCallState(call)
+            if call_state == CALL_STATE_RINGING:
+                ringing_call_id = call
+                break
+        #FIXME: magic number is magical
+        time.sleep(.2)
+
+    if not ringing_call_id:
+        log.error("Couldn't find a ringing call by ID!")
+        return False
+
+    ad.droid.phoneStartTrackingCallStateForSubscription(sub_id)
+    # Delay between ringing and answer.
+    time.sleep(delay_answer)
+
+    log.info("Accept on callee.")
+    ad.droid.telecomCallAnswer(ringing_call_id, video_state)
+
+    try:
+        ad.ed.wait_for_event(EventCallStateChanged,
+                             is_sub_event_match,
+                             timeout=WAIT_TIME_ACCEPT_CALL_TO_OFFHOOK_EVENT,
+                             sub_event=TELEPHONY_STATE_OFFHOOK)
+    except Empty:
+        if not ad.droid.telecomIsInCall():
+            log.error("Accept call failed.")
+            return False
+    finally:
+        ad.droid.phoneStopTrackingCallStateChangeForSubscription(sub_id)
+    if incall_ui_display == INCALL_UI_DISPLAY_FOREGROUND:
+        ad.droid.telecomShowInCallScreen()
+    elif incall_ui_display == INCALL_UI_DISPLAY_BACKGROUND:
+        ad.droid.showHomeScreen()
+    return True
+
+
+def video_call_setup_teardown(
+        log, ad_caller, ad_callee, ad_hangup=None,
+        video_state=VT_STATE_BIDIRECTIONAL, verify_caller_func=None,
+        verify_callee_func=None, wait_time_in_call=WAIT_TIME_IN_CALL,
+        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """ Call process, including make a phone call from caller,
+    accept from callee, and hang up. The call is on default subscription
+
+    In call process, call from <droid_caller> to <droid_callee>,
+    after ringing, wait <delay_answer> to accept the call,
+    (optional)then hang up from <droid_hangup>.
+
+    Args:
+        ad_caller: Caller Android Device Object.
+        ad_callee: Callee Android Device Object.
+        ad_hangup: Android Device Object end the phone call.
+            Optional. Default value is None, and phone call will continue.
+        video_state: video state for VT call.
+            Optional. Default value is VT_STATE_BIDIRECTIONAL
+        verify_caller_func: func_ptr to verify caller in correct mode
+            Optional. Default is None
+        verify_callee_func: func_ptr to verify callee in correct mode
+            Optional. Default is None
+        wait_time_in_call: wait time during call.
+            Optional. Default is WAIT_TIME_IN_CALL.
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True if call process without any error.
+        False if error happened.
+
+    """
+    return video_call_setup_teardown_for_subscription(
+        log, ad_caller, ad_callee,
+        ad_caller.droid.subscriptionGetDefaultVoiceSubId(),
+        ad_callee.droid.subscriptionGetDefaultVoiceSubId(),
+        ad_hangup, video_state, verify_caller_func,
+        verify_callee_func, wait_time_in_call,
+        incall_ui_display)
+
+
+#TODO: Might be able to refactor call_setup_teardown and add. Minimal changes.
+def video_call_setup_teardown_for_subscription(
+        log, ad_caller, ad_callee, subid_caller, subid_callee, ad_hangup=None,
+        video_state=VT_STATE_BIDIRECTIONAL, verify_caller_func=None,
+        verify_callee_func=None, wait_time_in_call=WAIT_TIME_IN_CALL,
+        incall_ui_display=INCALL_UI_DISPLAY_FOREGROUND):
+    """ Call process, including make a phone call from caller,
+    accept from callee, and hang up. The call is on specified subscription
+
+    In call process, call from <droid_caller> to <droid_callee>,
+    after ringing, wait <delay_answer> to accept the call,
+    (optional)then hang up from <droid_hangup>.
+
+    Args:
+        ad_caller: Caller Android Device Object.
+        ad_callee: Callee Android Device Object.
+        subid_caller: Caller subscription ID
+        subid_callee: Callee subscription ID
+        ad_hangup: Android Device Object end the phone call.
+            Optional. Default value is None, and phone call will continue.
+        video_state: video state for VT call.
+            Optional. Default value is VT_STATE_BIDIRECTIONAL
+        verify_caller_func: func_ptr to verify caller in correct mode
+            Optional. Default is None
+        verify_callee_func: func_ptr to verify callee in correct mode
+            Optional. Default is None
+        wait_time_in_call: wait time during call.
+            Optional. Default is WAIT_TIME_IN_CALL.
+        incall_ui_display: after answer the call, bring in-call UI to foreground or
+            background. Optional, default value is INCALL_UI_DISPLAY_FOREGROUND.
+            if = INCALL_UI_DISPLAY_FOREGROUND, bring in-call UI to foreground.
+            if = INCALL_UI_DISPLAY_BACKGROUND, bring in-call UI to background.
+            else, do nothing.
+
+    Returns:
+        True if call process without any error.
+        False if error happened.
+
+    """
+    class _CallSequenceException(Exception):
+        pass
+
+    caller_number = ad_caller.cfg['subscription'][subid_caller]['phone_num']
+    callee_number = ad_callee.cfg['subscription'][subid_callee]['phone_num']
+
+    log.info("Call from {} to {}".format(caller_number, callee_number))
+
+    try:
+        if not initiate_video_call(log, ad_caller, callee_number):
+            raise _CallSequenceException("Initiate call failed.")
+
+        if not wait_and_answer_video_call_for_subscription(
+                log, ad_callee, subid_callee, incoming_number=caller_number,
+                video_state=video_state,
+                incall_ui_display=incall_ui_display):
+            raise _CallSequenceException("Answer call fail.")
+
+        # ensure that all internal states are updated in telecom
+        # FIXME: more magic sleeps
+        time.sleep(2)
+
+        # Check if caller/callee dropped call. This is temporary code.
+        if not verify_incall_state(log, [ad_callee, ad_caller], True):
+            raise _CallSequenceException("Call Drop!")
+        # Check Callee first
+        # in case of VT call drop, it usually start from callee
+        if verify_callee_func and not verify_callee_func(log, ad_callee):
+            raise _CallSequenceException(
+                "Callee not in correct state!")
+        if verify_caller_func and not verify_caller_func(log, ad_caller):
+            raise _CallSequenceException(
+                "Caller not in correct state!")
+
+        #FIXME: Replace with reducing the volume as we want
+        # to test route switching
+        ad_caller.droid.telecomCallSetAudioRoute(AUDIO_ROUTE_EARPIECE)
+        ad_callee.droid.telecomCallSetAudioRoute(AUDIO_ROUTE_EARPIECE)
+
+        time.sleep(wait_time_in_call)
+
+        # Check Callee first
+        # in case of VT call drop, it usually start from callee
+        if not verify_callee_func:
+            callee_state_result = ad_callee.droid.telecomIsInCall()
+        else:
+            callee_state_result = verify_callee_func(log, ad_callee)
+        if not callee_state_result:
+            raise _CallSequenceException(
+                "Callee not in correct state after {} seconds"
+                .format(wait_time_in_call))
+
+        if not verify_caller_func:
+            caller_state_result = ad_caller.droid.telecomIsInCall()
+        else:
+            caller_state_result = verify_caller_func(log, ad_caller)
+        if not caller_state_result:
+            raise _CallSequenceException(
+                "Caller not in correct state after {} seconds"
+                .format(wait_time_in_call))
+
+        if not ad_hangup:
+            return True
+
+        if not hangup_call(log, ad_hangup):
+            raise _CallSequenceException(
+                "Error in Hanging-Up Call")
+        return True
+
+    except _CallSequenceException as e:
+        log.error(e)
+        return False
+    finally:
+        if ad_hangup:
+            for ad in [ad_caller, ad_callee]:
+                try:
+                    if ad.droid.telecomIsInCall():
+                        ad.droid.telecomEndCall()
+                except Exception as e:
+                    log.error(str(e))
+
+
+def video_call_modify_video(
+        log, ad_requester, call_id_requester, ad_responder, call_id_responder,
+        video_state_request, video_quality_request=VT_VIDEO_QUALITY_DEFAULT,
+        video_state_response=None, video_quality_response=None,
+        verify_func_between_request_and_response=None):
+    """Modifies an ongoing call to change the video_call state
+
+    Args:
+        log: logger object
+        ad_requester: android_device object of the requester
+        call_id_requester: the call_id of the call placing the modify request
+        ad_requester: android_device object of the responder
+        call_id_requester: the call_id of the call receiving the modify request
+        video_state_request: the requested video state
+        video_quality_request: the requested video quality, defaults to
+            QUALITY_DEFAULT
+        video_state_response: the responded video state or, or (default)
+            match the request if None
+        video_quality_response: the responded video quality, or (default)
+            match the request if None
+
+    Returns:
+        A call_id corresponding to the first call in the state, or None
+    """
+
+    if not video_state_response:
+        video_state_response = video_state_request
+    if not video_quality_response:
+        video_quality_response = video_quality_request
+
+    cur_video_state = ad_requester.droid.telecomCallVideoGetState(
+        call_id_requester)
+
+    log.info("State change request from {} to {} requested"
+             .format(cur_video_state, video_state_request))
+
+    if cur_video_state == video_state_request:
+        return True
+
+    ad_responder.ed.clear_events(
+        EventTelecomVideoCallSessionModifyRequestReceived)
+
+    ad_responder.droid.telecomCallVideoStartListeningForEvent(
+        call_id_responder, EventSessionModifyRequestRceived
+    )
+
+    ad_requester.droid.telecomCallVideoSendSessionModifyRequest(
+        call_id_requester, video_state_request, video_quality_request)
+
+    try:
+        request_event = ad_responder.ed.pop_event(
+            EventTelecomVideoCallSessionModifyRequestReceived,
+            WAIT_TIME_VIDEO_SESSION_EVENT)
+        log.info(request_event)
+    except Empty:
+        log.error("Failed to receive SessionModifyRequest!")
+        return False
+    finally:
+        ad_responder.droid.telecomCallVideoStopListeningForEvent(
+            call_id_responder, EventSessionModifyRequestRceived
+        )
+
+    if (verify_func_between_request_and_response and
+        not verify_func_between_request_and_response()):
+        log.error("verify_func_between_request_and_response failed.")
+        return False
+
+    #FIXME: Replace with reducing the volume as we want to test route switching
+    ad_requester.droid.telecomCallSetAudioRoute(AUDIO_ROUTE_EARPIECE)
+
+    ad_requester.droid.telecomCallVideoStartListeningForEvent(
+        call_id_requester, EventSessionModifyResponsetRceived)
+
+    ad_responder.droid.telecomCallVideoSendSessionModifyResponse(
+        call_id_responder, video_state_response, video_quality_response)
+
+    try:
+        response_event = ad_requester.ed.pop_event(
+            EventTelecomVideoCallSessionModifyResponseReceived,
+            WAIT_TIME_VIDEO_SESSION_EVENT)
+        log.info(response_event)
+    except Empty:
+        log.error("Failed to receive SessionModifyResponse!")
+        return False
+    finally:
+        ad_requester.droid.telecomCallVideoStopListeningForEvent(
+            call_id_requester, EventSessionModifyResponsetRceived)
+
+    #FIXME: Replace with reducing the volume as we want to test route switching
+    ad_responder.droid.telecomCallSetAudioRoute(AUDIO_ROUTE_EARPIECE)
+
+    return True
+
+def is_call_id_in_video_state(log, ad, call_id, video_state):
+    """Return is the call_id is in expected video_state
+
+    Args:
+        log: logger object
+        ad: android_device object
+        call_id: call id
+        video_state: valid VIDEO_STATE
+
+    Returns:
+        True is call_id in expected video_state; False if not.
+    """
+    return video_state == ad.droid.telecomCallVideoGetState(call_id)
+
+def get_call_id_in_video_state(log, ad, video_state):
+    """Gets the first call reporting a given video_state
+        from among the active calls
+
+    Args:
+        log: logger object
+        ad: android_device object
+        video_state: valid VIDEO_STATE
+
+    Returns:
+        A call_id corresponding to the first call in the state, or None
+    """
+
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return None
+    for call in ad.droid.telecomCallGetCallIds():
+        if is_call_id_in_video_state(log, ad, call, video_state):
+            return call
+    return None
+
+def video_call_downgrade(log,
+                         ad_requester, call_id_requester,
+                         ad_responder, call_id_responder,
+                         video_state_request=None,
+                         video_quality_request=VT_VIDEO_QUALITY_DEFAULT):
+    """Downgrade Video call to video_state_request.
+    Send telecomCallVideoSendSessionModifyRequest from ad_requester.
+    Get video call state from ad_requester and ad_responder.
+    Verify video calls states are correct and downgrade succeed.
+
+    Args:
+        log: logger object
+        ad_requester: android_device object of the requester
+        call_id_requester: the call_id of the call placing the modify request
+        ad_requester: android_device object of the responder
+        call_id_requester: the call_id of the call receiving the modify request
+        video_state_request: the requested downgrade video state
+            This parameter is optional. If this parameter is None:
+                if call_id_requester current is bi-directional, will downgrade to RX_ENABLED
+                if call_id_requester current is RX_ENABLED, will downgrade to AUDIO_ONLY
+        video_quality_request: the requested video quality, defaults to
+            QUALITY_DEFAULT
+    Returns:
+        True if downgrade succeed.
+    """
+    # TODO(yangxliu): This util may need further change.
+    if (call_id_requester is None) or (call_id_responder is None):
+        log.error("call_id_requester: {}, call_id_responder: {}".
+            format(call_id_requester, call_id_responder))
+        return False
+    current_video_state_requester = ad_requester.droid.telecomCallVideoGetState(
+        call_id_requester)
+    if video_state_request is None:
+        if (current_video_state_requester == VT_STATE_BIDIRECTIONAL or
+                current_video_state_requester == VT_STATE_BIDIRECTIONAL_PAUSED):
+            video_state_request = VT_STATE_RX_ENABLED
+        elif (current_video_state_requester == VT_STATE_TX_ENABLED or
+                current_video_state_requester == VT_STATE_TX_PAUSED):
+            video_state_request = VT_STATE_AUDIO_ONLY
+        else:
+            log.error(
+                "Can Not Downgrade. ad: {}, current state {}"
+                .format(ad_requester.serial, current_video_state_requester))
+            return False
+    expected_video_state_responder = {
+        VT_STATE_AUDIO_ONLY: VT_STATE_AUDIO_ONLY,
+        VT_STATE_RX_ENABLED: VT_STATE_TX_ENABLED
+    }[video_state_request]
+
+    ad_requester.droid.telecomCallVideoStartListeningForEvent(
+        call_id_requester, EventSessionModifyResponsetRceived)
+
+    ad_requester.droid.telecomCallVideoSendSessionModifyRequest(
+        call_id_requester, video_state_request, video_quality_request)
+
+    try:
+        response_event = ad_requester.ed.pop_event(
+            EventTelecomVideoCallSessionModifyResponseReceived,
+            WAIT_TIME_VIDEO_SESSION_EVENT)
+        log.info(response_event)
+    except Empty:
+        log.error("Failed to receive SessionModifyResponse!")
+        return False
+    finally:
+        ad_requester.droid.telecomCallVideoStopListeningForEvent(
+            call_id_requester, EventSessionModifyResponsetRceived)
+
+    time.sleep(WAIT_TIME_ANDROID_STATE_SETTLING)
+    # FIXME: Replace with reducing the volume as we want
+    # to test route switching
+    ad_requester.droid.telecomCallSetAudioRoute(AUDIO_ROUTE_EARPIECE)
+    ad_responder.droid.telecomCallSetAudioRoute(AUDIO_ROUTE_EARPIECE)
+
+    time.sleep(WAIT_TIME_IN_CALL)
+    if video_state_request != ad_requester.droid.telecomCallVideoGetState(
+            call_id_requester):
+        log.error("requester not in correct state. expected:{}, current:{}"
+                  .format(video_state_request,
+                          ad_requester.droid.telecomCallVideoGetState(
+                              call_id_requester)))
+        return False
+    if (expected_video_state_responder !=
+            ad_responder.droid.telecomCallVideoGetState(
+                call_id_responder)):
+        log.error("responder not in correct state. expected:{}, current:{}".
+                  format(expected_video_state_responder,
+                         ad_responder.droid.telecomCallVideoGetState(
+                             call_id_responder)))
+        return False
+
+    return True
+
+def verify_video_call_in_expected_state(log, ad, call_id,
+                                        call_video_state, call_state):
+    """Return True if video call is in expected video state and call state.
+
+    Args:
+        log: logger object
+        ad: android_device object
+        call_id: ad's call id
+        call_video_state: video state to validate.
+        call_state: call state to validate.
+
+    Returns:
+        True if video call is in expected video state and call state.
+    """
+    if not is_call_id_in_video_state(log, ad, call_id, call_video_state):
+        log.error("Call is not in expected {} state. Current state {}".
+                  format(call_video_state,
+                         ad.droid.telecomCallVideoGetState(call_id)))
+        return False
+    if ad.droid.telecomCallGetCallState(call_id) != call_state:
+        log.error("Call is not in expected {} state. Current state {}".
+                  format(call_state,
+                         ad.droid.telecomCallGetCallState(call_id)))
+        return False
+    return True
diff --git a/acts/framework/acts/test_utils/tel/tel_voice_utils.py b/acts/framework/acts/test_utils/tel/tel_voice_utils.py
new file mode 100644
index 0000000..8f7ba4f
--- /dev/null
+++ b/acts/framework/acts/test_utils/tel/tel_voice_utils.py
@@ -0,0 +1,1037 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - Google
+#
+#   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 .tel_lookup_tables import *
+from .tel_test_utils import *
+
+def two_phone_call_leave_voice_mail(
+        log,
+        caller,
+        caller_idle_func,
+        caller_in_call_check_func,
+        callee,
+        callee_idle_func,
+        wait_time_in_call=WAIT_TIME_TO_LEAVE_VOICE_MAIL
+):
+    """Call from caller to callee, reject on callee, caller leave a voice mail.
+
+    1. Caller call Callee.
+    2. Callee reject incoming call.
+    3. Caller leave a voice mail.
+    4. Verify callee received the voice mail notification.
+
+    Args:
+        caller: caller android device object.
+        caller_idle_func: function to check caller's idle state.
+        caller_in_call_check_func: function to check caller's in-call state.
+        callee: callee android device object.
+        callee_idle_func: function to check callee's idle state.
+        wait_time_in_call: time to wait when leaving a voice mail.
+            This is optional, default is WAIT_TIME_TO_LEAVE_VOICE_MAIL
+
+    Returns:
+        True: if voice message is received on callee successfully.
+        False: for errors
+    """
+
+    ads = [caller, callee]
+
+    # Make sure phones are idle.
+    ensure_phones_idle(log, ads)
+    if caller_idle_func and not caller_idle_func(log, caller):
+        log.error("Caller Failed to Reselect")
+        return False
+    if callee_idle_func and not callee_idle_func(log, callee):
+        log.error("Callee Failed to Reselect")
+        return False
+
+    # Temporary hack to give phone enough time to register.
+    # TODO: Proper check using SL4A API.
+    time.sleep(WAIT_TIME_BETWEEN_REG_AND_CALL)
+
+    # Make call and leave a message.
+    if not call_reject_leave_message(log,
+                                     caller,
+                                     callee,
+                                     caller_in_call_check_func,
+                                     wait_time_in_call):
+        log.error("make a call and leave a message failed.")
+        return False
+    return True
+
+def two_phone_call_short_seq(
+        log,
+        phone_a,
+        phone_a_idle_func,
+        phone_a_in_call_check_func,
+        phone_b,
+        phone_b_idle_func,
+        phone_b_in_call_check_func,
+        call_sequence_func=None,
+        wait_time_in_call=WAIT_TIME_IN_CALL
+):
+    """Call process short sequence.
+    1. Ensure phone idle and in idle_func check return True.
+    2. Call from PhoneA to PhoneB, accept on PhoneB.
+    3. Check phone state, hangup on PhoneA.
+    4. Ensure phone idle and in idle_func check return True.
+    5. Call from PhoneA to PhoneB, accept on PhoneB.
+    6. Check phone state, hangup on PhoneB.
+
+    Args:
+        phone_a: PhoneA's android device object.
+        phone_a_idle_func: function to check PhoneA's idle state.
+        phone_a_in_call_check_func: function to check PhoneA's in-call state.
+        phone_b: PhoneB's android device object.
+        phone_b_idle_func: function to check PhoneB's idle state.
+        phone_b_in_call_check_func: function to check PhoneB's in-call state.
+        call_sequence_func: default parameter, not implemented.
+        wait_time_in_call: time to wait in call.
+            This is optional, default is WAIT_TIME_IN_CALL
+
+    Returns:
+        True: if call sequence succeed.
+        False: for errors
+    """
+# TODO: call_sequence_func. Define the sequence of calls
+
+    ads = [phone_a, phone_b]
+
+    call_params = [(ads[0], ads[1], ads[0],
+                    phone_a_in_call_check_func,
+                    phone_b_in_call_check_func),
+                   (ads[0], ads[1], ads[1],
+                    phone_a_in_call_check_func,
+                    phone_b_in_call_check_func),
+                   ]
+
+    for param in call_params:
+        # Make sure phones are idle.
+        ensure_phones_idle(log, ads)
+        if phone_a_idle_func and not phone_a_idle_func(log, phone_a):
+            log.error("Phone A Failed to Reselect")
+            return False
+        if phone_b_idle_func and not phone_b_idle_func(log, phone_b):
+            log.error("Phone B Failed to Reselect")
+            return False
+
+        # Temporary hack to give phone enough time to register.
+        # TODO: Proper check using SL4A API.
+        time.sleep(WAIT_TIME_BETWEEN_REG_AND_CALL)
+
+        # Make call.
+        log.info("---> Call test: {} to {} <---".format(param[0].serial, param[1].serial))
+        if not call_setup_teardown(log, *param, wait_time_in_call=wait_time_in_call):
+            log.error("Call Iteration Failed")
+            return False
+
+    return True
+
+
+def two_phone_call_long_seq(
+        log,
+        phone_a,
+        phone_a_idle_func,
+        phone_a_in_call_check_func,
+        phone_b,
+        phone_b_idle_func,
+        phone_b_in_call_check_func,
+        call_sequence_func=None,
+        wait_time_in_call=WAIT_TIME_IN_CALL
+):
+    """Call process long sequence.
+    1. Ensure phone idle and in idle_func check return True.
+    2. Call from PhoneA to PhoneB, accept on PhoneB.
+    3. Check phone state, hangup on PhoneA.
+    4. Ensure phone idle and in idle_func check return True.
+    5. Call from PhoneA to PhoneB, accept on PhoneB.
+    6. Check phone state, hangup on PhoneB.
+    7. Ensure phone idle and in idle_func check return True.
+    8. Call from PhoneB to PhoneA, accept on PhoneA.
+    9. Check phone state, hangup on PhoneA.
+    10. Ensure phone idle and in idle_func check return True.
+    11. Call from PhoneB to PhoneA, accept on PhoneA.
+    12. Check phone state, hangup on PhoneB.
+
+    Args:
+        phone_a: PhoneA's android device object.
+        phone_a_idle_func: function to check PhoneA's idle state.
+        phone_a_in_call_check_func: function to check PhoneA's in-call state.
+        phone_b: PhoneB's android device object.
+        phone_b_idle_func: function to check PhoneB's idle state.
+        phone_b_in_call_check_func: function to check PhoneB's in-call state.
+        call_sequence_func: default parameter, not implemented.
+        wait_time_in_call: time to wait in call.
+            This is optional, default is WAIT_TIME_IN_CALL
+
+    Returns:
+        True: if call sequence succeed.
+        False: for errors
+    """
+# TODO: call_sequence_func. Define the sequence of calls
+
+    ads = [phone_a, phone_b]
+
+    call_params = [(ads[0], ads[1], ads[0],
+                    phone_a_in_call_check_func,
+                    phone_b_in_call_check_func),
+                   (ads[0], ads[1], ads[1],
+                    phone_a_in_call_check_func,
+                    phone_b_in_call_check_func),
+                   (ads[1], ads[0], ads[0],
+                    phone_b_in_call_check_func,
+                    phone_a_in_call_check_func),
+                   (ads[1], ads[0], ads[1],
+                    phone_b_in_call_check_func,
+                    phone_a_in_call_check_func),
+                   ]
+
+    for param in call_params:
+        # Make sure phones are idle.
+        ensure_phones_idle(log, ads)
+        if phone_a_idle_func and not phone_a_idle_func(log, phone_a):
+            log.error("Phone A Failed to Reselect")
+            return False
+        if phone_b_idle_func and not phone_b_idle_func(log, phone_b):
+            log.error("Phone B Failed to Reselect")
+            return False
+
+        # Temporary hack to give phone enough time to register.
+        # TODO: Proper check using SL4A API.
+        time.sleep(WAIT_TIME_BETWEEN_REG_AND_CALL)
+
+        # Make call.
+        log.info("---> Call test: {} to {} <---".format(param[0].serial, param[1].serial))
+        if not call_setup_teardown(log, *param, wait_time_in_call=wait_time_in_call):
+            log.error("Call Iteration Failed")
+            return False
+
+    return True
+
+
+def phone_setup_volte(log, ad):
+    """Setup VoLTE enable.
+
+    Args:
+        ad: android device object.
+
+    Returns:
+        True: if VoLTE is enabled successfully.
+        False: for errors
+    """
+    return phone_setup_volte_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_volte_for_subscription(log, ad, sub_id):
+    """Setup VoLTE enable for subscription id.
+
+    Args:
+        ad: android device object.
+        sub_id: subscription id.
+
+    Returns:
+        True: if VoLTE is enabled successfully.
+        False: for errors
+    """
+    toggle_airplane_mode(log, ad, False)
+    if not set_wfc_mode(log, ad, WFC_MODE_DISABLED):
+        log.error("{} Disable WFC failed.".format(ad.serial))
+        return False
+    toggle_volte_for_subscription(log, ad, sub_id, True)
+    if not ensure_network_rat_for_subscription(
+            log, ad, sub_id, RAT_LTE, WAIT_TIME_NW_SELECTION,
+            NETWORK_SERVICE_DATA):
+        return False
+    return phone_idle_volte_for_subscription(log, ad, sub_id)
+
+def phone_setup_iwlan(log, ad, is_airplane_mode, wfc_mode,
+                      wifi_ssid=None, wifi_pwd=None):
+    """Phone setup function for epdg call test.
+    Set WFC mode according to wfc_mode.
+    Set airplane mode according to is_airplane_mode.
+    Make sure phone connect to WiFi. (If wifi_ssid is not None.)
+    Wait for phone to be in iwlan data network type.
+    Wait for phone to report wfc enabled flag to be true.
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        is_airplane_mode: True to turn on airplane mode. False to turn off airplane mode.
+        wfc_mode: WFC mode to set to.
+        wifi_ssid: WiFi network SSID. This is optional.
+            If wifi_ssid is None, then phone_setup_iwlan will not attempt to connect to wifi.
+        wifi_pwd: WiFi network password. This is optional.
+
+    Returns:
+        True if success. False if fail.
+    """
+    return phone_setup_iwlan_for_subscription(log, ad,
+        ad.droid.subscriptionGetDefaultVoiceSubId(), is_airplane_mode, wfc_mode,
+        wifi_ssid, wifi_pwd)
+
+def phone_setup_iwlan_for_subscription(log, ad, sub_id, is_airplane_mode, wfc_mode,
+                                       wifi_ssid=None, wifi_pwd=None):
+    """Phone setup function for epdg call test for subscription id.
+    Set WFC mode according to wfc_mode.
+    Set airplane mode according to is_airplane_mode.
+    Make sure phone connect to WiFi. (If wifi_ssid is not None.)
+    Wait for phone to be in iwlan data network type.
+    Wait for phone to report wfc enabled flag to be true.
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        sub_id: subscription id.
+        is_airplane_mode: True to turn on airplane mode. False to turn off airplane mode.
+        wfc_mode: WFC mode to set to.
+        wifi_ssid: WiFi network SSID. This is optional.
+            If wifi_ssid is None, then phone_setup_iwlan will not attempt to connect to wifi.
+        wifi_pwd: WiFi network password. This is optional.
+
+    Returns:
+        True if success. False if fail.
+    """
+
+    toggle_airplane_mode(log, ad, False)
+    toggle_volte(log, ad, True)
+    if not is_airplane_mode and not ensure_network_rat_for_subscription(
+        log, ad, sub_id, RAT_LTE, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_DATA):
+            return False
+
+    if not set_wfc_mode(log, ad, wfc_mode):
+        log.error("{} set WFC mode failed.".format(ad.serial))
+        return False
+
+    toggle_airplane_mode(log, ad, is_airplane_mode)
+
+    if wifi_ssid is not None:
+        if not ensure_wifi_connected(log, ad, wifi_ssid, wifi_pwd):
+            log.error("{} connect to WiFi failed.".format(ad.serial))
+            return False
+
+    return phone_idle_iwlan_for_subscription(log, ad, sub_id)
+
+def phone_setup_iwlan_cellular_preferred(log, ad, wifi_ssid=None,
+                                                 wifi_pwd=None):
+    """Phone setup function for iwlan Non-APM CELLULAR_PREFERRED test.
+    Set WFC mode according to CELLULAR_PREFERRED.
+    Set airplane mode according to False.
+    Make sure phone connect to WiFi. (If wifi_ssid is not None.)
+    Make sure phone don't report iwlan data network type.
+    Make sure phone don't report wfc enabled flag to be true.
+
+    Args:
+        log: Log object.
+        ad: Android device object.
+        wifi_ssid: WiFi network SSID. This is optional.
+            If wifi_ssid is None, then phone_setup_iwlan will not attempt to connect to wifi.
+        wifi_pwd: WiFi network password. This is optional.
+
+    Returns:
+        True if success. False if fail.
+    """
+    toggle_airplane_mode(log, ad, False)
+    toggle_volte(log, ad, True)
+    if not ensure_network_rat(
+            log, ad, RAT_LTE, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_DATA):
+        return False
+    if not set_wfc_mode(log, ad, WFC_MODE_CELLULAR_PREFERRED):
+        log.error("{} set WFC mode failed.".format(ad.serial))
+        return False
+    if wifi_ssid is not None:
+        if not ensure_wifi_connected(log, ad, wifi_ssid, wifi_pwd):
+            log.error("{} connect to WiFi failed.".format(ad.serial))
+            return False
+    if wait_for_droid_in_network_rat(
+        log, ad, RAT_IWLAN, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_DATA):
+        log.error("{} data rat in iwlan mode.".format(ad.serial))
+        return False
+    if wait_for_wfc_enabled(log, ad, WAIT_TIME_WFC_ENABLED):
+        log.error("{} should not <report wfc enabled true> within {}s.".
+                       format(ad.serial, WAIT_TIME_WFC_ENABLED))
+        return False
+    return True
+
+def phone_setup_csfb(log, ad):
+    """Setup phone for CSFB call test.
+
+    Setup Phone to be in 4G mode.
+    Disabled VoLTE.
+
+    Args:
+        ad: Android device object.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    return phone_setup_csfb_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_csfb_for_subscription(log, ad, sub_id):
+    """Setup phone for CSFB call test for subscription id.
+
+    Setup Phone to be in 4G mode.
+    Disabled VoLTE.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    toggle_airplane_mode(log, ad, False)
+    if not set_wfc_mode(log, ad, WFC_MODE_DISABLED):
+        log.error("{} Disable WFC failed.".format(ad.serial))
+        return False
+    if ad.droid.imsIsEnhanced4gLteModeSettingEnabledByPlatform():
+        toggle_volte(log, ad, False)
+    if not ensure_network_rat_for_subscription(
+            log, ad, sub_id, RAT_LTE, WAIT_TIME_NW_SELECTION,
+            NETWORK_SERVICE_DATA):
+        return False
+
+    # FIXME: Delete when getVoiceNetworkType() is fixed
+    if get_operator_name(log, ad, sub_id) == CARRIER_VZW:
+        time.sleep(VZW_WAIT_TIME_IN_PHONE_SETUP_FUNC)
+
+    return phone_idle_csfb_for_subscription(log, ad, sub_id)
+
+
+def phone_setup_3g(log, ad):
+    """Setup phone for 3G call test.
+
+    Args:
+        ad: Android device object.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    return phone_setup_3g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_3g_for_subscription(log, ad, sub_id):
+    """Setup phone for 3G call test for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    toggle_airplane_mode(log, ad, False)
+    if not set_wfc_mode(log, ad, WFC_MODE_DISABLED):
+        log.error("{} Disable WFC failed.".format(ad.serial))
+        return False
+    if not ensure_network_generation_for_subscription(
+            log, ad, sub_id, RAT_3G, WAIT_TIME_NW_SELECTION,
+            NETWORK_SERVICE_DATA):
+        return False
+
+    # FIXME: Delete when getVoiceNetworkType() is fixed
+    if get_operator_name(log, ad, sub_id) == CARRIER_VZW:
+        time.sleep(VZW_WAIT_TIME_IN_PHONE_SETUP_FUNC)
+
+    return wait_for_droid_in_network_generation_for_subscription(
+        log, ad, sub_id, RAT_3G, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_VOICE)
+
+def phone_setup_voice_3g(log, ad):
+    return phone_setup_voice_3g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_voice_3g_for_subscription(log, ad, sub_id):
+    toggle_airplane_mode(log, ad, False)
+    if not set_wfc_mode(log, ad, WFC_MODE_DISABLED):
+        log.error("{} Disable WFC failed.".format(ad.serial))
+        return False
+
+    return ensure_network_generation_for_subscription(
+        log, ad, sub_id, RAT_3G, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_VOICE)
+
+def phone_setup_2g(log, ad):
+    """Setup phone for 2G call test.
+
+    Args:
+        ad: Android device object.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    return phone_setup_2g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_2g_for_subscription(log, ad, sub_id):
+    """Setup phone for 2G call test for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    toggle_airplane_mode(log, ad, False)
+    if not set_wfc_mode(log, ad, WFC_MODE_DISABLED):
+        log.error("{} Disable WFC failed.".format(ad.serial))
+        return False
+    if not ensure_network_generation_for_subscription(
+            log, ad, sub_id, RAT_2G, WAIT_TIME_NW_SELECTION,
+            NETWORK_SERVICE_DATA):
+        return False
+    return ensure_network_generation_for_subscription(
+        log, ad, sub_id, RAT_2G, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_VOICE)
+
+def phone_setup_voice_2g(log, ad):
+    return phone_setup_voice_2g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_voice_2g_for_subscription(log, ad, sub_id):
+    toggle_airplane_mode(log, ad, False)
+    if not set_wfc_mode(log, ad, WFC_MODE_DISABLED):
+        log.error("{} Disable WFC failed.".format(ad.serial))
+        return False
+
+    return ensure_network_generation_for_subscription(
+        log, ad, sub_id, RAT_2G, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_VOICE)
+
+def phone_setup_voice_general(log, ad):
+    """Setup phone for voice general call test.
+
+    Make sure phone attached to voice.
+    Make necessary delay.
+
+    Args:
+        ad: Android device object.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    return phone_setup_voice_general_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_setup_voice_general_for_subscription(log, ad, sub_id):
+    """Setup phone for voice general call test for subscription id.
+
+    Make sure phone attached to voice.
+    Make necessary delay.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+
+    Returns:
+        True if setup successfully.
+        False for errors.
+    """
+    toggle_airplane_mode(log, ad, False)
+    if not wait_for_voice_attach_for_subscription(log, ad, sub_id,
+                                          WAIT_TIME_NW_SELECTION):
+        # if phone can not attach voice, try phone_setup_3g
+        return phone_setup_3g_for_subscription(log, ad, sub_id)
+
+   # FIXME: Delete when getVoiceNetworkType() is fixed
+    if get_operator_name(log, ad, sub_id) == CARRIER_VZW:
+        time.sleep(VZW_WAIT_TIME_IN_PHONE_SETUP_FUNC)
+
+    return True
+
+def phone_idle_volte(log, ad):
+    """Return if phone is idle for VoLTE call test.
+
+    Args:
+        ad: Android device object.
+    """
+    return phone_idle_volte_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_idle_volte_for_subscription(log, ad, sub_id):
+    """Return if phone is idle for VoLTE call test for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not wait_for_droid_in_network_rat_for_subscription(
+        log, ad, sub_id, RAT_LTE, WAIT_TIME_NW_SELECTION,
+        NETWORK_SERVICE_VOICE):
+        log.error("{} voice rat not in LTE mode.".format(ad.serial))
+        return False
+    if not wait_for_volte_enabled(log, ad, WAIT_TIME_VOLTE_ENABLED):
+        log.error("{} failed to <report volte enabled true> within {}s.".
+                  format(ad.serial, WAIT_TIME_VOLTE_ENABLED))
+        return False
+    return True
+
+def phone_idle_iwlan(log, ad):
+    """Return if phone is idle for WiFi calling call test.
+
+    Args:
+        ad: Android device object.
+    """
+    return phone_idle_iwlan_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def phone_idle_iwlan_for_subscription(log, ad, sub_id):
+    """Return if phone is idle for WiFi calling call test for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not wait_for_droid_in_network_rat_for_subscription(
+        log, ad, sub_id, RAT_IWLAN, WAIT_TIME_NW_SELECTION,
+        NETWORK_SERVICE_DATA):
+        log.error("{} data rat not in iwlan mode.".format(ad.serial))
+        return False
+    if not wait_for_wfc_enabled(log, ad, WAIT_TIME_WFC_ENABLED):
+        log.error("{} failed to <report wfc enabled true> within {}s.".
+                  format(ad.serial, WAIT_TIME_WFC_ENABLED))
+        return False
+    return True
+
+
+def phone_idle_csfb(log, ad):
+    """Return if phone is idle for CSFB call test.
+
+    Args:
+        ad: Android device object.
+    """
+    return phone_idle_csfb_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_idle_csfb_for_subscription(log, ad, sub_id):
+    """Return if phone is idle for CSFB call test for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not wait_for_droid_in_network_rat_for_subscription(
+            log, ad, sub_id, RAT_LTE, WAIT_TIME_NW_SELECTION,
+            NETWORK_SERVICE_DATA):
+        log.error("{} data rat not in lte mode.".format(ad.serial))
+        return False
+
+#    FIXME: Re-enable when getVoiceNetworkType() is fixed
+#    FIXME:Support 2g
+#    if not wait_for_droid_in_network_generation(
+#            log, ad, WAIT_TIME_NW_SELECTION, RAT_3G, NETWORK_SERVICE_VOICE):
+#        return False
+    return True
+
+
+def phone_idle_3g(log, ad):
+    """Return if phone is idle for 3G call test.
+
+    Args:
+        ad: Android device object.
+    """
+    return phone_idle_3g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_idle_3g_for_subscription(log, ad, sub_id):
+    """Return if phone is idle for 3G call test for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    return wait_for_droid_in_network_generation_for_subscription(
+        log, ad, sub_id, RAT_3G, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_VOICE)
+
+
+def phone_idle_2g(log, ad):
+    """Return if phone is idle for 2G call test.
+
+    Args:
+        ad: Android device object.
+    """
+    return phone_idle_2g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def phone_idle_2g_for_subscription(log, ad, sub_id):
+    """Return if phone is idle for 2G call test for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    return wait_for_droid_in_network_generation_for_subscription(
+        log, ad, sub_id, RAT_2G, WAIT_TIME_NW_SELECTION, NETWORK_SERVICE_VOICE)
+
+def is_phone_in_call_volte(log, ad):
+    """Return if phone is in VoLTE call.
+
+    Args:
+        ad: Android device object.
+    """
+    return is_phone_in_call_volte_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+def is_phone_in_call_volte_for_subscription(log, ad, sub_id):
+    """Return if phone is in VoLTE call for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_type = get_network_rat_for_subscription(log, ad, sub_id,
+                                               NETWORK_SERVICE_VOICE)
+    if nw_type != RAT_LTE:
+        log.error("{} voice rat on: {}. Expected: LTE".format(ad.serial, nw_type))
+        return False
+    return True
+
+def is_phone_in_call_csfb(log, ad):
+    """Return if phone is in CSFB call.
+
+    Args:
+        ad: Android device object.
+    """
+    return is_phone_in_call_csfb_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def is_phone_in_call_csfb_for_subscription(log, ad, sub_id):
+    """Return if phone is in CSFB call for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_type = get_network_rat_for_subscription(log, ad, sub_id,
+                                               NETWORK_SERVICE_VOICE)
+    if nw_type == RAT_LTE:
+        log.error("{} voice rat on: {}. Expected: not LTE".format(ad.serial, nw_type))
+        return False
+    return True
+
+
+def is_phone_in_call_3g(log, ad):
+    """Return if phone is in 3G call.
+
+    Args:
+        ad: Android device object.
+    """
+    return is_phone_in_call_3g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def is_phone_in_call_3g_for_subscription(log, ad, sub_id):
+    """Return if phone is in 3G call for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_gen = get_network_gen_for_subscription(log, ad, sub_id,
+                                              NETWORK_SERVICE_VOICE)
+    if nw_gen != RAT_3G:
+        log.error("{} voice rat on: {}. Expected: 3g".format(ad.serial, nw_gen))
+        return False
+    return True
+
+
+def is_phone_in_call_2g(log, ad):
+    """Return if phone is in 2G call.
+
+    Args:
+        ad: Android device object.
+    """
+    return is_phone_in_call_2g_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def is_phone_in_call_2g_for_subscription(log, ad, sub_id):
+    """Return if phone is in 2G call for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_gen = get_network_gen_for_subscription(log, ad, sub_id,
+                                              NETWORK_SERVICE_VOICE)
+    if nw_gen != RAT_2G:
+        log.error("{} voice rat on: {}. Expected: 2g".format(ad.serial, nw_gen))
+        return False
+    return True
+
+def is_phone_in_call_1x(log, ad):
+    """Return if phone is in 1x call.
+
+    Args:
+        ad: Android device object.
+    """
+    return is_phone_in_call_1x_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def is_phone_in_call_1x_for_subscription(log, ad, sub_id):
+    """Return if phone is in 1x call for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_type = get_network_rat_for_subscription(log, ad, sub_id,
+                                               NETWORK_SERVICE_VOICE)
+    if nw_type != RAT_1XRTT:
+        log.error("{} voice rat on: {}. Expected: 1xrtt".format(ad.serial, nw_type))
+        return False
+    return True
+
+def is_phone_in_call_wcdma(log, ad):
+    """Return if phone is in WCDMA call.
+
+    Args:
+        ad: Android device object.
+    """
+    return is_phone_in_call_wcdma_for_subscription(
+        log, ad, ad.droid.subscriptionGetDefaultVoiceSubId())
+
+
+def is_phone_in_call_wcdma_for_subscription(log, ad, sub_id):
+    """Return if phone is in WCDMA call for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    # Currently checking 'umts'.
+    # Changes may needed in the future.
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_type = get_network_rat_for_subscription(log, ad, sub_id,
+                                               NETWORK_SERVICE_VOICE)
+    if nw_type != RAT_UMTS:
+        log.error("{} voice rat on: {}. Expected: umts".format(ad.serial, nw_type))
+        return False
+    return True
+
+def is_phone_in_call_iwlan(log, ad):
+    """Return if phone is in WiFi call.
+
+    Args:
+        ad: Android device object.
+    """
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_type = get_network_rat(log, ad, NETWORK_SERVICE_DATA)
+    if nw_type != RAT_IWLAN:
+        log.error("{} data rat on: {}. Expected: iwlan".format(ad.serial, nw_type))
+        return False
+    if not is_wfc_enabled(log, ad):
+        log.error("{} WiFi Calling feature bit is False.".format(ad.serial))
+        return False
+    return True
+
+def is_phone_in_call_not_iwlan(log, ad):
+    """Return if phone is in WiFi call for subscription id.
+
+    Args:
+        ad: Android device object.
+        sub_id: subscription id.
+    """
+    if not ad.droid.telecomIsInCall():
+        log.error("{} not in call.".format(ad.serial))
+        return False
+    nw_type = get_network_rat(log, ad, NETWORK_SERVICE_DATA)
+    if nw_type == RAT_IWLAN:
+        log.error("{} data rat on: {}. Expected: not iwlan".format(ad.serial, nw_type))
+        return False
+    if is_wfc_enabled(log, ad):
+        log.error("{} WiFi Calling feature bit is True.".format(ad.serial))
+        return False
+    return True
+
+def swap_calls(log, ads, call_hold_id, call_active_id, num_swaps=1,
+               check_call_status=True):
+    """PhoneA in call with B and C. Swap active/holding call on PhoneA.
+
+    Swap call and check status on PhoneA.
+        (This step may have multiple times according to 'num_swaps'.)
+    Check if all 3 phones are 'in-call'.
+
+    Args:
+        ads: list of ad object, at least three need to pass in.
+            Swap operation will happen on ads[0].
+            ads[1] and ads[2] are call participants.
+        call_hold_id: id for the holding call in ads[0].
+            call_hold_id should be 'STATE_HOLDING' when calling this function.
+        call_active_id: id for the active call in ads[0].
+            call_active_id should be 'STATE_ACTIVE' when calling this function.
+        num_swaps: how many swap/check operations will be done before return.
+        check_call_status: THis is optional. Default value is True.
+            If this value is True, then call status (active/hold) will be
+            be checked after each swap operation.
+
+    Returns:
+        If no error happened, return True, otherwise, return False.
+    """
+    if check_call_status:
+        # Check status before swap.
+        if ads[0].droid.telecomCallGetCallState(call_active_id) != CALL_STATE_ACTIVE:
+            log.error("Call_id:{}, state:{}, expected: STATE_ACTIVE".
+                format(call_active_id,
+                       ads[0].droid.telecomCallGetCallState(call_active_id)))
+            return False
+        if ads[0].droid.telecomCallGetCallState(call_hold_id) != CALL_STATE_HOLDING:
+            log.error("Call_id:{}, state:{}, expected: STATE_HOLDING".
+                format(call_hold_id,
+                       ads[0].droid.telecomCallGetCallState(call_hold_id)))
+            return False
+
+    i = 1
+    while(i <= num_swaps):
+        log.info("swap_test: {}. {} swap and check call status.".
+            format(i, ads[0].serial))
+        ads[0].droid.telecomCallHold(call_active_id)
+        time.sleep(WAIT_TIME_IN_CALL)
+        # Swap object reference
+        call_active_id, call_hold_id = call_hold_id, call_active_id
+        if check_call_status:
+            # Check status
+            if ads[0].droid.telecomCallGetCallState(call_active_id) != CALL_STATE_ACTIVE:
+                log.error("Call_id:{}, state:{}, expected: STATE_ACTIVE".
+                    format(call_active_id,
+                            ads[0].droid.telecomCallGetCallState(call_active_id)))
+                return False
+            if ads[0].droid.telecomCallGetCallState(call_hold_id) != CALL_STATE_HOLDING:
+                log.error("Call_id:{}, state:{}, expected: STATE_HOLDING".
+                    format(call_hold_id,
+                           ads[0].droid.telecomCallGetCallState(call_hold_id)))
+                return False
+        # TODO: Future add voice check.
+
+        i += 1
+
+    #In the end, check all three phones are 'in-call'.
+    if not verify_incall_state(log, [ads[0], ads[1], ads[2]], True):
+        return False
+
+    return True
+
+
+def get_audio_route(log, ad):
+    """Gets the audio route for the active call
+
+    Args:
+        log: logger object
+        ad: android_device object
+
+    Returns:
+        Audio route string ["BLUETOOTH", "EARPIECE", "SPEAKER", "WIRED_HEADSET"
+            "WIRED_OR_EARPIECE"]
+    """
+
+    audio_state = ad.droid.telecomCallGetAudioState()
+    return audio_state["AudioRoute"]
+
+
+def set_audio_route(log, ad, route):
+    """Sets the audio route for the active call
+
+    Args:
+        log: logger object
+        ad: android_device object
+        route: string ["BLUETOOTH", "EARPIECE", "SPEAKER", "WIRED_HEADSET"
+            "WIRED_OR_EARPIECE"]
+
+    Returns:
+        If no error happened, return True, otherwise, return False.
+    """
+    ad.droid.telecomCallSetAudioRoute(route)
+    return True
+
+def is_property_in_call_properties(log, ad, call_id, expected_property):
+    """Return if the call_id has the expected property
+
+    Args:
+        log: logger object
+        ad: android_device object
+        call_id: call id.
+        expected_property: expected property.
+
+    Returns:
+        True if call_id has expected_property. False if not.
+    """
+    properties = ad.droid.telecomCallGetProperties(call_id)
+    return (expected_property in properties)
+
+def is_call_hd(log, ad, call_id):
+    """Return if the call_id is HD call.
+
+    Args:
+        log: logger object
+        ad: android_device object
+        call_id: call id.
+
+    Returns:
+        True if call_id is HD call. False if not.
+    """
+    return is_property_in_call_properties(log, ad, call_id,
+                                          CALL_PROPERTY_HIGH_DEF_AUDIO)
+
+def get_cep_conference_call_id(ad):
+    """Get CEP conference call id if there is an ongoing CEP conference call.
+
+    Args:
+        ad: android device object.
+
+    Returns:
+        call id for CEP conference call if there is an ongoing CEP conference call.
+        None otherwise.
+    """
+    for call in ad.droid.telecomCallGetCallIds():
+        if len(ad.droid.telecomCallGetCallChildren(call)) != 0:
+            return call
+    return None
\ No newline at end of file
diff --git a/acts/framework/acts/test_utils/wifi_test_utils.py b/acts/framework/acts/test_utils/wifi_test_utils.py
new file mode 100755
index 0000000..1d9b9a4
--- /dev/null
+++ b/acts/framework/acts/test_utils/wifi_test_utils.py
@@ -0,0 +1,875 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 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.
+
+import time
+import pprint
+
+from enum import IntEnum
+from queue import Empty
+
+from acts.logger import LoggerProxy
+from acts.utils import exe_cmd
+from acts.utils import require_sl4a
+from acts.utils import sync_device_time
+from acts.utils import trim_model_name
+
+log = LoggerProxy()
+
+# Number of seconds to wait for events that are supposed to happen quickly.
+# Like onSuccess for start background scan and confirmation on wifi state
+# change.
+SHORT_TIMEOUT = 30
+
+# The currently supported devices that existed before release
+#TODO: (navtejsingh) Need to clean up the below lists going forward
+K_DEVICES = ["hammerhead", "razor", "razorg"]
+L_DEVICES = ["shamu", "ryu"]
+L_TAP_DEVICES = ["volantis", "volantisg"]
+M_DEVICES = ["angler"]
+
+# Speed of light in m/s.
+SPEED_OF_LIGHT = 299792458
+
+DEFAULT_PING_ADDR = "http://www.google.com/robots.txt"
+
+class WifiEnums():
+
+    SSID_KEY = "SSID"
+    BSSID_KEY = "BSSID"
+    PWD_KEY = "password"
+    frequency_key = "frequency"
+    APBAND_KEY = "apBand"
+
+    WIFI_CONFIG_APBAND_2G = 0
+    WIFI_CONFIG_APBAND_5G = 1
+
+    WIFI_WPS_INFO_PBC     = 0;
+    WIFI_WPS_INFO_DISPLAY = 1;
+    WIFI_WPS_INFO_KEYPAD  = 2;
+    WIFI_WPS_INFO_LABEL   = 3;
+    WIFI_WPS_INFO_INVALID = 4;
+
+    class CountryCode():
+        CHINA = "CN"
+        JAPAN = "JP"
+        UK = "GB"
+        US = "US"
+        UNKNOWN = "UNKNOWN"
+
+    # Start of Macros for EAP
+    # EAP types
+    class Eap(IntEnum):
+        NONE = -1
+        PEAP = 0
+        TLS  = 1
+        TTLS = 2
+        PWD  = 3
+        SIM  = 4
+        AKA  = 5
+
+    # EAP Phase2 types
+    class EapPhase2(IntEnum):
+        NONE        = 0
+        PAP         = 1
+        MSCHAP      = 2
+        MSCHAPV2    = 3
+        GTC         = 4
+
+    class Enterprise:
+    # Enterprise Config Macros
+        EMPTY_VALUE      = "NULL"
+        EAP              = "eap"
+        PHASE2           = "phase2"
+        IDENTITY         = "identity"
+        ANON_IDENTITY    = "anonymous_identity"
+        PASSWORD         = "password"
+        SUBJECT_MATCH    = "subject_match"
+        ALTSUBJECT_MATCH = "altsubject_match"
+        DOM_SUFFIX_MATCH = "domain_suffix_match"
+        CLIENT_CERT      = "client_cert"
+        CA_CERT          = "ca_cert"
+        ENGINE           = "engine"
+        ENGINE_ID        = "engine_id"
+        PRIVATE_KEY_ID   = "key_id"
+        REALM            = "realm"
+        PLMN             = "plmn"
+        FQDN             = "FQDN"
+        FRIENDLY_NAME    = "providerFriendlyName"
+        ROAMING_IDS      = "roamingConsortiumIds"
+    # End of Macros for EAP
+
+    # Macros for wifi p2p.
+    WIFI_P2P_SERVICE_TYPE_ALL = 0
+    WIFI_P2P_SERVICE_TYPE_BONJOUR = 1
+    WIFI_P2P_SERVICE_TYPE_UPNP = 2
+    WIFI_P2P_SERVICE_TYPE_VENDOR_SPECIFIC = 255
+
+    class ScanResult:
+        CHANNEL_WIDTH_20MHZ = 0
+        CHANNEL_WIDTH_40MHZ = 1
+        CHANNEL_WIDTH_80MHZ = 2
+        CHANNEL_WIDTH_160MHZ = 3
+        CHANNEL_WIDTH_80MHZ_PLUS_MHZ = 4
+
+    # Macros for wifi rtt.
+    class RttType(IntEnum):
+        TYPE_ONE_SIDED      = 1
+        TYPE_TWO_SIDED      = 2
+
+    class RttPeerType(IntEnum):
+        PEER_TYPE_AP        = 1
+        PEER_TYPE_STA       = 2 # Requires NAN.
+        PEER_P2P_GO         = 3
+        PEER_P2P_CLIENT     = 4
+        PEER_NAN            = 5
+
+    class RttPreamble(IntEnum):
+        PREAMBLE_LEGACY  = 0x01
+        PREAMBLE_HT      = 0x02
+        PREAMBLE_VHT     = 0x04
+
+    class RttBW(IntEnum):
+        BW_5_SUPPORT   = 0x01
+        BW_10_SUPPORT  = 0x02
+        BW_20_SUPPORT  = 0x04
+        BW_40_SUPPORT  = 0x08
+        BW_80_SUPPORT  = 0x10
+        BW_160_SUPPORT = 0x20
+
+    class Rtt(IntEnum):
+        STATUS_SUCCESS                  = 0
+        STATUS_FAILURE                  = 1
+        STATUS_FAIL_NO_RSP              = 2
+        STATUS_FAIL_REJECTED            = 3
+        STATUS_FAIL_NOT_SCHEDULED_YET   = 4
+        STATUS_FAIL_TM_TIMEOUT          = 5
+        STATUS_FAIL_AP_ON_DIFF_CHANNEL  = 6
+        STATUS_FAIL_NO_CAPABILITY       = 7
+        STATUS_ABORTED                  = 8
+        STATUS_FAIL_INVALID_TS          = 9
+        STATUS_FAIL_PROTOCOL            = 10
+        STATUS_FAIL_SCHEDULE            = 11
+        STATUS_FAIL_BUSY_TRY_LATER      = 12
+        STATUS_INVALID_REQ              = 13
+        STATUS_NO_WIFI                  = 14
+        STATUS_FAIL_FTM_PARAM_OVERRIDE  = 15
+
+        REASON_UNSPECIFIED              = -1
+        REASON_NOT_AVAILABLE            = -2
+        REASON_INVALID_LISTENER         = -3
+        REASON_INVALID_REQUEST          = -4
+
+    class RttParam:
+        device_type = "deviceType"
+        request_type = "requestType"
+        BSSID = "bssid"
+        channel_width = "channelWidth"
+        frequency = "frequency"
+        center_freq0 = "centerFreq0"
+        center_freq1 = "centerFreq1"
+        number_burst = "numberBurst"
+        interval = "interval"
+        num_samples_per_burst = "numSamplesPerBurst"
+        num_retries_per_measurement_frame = "numRetriesPerMeasurementFrame"
+        num_retries_per_FTMR = "numRetriesPerFTMR"
+        lci_request = "LCIRequest"
+        lcr_request = "LCRRequest"
+        burst_timeout = "burstTimeout"
+        preamble = "preamble"
+        bandwidth = "bandwidth"
+        margin = "margin"
+
+    RTT_MARGIN_OF_ERROR = {
+        RttBW.BW_80_SUPPORT: 2,
+        RttBW.BW_40_SUPPORT: 5,
+        RttBW.BW_20_SUPPORT: 5
+    }
+
+    # Macros as specified in the WifiScanner code.
+    WIFI_BAND_UNSPECIFIED = 0      # not specified
+    WIFI_BAND_24_GHZ = 1           # 2.4 GHz band
+    WIFI_BAND_5_GHZ = 2            # 5 GHz band without DFS channels
+    WIFI_BAND_5_GHZ_DFS_ONLY  = 4  # 5 GHz band with DFS channels
+    WIFI_BAND_5_GHZ_WITH_DFS  = 6  # 5 GHz band with DFS channels
+    WIFI_BAND_BOTH = 3             # both bands without DFS channels
+    WIFI_BAND_BOTH_WITH_DFS = 7    # both bands with DFS channels
+
+    REPORT_EVENT_AFTER_BUFFER_FULL = 0
+    REPORT_EVENT_AFTER_EACH_SCAN = 1
+    REPORT_EVENT_FULL_SCAN_RESULT = 2
+
+    # US Wifi frequencies
+    ALL_2G_FREQUENCIES = [2412, 2417, 2422, 2427, 2432, 2437, 2442, 2447, 2452,
+                          2457, 2462]
+    DFS_5G_FREQUENCIES = [5260, 5280, 5300, 5320, 5500, 5520, 5540, 5560, 5580,
+                          5600, 5620, 5640, 5660, 5680, 5700, 5720]
+    NONE_DFS_5G_FREQUENCIES = [5180, 5200, 5220, 5240, 5745, 5765, 5785, 5805,
+                               5825]
+    ALL_5G_FREQUENCIES = DFS_5G_FREQUENCIES + NONE_DFS_5G_FREQUENCIES
+
+    band_to_frequencies = {
+      WIFI_BAND_24_GHZ: ALL_2G_FREQUENCIES,
+      WIFI_BAND_5_GHZ: NONE_DFS_5G_FREQUENCIES,
+      WIFI_BAND_5_GHZ_DFS_ONLY: DFS_5G_FREQUENCIES,
+      WIFI_BAND_5_GHZ_WITH_DFS: ALL_5G_FREQUENCIES,
+      WIFI_BAND_BOTH: ALL_2G_FREQUENCIES + NONE_DFS_5G_FREQUENCIES,
+      WIFI_BAND_BOTH_WITH_DFS: ALL_5G_FREQUENCIES + ALL_2G_FREQUENCIES
+    }
+
+    # All Wifi frequencies to channels lookup.
+    freq_to_channel = {
+        2412: 1,
+        2417: 2,
+        2422: 3,
+        2427: 4,
+        2432: 5,
+        2437: 6,
+        2442: 7,
+        2447: 8,
+        2452: 9,
+        2457: 10,
+        2462: 11,
+        2467: 12,
+        2472: 13,
+        2484: 14,
+        4915: 183,
+        4920: 184,
+        4925: 185,
+        4935: 187,
+        4940: 188,
+        4945: 189,
+        4960: 192,
+        4980: 196,
+        5035: 7,
+        5040: 8,
+        5045: 9,
+        5055: 11,
+        5060: 12,
+        5080: 16,
+        5170: 34,
+        5180: 36,
+        5190: 38,
+        5200: 40,
+        5210: 42,
+        5220: 44,
+        5230: 46,
+        5240: 48,
+        5260: 52,
+        5280: 56,
+        5300: 60,
+        5320: 64,
+        5500: 100,
+        5520: 104,
+        5540: 108,
+        5560: 112,
+        5580: 116,
+        5600: 120,
+        5620: 124,
+        5640: 128,
+        5660: 132,
+        5680: 136,
+        5700: 140,
+        5745: 149,
+        5765: 153,
+        5785: 157,
+        5805: 161,
+        5825: 165,
+    }
+
+    # All Wifi channels to frequencies lookup.
+    channel_2G_to_freq = {
+        1: 2412,
+        2: 2417,
+        3: 2422,
+        4: 2427,
+        5: 2432,
+        6: 2437,
+        7: 2442,
+        8: 2447,
+        9: 2452,
+        10: 2457,
+        11: 2462,
+        12: 2467,
+        13: 2472,
+        14: 2484
+    }
+
+    channel_5G_to_freq = {
+        183: 4915,
+        184: 4920,
+        185: 4925,
+        187: 4935,
+        188: 4940,
+        189: 4945,
+        192: 4960,
+        196: 4980,
+        7: 5035,
+        8: 5040,
+        9: 5045,
+        11: 5055,
+        12: 5060,
+        16: 5080,
+        34: 5170,
+        36: 5180,
+        38: 5190,
+        40: 5200,
+        42: 5210,
+        44: 5220,
+        46: 5230,
+        48: 5240,
+        52: 5260,
+        56: 5280,
+        60: 5300,
+        64: 5320,
+        100: 5500,
+        104: 5520,
+        108: 5540,
+        112: 5560,
+        116: 5580,
+        120: 5600,
+        124: 5620,
+        128: 5640,
+        132: 5660,
+        136: 5680,
+        140: 5700,
+        149: 5745,
+        153: 5765,
+        157: 5785,
+        161: 5805,
+        165: 5825
+    }
+
+class WifiEventNames:
+    WIFI_CONNECTED = "WifiNetworkConnected"
+    SUPPLICANT_CON_CHANGED = "SupplicantConnectionChanged"
+    WIFI_FORGET_NW_SUCCESS = "WifiManagerForgetNetworkOnSuccess"
+
+class WifiTestUtilsError(Exception):
+    pass
+
+class WifiChannelBase:
+    ALL_2G_FREQUENCIES = []
+    DFS_5G_FREQUENCIES = []
+    NONE_DFS_5G_FREQUENCIES = []
+    ALL_5G_FREQUENCIES = DFS_5G_FREQUENCIES + NONE_DFS_5G_FREQUENCIES
+    MIX_CHANNEL_SCAN = []
+
+    def band_to_freq(self, band):
+        _band_to_frequencies = {
+            WifiEnums.WIFI_BAND_24_GHZ: self.ALL_2G_FREQUENCIES,
+            WifiEnums.WIFI_BAND_5_GHZ: self.NONE_DFS_5G_FREQUENCIES,
+            WifiEnums.WIFI_BAND_5_GHZ_DFS_ONLY: self.DFS_5G_FREQUENCIES,
+            WifiEnums.WIFI_BAND_5_GHZ_WITH_DFS: self.ALL_5G_FREQUENCIES,
+            WifiEnums.WIFI_BAND_BOTH: self.ALL_2G_FREQUENCIES + self.NONE_DFS_5G_FREQUENCIES,
+            WifiEnums.WIFI_BAND_BOTH_WITH_DFS: self.ALL_5G_FREQUENCIES + self.ALL_2G_FREQUENCIES
+        }
+        return _band_to_frequencies[band]
+
+class WifiChannelUS(WifiChannelBase):
+    # US Wifi frequencies
+    ALL_2G_FREQUENCIES = [2412, 2417, 2422, 2427, 2432, 2437, 2442, 2447, 2452,
+                          2457, 2462]
+    NONE_DFS_5G_FREQUENCIES = [5180, 5200, 5220, 5240, 5745, 5765, 5785, 5805,
+                               5825]
+    MIX_CHANNEL_SCAN = [2412, 2437, 2462, 5180, 5200, 5280, 5260, 5300,5500, 5320,
+                        5520, 5560, 5700, 5745, 5805]
+
+    def __init__(self, model=None):
+        if model and trim_model_name(model) in K_DEVICES:
+            self.DFS_5G_FREQUENCIES = []
+            self.ALL_5G_FREQUENCIES = self.NONE_DFS_5G_FREQUENCIES
+            self.MIX_CHANNEL_SCAN = [2412, 2437, 2462, 5180, 5200, 5240, 5745, 5765]
+        elif model and trim_model_name(model) in L_DEVICES:
+            self.DFS_5G_FREQUENCIES = [5260, 5280, 5300, 5320, 5500, 5520,
+                                       5540, 5560, 5580, 5660, 5680, 5700]
+            self.ALL_5G_FREQUENCIES = self.DFS_5G_FREQUENCIES + self.NONE_DFS_5G_FREQUENCIES
+        elif model and trim_model_name(model) in L_TAP_DEVICES:
+            self.DFS_5G_FREQUENCIES = [5260, 5280, 5300, 5320, 5500, 5520,
+                                       5540, 5560, 5580, 5660, 5680, 5700, 5720]
+            self.ALL_5G_FREQUENCIES = self.DFS_5G_FREQUENCIES + self.NONE_DFS_5G_FREQUENCIES
+        elif model and trim_model_name(model) in M_DEVICES:
+            self.DFS_5G_FREQUENCIES = [5260, 5280, 5300, 5320, 5500, 5520, 5540, 5560,5580,
+                                       5600, 5620, 5640, 5660, 5680, 5700]
+            self.ALL_5G_FREQUENCIES = self.DFS_5G_FREQUENCIES + self.NONE_DFS_5G_FREQUENCIES
+        else:
+            self.DFS_5G_FREQUENCIES = [5260, 5280, 5300, 5320, 5500, 5520, 5540, 5560,5580,
+                                       5600, 5620, 5640, 5660, 5680, 5700, 5720]
+            self.ALL_5G_FREQUENCIES = self.DFS_5G_FREQUENCIES + self.NONE_DFS_5G_FREQUENCIES
+
+def match_networks(target_params, networks):
+    """Finds the WiFi networks that match a given set of parameters in a list
+    of WiFi networks.
+
+    To be considered a match, a network needs to have all the target parameters
+    and the values of those parameters need to equal to those of the target
+    parameters.
+
+    Args:
+        target_params: The target parameters to match networks against.
+        networks: A list of dict objects representing WiFi networks.
+
+    Returns:
+        The networks that match the target parameters.
+    """
+    results = []
+    for n in networks:
+        if set(target_params.items()) <= set(n.items()):
+            results.append(n)
+    return results
+
+def wifi_toggle_state(droid, ed, new_state=None):
+  """Toggles the state of wifi.
+
+  Args:
+    droid: Sl4a session to use.
+    ed: event_dispatcher associated with the sl4a session.
+    new_state: Wifi state to set to. If None, opposite of the current state.
+
+  Returns:
+    True if the toggle was successful, False otherwise.
+  """
+  # Check if the new_state is already achieved, so we don't wait for the
+  # state change event by mistake.
+  if new_state == droid.wifiCheckState():
+    return True
+  droid.wifiStartTrackingStateChange()
+  log.info("Setting wifi state to {}".format(new_state))
+  droid.wifiToggleState(new_state)
+  try:
+    event = ed.pop_event(WifiEventNames.SUPPLICANT_CON_CHANGED, SHORT_TIMEOUT)
+    return event['data']['Connected'] == new_state
+  except Empty:
+    # Supplicant connection event is not always reliable. We double check here
+    # and call it a success as long as the new state equals the expected state.
+    return new_state == droid.wifiCheckState()
+  finally:
+    droid.wifiStopTrackingStateChange()
+
+def reset_droid_wifi(droid, ed):
+  """Disconnects and removes all configured Wifi networks on an android device.
+
+  Args:
+    droid: Sl4a session to use.
+    ed: Event dispatcher instance associated with the sl4a session.
+
+  Raises:
+    WIFIUTILError if forget network operation failed.
+  """
+  droid.wifiToggleState(True)
+  networks = droid.wifiGetConfiguredNetworks()
+  if not networks:
+    return
+  for n in networks:
+    droid.wifiForgetNetwork(n['networkId'])
+    try:
+      event = ed.pop_event(WifiEventNames.WIFI_FORGET_NW_SUCCESS,
+        SHORT_TIMEOUT)
+    except Empty:
+      raise WifiTestUtilsError("Failed to remove network {}.".format(n))
+
+def wifi_forget_network(ad, net_ssid):
+  """Remove configured Wifi network on an android device.
+
+  Args:
+    ad: android_device object for forget network.
+    net_ssid: ssid of network to be forget
+
+  Raises:
+    WIFIUTILError if forget network operation failed.
+  """
+  droid, ed = ad.droid, ad.ed
+  droid.wifiToggleState(True)
+  networks = droid.wifiGetConfiguredNetworks()
+  if not networks:
+    return
+  for n in networks:
+    if net_ssid in n[WifiEnums.SSID_KEY]:
+      droid.wifiForgetNetwork(n['networkId'])
+      try:
+        event = ed.pop_event(WifiEventNames.WIFI_FORGET_NW_SUCCESS,
+            SHORT_TIMEOUT)
+      except Empty:
+        raise WifiTestUtilsError("Failed to remove network {}.".format(n))
+
+def wifi_test_device_init(ad):
+    """Initializes an android device for wifi testing.
+
+    0. Make sure SL4A connection is established on the android device.
+    1. Disable location service's WiFi scan.
+    2. Turn WiFi on.
+    3. Clear all saved networks.
+    4. Set country code to US.
+    5. Enable WiFi verbose logging.
+    6. Sync device time with computer time.
+    7. Turn off cellular data.
+    """
+    require_sl4a((ad,))
+    ad.droid.wifiScannerToggleAlwaysAvailable(False)
+    msg = "Failed to turn off location service's scan."
+    assert not ad.droid.wifiScannerIsAlwaysAvailable(), msg
+    msg = "Failed to turn WiFi on %s" % ad.serial
+    assert wifi_toggle_state(ad.droid, ad.ed, True), msg
+    reset_droid_wifi(ad.droid, ad.ed)
+    msg = "Failed to clear configured networks."
+    assert not ad.droid.wifiGetConfiguredNetworks(), msg
+    ad.droid.wifiEnableVerboseLogging(1)
+    msg = "Failed to enable WiFi verbose loggin."
+    assert ad.droid.wifiGetVerboseLoggingLevel() == 1, msg
+    ad.droid.wifiScannerToggleAlwaysAvailable(False)
+    # We don't verify the following settings since they are not critical.
+    sync_device_time(ad)
+    ad.droid.toggleDataConnection(False)
+    # TODO(angli): need to verify the country code was actually set. No generic
+    # way to check right now.
+    ad.adb.shell("halutil -country %s" % WifiEnums.CountryCode.US)
+
+def sort_wifi_scan_results(results, key="level"):
+  """Sort wifi scan results by key.
+
+  Args:
+    results: A list of results to sort.
+    key: Name of the field to sort the results by.
+
+  Returns:
+    A list of results in sorted order.
+  """
+  return sorted(results, lambda d: (key not in d, d[key]))
+
+def start_wifi_connection_scan(droid, ed):
+    """Starts a wifi connection scan and wait for results to become available.
+
+    Args:
+        droid: Sl4a session to use.
+        ed: Event dispatcher instance associated with the sl4a session.
+    """
+    droid.wifiStartScan()
+    ed.pop_event("WifiManagerScanResultsAvailable", 60)
+
+def start_wifi_background_scan(ad, scan_setting):
+    """Starts wifi background scan.
+
+    Args:
+        ad: android_device object to initiate connection on.
+        scan_setting: A dict representing the settings of the scan.
+
+    Returns:
+        If scan was started successfully, event data of success event is returned.
+    """
+    droid, ed = ad.droids[0], ad.eds[0]
+    idx = droid.wifiScannerStartBackgroundScan(scan_setting)
+    event = ed.pop_event("WifiScannerScan{}onSuccess".format(idx),
+                         SHORT_TIMEOUT)
+    return event['data']
+
+def start_wifi_tracking_change(droid, ed):
+    """Starts tracking wifi change.
+
+    Args:
+        droid: Sl4a session to use.
+        ed: Event dispatcher instance associated with the sl4a session.
+
+    Returns:
+        If scan was started successfully, event data of success event is returned.
+    """
+    idx = droid.wifiScannerStartTrackingChange()
+    event = ed.pop_event("WifiScannerChange{}onSuccess".format(idx),
+                         SHORT_TIMEOUT)
+    return event['data']
+
+def start_wifi_tethering(ad, ssid, password, band=None):
+    """Starts wifi tethering on an android_device.
+
+    Args:
+        ad: android_device to start wifi tethering on.
+        ssid: The SSID the soft AP should broadcast.
+        password: The password the soft AP should use.
+        band: The band the soft AP should be set on. It should be either
+            WifiEnums.WIFI_CONFIG_APBAND_2G or WifiEnums.WIFI_CONFIG_APBAND_5G.
+
+    Returns:
+        True if soft AP was started successfully, False otherwise.
+    """
+    droid, ed = ad.droid, ad.ed
+    droid.wifiStartTrackingStateChange()
+    config = {
+        WifiEnums.SSID_KEY: ssid
+    }
+    if password:
+        config[WifiEnums.PWD_KEY] = password
+    if band:
+        config[WifiEnums.APBAND_KEY] = band
+    if not droid.wifiSetApEnabled(True, config):
+        return False
+    ed.pop_event("WifiManagerApEnabled", 30)
+    ed.wait_for_event("TetherStateChanged",
+        lambda x : x["data"]["ACTIVE_TETHER"], 30)
+    droid.wifiStopTrackingStateChange()
+    return True
+
+def stop_wifi_tethering(ad):
+    """Stops wifi tethering on an android_device.
+
+    Args:
+        ad: android_device to stop wifi tethering on.
+    """
+    droid, ed = ad.droid, ad.ed
+    droid.wifiStartTrackingStateChange()
+    droid.wifiSetApEnabled(False, None)
+    ed.pop_event("WifiManagerApDisabled", 30)
+    ed.wait_for_event("TetherStateChanged",
+        lambda x : not x["data"]["ACTIVE_TETHER"], 30)
+    droid.wifiStopTrackingStateChange()
+
+def wifi_connect(ad, ssid, pwd=None):
+    """Connects to a wifi network.
+
+    Initiate connection to a wifi network, wait for the "connected" event, then
+    confirm the connected ssid is the one requested. If pwd is not given, treat
+    the ssid as open network.
+
+    Args:
+        ad: android_device object to initiate connection on.
+        ssid: SSID of the wifi network to connect to.
+        pwd: Password of the wifi network.
+
+    Returns:
+        True if connection is successful, False otherwise.
+    """
+    droid, ed = ad.droid, ad.ed
+    droid.wifiStartTrackingStateChange()
+    try:
+        c = {WifiEnums.SSID_KEY: ssid}
+        if pwd:
+            c["password"] = pwd
+        if droid.wifiConnect(c):
+            event = ed.pop_event("WifiNetworkConnected", 120)
+            return event['data'][WifiEnums.SSID_KEY] == ssid
+        return False
+    finally:
+        droid.wifiStopTrackingStateChange()
+
+def start_wifi_single_scan(ad, scan_setting):
+    """Starts wifi single shot scan.
+
+    Args:
+        ad: android_device object to initiate connection on.
+        scan_setting: A dict representing the settings of the scan.
+
+    Returns:
+        If scan was started successfully, event data of success event is returned.
+    """
+    droid, ed = ad.droid, ad.ed
+    idx = droid.wifiScannerStartScan(scan_setting)
+    event = ed.pop_event("WifiScannerScan{}onSuccess".format(idx),
+                         SHORT_TIMEOUT)
+    log.debug("event {}".format(event))
+    return event['data']
+
+def track_connection(ad, network_ssid, check_connection_count):
+    """Track wifi connection to network changes for given number of counts
+
+    Args:
+        ad: android_device object for forget network.
+        network_ssid: network ssid to which connection would be tracked
+        check_connection_count: Integer for maximum number network connection
+            check.
+    Returns:
+
+        True if connection to given network happen, else return False.
+    """
+    droid, ed = ad.droid, ad.ed
+    droid.wifiStartTrackingStateChange()
+    while check_connection_count > 0:
+      connect_network = ed.pop_event("WifiNetworkConnected", 120)
+      log.info("connect_network {}".format(connect_network))
+      if (WifiEnums.SSID_KEY in connect_network['data']
+                            and connect_network['data'][WifiEnums.SSID_KEY]==network_ssid):
+        return True
+      check_connection_count -= 1
+    droid.wifiStopTrackingStateChange()
+    return False
+
+def get_scan_time_and_channels(wifi_chs, scan_setting, stime_channel):
+    """Calculate the scan time required based on the band or channels in scan
+    setting
+
+    Args:
+        wifi_chs: Object of channels supported
+        scan_setting: scan setting used for start scan
+        stime_channel: scan time per channel
+
+    Returns:
+        scan_time: time required for completing a scan
+        scan_channels: channel used for scanning
+    """
+    scan_time = 0
+    scan_channels = []
+    if "band" in scan_setting and "channels" not in scan_setting:
+      scan_channels = wifi_chs.band_to_freq(scan_setting["band"])
+    elif "channels" in scan_setting and "band" not in scan_setting:
+      scan_channels = scan_setting["channels"]
+    scan_time = len(scan_channels) * stime_channel
+    for channel in scan_channels:
+      if channel in WifiEnums.DFS_5G_FREQUENCIES:
+        scan_time += 132 #passive scan time on DFS
+    return scan_time, scan_channels
+
+def start_wifi_track_bssid(ad, track_setting):
+    """Start tracking Bssid for the given settings.
+
+    Args:
+      ad: android_device object.
+      track_setting: Setting for which the bssid tracking should be started
+
+    Returns:
+      If tracking started successfully, event data of success event is returned.
+    """
+    droid, ed = ad.droid, ad.ed
+    idx = droid.wifiScannerStartTrackingBssids(
+        track_setting["bssidInfos"],
+        track_setting["apLostThreshold"]
+        )
+    event = ed.pop_event("WifiScannerBssid{}onSuccess".format(idx),
+                         SHORT_TIMEOUT)
+    return event['data']
+
+def convert_pem_key_to_pkcs8(in_file, out_file):
+    """Converts the key file generated by us to the format required by
+    Android using openssl.
+
+    The input file must have the extension "pem". The output file must
+    have the extension "der".
+
+    Args:
+        in_file: The original key file.
+        out_file: The full path to the converted key file, including
+        filename.
+    """
+    assert in_file.endswith(".pem")
+    assert out_file.endswith(".der")
+    cmd = ("openssl pkcs8 -inform PEM -in {} -outform DER -out {} -nocrypt"
+           " -topk8").format(in_file, out_file)
+    exe_cmd(cmd)
+
+def check_internet_connection(ad, ping_addr):
+    """Validate internet connection by pinging the address provided.
+
+    Args:
+        ad: android_device object.
+        ping_addr: address on internet for pinging.
+
+    Returns:
+        True, if address ping successful
+    """
+    droid, ed = ad.droid, ad.ed
+    ping = droid.httpPing(ping_addr)
+    log.info("Http ping result: {}".format(ping))
+    return ping
+
+#TODO(angli): This can only verify if an actual value is exactly the same.
+# Would be nice to be able to verify an actual value is one of serveral.
+def verify_wifi_connection_info(ad, expected_con):
+    """Verifies that the information of the currently connected wifi network is
+    as expected.
+
+    Args:
+        expected_con: A dict representing expected key-value pairs for wifi
+            connection. e.g. {"SSID": "test_wifi"}
+    """
+    current_con = ad.droid.wifiGetConnectionInfo()
+    log.debug("Current connection: %s" % current_con)
+    for k, expected_v in expected_con.items():
+        msg = "Field %s does not exist in wifi connection info %s." % (k,
+            current_con)
+        assert k in current_con, msg
+        actual_v = current_con[k].lower()
+        msg = "Expected %s to be %s, actual %s is %s." % (k, expected_v, k,
+            actual_v)
+        expected_v = expected_v.lower()
+        assert actual_v == expected_v, msg
+
+def eap_connect(config, ad, validate_con=True, ping_addr=DEFAULT_PING_ADDR):
+    """Connects to an enterprise network and verify connection.
+
+    This logic expect the enterprise network to have Internet access.
+
+    Args:
+        config: A dict representing a wifi enterprise configuration.
+        ad: The android_device to operate with.
+        validate_con: If True, validate Internet connection after connecting to
+            the network.
+
+    Returns:
+        True if the connection is successful and Internet access works.
+    """
+    droid, ed = ad.droid, ad.ed
+    start_wifi_connection_scan(droid, ed)
+    expect_ssid = None
+    if WifiEnums.SSID_KEY in config:
+        expect_ssid = config[WifiEnums.SSID_KEY]
+        log.info("Connecting to %s." % expect_ssid)
+    else:
+        log.info("Connecting.")
+    log.debug(pprint.pformat(config, indent=4))
+    droid.wifiEnterpriseConnect(config)
+    try:
+        event = ed.pop_event("WifiManagerEnterpriseConnectOnSuccess", 30)
+        log.info("Started connecting...")
+        event = ed.pop_event(WifiEventNames.WIFI_CONNECTED, 60)
+    except Empty:
+        log.info("Failed to connect.")
+        return False
+    log.debug(event)
+    if expect_ssid:
+        actual_ssid = event["data"][WifiEnums.SSID_KEY]
+        msg = "Expected SSID {}, got {}".format(expect_ssid, actual_ssid)
+        if expect_ssid != actual_ssid:
+            log.error(msg)
+            return False
+        log.info("Connected to %s." % expect_ssid)
+    else:
+        log.info("Connected successfully.")
+    if validate_con:
+        log.info("Checking Internet access.")
+        # Wait for data connection to stabilize.
+        time.sleep(4)
+        ping = droid.httpPing(ping_addr)
+        log.info("Http ping result: {}".format(ping))
+        if not ping:
+            log.error("No Internet access.")
+            return False
+    return True
+
+def expand_enterprise_config_by_phase2(config):
+    """Take an enterprise config and generate a list of configs, each with
+    a different phase2 auth type.
+
+    Args:
+        config: A dict representing enterprise config.
+
+    Returns
+        A list of enterprise configs.
+    """
+    results = []
+    for phase2_type in WifiEnums.EapPhase2:
+        # Skip a special case for passpoint TTLS.
+        if (WifiEnums.Enterprise.FQDN in config and
+            phase2_type == WifiEnums.EapPhase2.GTC):
+            continue
+        c = dict(config)
+        c[WifiEnums.Enterprise.PHASE2] = phase2_type
+        results.append(c)
+    return results
diff --git a/acts/framework/acts/utils.py b/acts/framework/acts/utils.py
new file mode 100755
index 0000000..aed0e05
--- /dev/null
+++ b/acts/framework/acts/utils.py
@@ -0,0 +1,496 @@
+#!/usr/bin/python3.4
+#
+#   Copyright 2014 - 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 base64
+import concurrent.futures
+import datetime
+import json
+import functools
+import os
+import random
+import re
+import signal
+import string
+import subprocess
+import time
+import traceback
+
+class NexusModelNames:
+    # TODO(angli): This will be fixed later by angli.
+    ONE = 'sprout'
+    N5 = 'hammerhead'
+    N5v2 = 'bullhead'
+    N6 = 'shamu'
+    N6v2 = 'angler'
+
+ascii_letters_and_digits = string.ascii_letters + string.digits
+valid_filename_chars = "-_." + ascii_letters_and_digits
+
+models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg",
+    "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu", "ryu")
+
+manufacture_name_to_model = {
+    "flo": "razor",
+    "flo_lte": "razorg",
+    "flounder": "volantis",
+    "flounder_lte": "volantisg",
+    "dragon": "ryu"
+}
+
+GMT_to_olson = {
+    "GMT-9":  "America/Anchorage",
+    "GMT-8":  "US/Pacific",
+    "GMT-7":  "US/Mountain",
+    "GMT-6":  "US/Central",
+    "GMT-5":  "US/Eastern",
+    "GMT-4":  "America/Barbados",
+    "GMT-3":  "America/Buenos_Aires",
+    "GMT-2":  "Atlantic/South_Georgia",
+    "GMT-1":  "Atlantic/Azores",
+    "GMT+0":  "Africa/Casablanca",
+    "GMT+1":  "Europe/Amsterdam",
+    "GMT+2":  "Europe/Athens",
+    "GMT+3":  "Europe/Moscow",
+    "GMT+4":  "Asia/Baku",
+    "GMT+5":  "Asia/Oral",
+    "GMT+6":  "Asia/Almaty",
+    "GMT+7":  "Asia/Bangkok",
+    "GMT+8":  "Asia/Hong_Kong",
+    "GMT+9":  "Asia/Tokyo",
+    "GMT+10": "Pacific/Guam",
+    "GMT+11": "Pacific/Noumea",
+    "GMT+12": "Pacific/Fiji",
+    "GMT+13": "Pacific/Tongatapu",
+    "GMT-11": "Pacific/Midway",
+    "GMT-10": "Pacific/Honolulu"
+}
+
+def abs_path(path):
+    """Resolve the '.' and '~' in a path to get the absolute path.
+
+    Args:
+        path: The path to expand.
+
+    Returns:
+        The absolute path of the input path.
+    """
+    return os.path.abspath(os.path.expanduser(path))
+
+def create_dir(path):
+    """Creates a directory if it does not exist already.
+
+    Args:
+        path: The path of the directory to create.
+    """
+    full_path = abs_path(path)
+    if not os.path.exists(full_path):
+        os.makedirs(full_path)
+
+def get_current_epoch_time():
+    """Current epoch time in milliseconds.
+
+    Returns:
+        An integer representing the current epoch time in milliseconds.
+    """
+    return int(round(time.time() * 1000))
+
+def get_current_human_time():
+    """Returns the current time in human readable format.
+
+    Returns:
+        The current time stamp in Month-Day-Year Hour:Min:Sec format.
+    """
+    return time.strftime("%m-%d-%Y %H:%M:%S ")
+
+def epoch_to_human_time(epoch_time):
+    """Converts an epoch timestamp to human readable time.
+
+    This essentially converts an output of get_current_epoch_time to an output
+    of get_current_human_time
+
+    Args:
+        epoch_time: An integer representing an epoch timestamp in milliseconds.
+
+    Returns:
+        A time string representing the input time.
+        None if input param is invalid.
+    """
+    if isinstance(epoch_time, int):
+        try:
+            d = datetime.datetime.fromtimestamp(epoch_time / 1000)
+            return d.strftime("%m-%d-%Y %H:%M:%S ")
+        except ValueError:
+            return None
+
+def get_timezone_olson_id():
+    """Return the Olson ID of the local (non-DST) timezone.
+
+    Returns:
+        A string representing one of the Olson IDs of the local (non-DST)
+        timezone.
+    """
+    tzoffset = int(time.timezone/3600)
+    gmt = None
+    if tzoffset <= 0:
+        gmt = "GMT+{}".format(-tzoffset)
+    else:
+        gmt = "GMT-{}".format(tzoffset)
+    return GMT_to_olson[gmt]
+
+def find_files(paths, file_predicate):
+    """Locate files whose names and extensions match the given predicate in
+    the specified directories.
+
+    Args:
+        paths: A list of directory paths where to find the files.
+        file_predicate: A function that returns True if the file name and
+          extension are desired.
+
+    Returns:
+        A list of files that match the predicate.
+    """
+    file_list = []
+    for path in paths:
+        p = abs_path(path)
+        for dirPath, subdirList, fileList in os.walk(p):
+            for fname in fileList:
+                name, ext = os.path.splitext(fname)
+                if file_predicate(name, ext):
+                  file_list.append((dirPath, name, ext))
+    return file_list
+
+def load_config(file_full_path):
+    """Loads a JSON config file.
+
+    Returns:
+        A JSON object.
+    """
+    with open(file_full_path, 'r') as f:
+        conf = json.load(f)
+        return conf
+
+def load_file_to_base64_str(f_path):
+    """Loads the content of a file into a base64 string.
+
+    Args:
+        f_path: full path to the file including the file name.
+
+    Returns:
+        A base64 string representing the content of the file in utf-8 encoding.
+    """
+    path = abs_path(f_path)
+    with open(path, 'rb') as f:
+      f_bytes = f.read()
+      base64_str = base64.b64encode(f_bytes).decode("utf-8")
+      return base64_str
+
+def find_field(item_list, cond, comparator, target_field):
+    """Finds the value of a field in a dict object that satisfies certain
+    conditions.
+
+    Args:
+        item_list: A list of dict objects.
+        cond: A param that defines the condition.
+        comparator: A function that checks if an dict satisfies the condition.
+        target_field: Name of the field whose value to be returned if an item
+            satisfies the condition.
+
+    Returns:
+        Target value or None if no item satisfies the condition.
+    """
+    for item in item_list:
+       if comparator(item, cond) and target_field in item:
+          return item[target_field]
+    return None
+
+def rand_ascii_str(length):
+    """Generates a random string of specified length, composed of ascii letters
+    and digits.
+
+    Args:
+        length: The number of characters in the string.
+
+    Returns:
+        The random string generated.
+    """
+    letters = [random.choice(ascii_letters_and_digits) for i in range(length)]
+    return ''.join(letters)
+
+# Thead/Process related functions.
+def concurrent_exec(func, param_list):
+    """Executes a function with different parameters pseudo-concurrently.
+
+    This is basically a map function. Each element (should be an iterable) in
+    the param_list is unpacked and passed into the function. Due to Python's
+    GIL, there's no true concurrency. This is suited for IO-bound tasks.
+
+    Args:
+        func: The function that parforms a task.
+        param_list: A list of iterables, each being a set of params to be
+            passed into the function.
+    """
+    with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor:
+        # Start the load operations and mark each future with its params
+        future_to_params = {executor.submit(func, *p): p for p in param_list}
+        for future in concurrent.futures.as_completed(future_to_params):
+            params = future_to_params[future]
+            try:
+                data = future.result()
+            except Exception as exc:
+                print("{} generated an exception: {}".format(params,
+                    traceback.format_exc()))
+
+def exe_cmd(*cmds):
+    """Executes commands in a new shell.
+
+    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 = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
+    (out, err) = proc.communicate()
+    if not err:
+        return out
+    raise Exception(err)
+
+def require_sl4a(android_devices):
+    """Makes sure sl4a connection is established on the given AndroidDevice
+    objects.
+
+    Args:
+        android_devices: A list of AndroidDevice objects.
+
+    Raises:
+        AssertionError is raised if any given android device does not have SL4A
+        connection established.
+    """
+    for ad in android_devices:
+        msg = "SL4A connection not established properly on %s." % ad.serial
+        assert ad.droid, msg
+
+def start_standing_subprocess(cmd):
+    """Starts a non-blocking subprocess that is going to continue running after
+    this function returns.
+
+    A subprocess group is actually started by setting sid, so we can kill all
+    the processes spun out from the subprocess when stopping it. This is
+    necessary in case users pass in pipe commands.
+
+    Args:
+        cmd: Command to start the subprocess with.
+
+    Returns:
+        The subprocess that got started.
+    """
+    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True,
+                preexec_fn=os.setpgrp)
+    return p
+
+def stop_standing_subprocess(p):
+    """Stops a subprocess started by start_standing_subprocess.
+
+    Catches and ignores the PermissionError which only happens on Macs.
+
+    Args:
+        p: Subprocess to terminate.
+    """
+    try:
+        os.killpg(p.pid, signal.SIGTERM)
+    except PermissionError:
+        pass
+
+def sync_device_time(ad):
+    """Sync the time of an android device with the current system time.
+
+    Both epoch time and the timezone will be synced.
+
+    Args:
+        ad: The android device to sync time on.
+    """
+    droid = ad.droid
+    droid.setTimeZone(get_timezone_olson_id())
+    droid.setTime(get_current_epoch_time())
+
+# Timeout decorator block
+class TimeoutError(Exception):
+    """Exception for timeout decorator related errors.
+    """
+    pass
+
+def _timeout_handler(signum, frame):
+    """Handler function used by signal to terminate a timed out function.
+    """
+    raise TimeoutError()
+
+def timeout(sec):
+    """A decorator used to add time out check to a function.
+
+    Args:
+        sec: Number of seconds to wait before the function times out.
+            No timeout if set to 0
+
+    Returns:
+        What the decorated function returns.
+
+    Raises:
+        TimeoutError is raised when time out happens.
+    """
+    def decorator(func):
+        @functools.wraps(func)
+        def wrapper(*args, **kwargs):
+            if sec:
+                signal.signal(signal.SIGALRM, _timeout_handler)
+                signal.alarm(sec)
+            try:
+                return func(*args, **kwargs)
+            except TimeoutError:
+                raise TimeoutError(("Function {} timed out after {} "
+                  "seconds.").format(func.__name__, sec))
+            finally:
+                signal.alarm(0)
+        return wrapper
+    return decorator
+
+def trim_model_name(model):
+    """Trim any prefix and postfix and return the android designation of the
+    model name.
+
+    e.g. "m_shamu" will be trimmed to "shamu".
+
+    Args:
+        model: model name to be trimmed.
+
+    Returns
+        Trimmed model name if one of the known model names is found.
+        None otherwise.
+    """
+    # Directly look up first.
+    if model in models:
+        return model
+    if model in manufacture_name_to_model:
+        return manufacture_name_to_model[model]
+    # If not found, try trimming off prefix/postfix and look up again.
+    tokens = re.split("_|-", model)
+    for t in tokens:
+        if t in models:
+            return t
+        if t in manufacture_name_to_model:
+            return manufacture_name_to_model[t]
+    return None
+
+def enable_doze(ad):
+    """Force the device into doze mode.
+
+    Args:
+        ad: android device object.
+
+    Returns:
+        True if device is in doze mode.
+        False otherwise.
+    """
+    ad.adb.shell("dumpsys battery unplug")
+    ad.adb.shell("dumpsys deviceidle enable")
+    if (ad.adb.shell("dumpsys deviceidle force-idle") !=
+        b'Now forced in to idle mode\r\n'):
+        return False
+    ad.droid.goToSleepNow()
+    time.sleep(5)
+    adb_shell_result = ad.adb.shell("dumpsys deviceidle step")
+    if adb_shell_result not in [b'Stepped to: IDLE_MAINTENANCE\r\n',
+                                b'Stepped to: IDLE\r\n']:
+        info = ("dumpsys deviceidle step: {}dumpsys battery: {}"
+                "dumpsys deviceidle: {}".
+                format(adb_shell_result.decode('utf-8'),
+                       ad.adb.shell("dumpsys battery").decode('utf-8'),
+                       ad.adb.shell("dumpsys deviceidle").decode('utf-8')))
+        print(info)
+        return False
+    return True
+
+def disable_doze(ad):
+    """Force the device not in doze mode.
+
+    Args:
+        ad: android device object.
+
+    Returns:
+        True if device is not in doze mode.
+        False otherwise.
+    """
+    ad.adb.shell("dumpsys deviceidle disable")
+    ad.adb.shell("dumpsys battery reset")
+    adb_shell_result = ad.adb.shell("dumpsys deviceidle step")
+    if ( adb_shell_result != b'Stepped to: ACTIVE\r\n'):
+        info = ("dumpsys deviceidle step: {}dumpsys battery: {}"
+                "dumpsys deviceidle: {}".
+                format(adb_shell_result.decode('utf-8'),
+                       ad.adb.shell("dumpsys battery").decode('utf-8'),
+                       ad.adb.shell("dumpsys deviceidle").decode('utf-8')))
+        print(info)
+        return False
+    return True
+
+def set_ambient_display(ad, new_state):
+    """Set "Ambient Display" in Settings->Display
+
+    Args:
+        ad: android device object.
+        new_state: new state for "Ambient Display". True or False.
+    """
+    ad.adb.shell("settings put secure doze_enabled {}".
+        format(1 if new_state else 0))
+
+def set_adaptive_brightness(ad, new_state):
+    """Set "Adaptive Brightness" in Settings->Display
+
+    Args:
+        ad: android device object.
+        new_state: new state for "Adaptive Brightness". True or False.
+    """
+    ad.adb.shell("settings put system screen_brightness_mode {}".
+        format(1 if new_state else 0))
+
+def set_auto_rotate(ad, new_state):
+    """Set "Auto-rotate" in QuickSetting
+
+    Args:
+        ad: android device object.
+        new_state: new state for "Auto-rotate". True or False.
+    """
+    ad.adb.shell("settings put system accelerometer_rotation {}".
+        format(1 if new_state else 0))
+
+def set_location_service(ad, new_state):
+    """Set Location service on/off in Settings->Location
+
+    Args:
+        ad: android device object.
+        new_state: new state for "Location service".
+            If new_state is False, turn off location service.
+            If new_state if True, set location service to "High accuracy".
+    """
+    if new_state:
+        ad.adb.shell("settings put secure location_providers_allowed +gps")
+        ad.adb.shell("settings put secure location_providers_allowed +network")
+    else:
+        ad.adb.shell("settings put secure location_providers_allowed -gps")
+        ad.adb.shell("settings put secure location_providers_allowed -network")
diff --git a/acts/framework/sample_config.json b/acts/framework/sample_config.json
new file mode 100644
index 0000000..a0244da
--- /dev/null
+++ b/acts/framework/sample_config.json
@@ -0,0 +1,23 @@
+{   "_description": "This is an example skeleton test configuration file.",
+    "testbed":
+    [
+        {
+            "_description": "A testbed with two android devices.",
+            "name": "Enterprise-D",
+            "AndroidDevice": ["<serial>", "<serial>"]
+        },
+        {
+            "_description": "A testbed with two android devices.",
+            "name": "Enterprise-E",
+            "AndroidDevice": [{"serial": "<serial>", "label": "caller"},
+                              {"serial": "<serial>", "label": "callee", "whatever": "anything"}]
+        },
+        {
+            "_description": "Another testbed with one android device.",
+            "name": "SampleTestBed"
+        }
+    ],
+    "logpath": "/tmp/logs",
+    "testpaths": ["./test_cases"],
+    "custom_param1": {"favorite_food": "Icecream!"}
+}
diff --git a/acts/framework/setup.py b/acts/framework/setup.py
new file mode 100755
index 0000000..029b26e
--- /dev/null
+++ b/acts/framework/setup.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python3.4
+
+from setuptools import setup
+from setuptools import find_packages
+
+setup(
+    name='acts',
+    version = '0.9',
+    description = 'Android Comms Test Suite',
+    license = 'Apache2.0',
+    packages = find_packages(),
+    include_package_data = False,
+    install_requires = [
+        'pyserial',
+    ],
+    scripts = ['acts/act.py','acts/monsoon.py'],
+    url = "http://www.android.com/"
+)