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/"
+)