Merge "Add a common controller for Headsets."
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 053c580..b76b797 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,32 +1,7 @@
 [Hook Scripts]
-acts_adb_test = ./acts/framework/tests/acts_adb_test.py
-acts_android_device_test = ./acts/framework/tests/acts_android_device_test.py
-acts_asserts_test = ./acts/framework/tests/acts_asserts_test.py
-acts_base_class_test = ./acts/framework/tests/acts_base_class_test.py
-acts_config_test = ./acts/framework/tests/config/unittest_bundle.py
-acts_context_test = ./acts/framework/tests/acts_context_test.py
-acts_error_test = ./acts/framework/tests/acts_error_test.py
-acts_host_utils_test = ./acts/framework/tests/acts_host_utils_test.py
-acts_import_test_utils_test = ./acts/framework/tests/acts_import_test_utils_test.py
-acts_import_unit_test = ./acts/framework/tests/acts_import_unit_test.py
-acts_job_test = ./acts/framework/tests/acts_job_test.py
-acts_libs_ota_tests = ./acts/framework/tests/libs/ota/unittest_bundle.py
-acts_logger_test = ./acts/framework/tests/acts_logger_test.py
-acts_metrics_test = ./acts/framework/tests/libs/metrics/unittest_bundle.py
-acts_records_test = ./acts/framework/tests/acts_records_test.py
-acts_relay_controller_test = ./acts/framework/tests/acts_relay_controller_test.py
-acts_test_runner_test = ./acts/framework/tests/acts_test_runner_test.py
-acts_unittest_suite = ./acts/framework/tests/acts_unittest_suite.py
-acts_utils_test = ./acts/framework/tests/acts_utils_test.py
-android_lib_unittest_bundle = ./acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py
-event_unittest_bundle = ./acts/framework/tests/event/event_unittest_bundle.py
-instrumentation_unit_test_suite = ./acts/framework/tests/test_utils/instrumentation/unit_test_suite.py
-logging_unittest_bundle = ./acts/framework/tests/libs/logging/logging_unittest_bundle.py
-metrics_tests = ./acts/framework/tests/metrics/unittest_bundle.py
-proc_unittest_bundle = ./acts/framework/tests/libs/proc/proc_unittest_bundle.py
-sl4a_lib_suite = ./acts/framework/tests/controllers/sl4a_lib/test_suite.py
-test_runner_test = ./acts/framework/tests/test_runner_test.py
-version_selector_tests = ./acts/framework/tests/libs/version_selector_test.py
+create_virtualenv = ./tools/create_virtualenv.sh
+acts_unittests = /tmp/acts_preupload_virtualenv/bin/python3 ./acts/tests/meta/ActsUnitTest.py
+destroy_virtualenv = rm -rf /tmp/acts_preupload_virtualenv/
 
 lab_test = ./tools/lab/lab_upload_hooks.py
 proto_check = ./tools/proto_check.py
diff --git a/acts/framework/acts/base_test.py b/acts/framework/acts/base_test.py
index 0e250f4..4e3c4de 100755
--- a/acts/framework/acts/base_test.py
+++ b/acts/framework/acts/base_test.py
@@ -198,16 +198,6 @@
             class_name=self.__class__.__name__,
             controller_configs=self.testbed_configs)
 
-        # Import and register the built-in controller modules specified
-        # in testbed config.
-        for module in self._import_builtin_controllers():
-            self.register_controller(module, builtin=True)
-        if hasattr(self, 'android_devices'):
-            for ad in self.android_devices:
-                if ad.droid:
-                    utils.set_location_service(ad, False)
-                    utils.sync_device_time(ad)
-
     def _import_builtin_controllers(self):
         """Import built-in controller modules.
 
@@ -386,6 +376,10 @@
         is called.
         """
         event_bus.post(TestClassBeginEvent(self))
+        # Import and register the built-in controller modules specified
+        # in testbed config.
+        for module in self._import_builtin_controllers():
+            self.register_controller(module, builtin=True)
         return self.setup_class()
 
     def _teardown_class(self):
@@ -899,6 +893,7 @@
                 self._block_all_test_cases(tests)
                 setup_fail = True
         except signals.TestAbortClass:
+            self.log.exception('Test class %s aborted' % self.TAG)
             setup_fail = True
         except Exception as e:
             self.log.exception("Failed to setup %s.", self.TAG)
@@ -917,6 +912,7 @@
                     self.exec_one_testcase(test_name, test_func, self.cli_args)
             return self.results
         except signals.TestAbortClass:
+            self.log.exception('Test class %s aborted' % self.TAG)
             return self.results
         except signals.TestAbortAll as e:
             # Piggy-back test results on this exception object so we don't lose
diff --git a/acts/framework/acts/controllers/android_device.py b/acts/framework/acts/controllers/android_device.py
index 52404fa..9f03c93 100755
--- a/acts/framework/acts/controllers/android_device.py
+++ b/acts/framework/acts/controllers/android_device.py
@@ -100,6 +100,10 @@
                  " but is not attached.") % ad.serial,
                 serial=ad.serial)
     _start_services_on_ads(ads)
+    for ad in ads:
+        if ad.droid:
+            utils.set_location_service(ad, False)
+            utils.sync_device_time(ad)
     return ads
 
 
diff --git a/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py b/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py
index 892a786..23fee41 100644
--- a/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py
+++ b/acts/framework/acts/controllers/anritsu_lib/md8475_cellular_simulator.py
@@ -14,7 +14,10 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import ntpath
+import time
 import acts.controllers.cellular_simulator as cc
+from acts.test_utils.power.tel_simulations import LteSimulation
 from acts.controllers.anritsu_lib import md8475a
 from acts.controllers.anritsu_lib import _anritsu_utils as anritsu
 
@@ -23,20 +26,501 @@
 
     MD8475_VERSION = 'A'
 
+    # Indicates if it is able to use 256 QAM as the downlink modulation for LTE
+    LTE_SUPPORTS_DL_256QAM = False
+
+    # Indicates if it is able to use 64 QAM as the uplink modulation for LTE
+    LTE_SUPPORTS_UL_64QAM = False
+
+    # Indicates if 4x4 MIMO is supported for LTE
+    LTE_SUPPORTS_4X4_MIMO = False
+
+    # The maximum number of carriers that this simulator can support for LTE
+    LTE_MAX_CARRIERS = 2
+
+    # The maximum power that the equipment is able to transmit
+    MAX_DL_POWER = -10
+
+    # Simulation config files in the callbox computer.
+    # These should be replaced in the future by setting up
+    # the same configuration manually.
+    LTE_BASIC_SIM_FILE = 'SIM_default_LTE.wnssp'
+    LTE_BASIC_CELL_FILE = 'CELL_LTE_config.wnscp'
+    LTE_CA_BASIC_SIM_FILE = 'SIM_LTE_CA.wnssp'
+    LTE_CA_BASIC_CELL_FILE = 'CELL_LTE_CA_config.wnscp'
+
+    # Filepath to the config files stored in the Anritsu callbox. Needs to be
+    # formatted to replace {} with either A or B depending on the model.
+    CALLBOX_CONFIG_PATH = 'C:\\Users\\MD8475A\\Documents\\DAN_configs\\'
+
     def __init__(self, ip_address):
+        """ Initializes the cellular simulator.
+
+        Args:
+            ip_address: the ip address of the MD8475 instrument
+        """
+        super().__init__()
+
         try:
             self.anritsu = md8475a.MD8475A(ip_address,
                                            md8475_version=self.MD8475_VERSION)
         except anritsu.AnristuError:
             raise cc.CellularSimulatorError('Could not connect to MD8475.')
 
+        self.bts = None
+
     def destroy(self):
         """ Sends finalization commands to the cellular equipment and closes
         the connection. """
         self.anritsu.stop_simulation()
         self.anritsu.disconnect()
 
+    def setup_lte_scenario(self):
+        """ Configures the equipment for an LTE simulation. """
+        cell_file_name = self.LTE_BASIC_CELL_FILE
+        sim_file_name = self.LTE_BASIC_SIM_FILE
+
+        cell_file_path = ntpath.join(self.CALLBOX_CONFIG_PATH, cell_file_name)
+        sim_file_path = ntpath.join(self.CALLBOX_CONFIG_PATH, sim_file_name)
+
+        self.anritsu.load_simulation_paramfile(sim_file_path)
+        self.anritsu.load_cell_paramfile(cell_file_path)
+        self.anritsu.start_simulation()
+
+        self.bts = [self.anritsu.get_BTS(md8475a.BtsNumber.BTS1)]
+
+    def setup_lte_ca_scenario(self):
+        """ Configures the equipment for an LTE with CA simulation. """
+        cell_file_name = self.LTE_CA_BASIC_CELL_FILE
+        sim_file_name = self.LTE_CA_BASIC_SIM_FILE
+
+        cell_file_path = ntpath.join(self.CALLBOX_CONFIG_PATH, cell_file_name)
+        sim_file_path = ntpath.join(self.CALLBOX_CONFIG_PATH, sim_file_name)
+
+        self.anritsu.load_simulation_paramfile(sim_file_path)
+        self.anritsu.load_cell_paramfile(cell_file_path)
+        self.anritsu.start_simulation()
+
+        self.bts = [
+            self.anritsu.get_BTS(md8475a.BtsNumber.BTS1),
+            self.anritsu.get_BTS(md8475a.BtsNumber.BTS2)
+        ]
+
+    def set_input_power(self, bts_index, input_power):
+        """ Sets the input power for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            input_power: the new input power
+        """
+        self.bts[bts_index].input_level = input_power
+
+    def set_output_power(self, bts_index, output_power):
+        """ Sets the output power for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            output_power: the new output power
+        """
+        self.bts[bts_index].output_level = output_power
+
+    def set_downlink_channel_number(self, bts_index, channel_number):
+        """ Sets the downlink channel number for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            channel_number: the new channel number
+        """
+        # Temporarily adding this line to workaround a bug in the
+        # Anritsu callbox in which the channel number needs to be set
+        # to a different value before setting it to the final one.
+        self.bts[bts_index].dl_channel = str(channel_number + 1)
+        time.sleep(8)
+        self.bts[bts_index].dl_channel = str(channel_number)
+
+    def set_enabled_for_ca(self, bts_index, enabled):
+        """ Enables or disables the base station during carrier aggregation.
+
+        Args:
+            bts_index: the base station number
+            enabled: whether the base station should be enabled for ca.
+        """
+        self.bts[bts_index].dl_cc_enabled = enabled
+
+    def set_dl_modulation(self, bts_index, modulation):
+        """ Sets the DL modulation for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            modulation: the new DL modulation
+        """
+        self.bts[bts_index].lte_dl_modulation_order = modulation
+
+    def set_ul_modulation(self, bts_index, modulation):
+        """ Sets the UL modulation for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            modulation: the new UL modulation
+        """
+        self.bts[bts_index].lte_ul_modulation_order = modulation
+
+    def set_tbs_pattern_on(self, bts_index, tbs_pattern_on):
+        """ Enables or disables TBS pattern in the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            tbs_pattern_on: the new TBS pattern setting
+        """
+        if tbs_pattern_on:
+            self.bts[bts_index].tbs_pattern = 'FULLALLOCATION'
+        else:
+            self.bts[bts_index].tbs_pattern = 'OFF'
+
+    def set_band(self, bts_index, band):
+        """ Sets the right duplex mode before switching to a new band.
+
+        Args:
+            bts_index: the base station number
+            band: desired band
+        """
+        bts = self.bts[bts_index]
+
+        # The callbox won't restore the band-dependent default values if the
+        # request is to switch to the same band as the one the base station is
+        # currently using. To ensure that default values are restored, go to a
+        # different band before switching.
+        if int(bts.band) == band:
+            # Using bands 1 and 2 but it could be any others
+            bts.band = '1' if band != 1 else '2'
+            # Switching to config.band will be handled by the parent class
+            # implementation of this method.
+
+        bts.duplex_mode = self.get_duplex_mode(band).value
+        bts.band = band
+        time.sleep(5)  # It takes some time to propagate the new band
+
+    def get_duplex_mode(self, band):
+        """ Determines if the band uses FDD or TDD duplex mode
+
+        Args:
+            band: a band number
+        Returns:
+            an variable of class DuplexMode indicating if band is FDD or TDD
+        """
+
+        if 33 <= int(band) <= 46:
+            return LteSimulation.DuplexMode.TDD
+        else:
+            return LteSimulation.DuplexMode.FDD
+
+    def set_tdd_config(self, bts_index, config):
+        """ Sets the frame structure for TDD bands.
+
+        Args:
+            bts_index: the base station number
+            config: the desired frame structure. An int between 0 and 6.
+        """
+
+        if not 0 <= config <= 6:
+            raise ValueError("The frame structure configuration has to be a "
+                             "number between 0 and 6")
+
+        self.bts[bts_index].uldl_configuration = config
+
+        # Wait for the setting to propagate
+        time.sleep(5)
+
+    def set_bandwidth(self, bts_index, bandwidth):
+        """ Sets the LTE channel bandwidth (MHz)
+
+        Args:
+            bts_index: the base station number
+            bandwidth: desired bandwidth (MHz)
+        """
+        bts = self.bts[bts_index]
+
+        if bandwidth == 20:
+            bts.bandwidth = md8475a.BtsBandwidth.LTE_BANDWIDTH_20MHz
+        elif bandwidth == 15:
+            bts.bandwidth = md8475a.BtsBandwidth.LTE_BANDWIDTH_15MHz
+        elif bandwidth == 10:
+            bts.bandwidth = md8475a.BtsBandwidth.LTE_BANDWIDTH_10MHz
+        elif bandwidth == 5:
+            bts.bandwidth = md8475a.BtsBandwidth.LTE_BANDWIDTH_5MHz
+        elif bandwidth == 3:
+            bts.bandwidth = md8475a.BtsBandwidth.LTE_BANDWIDTH_3MHz
+        elif bandwidth == 1.4:
+            bts.bandwidth = md8475a.BtsBandwidth.LTE_BANDWIDTH_1dot4MHz
+        else:
+            msg = "Bandwidth = {} MHz is not valid for LTE".format(bandwidth)
+            self.log.error(msg)
+            raise ValueError(msg)
+        time.sleep(5)  # It takes some time to propagate the new settings
+
+    def set_mimo_mode(self, bts_index, mimo):
+        """ Sets the number of DL antennas for the desired MIMO mode.
+
+        Args:
+            bts_index: the base station number
+            mimo: object of class MimoMode
+        """
+
+        bts = self.bts[bts_index]
+
+        # If the requested mimo mode is not compatible with the current TM,
+        # warn the user before changing the value.
+
+        if mimo == LteSimulation.MimoMode.MIMO_1x1:
+            if bts.transmode not in [
+                    LteSimulation.TransmissionMode.TM1,
+                    LteSimulation.TransmissionMode.TM7
+            ]:
+                self.log.warning(
+                    "Using only 1 DL antennas is not allowed with "
+                    "the current transmission mode. Changing the "
+                    "number of DL antennas will override this "
+                    "setting.")
+            bts.dl_antenna = 1
+        elif mimo == LteSimulation.MimoMode.MIMO_2x2:
+            if bts.transmode not in [
+                    LteSimulation.TransmissionMode.TM2,
+                    LteSimulation.TransmissionMode.TM3,
+                    LteSimulation.TransmissionMode.TM4,
+                    LteSimulation.TransmissionMode.TM8,
+                    LteSimulation.TransmissionMode.TM9
+            ]:
+                self.log.warning("Using two DL antennas is not allowed with "
+                                 "the current transmission mode. Changing the "
+                                 "number of DL antennas will override this "
+                                 "setting.")
+            bts.dl_antenna = 2
+        elif mimo == LteSimulation.MimoMode.MIMO_4x4:
+            if bts.transmode not in [
+                    LteSimulation.TransmissionMode.TM2,
+                    LteSimulation.TransmissionMode.TM3,
+                    LteSimulation.TransmissionMode.TM4,
+                    LteSimulation.TransmissionMode.TM9
+            ]:
+                self.log.warning("Using four DL antennas is not allowed with "
+                                 "the current transmission mode. Changing the "
+                                 "number of DL antennas will override this "
+                                 "setting.")
+
+            bts.dl_antenna = 4
+        else:
+            RuntimeError("The requested MIMO mode is not supported.")
+
+    def set_scheduling_mode(self, bts_index, scheduling, mcs_dl, mcs_ul,
+                            nrb_dl, nrb_ul):
+        """ Sets the scheduling mode for LTE
+
+        Args:
+            bts_index: the base station number
+            scheduling: DYNAMIC or STATIC scheduling (Enum list)
+            mcs_dl: Downlink MCS (only for STATIC scheduling)
+            mcs_ul: Uplink MCS (only for STATIC scheduling)
+            nrb_dl: Number of RBs for downlink (only for STATIC scheduling)
+            nrb_ul: Number of RBs for uplink (only for STATIC scheduling)
+        """
+
+        bts = self.bts[bts_index]
+        bts.lte_scheduling_mode = scheduling.value
+
+        if scheduling == LteSimulation.SchedulingMode.STATIC:
+
+            if not all([nrb_dl, nrb_ul, mcs_dl, mcs_ul]):
+                raise ValueError('When the scheduling mode is set to manual, '
+                                 'the RB and MCS parameters are required.')
+
+            bts.packet_rate = md8475a.BtsPacketRate.LTE_MANUAL
+            bts.lte_mcs_dl = mcs_dl
+            bts.lte_mcs_ul = mcs_ul
+            bts.nrb_dl = nrb_dl
+            bts.nrb_ul = nrb_ul
+
+        time.sleep(5)  # It takes some time to propagate the new settings
+
+    def lte_attach_secondary_carriers(self):
+        """ Activates the secondary carriers for CA. Requires the DUT to be
+        attached to the primary carrier first. """
+
+        testcase = self.anritsu.get_AnritsuTestCases()
+        # Setting the procedure to selection is needed because of a bug in the
+        # instrument's software (b/139547391).
+        testcase.procedure = md8475a.TestProcedure.PROCEDURE_SELECTION
+        testcase.procedure = md8475a.TestProcedure.PROCEDURE_MULTICELL
+        testcase.power_control = md8475a.TestPowerControl.POWER_CONTROL_DISABLE
+        testcase.measurement_LTE = md8475a.TestMeasurement.MEASUREMENT_DISABLE
+
+        self.anritsu.start_testcase()
+
+        retry_counter = 0
+        self.log.info("Waiting for the test case to start...")
+        time.sleep(5)
+
+        while self.anritsu.get_testcase_status() == "0":
+            retry_counter += 1
+            if retry_counter == 3:
+                raise RuntimeError(
+                    "The test case failed to start after {} "
+                    "retries. The connection between the phone "
+                    "and the base station might be unstable.".format(
+                        retry_counter))
+            time.sleep(10)
+
+    def set_transmission_mode(self, bts_index, tmode):
+        """ Sets the transmission mode for the LTE basetation
+
+        Args:
+            bts_index: the base station number
+            tmode: Enum list from class 'TransmissionModeLTE'
+        """
+
+        bts = self.bts[bts_index]
+
+        # If the selected transmission mode does not support the number of DL
+        # antennas, throw an exception.
+        if (tmode in [
+                LteSimulation.TransmissionMode.TM1,
+                LteSimulation.TransmissionMode.TM7
+        ] and bts.dl_antenna != '1'):
+            # TM1 and TM7 only support 1 DL antenna
+            raise ValueError("{} allows only one DL antenna. Change the "
+                             "number of DL antennas before setting the "
+                             "transmission mode.".format(tmode.value))
+        elif (tmode == LteSimulation.TransmissionMode.TM8
+              and bts.dl_antenna != '2'):
+            # TM8 requires 2 DL antennas
+            raise ValueError("TM2 requires two DL antennas. Change the "
+                             "number of DL antennas before setting the "
+                             "transmission mode.")
+        elif (tmode in [
+                LteSimulation.TransmissionMode.TM2,
+                LteSimulation.TransmissionMode.TM3,
+                LteSimulation.TransmissionMode.TM4,
+                LteSimulation.TransmissionMode.TM9
+        ] and bts.dl_antenna == '1'):
+            # TM2, TM3, TM4 and TM9 require 2 or 4 DL antennas
+            raise ValueError("{} requires at least two DL atennas. Change the "
+                             "number of DL antennas before setting the "
+                             "transmission mode.".format(tmode.value))
+
+        # The TM mode is allowed for the current number of DL antennas, so it
+        # is safe to change this setting now
+        bts.transmode = tmode.value
+
+        time.sleep(5)  # It takes some time to propagate the new settings
+
+    def wait_until_attached(self, timeout=120):
+        """ Waits until the DUT is attached to the primary carrier.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        try:
+            self.anritsu.wait_for_registration_state(time_to_wait=timeout)
+        except anritsu.AnritsuError:
+            raise cc.CellularSimulatorError('The phone did not attach before '
+                                            'the timeout period ended.')
+
+    def wait_until_communication_state(self, timeout=120):
+        """ Waits until the DUT is in Communication state.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        try:
+            self.anritsu.wait_for_communication_state(time_to_wait=timeout)
+        except anritsu.AnritsuError:
+            raise cc.CellularSimulatorError('The phone was not in '
+                                            'Communication state before '
+                                            'the timeout period ended.')
+
+    def wait_until_idle_state(self, timeout=120):
+        """ Waits until the DUT is in Idle state.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        try:
+            self.anritsu.wait_for_idle_state(time_to_wait=timeout)
+        except anritsu.AnritsuError:
+            raise cc.CellularSimulatorError('The phone was not in Idle state '
+                                            'before the time the timeout '
+                                            'period ended.')
+
+    def detach(self):
+        """ Turns off all the base stations so the DUT loose connection."""
+        self.anritsu.set_simulation_state_to_poweroff()
+
+    def stop(self):
+        """ Stops current simulation. After calling this method, the simulator
+        will need to be set up again. """
+        self.simulator.stop()
+
+    def start_data_traffic(self):
+        """ Starts transmitting data from the instrument to the DUT. """
+        try:
+            self.anritsu.start_ip_traffic()
+        except md8475a.AnritsuError as inst:
+            # This typically happens when traffic is already running.
+            # TODO (b/141962691): continue only if traffic is running
+            self.log.warning(str(inst))
+        time.sleep(4)
+
+    def stop_data_traffic(self):
+        """ Stops transmitting data from the instrument to the DUT. """
+        try:
+            self.anritsu.stop_ip_traffic()
+        except md8475a.AnritsuError as inst:
+            # This typically happens when traffic has already been stopped
+            # TODO (b/141962691): continue only if traffic is stopped
+            self.log.warning(str(inst))
+        time.sleep(2)
+
 
 class MD8475BCellularSimulator(MD8475CellularSimulator):
 
     MD8475_VERSION = 'B'
+
+    # Indicates if it is able to use 256 QAM as the downlink modulation for LTE
+    LTE_SUPPORTS_DL_256QAM = True
+
+    # Indicates if it is able to use 64 QAM as the uplink modulation for LTE
+    LTE_SUPPORTS_UL_64QAM = True
+
+    # Indicates if 4x4 MIMO is supported for LTE
+    LTE_SUPPORTS_4X4_MIMO = True
+
+    # The maximum number of carriers that this simulator can support for LTE
+    LTE_MAX_CARRIERS = 4
+
+    # The maximum power that the equipment is able to transmit
+    MAX_DL_POWER = -30
+
+    # Simulation config files in the callbox computer.
+    # These should be replaced in the future by setting up
+    # the same configuration manually.
+    LTE_BASIC_SIM_FILE = 'SIM_default_LTE.wnssp2'
+    LTE_BASIC_CELL_FILE = 'CELL_LTE_config.wnscp2'
+    LTE_CA_BASIC_SIM_FILE = 'SIM_LTE_CA.wnssp2'
+    LTE_CA_BASIC_CELL_FILE = 'CELL_LTE_CA_config.wnscp2'
+
+    # Filepath to the config files stored in the Anritsu callbox. Needs to be
+    # formatted to replace {} with either A or B depending on the model.
+    CALLBOX_CONFIG_PATH = 'C:\\Users\\MD8475B\\Documents\\DAN_configs\\'
+
+    def setup_lte_ca_scenario(self):
+        """ The B model can support up to five carriers. """
+
+        super().setup_lte_ca_scenario()
+
+        self.bts.extend([
+            self.anritsu.get_BTS(md8475a.BtsNumber.BTS3),
+            self.anritsu.get_BTS(md8475a.BtsNumber.BTS4),
+            self.anritsu.get_BTS(md8475a.BtsNumber.BTS5)
+        ])
diff --git a/acts/framework/acts/controllers/cellular_simulator.py b/acts/framework/acts/controllers/cellular_simulator.py
index 8c82792..a140c0f 100644
--- a/acts/framework/acts/controllers/cellular_simulator.py
+++ b/acts/framework/acts/controllers/cellular_simulator.py
@@ -13,6 +13,8 @@
 #   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 import logger
+from acts.test_utils.power import tel_simulations as sims
 
 
 class AbstractCellularSimulator:
@@ -22,11 +24,286 @@
 
     This class defines the interface that every cellular simulator controller
     needs to implement and shouldn't be instantiated by itself. """
+
+    # Indicates if it is able to use 256 QAM as the downlink modulation for LTE
+    LTE_SUPPORTS_DL_256QAM = None
+
+    # Indicates if it is able to use 64 QAM as the uplink modulation for LTE
+    LTE_SUPPORTS_UL_64QAM = None
+
+    # Indicates if 4x4 MIMO is supported for LTE
+    LTE_SUPPORTS_4X4_MIMO = None
+
+    # The maximum number of carriers that this simulator can support for LTE
+    LTE_MAX_CARRIERS = None
+
+    # The maximum power that the equipment is able to transmit
+    MAX_DL_POWER = None
+
+    def __init__(self):
+        """ Initializes the cellular simulator. """
+        self.log = logger.create_tagged_trace_logger('CellularSimulator')
+
     def destroy(self):
         """ Sends finalization commands to the cellular equipment and closes
         the connection. """
         raise NotImplementedError()
 
+    def setup_lte_scenario(self):
+        """ Configures the equipment for an LTE simulation. """
+        raise NotImplementedError()
+
+    def setup_lte_ca_scenario(self):
+        """ Configures the equipment for an LTE with CA simulation. """
+        raise NotImplementedError()
+
+    def configure_bts(self, config, bts_index=0):
+        """ Commands the equipment to setup a base station with the required
+        configuration. This method applies configurations that are common to all
+        RATs.
+
+        Args:
+            config: a BaseSimulation.BtsConfig object.
+            bts_index: the base station number.
+        """
+
+        if config.output_power:
+            self.set_output_power(bts_index, config.output_power)
+
+        if config.input_power:
+            self.set_input_power(bts_index, config.input_power)
+
+        if isinstance(config, sims.LteSimulation.LteSimulation.BtsConfig):
+            self.configure_lte_bts(config, bts_index)
+
+    def configure_lte_bts(self, config, bts_index=0):
+        """ Commands the equipment to setup an LTE base station with the
+        required configuration.
+
+        Args:
+            config: an LteSimulation.BtsConfig object.
+            bts_index: the base station number.
+        """
+        if config.band:
+            self.set_band(bts_index, config.band)
+
+        if config.dlul_config:
+            self.set_tdd_config(bts_index, config.dlul_config)
+
+        if config.bandwidth:
+            self.set_bandwidth(bts_index, config.bandwidth)
+
+        if config.dl_channel:
+            self.set_downlink_channel_number(bts_index, config.dl_channel)
+
+        if config.mimo_mode:
+            self.set_mimo_mode(bts_index, config.mimo_mode)
+
+        if config.transmission_mode:
+            self.set_transmission_mode(bts_index, config.transmission_mode)
+
+        if config.scheduling_mode:
+
+            if (config.scheduling_mode ==
+                    sims.LteSimulation.SchedulingMode.STATIC
+                    and not (config.dl_rbs and config.ul_rbs and config.dl_mcs
+                             and config.ul_mcs)):
+                raise ValueError('When the scheduling mode is set to manual, '
+                                 'the RB and MCS parameters are required.')
+
+            # If scheduling mode is set to Dynamic, the RB and MCS parameters
+            # will be ignored by set_scheduling_mode.
+            self.set_scheduling_mode(bts_index, config.scheduling_mode,
+                                     config.dl_mcs, config.ul_mcs,
+                                     config.dl_rbs, config.ul_rbs)
+
+        # This variable stores a boolean value so the following is needed to
+        # differentiate False from None
+        if config.dl_cc_enabled is not None:
+            self.set_enabled_for_ca(bts_index, config.dl_cc_enabled)
+
+        if config.dl_modulation_order:
+            self.set_dl_modulation(bts_index, config.dl_modulation_order)
+
+        if config.ul_modulation_order:
+            self.set_ul_modulation(bts_index, config.ul_modulation_order)
+
+        # This variable stores a boolean value so the following is needed to
+        # differentiate False from None
+        if config.tbs_pattern_on is not None:
+            self.set_tbs_pattern_on(bts_index, config.tbs_pattern_on)
+
+    def set_band(self, bts_index, band):
+        """ Sets the band for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            band: the new band
+        """
+        raise NotImplementedError()
+
+    def set_input_power(self, bts_index, input_power):
+        """ Sets the input power for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            input_power: the new input power
+        """
+        raise NotImplementedError()
+
+    def set_output_power(self, bts_index, output_power):
+        """ Sets the output power for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            output_power: the new output power
+        """
+        raise NotImplementedError()
+
+    def set_tdd_config(self, bts_index, tdd_config):
+        """ Sets the tdd configuration number for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            tdd_config: the new tdd configuration number
+        """
+        raise NotImplementedError()
+
+    def set_bandwidth(self, bts_index, bandwidth):
+        """ Sets the bandwidth for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            bandwidth: the new bandwidth
+        """
+        raise NotImplementedError()
+
+    def set_downlink_channel_number(self, bts_index, channel_number):
+        """ Sets the downlink channel number for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            channel_number: the new channel number
+        """
+        raise NotImplementedError()
+
+    def set_mimo_mode(self, bts_index, mimo_mode):
+        """ Sets the mimo mode for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            mimo_mode: the new mimo mode
+        """
+        raise NotImplementedError()
+
+    def set_transmission_mode(self, bts_index, transmission_mode):
+        """ Sets the transmission mode for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            transmission_mode: the new transmission mode
+        """
+        raise NotImplementedError()
+
+    def set_scheduling_mode(self, bts_index, scheduling_mode, mcs_dl, mcs_ul,
+                            nrb_dl, nrb_ul):
+        """ Sets the scheduling mode for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            scheduling_mode: the new scheduling mode
+            mcs_dl: Downlink MCS (only for STATIC scheduling)
+            mcs_ul: Uplink MCS (only for STATIC scheduling)
+            nrb_dl: Number of RBs for downlink (only for STATIC scheduling)
+            nrb_ul: Number of RBs for uplink (only for STATIC scheduling)
+        """
+        raise NotImplementedError()
+
+    def set_enabled_for_ca(self, bts_index, enabled):
+        """ Enables or disables the base station during carrier aggregation.
+
+        Args:
+            bts_index: the base station number
+            enabled: whether the base station should be enabled for ca.
+        """
+        raise NotImplementedError()
+
+    def set_dl_modulation(self, bts_index, modulation):
+        """ Sets the DL modulation for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            modulation: the new DL modulation
+        """
+        raise NotImplementedError()
+
+    def set_ul_modulation(self, bts_index, modulation):
+        """ Sets the UL modulation for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            modulation: the new UL modulation
+        """
+        raise NotImplementedError()
+
+    def set_tbs_pattern_on(self, bts_index, tbs_pattern_on):
+        """ Enables or disables TBS pattern in the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            tbs_pattern_on: the new TBS pattern setting
+        """
+        raise NotImplementedError()
+
+    def lte_attach_secondary_carriers(self):
+        """ Activates the secondary carriers for CA. Requires the DUT to be
+        attached to the primary carrier first. """
+        raise NotImplementedError()
+
+    def wait_until_attached(self, timeout=120):
+        """ Waits until the DUT is attached to the primary carrier.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        raise NotImplementedError()
+
+    def wait_until_communication_state(self, timeout=120):
+        """ Waits until the DUT is in Communication state.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        raise NotImplementedError()
+
+    def wait_until_idle_state(self, timeout=120):
+        """ Waits until the DUT is in Idle state.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        raise NotImplementedError()
+
+    def detach(self):
+        """ Turns off all the base stations so the DUT loose connection."""
+        raise NotImplementedError()
+
+    def stop(self):
+        """ Stops current simulation. After calling this method, the simulator
+        will need to be set up again. """
+        raise NotImplementedError()
+
+    def start_data_traffic(self):
+        """ Starts transmitting data from the instrument to the DUT. """
+        raise NotImplementedError()
+
+    def stop_data_traffic(self):
+        """ Stops transmitting data from the instrument to the DUT. """
+        raise NotImplementedError()
+
 
 class CellularSimulatorError(Exception):
     """ Exceptions thrown when the cellular equipment is unreachable or it
diff --git a/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/packet.py b/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/packet.py
index f62975d..2233370 100644
--- a/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/packet.py
+++ b/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/packet.py
@@ -53,8 +53,8 @@
      1  │    2   │ uint16 │  Main   │ Fine    │ Calibration/Measurement value
      2  │    4   │ uint16 │  USB    │ Coarse  │ Calibration/Measurement value
      3  │    6   │ uint16 │  USB    │ Fine    │ Calibration/Measurement value
-     4  │    8   │  int16 │  Aux    │ Coarse  │ Calibration/Measurement value
-     5  │   10   │  int16 │  Aux    │ Fine    │ Calibration/Measurement value
+     4  │    8   │ uint16 │  Aux    │ Coarse  │ Calibration/Measurement value
+     5  │   10   │ uint16 │  Aux    │ Fine    │ Calibration/Measurement value
      6  │   12   │ uint16 │  Main   │ Voltage │ Main V measurement, or Aux V
         │        │        │         │         │    if setVoltageChannel == 1
      7  │   14   │ uint16 │  USB    │ Voltage │ USB Voltage
@@ -76,7 +76,7 @@
     SIZE = 18
 
     def __init__(self, raw_data, sample_time):
-        self.values = struct.unpack('>4H2h2H2B', raw_data)
+        self.values = struct.unpack('>8H2B', raw_data)
         self._sample_time = sample_time
 
     def __getitem__(self, channel_and_reading_granularity):
diff --git a/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/transformers.py b/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/transformers.py
index e91a89b..5ddc23c 100644
--- a/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/transformers.py
+++ b/acts/framework/acts/controllers/monsoon_lib/sampling/hvpm/transformers.py
@@ -438,10 +438,10 @@
                 zero_offset += cal_zero
                 if cal_ref - zero_offset != 0:
                     slope = scale / (cal_ref - zero_offset)
-                    if granularity == Granularity.FINE:
-                        slope /= 1000
                 else:
                     slope = 0
+                if granularity == Granularity.FINE:
+                    slope /= 1000
 
                 index = HvpmMeasurement.get_index(channel, granularity)
                 calibrated_value[:, granularity] = slope * (
@@ -452,7 +452,7 @@
             readings[:, channel] = np.where(
                 measurements[:, fine_data_position] < self.fine_threshold,
                 calibrated_value[:, Granularity.FINE],
-                calibrated_value[:, Granularity.COARSE])
+                calibrated_value[:, Granularity.COARSE]) / 1000.0  # to mA
 
         main_voltage_index = HvpmMeasurement.get_index(Channel.MAIN,
                                                        Reading.VOLTAGE)
diff --git a/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/packet.py b/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/packet.py
index 80c8274..b0f8839 100644
--- a/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/packet.py
+++ b/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/packet.py
@@ -54,9 +54,9 @@
     Val │  Byte  │  Type  │ Monsoon │ Reading │
     Pos │ Offset │ Format │ Channel │  Type   │ Description
     ────┼────────┼────────┼─────────┼─────────┼──────────────────────────────
-     0  │   0    │ uint16 │  Main   │ Current │ Calibration value.
-     1  │   2    │ uint16 │  USB    │ Current │ Calibration value.
-     2  │   4    │ uint16 │  Aux    │ Current │ Calibration value.
+     0  │   0    │  int16 │  Main   │ Current │ Calibration value.
+     1  │   2    │  int16 │  USB    │ Current │ Calibration value.
+     2  │   4    │  int16 │  Aux    │ Current │ Calibration value.
      3  │   6    │ uint16 │  Main   │ Voltage │ Calibration value.
 
     If the measurement is a power reading:
@@ -64,11 +64,11 @@
     Val │  Byte  │  Type  │ Monsoon │ Reading │
     Pos │ Offset │ Format │ Channel │  Type   │ Description
     ────┼────────┼────────┼─────────┼─────────┼──────────────────────────────
-     0  │   0    │ uint16 │  Main   │ Current │ b0: if 1, Coarse, else Fine
+     0  │   0    │  int16 │  Main   │ Current │ b0: if 1, Coarse, else Fine
         │        │        │         │         │ b1-7: Measurement value.
-     1  │   2    │ uint16 │  USB    │ Current │ b0: if 1, Coarse, else Fine
+     1  │   2    │  int16 │  USB    │ Current │ b0: if 1, Coarse, else Fine
         │        │        │         │         │ b1-7: Measurement value.
-     2  │   4    │ uint16 │  Aux    │ Current │ b0: if 1, Coarse, else Fine
+     2  │   4    │  int16 │  Aux    │ Current │ b0: if 1, Coarse, else Fine
         │        │        │         │         │ b1-7: Measurement value.
      3  │   6    │ uint16 │  Main   │ Voltage │ Measurement value.
 
@@ -86,7 +86,7 @@
             sample_type: The type of sample that was recorded.
             entry_index: The index of the measurement within the packet.
         """
-        self.values = struct.unpack('>4H', raw_data)
+        self.values = struct.unpack('>3hH', raw_data)
         self._sample_time = sample_time
         self._sample_type = sample_type
 
diff --git a/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/stock_transformers.py b/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/stock_transformers.py
index eaf60b0..becc4ee 100644
--- a/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/stock_transformers.py
+++ b/acts/framework/acts/controllers/monsoon_lib/sampling/lvpm_stock/stock_transformers.py
@@ -340,32 +340,6 @@
             return False
         return True
 
-    @staticmethod
-    def _get_currents(sample, calibration_data):
-        """Returns the list of current values for each channel.
-
-        Args:
-            sample: The Sample object to determine the current values of.
-            calibration_data: The CalibrationCollection used to calibrate the
-                sample.
-
-        Returns:
-
-        """
-        currents = [0] * 3
-        for channel in Channel.values:
-            current = sample[channel]
-            granularity = Granularity.FINE
-            if current & 1:
-                current &= ~1
-                granularity = Granularity.COARSE
-
-            zero = calibration_data.get(channel, Origin.ZERO, granularity)
-            scale = calibration_data.get(channel, Origin.SCALE, granularity)
-            currents[channel] = (current - zero) * scale
-
-        return currents
-
     def _transform_buffer(self, buffer):
         calibration_data = buffer.calibration_data
 
@@ -393,7 +367,7 @@
             # Monsoon.py algorithm.
             readings[:, channel] = np.where(
                 measurements[:, channel] & 1,
-                (measurements[:, channel] - 1 - coarse_zero) * coarse_scale,
+                ((measurements[:, channel] & ~1) - coarse_zero) * coarse_scale,
                 (measurements[:, channel] - fine_zero) * fine_scale)
 
         for i in range(len(buffer.samples)):
diff --git a/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_cellular_simulator.py b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_cellular_simulator.py
new file mode 100644
index 0000000..6c76e62
--- /dev/null
+++ b/acts/framework/acts/controllers/rohdeschwarz_lib/cmw500_cellular_simulator.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2019 - 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 acts.controllers.rohdeschwarz_lib import cmw500
+from acts.controllers import cellular_simulator as cc
+
+
+class CMW500CellularSimulator:
+    """ A cellular simulator for telephony simulations based on the CMW 500
+    controller. """
+
+    # Indicates if it is able to use 256 QAM as the downlink modulation for LTE
+    LTE_SUPPORTS_DL_256QAM = None
+
+    # Indicates if it is able to use 64 QAM as the uplink modulation for LTE
+    LTE_SUPPORTS_UL_64QAM = None
+
+    # Indicates if 4x4 MIMO is supported for LTE
+    LTE_SUPPORTS_4X4_MIMO = None
+
+    # The maximum number of carriers that this simulator can support for LTE
+    LTE_MAX_CARRIERS = None
+
+    def __init__(self, ip_address, port):
+        """ Initializes the cellular simulator.
+
+        Args:
+            ip_address: the ip address of the CMW500
+            port: the port number for the CMW500 controller
+        """
+        try:
+            self.cmw = cmw500.Cmw500(ip_address, port)
+        except cmw500.CmwError:
+            raise cc.CellularSimulatorError('Could not connect to CMW500.')
+
+    def destroy(self):
+        """ Sends finalization commands to the cellular equipment and closes
+        the connection. """
+        raise NotImplementedError()
+
+    def setup_lte_scenario(self):
+        """ Configures the equipment for an LTE simulation. """
+        raise NotImplementedError()
+
+    def setup_lte_ca_scenario(self):
+        """ Configures the equipment for an LTE with CA simulation. """
+        raise NotImplementedError()
+
+    def set_band(self, bts_index, band):
+        """ Sets the band for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            band: the new band
+        """
+        raise NotImplementedError()
+
+    def set_input_power(self, bts_index, input_power):
+        """ Sets the input power for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            input_power: the new input power
+        """
+        raise NotImplementedError()
+
+    def set_output_power(self, bts_index, output_power):
+        """ Sets the output power for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            output_power: the new output power
+        """
+        raise NotImplementedError()
+
+    def set_tdd_config(self, bts_index, tdd_config):
+        """ Sets the tdd configuration number for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            tdd_config: the new tdd configuration number
+        """
+        raise NotImplementedError()
+
+    def set_bandwidth(self, bts_index, bandwidth):
+        """ Sets the bandwidth for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            bandwidth: the new bandwidth
+        """
+        raise NotImplementedError()
+
+    def set_downlink_channel_number(self, bts_index, channel_number):
+        """ Sets the downlink channel number for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            channel_number: the new channel number
+        """
+        raise NotImplementedError()
+
+    def set_mimo_mode(self, bts_index, mimo_mode):
+        """ Sets the mimo mode for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            mimo_mode: the new mimo mode
+        """
+        raise NotImplementedError()
+
+    def set_transmission_mode(self, bts_index, transmission_mode):
+        """ Sets the transmission mode for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            transmission_mode: the new transmission mode
+        """
+        raise NotImplementedError()
+
+    def set_scheduling_mode(self, bts_index, scheduling_mode, mcs_dl, mcs_ul,
+                            nrb_dl, nrb_ul):
+        """ Sets the scheduling mode for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            scheduling_mode: the new scheduling mode
+            mcs_dl: Downlink MCS (only for STATIC scheduling)
+            mcs_ul: Uplink MCS (only for STATIC scheduling)
+            nrb_dl: Number of RBs for downlink (only for STATIC scheduling)
+            nrb_ul: Number of RBs for uplink (only for STATIC scheduling)
+        """
+        raise NotImplementedError()
+
+    def set_enabled_for_ca(self, bts_index, enabled):
+        """ Enables or disables the base station during carrier aggregation.
+
+        Args:
+            bts_index: the base station number
+            enabled: whether the base station should be enabled for ca.
+        """
+        raise NotImplementedError()
+
+    def set_dl_modulation(self, bts_index, modulation):
+        """ Sets the DL modulation for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            modulation: the new DL modulation
+        """
+        raise NotImplementedError()
+
+    def set_ul_modulation(self, bts_index, modulation):
+        """ Sets the UL modulation for the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            modulation: the new UL modulation
+        """
+        raise NotImplementedError()
+
+    def set_tbs_pattern_on(self, bts_index, tbs_pattern_on):
+        """ Enables or disables TBS pattern in the indicated base station.
+
+        Args:
+            bts_index: the base station number
+            tbs_pattern_on: the new TBS pattern setting
+        """
+        raise NotImplementedError()
+
+    def lte_attach_secondary_carriers(self):
+        """ Activates the secondary carriers for CA. Requires the DUT to be
+        attached to the primary carrier first. """
+        raise NotImplementedError()
+
+    def wait_until_attached(self, timeout=120):
+        """ Waits until the DUT is attached to the primary carrier.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        raise NotImplementedError()
+
+    def wait_until_communication_state(self, timeout=120):
+        """ Waits until the DUT is in Communication state.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        raise NotImplementedError()
+
+    def wait_until_idle_state(self, timeout=120):
+        """ Waits until the DUT is in Idle state.
+
+        Args:
+            timeout: after this amount of time the method will raise a
+                CellularSimulatorError exception. Default is 120 seconds.
+        """
+        raise NotImplementedError()
+
+    def detach(self):
+        """ Turns off all the base stations so the DUT loose connection."""
+        raise NotImplementedError()
+
+    def stop(self):
+        """ Stops current simulation. After calling this method, the simulator
+        will need to be set up again. """
+        raise NotImplementedError()
+
+    def start_data_traffic(self):
+        """ Starts transmitting data from the instrument to the DUT. """
+        raise NotImplementedError()
+
+    def stop_data_traffic(self):
+        """ Stops transmitting data from the instrument to the DUT. """
+        raise NotImplementedError()
diff --git a/acts/framework/acts/controllers/rohdeschwarz_lib/contest.py b/acts/framework/acts/controllers/rohdeschwarz_lib/contest.py
index 7d6c790..f34a62b 100644
--- a/acts/framework/acts/controllers/rohdeschwarz_lib/contest.py
+++ b/acts/framework/acts/controllers/rohdeschwarz_lib/contest.py
@@ -47,7 +47,7 @@
     MAXIMUM_OUTPUT_READ_RETRIES = 25
 
     # Root directory for the FTP server in the remote computer
-    FTP_ROOT = 'D:\\Contest\\Reports\\reports\\'
+    FTP_ROOT = 'D:\\Logs\\'
 
     def __init__(self, logger, remote_ip, remote_port, automation_listen_ip,
                  automation_port, dut_on_func, dut_off_func, ftp_usr, ftp_pwd):
@@ -212,6 +212,9 @@
              a JSON object containing the test results
         """
 
+        if not testplan_directory:
+            raise ValueError('Invalid testplan directory.')
+
         # Download test reports from the remote host
         job.run('wget -r --user={} --password={} -P {} ftp://{}/{}'.format(
             self.ftp_user, self.ftp_pass, logging.log_path,
@@ -387,14 +390,17 @@
                               "DUT to off state.")
                 self.dut_off_func()
                 self.send_ok()
+            elif command.startswith(self.NOTIFICATION_TESTPLAN_START):
+                self.log.info('Test plan is starting.')
+                self.send_ok()
             elif command.startswith(self.NOTIFICATION_TESTCASE_START):
                 self.log.info('Test case is starting.')
                 self.send_ok()
-            elif command in [
-                self.NOTIFICATION_TESTPLAN_START,
-                self.NOTIFICATION_TESCASE_END,
-                self.NOTIFICATION_TESTPLAN_END
-            ]:
+            elif command.startswith(self.NOTIFICATION_TESCASE_END):
+                self.log.info('Test case finished.')
+                self.send_ok()
+            elif command.startswith(self.NOTIFICATION_TESTPLAN_END):
+                self.log.info('Test plan finished.')
                 self.send_ok()
             else:
                 self.log.error('Unhandled automation command: ' + command)
diff --git a/acts/framework/acts/metrics/loggers/blackbox.py b/acts/framework/acts/metrics/loggers/blackbox.py
index 0e33164..8d7aeca 100644
--- a/acts/framework/acts/metrics/loggers/blackbox.py
+++ b/acts/framework/acts/metrics/loggers/blackbox.py
@@ -14,12 +14,15 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import shutil
+
 from acts.metrics.core import ProtoMetric
 from acts.metrics.logger import MetricLogger
 
 
-class BlackboxMetricLogger(MetricLogger):
-    """A MetricLogger for logging and publishing Blackbox metrics.
+class BlackboxMappedMetricLogger(MetricLogger):
+    """A MetricLogger for logging and publishing Blackbox metrics from a dict.
+    The dict maps the metric name to the metric value.
 
     The logger will publish an ActsBlackboxMetricResult message, containing
     data intended to be uploaded to Blackbox. The message itself contains only
@@ -32,47 +35,68 @@
 
     Attributes:
         proto_module: The proto module for ActsBlackboxMetricResult.
-        metric_name: The name of the metric, used to determine output filename.
         metric_key: The metric key to use. If unset, the logger will use the
                     context's identifier.
-        metric_value: The metric value.
+        _metric_map: the map of metric_name -> metric_value to publish
+                to blackbox. If the metric value is set to None, the
+                metric will not be reported.
     """
 
     PROTO_FILE = 'protos/acts_blackbox.proto'
 
-    def __init__(self, metric_name, metric_key=None, event=None):
+    def __init__(self, metric_key=None, event=None, compiler_out=None):
         """Initializes a logger for Blackbox metrics.
 
         Args:
-            metric_name: The name of the metric.
             metric_key: The metric key to use. If unset, the logger will use
                         the context's identifier.
             event: The event triggering the creation of this logger.
+            compiler_out: The directory to store the compiled proto module.
         """
         super().__init__(event=event)
-        self.proto_module = self._compile_proto(self.PROTO_FILE)
-        if not metric_name:
-            raise ValueError("metric_name must be supplied.")
-        self.metric_name = metric_name
+        self.proto_module = self._compile_proto(self.PROTO_FILE,
+                                                compiler_out=compiler_out)
         self.metric_key = metric_key
-        self.metric_value = None
+        self._metric_map = {}
 
-    def _get_metric_key(self):
+    def _get_metric_key(self, metric_name):
         """Gets the metric key to use.
 
         If the metric_key is explicitly set, returns that value. Otherwise,
         extracts an identifier from the context.
+
+        Args:
+            metric_name: The name of the metric to report.
         """
         if self.metric_key:
             key = self.metric_key
         else:
             key = self._get_blackbox_identifier()
-        key = '%s.%s' % (key, self.metric_name)
+        key = '%s.%s' % (key, metric_name)
         return key
 
-    def _get_file_name(self):
-        """Gets the base file name to publish to."""
-        return 'blackbox_%s' % self.metric_name
+    def set_metric_data(self, metric_map):
+        """Sets the map of metrics to be uploaded to Blackbox. Note that
+        this will overwrite all existing added by this function or add_metric.
+
+        Args:
+            metric_map: the map of metric_name -> metric_value to publish
+                to blackbox. If the metric value is set to None, the
+                metric will not be reported.
+        """
+        self._metric_map = metric_map
+
+    def add_metric(self, metric_name, metric_value):
+        """Adds a metric value to be published later.
+
+        Note that if the metric name has already been added, the metric value
+        will be overwritten.
+
+        Args:
+            metric_name: the name of the metric.
+            metric_value: the value of the metric.
+        """
+        self._metric_map[metric_name] = metric_value
 
     def _get_blackbox_identifier(self):
         """Returns the testcase identifier, as expected by Blackbox."""
@@ -82,20 +106,63 @@
         parts = identifier.rsplit('.', 1)
         return '#'.join(parts)
 
-    def end(self, event):
+    def end(self, _):
         """Creates and publishes a ProtoMetric with blackbox data.
 
-        Builds an ActsBlackboxMetricResult message based on the result
-        generated, and passes it off to the publisher.
+        Builds a list of ActsBlackboxMetricResult messages from the set
+        metric data, and sends them to the publisher.
+        """
+        metrics = []
+        for metric_name, metric_value in self._metric_map.items():
+            if metric_value is None:
+                continue
+            result = self.proto_module.ActsBlackboxMetricResult()
+            result.test_identifier = self._get_blackbox_identifier()
+            result.metric_key = self._get_metric_key(metric_name)
+            result.metric_value = metric_value
+
+            metrics.append(
+                ProtoMetric(name='blackbox_%s' % metric_name, data=result))
+
+        return self.publisher.publish(metrics)
+
+
+class BlackboxMetricLogger(BlackboxMappedMetricLogger):
+    """A MetricLogger for logging and publishing individual Blackbox metrics.
+
+    For additional information on reporting to Blackbox, see
+    BlackboxMappedMetricLogger.
+
+    Attributes:
+        proto_module: The proto module for ActsBlackboxMetricResult.
+        metric_name: The name of the metric, used to determine output filename.
+        metric_key: The metric key to use. If unset, the logger will use the
+                    context's identifier.
+        metric_value: The metric value.
+    """
+
+    def __init__(self, metric_name, metric_key=None, event=None,
+                 compiler_out=None):
+        """Initializes a logger for Blackbox metrics.
 
         Args:
-            event: The triggering event.
+            metric_name: The name of the metric.
+            metric_key: The metric key to use. If unset, the logger will use
+                        the context's identifier.
+            event: The event triggering the creation of this logger.
+            compiler_out: The directory to store the compiled proto module
         """
-        result = self.proto_module.ActsBlackboxMetricResult()
-        result.test_identifier = self._get_blackbox_identifier()
-        result.metric_key = self._get_metric_key()
-        if self.metric_value is not None:
-            result.metric_value = self.metric_value
+        super().__init__(metric_key=metric_key, event=event,
+                         compiler_out=compiler_out)
+        if not metric_name:
+            raise ValueError("metric_name must be supplied.")
+        self.metric_name = metric_name
+        self.metric_value = None
 
-        metric = ProtoMetric(name=self._get_file_name(), data=result)
-        return self.publisher.publish(metric)
+    @property
+    def metric_value(self):
+        return self._metric_map[self.metric_name]
+
+    @metric_value.setter
+    def metric_value(self, value):
+        self.add_metric(self.metric_name, value)
diff --git a/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py b/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
index ec34b04..d6bcd05 100644
--- a/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
+++ b/acts/framework/acts/test_utils/bt/BluetoothBaseTest.py
@@ -49,18 +49,6 @@
     start_time = 0
     timer_list = []
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
-        for ad in self.android_devices:
-            self._setup_bt_libs(ad)
-        if 'preferred_device_order' in self.user_params:
-            prefered_device_order = self.user_params['preferred_device_order']
-            for i, ad in enumerate(self.android_devices):
-                if ad.serial in prefered_device_order:
-                    index = prefered_device_order.index(ad.serial)
-                    self.android_devices[i], self.android_devices[index] = \
-                        self.android_devices[index], self.android_devices[i]
-
     def collect_bluetooth_manager_metrics_logs(self, ads, test_name):
         """
         Collect Bluetooth metrics logs, save an ascii log to disk and return
@@ -131,6 +119,17 @@
         return _safe_wrap_test_case
 
     def setup_class(self):
+        super().setup_class()
+        for ad in self.android_devices:
+            self._setup_bt_libs(ad)
+        if 'preferred_device_order' in self.user_params:
+            prefered_device_order = self.user_params['preferred_device_order']
+            for i, ad in enumerate(self.android_devices):
+                if ad.serial in prefered_device_order:
+                    index = prefered_device_order.index(ad.serial)
+                    self.android_devices[i], self.android_devices[index] = \
+                        self.android_devices[index], self.android_devices[i]
+
         if "reboot_between_test_class" in self.user_params:
             threads = []
             for a in self.android_devices:
diff --git a/acts/framework/acts/test_utils/bt/pts/pts_base_class.py b/acts/framework/acts/test_utils/bt/pts/pts_base_class.py
index 32f8b1b..33fa81b 100644
--- a/acts/framework/acts/test_utils/bt/pts/pts_base_class.py
+++ b/acts/framework/acts/test_utils/bt/pts/pts_base_class.py
@@ -46,8 +46,8 @@
     scan_timeout_seconds = 10
     peer_identifier = None
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         if 'dut' in self.user_params:
             if self.user_params['dut'] == 'fuchsia_devices':
                 self.dut = create_bluetooth_device(self.fuchsia_devices[0])
@@ -122,7 +122,6 @@
             }
         }
 
-    def setup_class(self):
         self.pts.setup_pts()
         self.pts.bind_to(self.process_next_action)
 
diff --git a/acts/framework/acts/test_utils/coex/audio_capture.py b/acts/framework/acts/test_utils/coex/audio_capture.py
index 7e011f1..bb29dcc 100644
--- a/acts/framework/acts/test_utils/coex/audio_capture.py
+++ b/acts/framework/acts/test_utils/coex/audio_capture.py
@@ -27,6 +27,9 @@
 class DeviceNotFound(Exception):
     """Raises exception if audio capture device is not found."""
 
+# TODO: (@sairamganesh) This class will be deprecated for
+# ../acts/test_utils/coex/audio_capture_device.py
+
 
 class AudioCapture:
 
diff --git a/acts/framework/acts/test_utils/coex/audio_capture_device.py b/acts/framework/acts/test_utils/coex/audio_capture_device.py
new file mode 100644
index 0000000..7f32030
--- /dev/null
+++ b/acts/framework/acts/test_utils/coex/audio_capture_device.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2019 - 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 logging
+import os
+import pyaudio
+import wave
+
+from acts import context
+from acts import utils
+
+
+WAVE_FILE_TEMPLATE = 'recorded_audio_%s.wav'
+ADB_PATH = 'sdcard/Music/'
+ADB_FILE = 'rec.pcm'
+
+
+class AudioCaptureBase(object):
+    """Base class for Audio capture."""
+
+    def __init__(self):
+
+        self.wave_file = os.path.join(self.log_path, WAVE_FILE_TEMPLATE)
+        self.file_dir = self.log_path
+
+    @property
+    def log_path(self):
+        """Returns current log path."""
+        current_context = context.get_current_context()
+        full_out_dir = os.path.join(current_context.get_full_output_path(),
+                                    'AudioCapture')
+
+        utils.create_dir(full_out_dir)
+        return full_out_dir
+
+    @property
+    def next_fileno(self):
+        counter = 0
+        while os.path.exists(self.wave_file % counter):
+            counter += 1
+        return counter
+
+    @property
+    def last_fileno(self):
+        return self.next_fileno - 1
+
+    def write_record_file(self, audio_params, frames):
+        """Writes the recorded audio into the file.
+
+        Args:
+            audio_params: A dict with audio configuration.
+            frames: Recorded audio frames.
+
+        Returns:
+            file_name: wave file name.
+        """
+        file_name = self.wave_file % self.next_fileno
+        logging.debug('writing to %s' % file_name)
+        wf = wave.open(file_name, 'wb')
+        wf.setnchannels(audio_params['channel'])
+        wf.setsampwidth(audio_params['sample_width'])
+        wf.setframerate(audio_params['sample_rate'])
+        wf.writeframes(frames)
+        wf.close()
+        return file_name
+
+
+class CaptureAudioOverAdb(AudioCaptureBase):
+    """Class to capture audio over android device which acts as the
+    a2dp sink or hfp client. This captures the digital audio and converts
+    to analog audio for post processing.
+    """
+
+    def __init__(self, ad, audio_params):
+        """Initializes CaptureAudioOverAdb.
+
+        Args:
+            ad: An android device object.
+            audio_params: Dict containing audio record settings.
+        """
+        super().__init__()
+        self._ad = ad
+        self.audio_params = audio_params
+        self.adb_path = None
+
+    def start(self):
+        """Start the audio capture over adb."""
+        self.adb_path = os.path.join(ADB_PATH, ADB_FILE)
+        cmd = 'ap2f --usage 1 --start --duration {} --target {}'.format(
+            self.audio_params['duration'], self.adb_path,
+        )
+        self._ad.adb.shell(cmd)
+
+    def stop(self):
+        """Stops the audio capture and stores it in wave file.
+
+        Returns:
+            File name of the recorded file.
+        """
+        cmd = '{} {}'.format(self.adb_path, self.file_dir)
+        self._ad.adb.pull(cmd)
+        self._ad.adb.shell('rm {}'.format(self.adb_path))
+        return self._convert_pcm_to_wav()
+
+    def _convert_pcm_to_wav(self):
+        """Converts raw pcm data into wave file.
+
+        Returns:
+            file_path: Returns the file path of the converted file
+            (digital to analog).
+        """
+        file_to_read = os.path.join(self.file_dir, ADB_FILE)
+        with open(file_to_read, 'rb') as pcm_file:
+            frames = pcm_file.read()
+        file_path = self.write_record_file(self.audio_params, frames)
+        return file_path
+
+
+class CaptureAudioOverLocal(AudioCaptureBase):
+    """Class to capture audio on local server using the audio input devices
+    such as iMic/AudioBox. This class mandates input deivce to be connected to
+    the machine.
+    """
+    def __init__(self, audio_params):
+        """Initializes CaptureAudioOverLocal.
+
+        Args:
+            audio_params: Dict containing audio record settings.
+        """
+        super().__init__()
+        self.audio_params = audio_params
+        self.channels = self.audio_params['channel']
+        self.chunk = self.audio_params['chunk']
+        self.sample_rate = self.audio_params['sample_rate']
+        self.__input_device = None
+        self.audio = None
+        self.frames = []
+
+    @property
+    def name(self):
+        return self.__input_device["name"]
+
+    def __get_input_device(self):
+        """Checks for the audio capture device."""
+        if self.__input_device is None:
+            for i in range(self.audio.get_device_count()):
+                device_info = self.audio.get_device_info_by_index(i)
+                logging.debug('Device Information: {}'.format(device_info))
+                if self.audio_params['input_device'] in device_info['name']:
+                    self.__input_device = device_info
+                    break
+            else:
+                raise DeviceNotFound(
+                    'Audio Capture device {} not found.'.format(
+                        self.audio_params['input_device']))
+        return self.__input_device
+
+    def start(self, trim_beginning=0, trim_end=0):
+        """Starts audio recording on host machine.
+
+        Args:
+            trim_beginning: how many seconds to trim from the beginning
+            trim_end: how many seconds to trim from the end
+        """
+        self.audio = pyaudio.PyAudio()
+        self.__input_device = self.__get_input_device()
+        stream = self.audio.open(
+            format=pyaudio.paInt16,
+            channels=self.channels,
+            rate=self.sample_rate,
+            input=True,
+            frames_per_buffer=self.chunk,
+            input_device_index=self.__input_device['index'])
+        b_chunks = trim_beginning * (self.sample_rate // self.chunk)
+        e_chunks = trim_end * (self.sample_rate // self.chunk)
+        total_chunks = self.sample_rate // self.chunk * self.audio_params[
+            'duration']
+        for i in range(total_chunks):
+            try:
+                data = stream.read(self.chunk, exception_on_overflow=False)
+            except IOError as ex:
+                logging.error('Cannot record audio: {}'.format(ex))
+                return False
+            if b_chunks <= i < total_chunks - e_chunks:
+                self.frames.append(data)
+
+        stream.stop_stream()
+        stream.close()
+
+    def stop(self):
+        """Terminates the pulse audio instance.
+
+        Returns:
+            File name of the recorded audio file.
+        """
+        self.audio.terminate()
+        frames = b''.join(self.frames)
+        return self.write_record_file(self.audio_params, frames)
+
+
+class DeviceNotFound(Exception):
+    """Raises exception if audio capture device is not found."""
diff --git a/acts/framework/acts/test_utils/coex/audio_test_utils.py b/acts/framework/acts/test_utils/coex/audio_test_utils.py
index efee428..0fca41a 100644
--- a/acts/framework/acts/test_utils/coex/audio_test_utils.py
+++ b/acts/framework/acts/test_utils/coex/audio_test_utils.py
@@ -18,6 +18,8 @@
 import os
 import wave
 
+from acts.test_utils.coex.audio_capture_device import CaptureAudioOverAdb
+from acts.test_utils.coex.audio_capture_device import CaptureAudioOverLocal
 from acts.controllers.utils_lib.ssh import connection
 from acts.controllers.utils_lib.ssh import settings
 from acts.test_utils.audio_analysis_lib import audio_analysis
@@ -32,9 +34,47 @@
 bits_per_sample = 32
 
 
+def get_audio_capture_device(test_class_instance):
+    """Gets the device object of the audio capture device connected to server.
+
+    The audio capture device returned is specified by the audio_params
+    within user_params. audio_params must specify a "type" field, that
+    is either "AndroidDevice" or "Local"
+
+    Args:
+        test_class_instance: object self of test class.
+
+    Returns:
+        Object of the audio capture device.
+
+    Raises:
+        ValueError if audio_params['type'] is not "AndroidDevice" or
+            "Local".
+        ValueError if "AndroidDevice" is specified, but there is only one
+            AndroidDevice within the testbed.
+    """
+    audio_params = test_class_instance.user_params.get('audio_params')
+
+    if audio_params['type'] == 'AndroidDevice':
+        if len(test_class_instance.android_devices) > 1:
+            return CaptureAudioOverAdb(
+                test_class_instance.android_devices[-1], audio_params)
+        else:
+            raise ValueError('At least 2 or more AndroidDevice should be '
+                             'specified to use as audio capture endpoint.')
+    elif audio_params['type'] == 'Local':
+        return CaptureAudioOverLocal(audio_params)
+    else:
+        raise ValueError('Unrecognized audio capture device '
+                         '%s' % audio_params['type'])
+
+
 class FileNotFound(Exception):
     """Raises Exception if file is not present"""
 
+# TODO @sairamganesh Rename this class to AudioCaptureResult and
+# remove duplicates which are in ../test_utils/coex/audio_capture_device.py.
+
 
 class SshAudioCapture(AudioCapture):
 
diff --git a/acts/framework/acts/test_utils/gnss/gnss_test_utils.py b/acts/framework/acts/test_utils/gnss/gnss_test_utils.py
index 16285c2..2b4e6f3 100644
--- a/acts/framework/acts/test_utils/gnss/gnss_test_utils.py
+++ b/acts/framework/acts/test_utils/gnss/gnss_test_utils.py
@@ -42,6 +42,15 @@
     "TTFF_REPORT", "ttff_loop ttff_sec ttff_pe ttff_cn")
 TRACK_REPORT = collections.namedtuple(
     "TRACK_REPORT", "track_l5flag track_pe track_top4cn track_cn")
+LOCAL_PROP_FILE_CONTENTS =  """\
+log.tag.LocationManagerService=VERBOSE
+log.tag.GnssLocationProvider=VERBOSE
+log.tag.GnssMeasurementsProvider=VERBOSE
+log.tag.GpsNetInitiatedHandler=VERBOSE
+log.tag.GnssNetworkConnectivityHandler=VERBOSE
+log.tag.ConnectivityService=VERBOSE
+log.tag.ConnectivityManager=VERBOSE
+log.tag.GnssVisibilityControl=VERBOSE"""
 
 
 class GnssTestUtilsError(Exception):
@@ -88,11 +97,7 @@
     remount_device(ad)
     ad.log.info("Enable GNSS VERBOSE Logging and persistent logcat.")
     ad.adb.shell("echo DEBUG_LEVEL = 5 >> /vendor/etc/gps.conf")
-    ad.adb.shell("echo log.tag.LocationManagerService=VERBOSE >> /data/local.prop")
-    ad.adb.shell("echo log.tag.GnssLocationProvider=VERBOSE >> /data/local.prop")
-    ad.adb.shell("echo log.tag.GnssMeasurementsProvider=VERBOSE >> /data/local.prop")
-    ad.adb.shell("echo log.tag.GpsNetInitiatedHandler=VERBOSE >> /data/local.prop")
-    ad.adb.shell("echo log.tag.GnssNetworkConnectivityHandler=VERBOSE >> /data/local.prop")
+    ad.adb.shell("echo %r >> /data/local.prop" % LOCAL_PROP_FILE_CONTENTS)
     ad.adb.shell("chmod 644 /data/local.prop")
     ad.adb.shell("setprop persist.logd.size 16777216")
     ad.adb.shell("setprop persist.vendor.radio.adb_log_on 1")
@@ -284,7 +289,8 @@
     shutil.make_archive(gnss_log_path, "zip", gnss_log_path)
     shutil.rmtree(gnss_log_path)
     output_path = os.path.join(DEFAULT_QXDM_LOG_PATH, "logs/.")
-    file_count = ad.adb.shell("find %s -type f -iname *.qmdl | wc -l" % output_path)
+    file_count = ad.adb.shell(
+        "find %s -type f -iname *.qmdl | wc -l" % output_path)
     if not int(file_count) == 0:
         qxdm_log_name = "QXDM_%s_%s" % (ad.model, ad.serial)
         qxdm_log_path = os.path.join(log_path, qxdm_log_name)
@@ -328,7 +334,8 @@
         ad.log.error("Mobile data is at unknown state and set to %d" % out)
 
 def gnss_trigger_modem_ssr(ad, dwelltime=60):
-    """Trigger modem SSR crash and verify if modem crash and recover successfully.
+    """Trigger modem SSR crash and verify if modem crash and recover
+    successfully.
 
     Args:
         ad: An AndroidDevice object.
@@ -415,7 +422,6 @@
     remount_device(ad)
     pull_gtw_gpstool(ad)
     ad.adb.shell("settings put global verifier_verify_adb_installs 0")
-    ad.adb.shell("settings put global package_verifier_enable 0")
     reinstall_gtw_gpstool(ad)
 
 def fastboot_factory_reset(ad):
@@ -466,7 +472,6 @@
                 break
             ad.log.info("Re-install sl4a")
             ad.adb.shell("settings put global verifier_verify_adb_installs 0")
-            ad.adb.shell("settings put global package_verifier_enable 0")
             ad.adb.install("-r -g -t /tmp/base.apk")
             reinstall_gtw_gpstool(ad)
             time.sleep(10)
@@ -532,7 +537,8 @@
     for i in range(retries):
         begin_time = get_current_epoch_time()
         clear_aiding_data_by_gtw_gpstool(ad)
-        ad.log.info("Start %s on GTW_GPSTool - attempt %d" % (type.upper(), i+1))
+        ad.log.info("Start %s on GTW_GPSTool - attempt %d" % (type.upper(),
+                                                              i+1))
         start_gnss_by_gtw_gpstool(ad, True, type)
         for _ in range(10 + criteria):
             logcat_results = ad.search_logcat("First fixed", begin_time)
@@ -544,16 +550,16 @@
                 if (first_fixed/1000) <= criteria:
                     return True
                 start_gnss_by_gtw_gpstool(ad, False, type)
-                raise signals.TestFailure("Fail to get %s location fixed within "
-                                          "%d seconds criteria." %
-                                          (type.upper(), criteria))
+                raise signals.TestFailure("Fail to get %s location fixed "
+                                          "within %d seconds criteria."
+                                          % (type.upper(), criteria))
             time.sleep(1)
         if not ad.is_adb_logcat_on:
             ad.start_adb_logcat()
         check_currrent_focus_app(ad)
         start_gnss_by_gtw_gpstool(ad, False, type)
-    raise signals.TestFailure("Fail to get %s location fixed within %d attempts."
-                              % (type.upper(), retries))
+    raise signals.TestFailure("Fail to get %s location fixed within %d "
+                              "attempts." % (type.upper(), retries))
 
 def start_ttff_by_gtw_gpstool(ad, ttff_mode, iteration):
     """Identify which TTFF mode for different test items.
@@ -578,9 +584,9 @@
                             begin_time):
             ad.log.info("Send TTFF start_test_action successfully.")
             break
-        if i == 3:
-            check_currrent_focus_app(ad)
-            raise signals.TestFailure("Fail to send TTFF start_test_action.")
+    else:
+        check_currrent_focus_app(ad)
+        raise signals.TestFailure("Fail to send TTFF start_test_action.")
 
 def gnss_tracking_via_gtw_gpstool(ad, criteria, type="gnss", testtime=60):
     """Start GNSS/FLP tracking tests for input testtime on GTW_GPSTool.
@@ -621,6 +627,7 @@
     track_data = {}
     history_top4_cn = 0
     history_cn = 0
+    l5flag = "false"
     file_count = int(ad.adb.shell("find %s -type f -iname *.txt | wc -l"
                                   % GNSSSTATUS_LOG_PATH))
     if file_count != 1:
@@ -777,7 +784,8 @@
     elif any(float(ttff_data[key].ttff_sec) == 0.0 for key in ttff_data.keys()):
         ad.log.error("One or more TTFF %s Timeout" % ttff_mode)
         return False
-    elif any(float(ttff_data[key].ttff_sec) >= criteria for key in ttff_data.keys()):
+    elif any(float(ttff_data[key].ttff_sec) >= criteria for key in
+             ttff_data.keys()):
         ad.log.error("One or more TTFF %s are over test criteria %d seconds"
                      % (ttff_mode, criteria))
         return False
@@ -862,7 +870,8 @@
         ad: An AndroidDevice object.
     """
     time.sleep(1)
-    current = ad.adb.shell("dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'")
+    current = ad.adb.shell(
+        "dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'")
     ad.log.debug("\n"+current)
 
 def check_location_api(ad, retries):
@@ -884,7 +893,8 @@
             logcat_results = ad.search_logcat("REPORT_LOCATION", begin_time)
             if logcat_results:
                 ad.log.info("%s" % logcat_results[-1]["log_message"])
-                ad.log.info("GnssLocationProvider reports location successfully.")
+                ad.log.info("GnssLocationProvider reports location "
+                            "successfully.")
                 return True
         if not ad.is_adb_logcat_on:
             ad.start_adb_logcat()
@@ -907,10 +917,12 @@
         time.sleep(1)
         begin_time = get_current_epoch_time()
         ad.log.info("Try to get NLP status - attempt %d" % (i+1))
-        ad.adb.shell("am start -S -n com.android.gpstool/.GPSTool --es mode nlp")
+        ad.adb.shell(
+            "am start -S -n com.android.gpstool/.GPSTool --es mode nlp")
         while get_current_epoch_time() - begin_time <= 30000:
-            logcat_results = ad.search_logcat(
-                "LocationManagerService: incoming location: Location", begin_time)
+            logcat_results = ad.search_logcat("LocationManagerService: "
+                                              "incoming location: Location",
+                                              begin_time)
             if logcat_results:
                 for logcat_result in logcat_results:
                     if location_type in logcat_result["log_message"]:
@@ -932,7 +944,8 @@
         atten_value: attenuation value
     """
     try:
-        ad.log.info("Set attenuation value to \"%d\" for GNSS signal." % atten_value)
+        ad.log.info(
+            "Set attenuation value to \"%d\" for GNSS signal." % atten_value)
         attenuator[0].set_atten(atten_value)
     except Exception as e:
         ad.log.error(e)
@@ -987,7 +1000,8 @@
         ad.log.info("Open an youtube video - attempt %d" % (i+1))
         ad.adb.shell("am start -a android.intent.action.VIEW -d \"%s\"" % url)
         time.sleep(2)
-        out = ad.adb.shell("dumpsys activity | grep NewVersionAvailableActivity")
+        out = ad.adb.shell(
+            "dumpsys activity | grep NewVersionAvailableActivity")
         if out:
             ad.log.info("Skip Youtube New Version Update.")
             ad.send_keycode("BACK")
@@ -1008,12 +1022,72 @@
     """
     try:
         baseband_version = ad.adb.getprop("gsm.version.baseband")
-        gms_version = ad.adb.shell("dumpsys package com.google.android.gms | "
-                                   "grep versionName").split("\n")[0].split("=")[1]
+        gms_version = ad.adb.shell(
+            "dumpsys package com.google.android.gms | grep versionName"
+        ).split("\n")[0].split("=")[1]
+        mpss_version = ad.adb.shell("cat /sys/devices/soc0/images | grep MPSS "
+                                    "| cut -d ':' -f 3")
         if not extra_msg:
             ad.log.info("TestResult Baseband_Version %s" % baseband_version)
-            ad.log.info("TestResult GMS_Version %s" % gms_version.replace(" ", ""))
+            ad.log.info(
+                "TestResult GMS_Version %s" % gms_version.replace(" ", ""))
+            ad.log.info("TestResult MPSS_Version %s" % mpss_version)
         else:
-            ad.log.info("%s, Baseband_Version = %s" % (extra_msg, baseband_version))
+            ad.log.info(
+                "%s, Baseband_Version = %s" % (extra_msg, baseband_version))
     except Exception as e:
-        ad.log.error(e)
\ No newline at end of file
+        ad.log.error(e)
+
+def start_toggle_gnss_by_gtw_gpstool(ad, iteration):
+    """Send toggle gnss off/on start_test_action
+
+    Args:
+        ad: An AndroidDevice object.
+        iteration: Iteration of toggle gnss off/on cycles.
+    """
+    msg_list = []
+    begin_time = get_current_epoch_time()
+    try:
+        for i in range(1, 4):
+            ad.adb.shell("am start -S -n com.android.gpstool/.GPSTool "
+                         "--es mode toggle --es cycle %d" % iteration)
+            time.sleep(1)
+            if ad.search_logcat("cmp=com.android.gpstool/.ToggleGPS",
+                                begin_time):
+                ad.log.info("Send ToggleGPS start_test_action successfully.")
+                break
+        else:
+            check_currrent_focus_app(ad)
+            raise signals.TestFailure("Fail to send ToggleGPS "
+                                      "start_test_action within 3 attempts.")
+        time.sleep(2)
+        test_start = ad.search_logcat("GPSTool_ToggleGPS: startService",
+                                      begin_time)
+        if test_start:
+            ad.log.info(test_start[-1]["log_message"].split(":")[-1].strip())
+        else:
+            raise signals.TestFailure("Fail to start toggle GPS off/on test.")
+        # Every iteration is expected to finish within 4 minutes.
+        while get_current_epoch_time() - begin_time <= iteration * 240000:
+            crash_end = ad.search_logcat("Force finishing activity "
+                                         "com.android.gpstool/.GPSTool",
+                                         begin_time)
+            if crash_end:
+                raise signals.TestFailure("GPSTool crashed. Abort test.")
+            toggle_results = ad.search_logcat("GPSTool : msg", begin_time)
+            if toggle_results:
+                for toggle_result in toggle_results:
+                    msg = toggle_result["log_message"]
+                    if not msg in msg_list:
+                        ad.log.info(msg.split(":")[-1].strip())
+                        msg_list.append(msg)
+                    if "timeout" in msg:
+                        raise signals.TestFailure("Fail to get location fixed "
+                                                  "within 60 seconds.")
+                    if "Test end" in msg:
+                        raise signals.TestPass("Completed quick toggle GNSS "
+                                               "off/on test.")
+        raise signals.TestFailure("Fail to finish toggle GPS off/on test "
+                                  "within %d minutes" % (iteration * 4))
+    finally:
+        ad.send_keycode("HOME")
diff --git a/acts/framework/acts/test_utils/instrumentation/adb_commands/common.py b/acts/framework/acts/test_utils/instrumentation/adb_commands/common.py
index 2991600..ffd43dd 100644
--- a/acts/framework/acts/test_utils/instrumentation/adb_commands/common.py
+++ b/acts/framework/acts/test_utils/instrumentation/adb_commands/common.py
@@ -62,6 +62,11 @@
 nfc = DeviceState('svc nfc', 'enable', 'disable')
 
 
+# Calling
+
+disable_dialing = DeviceSetprop('ro.telephony.disable-call', 'true', 'false')
+
+
 # Screen
 
 screen_adaptive_brightness = DeviceSetting(
@@ -115,3 +120,12 @@
 
 disable_doze = 'dumpsys deviceidle disable'
 
+
+# Miscellaneous
+
+test_harness = DeviceBinaryCommandSeries(
+    [
+        DeviceSetprop('ro.monkey'),
+        DeviceSetprop('ro.test_harness')
+    ]
+)
diff --git a/acts/framework/acts/test_utils/instrumentation/instrumentation_base_test.py b/acts/framework/acts/test_utils/instrumentation/instrumentation_base_test.py
index d0e7948..fe00d33 100644
--- a/acts/framework/acts/test_utils/instrumentation/instrumentation_base_test.py
+++ b/acts/framework/acts/test_utils/instrumentation/instrumentation_base_test.py
@@ -140,25 +140,51 @@
         """Clean up device after test completion."""
         pass
 
-    def _get_controller_config(self, controller_name):
-        """Get the controller config from the instrumentation config, at the
-        level of the current test class or test case.
+    def _get_merged_config(self, config_name):
+        """Takes the configs with config_name from the base, testclass, and
+        testcase levels and merges them together. When the same parameter is
+        defined in different contexts, the value from the most specific context
+        is taken.
+
+        Example:
+            self._instrumentation_config = {
+                'sample_config': {
+                    'val_a': 5,
+                    'val_b': 7
+                },
+                'ActsTestClass': {
+                    'sample_config': {
+                        'val_b': 3,
+                        'val_c': 6
+                    },
+                    'acts_test_case': {
+                        'sample_config': {
+                            'val_c': 10,
+                            'val_d': 2
+                        }
+                    }
+                }
+            }
+
+            self._get_merged_config('sample_config') returns
+            {
+                'val_a': 5,
+                'val_b': 3,
+                'val_c': 10,
+                'val_d': 2
+            }
 
         Args:
-            controller_name: Name of the controller config to fetch
-        Returns: The controller config, as a ConfigWrapper
+            config_name: Name of the config to fetch
+        Returns: The merged config, as a ConfigWrapper
         """
+        merged_config = self._instrumentation_config.get_config(
+            config_name)
+        merged_config.update(self._class_config.get_config(config_name))
         if self.current_test_name:
-            # Return the testcase level config, used for setting up test
             case_config = self._class_config.get_config(self.current_test_name)
-            return case_config.get_config(controller_name)
-        else:
-            # Merge the base and testclass level configs, used for setting up
-            # class.
-            merged_config = self._instrumentation_config.get_config(
-                controller_name)
-            merged_config.update(self._class_config.get_config(controller_name))
-            return merged_config
+            merged_config.update(case_config.get_config(config_name))
+        return merged_config
 
     def adb_run(self, cmds):
         """Run the specified command, or list of commands, with the ADB shell.
diff --git a/acts/framework/acts/test_utils/instrumentation/intent_builder.py b/acts/framework/acts/test_utils/instrumentation/intent_builder.py
index 1fe4035..a1cc529 100644
--- a/acts/framework/acts/test_utils/instrumentation/intent_builder.py
+++ b/acts/framework/acts/test_utils/instrumentation/intent_builder.py
@@ -14,17 +14,10 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-from collections import defaultdict
+import collections
 
-TYPE_TO_FLAG = defaultdict(lambda: '--es')
-TYPE_TO_FLAG.update(
-    {
-        bool: '--ez',
-        int: '--ei',
-        float: '--ef',
-        str: '--es'
-    }
-)
+TYPE_TO_FLAG = collections.defaultdict(lambda: '--es')
+TYPE_TO_FLAG.update({bool: '--ez', int: '--ei', float: '--ef', str: '--es'})
 
 
 class IntentBuilder(object):
@@ -41,7 +34,7 @@
         self._component = None
         self._data_uri = None
         self._flags = []
-        self._key_value_params = {}
+        self._key_value_params = collections.OrderedDict()
 
     def set_action(self, action):
         """Set the intent action, as marked by the -a flag"""
@@ -85,6 +78,6 @@
                 str_value = str(value)
                 if isinstance(value, bool):
                     str_value = str_value.lower()
-                cmd.append(
-                    ' '.join((TYPE_TO_FLAG[type(value)], key, str_value)))
+                cmd.append(' '.join((TYPE_TO_FLAG[type(value)], key,
+                                     str_value)))
         return ' '.join(cmd).strip()
diff --git a/acts/framework/acts/test_utils/power/PowerBaseTest.py b/acts/framework/acts/test_utils/power/PowerBaseTest.py
index 08e7a9a..92fc603 100644
--- a/acts/framework/acts/test_utils/power/PowerBaseTest.py
+++ b/acts/framework/acts/test_utils/power/PowerBaseTest.py
@@ -50,7 +50,6 @@
     """Create a random obj with unknown attributes and value.
 
     """
-
     def __init__(self, **kwargs):
         self.__dict__.update(kwargs)
 
@@ -69,7 +68,6 @@
     """Base class for all wireless power related tests.
 
     """
-
     def __init__(self, controllers):
 
         base_test.BaseTestClass.__init__(self, controllers)
@@ -94,8 +92,7 @@
         self.mon.attach_device(self.dut)
 
         # Unpack the test/device specific parameters
-        TEST_PARAMS = self.TAG + '_params'
-        req_params = [TEST_PARAMS, 'custom_files']
+        req_params = ['custom_files']
         self.unpack_userparams(req_params)
         # Unpack the custom files based on the test configs
         for file in self.custom_files:
@@ -111,11 +108,14 @@
         asserts.abort_class_if(
             not self.threshold_file,
             'Required test pass/fail threshold file is missing')
-        asserts.abort_class_if(not self.rockbottom_script,
-                               'Required rockbottom setting script is missing')
+        asserts.abort_class_if(
+            not self.rockbottom_script,
+            'Required rockbottom setting script is missing')
 
         # Unpack test specific configs
-        self.unpack_testparams(getattr(self, TEST_PARAMS))
+        TEST_PARAMS = self.TAG + '_params'
+        self.test_params = self.user_params.get(TEST_PARAMS, {})
+        self.unpack_testparams(self.test_params)
         if hasattr(self, 'attenuators'):
             self.num_atten = self.attenuators[0].instrument.num_atten
             self.atten_level = self.unpack_custom_file(self.attenuation_file)
@@ -124,7 +124,7 @@
         self.mon_info = self.create_monsoon_info()
 
         # Sync device time, timezone and country code
-        utils.require_sl4a((self.dut,))
+        utils.require_sl4a((self.dut, ))
         utils.sync_device_time(self.dut)
         self.dut.droid.wifiSetCountryCode('US')
 
@@ -159,11 +159,18 @@
         self.mon.usb('on')
         self.power_logger.set_avg_power(self.power_result.metric_value)
         self.power_logger.set_testbed(self.testbed_name)
+
         # Take Bugreport
         if self.bug_report:
             begin_time = utils.get_current_epoch_time()
             self.dut.take_bug_report(self.test_name, begin_time)
 
+        # Allow the device to cooldown before executing the next test
+        last_test = self.current_test_name == self.results.requested[-1]
+        cooldown = self.test_params.get('cooldown', None)
+        if cooldown and not last_test:
+            time.sleep(cooldown)
+
     def teardown_class(self):
         """Clean up the test class after tests finish running
 
@@ -444,11 +451,10 @@
             iperf_result = ipf.IPerfResult(iperf_file)
 
             # Compute the throughput in Mbit/s
-            throughput = (
-                math.fsum(
-                    iperf_result.instantaneous_rates[self.start_meas_time:-1]) /
-                len(iperf_result.instantaneous_rates[self.start_meas_time:-1])
-            ) * 8 * (1.024**2)
+            throughput = (math.fsum(
+                iperf_result.instantaneous_rates[self.start_meas_time:-1]
+            ) / len(iperf_result.instantaneous_rates[self.start_meas_time:-1])
+                          ) * 8 * (1.024**2)
 
             self.log.info('The average throughput is {}'.format(throughput))
         except ValueError:
diff --git a/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py b/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py
index 9a3b100..d7bc63c 100644
--- a/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py
+++ b/acts/framework/acts/test_utils/power/PowerCellularLabBaseTest.py
@@ -19,6 +19,7 @@
 import acts.test_utils.power.PowerBaseTest as PBT
 import acts.controllers.cellular_simulator as simulator
 from acts.controllers.anritsu_lib import md8475_cellular_simulator as anritsu
+from acts.controllers.rohdeschwarz_lib import cmw500_cellular_simulator as cmw
 from acts.test_utils.power.tel_simulations.GsmSimulation import GsmSimulation
 from acts.test_utils.power.tel_simulations.LteSimulation import LteSimulation
 from acts.test_utils.power.tel_simulations.UmtsSimulation import UmtsSimulation
@@ -75,10 +76,6 @@
 
         super().setup_class()
 
-        # Gets the name of the interface from which packets are sent
-        if hasattr(self, 'packet_senders'):
-            self.pkt_sender = self.packet_senders[0]
-
         # Load calibration tables
         filename_calibration_table = (
             self.FILENAME_CALIBRATION_TABLE_UNFORMATTED.format(
@@ -91,9 +88,8 @@
                 self.log.debug(self.calibration_table)
                 break
 
-        # Store the value of the key to access the test config in the
-        # user_params dictionary.
-        self.PARAMS_KEY = self.TAG + "_params"
+        # Ensure the calibration table only contains non-negative values
+        self.ensure_valid_calibration_table(self.calibration_table)
 
         # Turn on airplane mode for all devices, as some might
         # be unused during the test
@@ -135,6 +131,18 @@
                     self.md8475a_ip_address)
             else:
                 raise ValueError('Invalid MD8475 version.')
+
+        elif hasattr(self, 'cmw500_ip') or hasattr(self, 'cmw500_port'):
+
+            for key in ['cmw500_ip', 'cmw500_port']:
+                if not hasattr(self, key):
+                    raise RuntimeError('The CMW500 cellular simulator '
+                                       'requires %s to be set in the '
+                                       'config file.' % key)
+
+            return cmw.CMW500CellularSimulator(self.cmw500_ip,
+                                               self.cmw500_port)
+
         else:
             raise RuntimeError(
                 'The simulator could not be initialized because '
@@ -196,14 +204,9 @@
         # Wait for new params to settle
         time.sleep(5)
 
-        # Start the simulation. This method will raise a RuntimeException if
+        # Start the simulation. This method will raise an exception if
         # the phone is unable to attach.
-        try:
-            self.simulation.start()
-        except RuntimeError:
-            return False
-
-        self.simulation.start_test_case()
+        self.simulation.start()
 
         # Make the device go to sleep
         self.dut.droid.goToSleepNow()
@@ -352,5 +355,22 @@
         # Instantiate a new simulation
         self.simulation = simulation_class(self.cellular_simulator, self.log,
                                            self.dut,
-                                           self.user_params[self.PARAMS_KEY],
+                                           self.test_params,
                                            self.calibration_table[sim_type])
+
+    def ensure_valid_calibration_table(self, calibration_table):
+        """ Ensures the calibration table has the correct structure.
+
+        A valid calibration table is a nested dictionary with non-negative
+        number values
+
+        """
+        if not calibration_table or not isinstance(calibration_table, dict):
+            raise TypeError('The calibration table must be a dictionary')
+        for val in calibration_table.values():
+            if isinstance(val, dict):
+                self.ensure_valid_calibration_table(val)
+            elif not isinstance(val, float) and not isinstance(val, int):
+                raise TypeError('Calibration table value must be a number')
+            elif val < 0.0:
+                raise ValueError('Calibration table contains negative values')
diff --git a/acts/framework/acts/test_utils/power/tel_simulations/BaseSimulation.py b/acts/framework/acts/test_utils/power/tel_simulations/BaseSimulation.py
index 6ce7f85..091c18e 100644
--- a/acts/framework/acts/test_utils/power/tel_simulations/BaseSimulation.py
+++ b/acts/framework/acts/test_utils/power/tel_simulations/BaseSimulation.py
@@ -18,16 +18,14 @@
 from enum import Enum
 
 import numpy as np
-
-from acts.controllers.anritsu_lib._anritsu_utils import AnritsuError
-from acts.controllers.anritsu_lib.md8475a import BtsNumber
+from acts.controllers import cellular_simulator
 from acts.test_utils.tel.tel_test_utils import get_telephony_signal_strength
 from acts.test_utils.tel.tel_test_utils import toggle_airplane_mode
 from acts.test_utils.tel.tel_test_utils import toggle_cell_data_roaming
 
 
 class BaseSimulation():
-    """ Base class for an Anritsu Simulation abstraction.
+    """ Base class for cellular connectivity simulations.
 
     Classes that inherit from this base class implement different simulation
     setups. The base class contains methods that are common to all simulation
@@ -37,10 +35,8 @@
 
     NUM_UL_CAL_READS = 3
     NUM_DL_CAL_READS = 5
-    DL_CAL_TARGET_POWER = {'A': -15.0, 'B': -35.0}
     MAX_BTS_INPUT_POWER = 30
     MAX_PHONE_OUTPUT_POWER = 23
-    DL_MAX_POWER = {'A': -10.0, 'B': -30.0}
     UL_MIN_POWER = -60.0
 
     # Key to read the calibration setting from the test_config dictionary.
@@ -86,6 +82,7 @@
             parameters to None. """
             self.output_power = None
             self.input_power = None
+            self.band = None
 
         def incorporate(self, new_config):
             """ Incorporates a different configuration by replacing the current
@@ -111,7 +108,6 @@
         """
 
         self.simulator = simulator
-        self.anritsu = simulator.anritsu
         self.log = log
         self.dut = dut
         self.calibration_table = calibration_table
@@ -128,9 +124,6 @@
         self.calibration_required = test_config.get(self.KEY_CALIBRATION,
                                                     False)
 
-        # Gets BTS1 since this sim only has 1 BTS
-        self.bts1 = self.anritsu.get_BTS(BtsNumber.BTS1)
-
         # Configuration object for the primary base station
         self.primary_config = self.BtsConfig()
 
@@ -155,25 +148,17 @@
         # Enable roaming on the phone
         toggle_cell_data_roaming(self.dut, True)
 
-        # Load callbox config files
-        self.callbox_config_path = self.CALLBOX_PATH_FORMAT_STR.format(
-            self.anritsu._md8475_version)
-        self.load_config_files()
-
         # Make sure airplane mode is on so the phone won't attach right away
         toggle_airplane_mode(self.log, self.dut, True)
 
         # Wait for airplane mode setting to propagate
         time.sleep(2)
 
-        # Start simulation if it wasn't started
-        self.anritsu.start_simulation()
+        # Prepare the simulator for this simulation setup
+        self.setup_simulator()
 
-    def load_config_files(self):
-        """ Loads configuration files for the simulation.
-
-        This method needs to be implement by derived simulation classes. """
-
+    def setup_simulator(self):
+        """ Do initial configuration in the simulator. """
         raise NotImplementedError()
 
     def attach(self):
@@ -196,7 +181,7 @@
         new_config = self.BtsConfig()
         new_config.input_power = -10
         new_config.output_power = -30
-        self.configure_bts(self.bts1, new_config)
+        self.simulator.configure_bts(new_config)
         self.primary_config.incorporate(new_config)
 
         # Try to attach the phone.
@@ -208,15 +193,14 @@
                 toggle_airplane_mode(self.log, self.dut, False)
 
                 # Wait for the phone to attach.
-                self.anritsu.wait_for_registration_state(
-                    time_to_wait=self.ATTACH_WAITING_TIME)
+                self.simulator.wait_until_attached(
+                    timeout=self.ATTACH_WAITING_TIME)
 
-            except AnritsuError as e:
+            except cellular_simulator.CellularSimulatorError:
 
                 # The phone failed to attach
                 self.log.info(
                     "UE failed to attach on attempt number {}.".format(i + 1))
-                self.log.info("Error message: {}".format(str(e)))
 
                 # Turn airplane mode on to prepare the phone for a retry.
                 toggle_airplane_mode(self.log, self.dut, True)
@@ -253,21 +237,13 @@
         # Wait for APM to propagate
         time.sleep(2)
 
-        # Try to power off the basestation. An exception will be raised if the
-        # simulation is not running, which is ok because it means the phone is
-        # not attached.
-        try:
-            self.anritsu.set_simulation_state_to_poweroff()
-        except AnritsuError:
-            self.log.warning('Could not power off the basestation. The '
-                             'simulation might be stopped.')
+        # Power off basestation
+        self.simulator.detach()
 
     def stop(self):
         """  Detach phone from the basestation by stopping the simulation.
 
-        Send stop command to anritsu and turn on airplane mode.
-
-        """
+        Stop the simulation and turn airplane mode on. """
 
         # Set the DUT to airplane mode so it doesn't see the
         # cellular network going off
@@ -277,28 +253,39 @@
         time.sleep(2)
 
         # Stop the simulation
-        self.anritsu.stop_simulation()
+        self.simulator.stop()
 
     def start(self):
         """ Start the simulation by attaching the phone and setting the
         required DL and UL power.
 
         Note that this refers to starting the simulated testing environment
-        and not to starting the simulation in the Anritsu callbox, which was
-        done during the class initialization. """
+        and not to starting the signaling on the cellular instruments,
+        which might have been done earlier depending on the cellular
+        instrument controller implementation. """
 
         if not self.attach():
             raise RuntimeError('Could not attach to base station.')
 
+        # Starts IP traffic while changing this setting to force the UE to be
+        # in Communication state, as UL power cannot be set in Idle state
+        self.start_traffic_for_calibration()
+
+        # Wait until it goes to communication state
+        self.simulator.wait_until_communication_state()
+
         # Set signal levels obtained from the test parameters
         new_config = self.BtsConfig()
         new_config.output_power = self.calibrated_downlink_rx_power(
             self.primary_config, self.sim_dl_power)
         new_config.input_power = self.calibrated_uplink_tx_power(
             self.primary_config, self.sim_ul_power)
-        self.configure_bts(self.bts1, new_config)
+        self.simulator.configure_bts(new_config)
         self.primary_config.incorporate(new_config)
 
+        # Stop IP traffic after setting the UL power level
+        self.stop_traffic_for_calibration()
+
     def parse_parameters(self, parameters):
         """ Configures simulation using a list of parameters.
 
@@ -311,22 +298,6 @@
 
         raise NotImplementedError()
 
-    def configure_bts(self, bts_handle, config):
-        """ Configures the base station in the Anritsu callbox.
-
-        Parameters set to None in the configuration object are skipped.
-
-        Args:
-            bts_handle: a handle to the Anritsu base station controller.
-            config: a BtsConfig object containing the desired configuration.
-        """
-
-        if config.output_power:
-            bts_handle.output_level = config.output_power
-
-        if config.input_power:
-            bts_handle.input_level = config.input_power
-
     def consume_parameter(self, parameters, parameter_name, num_values=0):
         """ Parses a parameter from a list.
 
@@ -419,14 +390,12 @@
         # throw an TypeError exception
         try:
             calibrated_power = round(power + self.dl_path_loss)
-            if (calibrated_power >
-                    self.DL_MAX_POWER[self.anritsu._md8475_version]):
+            if calibrated_power > self.simulator.MAX_DL_POWER:
                 self.log.warning(
                     "Cannot achieve phone DL Rx power of {} dBm. Requested TX "
                     "power of {} dBm exceeds callbox limit!".format(
                         power, calibrated_power))
-                calibrated_power = self.DL_MAX_POWER[
-                    self.anritsu._md8475_version]
+                calibrated_power = self.simulator.MAX_DL_POWER
                 self.log.warning(
                     "Setting callbox Tx power to max possible ({} dBm)".format(
                         calibrated_power))
@@ -467,13 +436,6 @@
         else:
             power = signal_level
 
-        # Starts IP traffic while changing this setting to force the UE to be
-        # in Communication state, as UL power cannot be set in Idle state
-        self.start_traffic_for_calibration()
-
-        # Wait until it goes to communication state
-        self.anritsu.wait_for_communication_state()
-
         # Try to use measured path loss value. If this was not set, it will
         # throw an TypeError exception
         try:
@@ -503,9 +465,6 @@
                           "uncalibrated).".format(round(power)))
             return round(power)
 
-        # Stop IP traffic after setting the UL power level
-        self.stop_traffic_for_calibration()
-
     def calibrate(self, band):
         """ Calculates UL and DL path loss if it wasn't done before.
 
@@ -540,27 +499,15 @@
             Starts UDP IP traffic before running calibration. Uses APN_1
             configured in the phone.
         """
-        try:
-            self.anritsu.start_ip_traffic()
-        except AnritsuError as inst:
-            # This typically happens when traffic is already running
-            self.log.warning("{}\n".format(inst))
-        time.sleep(4)
+        self.simulator.start_data_traffic()
 
     def stop_traffic_for_calibration(self):
         """
             Stops IP traffic after calibration.
         """
-        try:
-            self.anritsu.stop_ip_traffic()
-        except AnritsuError as inst:
-            # This typically happens when traffic has already been stopped
-            self.log.warning("{}\n".format(inst))
-        time.sleep(2)
+        self.simulator.stop_data_traffic()
 
-    def downlink_calibration(self,
-                             rat=None,
-                             power_units_conversion_func=None):
+    def downlink_calibration(self, rat=None, power_units_conversion_func=None):
         """ Computes downlink path loss and returns the calibration value
 
         The DUT needs to be attached to the base station before calling this
@@ -591,9 +538,8 @@
         # Set BTS to a good output level to minimize measurement error
         initial_screen_timeout = self.dut.droid.getScreenTimeout()
         new_config = self.BtsConfig()
-        new_config.output_power = self.DL_CAL_TARGET_POWER[
-            self.anritsu._md8475_version]
-        self.configure_bts(self.bts1, new_config)
+        new_config.output_power = self.simulator.MAX_DL_POWER - 5
+        self.simulator.configure_bts(new_config)
 
         # Set phone sleep time out
         self.dut.droid.setScreenTimeout(1800)
@@ -619,7 +565,7 @@
         # Reset phone and bts to original settings
         self.dut.droid.goToSleepNow()
         self.dut.droid.setScreenTimeout(initial_screen_timeout)
-        self.configure_bts(self.bts1, restoration_config)
+        self.simulator.configure_bts(restoration_config)
         time.sleep(2)
 
         # Calculate the mean of the measurements
@@ -633,8 +579,7 @@
             avg_down_power = reported_asu_power
 
         # Calculate Path Loss
-        dl_target_power = self.DL_CAL_TARGET_POWER[
-            self.anritsu._md8475_version]
+        dl_target_power = self.simulator.MAX_DL_POWER - 5
         down_call_path_loss = dl_target_power - avg_down_power
 
         # Validate the result
@@ -668,7 +613,7 @@
         initial_screen_timeout = self.dut.droid.getScreenTimeout()
         new_config = self.BtsConfig()
         new_config.input_power = self.MAX_BTS_INPUT_POWER
-        self.configure_bts(self.bts1, new_config)
+        self.simulator.configure_bts(new_config)
 
         # Set phone sleep time out
         self.dut.droid.setScreenTimeout(1800)
@@ -706,7 +651,7 @@
         # Reset phone and bts to original settings
         self.dut.droid.goToSleepNow()
         self.dut.droid.setScreenTimeout(initial_screen_timeout)
-        self.configure_bts(self.bts1, restoration_config)
+        self.simulator.configure_bts(restoration_config)
         time.sleep(2)
 
         # Phone only supports 1x1 Uplink so always chain 0
@@ -729,26 +674,18 @@
 
         return up_call_path_loss
 
-    def set_band(self, bts, band, calibrate_if_necessary=True):
-        """ Sets the band used for communication.
-
-        When moving to a new band, recalibrate the link.
-
-        Args:
-            bts: basestation handle
-            band: desired band
-            calibrate_if_necessary: if False calibration will be skipped
-        """
-
-        bts.band = band
-        time.sleep(5)  # It takes some time to propagate the new band
-
-        # Invalidate the calibration values
+    def load_pathloss_if_required(self):
+        """ If calibration is required, try to obtain the pathloss values from
+        the calibration table and measure them if they are not available. """
+        # Invalidate the previous values
         self.dl_path_loss = None
         self.ul_path_loss = None
 
-        # Only calibrate when required.
-        if self.calibration_required and calibrate_if_necessary:
+        # Load the new ones
+        if self.calibration_required:
+
+            band = self.primary_config.band
+
             # Try loading the path loss values from the calibration table. If
             # they are not available, use the automated calibration procedure.
             try:
@@ -791,19 +728,3 @@
             Maximum throughput in mbps
         """
         raise NotImplementedError()
-
-    def start_test_case(self):
-        """ Starts a test case in the current simulation.
-
-        Requires the phone to be attached.
-        """
-
-        pass
-
-    def wait_for_rrc_idle_state(self, wait_time):
-        """ Waits for UE RRC state change to idle mode.
-
-        Raises exception when UE fails to move to idle state
-        """
-
-        self.anritsu.wait_for_idle_state(wait_time)
diff --git a/acts/framework/acts/test_utils/power/tel_simulations/GsmSimulation.py b/acts/framework/acts/test_utils/power/tel_simulations/GsmSimulation.py
index 4af6bf2..6dc2082 100644
--- a/acts/framework/acts/test_utils/power/tel_simulations/GsmSimulation.py
+++ b/acts/framework/acts/test_utils/power/tel_simulations/GsmSimulation.py
@@ -16,7 +16,9 @@
 
 import ntpath
 
+import time
 from acts.controllers.anritsu_lib.md8475a import BtsGprsMode
+from acts.controllers.anritsu_lib.md8475a import BtsNumber
 from acts.controllers.anritsu_lib import md8475_cellular_simulator as anritsusim
 from acts.test_utils.power.tel_simulations.BaseSimulation import BaseSimulation
 from acts.test_utils.tel.anritsu_utils import GSM_BAND_DCS1800
@@ -27,9 +29,7 @@
 
 
 class GsmSimulation(BaseSimulation):
-    """ Simple GSM simulation with only one basestation.
-
-    """
+    """ Single base station GSM. """
 
     # Simulation config files in the callbox computer.
     # These should be replaced in the future by setting up
@@ -55,7 +55,7 @@
     }
 
     def __init__(self, simulator, log, dut, test_config, calibration_table):
-        """ Configures Anritsu system for GSM simulation with 1 basetation
+        """ Initializes the simulator for a single-carrier GSM simulation.
 
         Loads a simple LTE simulation enviroment with 1 basestation. It also
         creates the BTS handle so we can change the parameters as desired.
@@ -69,15 +69,18 @@
                 different bands.
 
         """
-
-        super().__init__(simulator, log, dut, test_config, calibration_table)
-
         # The GSM simulation relies on the cellular simulator to be a MD8475
         if not isinstance(self.simulator, anritsusim.MD8475CellularSimulator):
             raise ValueError('The GSM simulation relies on the simulator to '
                              'be an Anritsu MD8475 A/B instrument.')
 
+        # The Anritsu controller needs to be unwrapped before calling
+        # super().__init__ because setup_simulator() requires self.anritsu and
+        # will be called during the parent class initialization.
         self.anritsu = self.simulator.anritsu
+        self.bts1 = self.anritsu.get_BTS(BtsNumber.BTS1)
+
+        super().__init__(simulator, log, dut, test_config, calibration_table)
 
         if not dut.droid.telephonySetPreferredNetworkTypesForSubscription(
                 NETWORK_MODE_GSM_ONLY,
@@ -86,13 +89,20 @@
         else:
             log.info("Preferred network type set.")
 
-    def load_config_files(self):
-        """ Loads configuration files for the simulation. """
+    def setup_simulator(self):
+        """ Do initial configuration in the simulator. """
+
+        # Load callbox config files
+        callbox_config_path = self.CALLBOX_PATH_FORMAT_STR.format(
+            self.anritsu._md8475_version)
 
         self.anritsu.load_simulation_paramfile(
-            ntpath.join(self.callbox_config_path, self.GSM_BASIC_SIM_FILE))
+            ntpath.join(callbox_config_path, self.GSM_BASIC_SIM_FILE))
         self.anritsu.load_cell_paramfile(
-            ntpath.join(self.callbox_config_path, self.GSM_CELL_FILE))
+            ntpath.join(callbox_config_path, self.GSM_CELL_FILE))
+
+        # Start simulation if it wasn't started
+        self.anritsu.start_simulation()
 
     def parse_parameters(self, parameters):
         """ Configs a GSM simulation using a list of parameters.
@@ -113,6 +123,7 @@
                 "the required band number.".format(self.PARAM_BAND))
 
         self.set_band(self.bts1, values[1])
+        self.load_pathloss_if_required()
 
         # Setup GPRS mode
 
@@ -139,3 +150,14 @@
                     self.PARAM_SLOTS))
 
         self.bts1.gsm_slots = (int(values[1]), int(values[2]))
+
+    def set_band(self, bts, band):
+        """ Sets the band used for communication.
+
+        Args:
+            bts: basestation handle
+            band: desired band
+        """
+
+        bts.band = band
+        time.sleep(5)  # It takes some time to propagate the new band
diff --git a/acts/framework/acts/test_utils/power/tel_simulations/LteCaSimulation.py b/acts/framework/acts/test_utils/power/tel_simulations/LteCaSimulation.py
index d5e2840..6273833 100644
--- a/acts/framework/acts/test_utils/power/tel_simulations/LteCaSimulation.py
+++ b/acts/framework/acts/test_utils/power/tel_simulations/LteCaSimulation.py
@@ -14,19 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 import re
-import time
-
+from acts.controllers.anritsu_lib.md8475a import BtsNumber
 from acts.controllers.anritsu_lib.md8475a import BtsTechnology
 from acts.controllers.anritsu_lib.md8475a import LteMimoMode
-from acts.controllers.anritsu_lib.md8475a import BtsNumber
-from acts.controllers.anritsu_lib.md8475a import BtsPacketRate
-from acts.controllers.anritsu_lib.md8475a import TestProcedure
-from acts.controllers.anritsu_lib.md8475a import TestPowerControl
-from acts.controllers.anritsu_lib.md8475a import TestMeasurement
-from acts.test_utils.power.tel_simulations.LteSimulation import LteSimulation
+from acts.test_utils.power.tel_simulations import LteSimulation
 
 
-class LteCaSimulation(LteSimulation):
+class LteCaSimulation(LteSimulation.LteSimulation):
+    """ Carrier aggregation LTE simulation. """
 
     # Dictionary of lower DL channel number bound for each band.
     LOWEST_DL_CN_DICTIONARY = {
@@ -72,26 +67,20 @@
         42: 45590
     }
 
-    # Simulation config files in the callbox computer.
-    # These should be replaced in the future by setting up
-    # the same configuration manually.
-    LTE_BASIC_SIM_FILE = 'SIM_LTE_CA.wnssp'
-    LTE_BASIC_CELL_FILE = 'CELL_LTE_CA_config.wnscp'
-
     # Simulation config keywords contained in the test name
     PARAM_CA = 'ca'
 
     # Test config keywords
     KEY_FREQ_BANDS = "freq_bands"
 
-    def __init__(self, anritsu, log, dut, test_config, calibration_table):
-        """ Configures Anritsu system for LTE simulation with carrier
+    def __init__(self, simulator, log, dut, test_config, calibration_table):
+        """ Initializes the simulator for LTE simulation with carrier
         aggregation.
 
         Loads a simple LTE simulation enviroment with 5 basestations.
 
         Args:
-            anritsu: the Anritsu callbox controller
+            simulator: the cellular instrument controller
             log: a logger handle
             dut: the android device handler
             test_config: test configuration obtained from the config file
@@ -100,72 +89,57 @@
 
         """
 
-        super().__init__(anritsu, log, dut, test_config, calibration_table)
+        super().__init__(simulator, log, dut, test_config, calibration_table)
 
-        self.bts = [self.bts1, self.anritsu.get_BTS(BtsNumber.BTS2)]
+        self.anritsu = simulator.anritsu
+
+        self.bts = [
+            self.anritsu.get_BTS(BtsNumber.BTS1),
+            self.anritsu.get_BTS(BtsNumber.BTS2)
+        ]
 
         if self.anritsu._md8475_version == 'B':
             self.bts.extend([
-                anritsu.get_BTS(BtsNumber.BTS3),
-                anritsu.get_BTS(BtsNumber.BTS4)
+                self.anritsu.get_BTS(BtsNumber.BTS3),
+                self.anritsu.get_BTS(BtsNumber.BTS4)
             ])
 
         # Create a configuration object for each base station and copy initial
         # settings from the PCC base station.
         self.bts_configs = [self.primary_config]
 
-        for bts_index in range(1, len(self.bts)):
+        for bts_index in range(1, self.simulator.LTE_MAX_CARRIERS):
             new_config = self.BtsConfig()
             new_config.incorporate(self.primary_config)
-            self.configure_bts(self.bts[bts_index], new_config)
+            self.simulator.configure_bts(new_config, bts_index)
             self.bts_configs.append(new_config)
 
         # Get LTE CA frequency bands setting from the test configuration
         if self.KEY_FREQ_BANDS not in test_config:
             self.log.warning("The key '{}' is not set in the config file. "
                              "Setting to null by default.".format(
-                self.KEY_FREQ_BANDS))
+                                 self.KEY_FREQ_BANDS))
 
         self.freq_bands = test_config.get(self.KEY_FREQ_BANDS, True)
 
-    def configure_bts(self, bts_handle, config):
-        """ Adds LTE with CA specific procedures. See parent class
-        implementation for more details.
-
-        Args:
-            bts_handle: a handle to the Anritsu base station controller.
-            config: a BtsConfig object containing the desired configuration.
-        """
-
-        # The callbox won't restore the band-dependent default values if the
-        # request is to switch to the same band as the one the base station is
-        # currently using. To ensure that default values are restored, go to a
-        # different band before switching.
-        if config.band and int(bts_handle.band) == config.band:
-            # Using bands 1 and 2 but it could be any others
-            bts_handle.band = '1' if config.band != 1 else '2'
-            # Switching to config.band will be handled by the parent class
-            # implementation of this method.
-
-        super().configure_bts(bts_handle, config)
+    def setup_simulator(self):
+        """ Do initial configuration in the simulator. """
+        self.simulator.setup_lte_ca_scenario()
 
     def parse_parameters(self, parameters):
         """ Configs an LTE simulation with CA using a list of parameters.
 
-        Calls the parent method first, then consumes parameters specific to LTE
-
         Args:
             parameters: list of parameters
         """
 
         # Enable all base stations initially. The ones that are not needed after
         # parsing the CA combo string can be removed.
-        self.anritsu.set_simulation_model(
-            BtsTechnology.LTE,
-            BtsTechnology.LTE,
-            BtsTechnology.LTE,
-            BtsTechnology.LTE,
-            reset=False)
+        self.anritsu.set_simulation_model(BtsTechnology.LTE,
+                                          BtsTechnology.LTE,
+                                          BtsTechnology.LTE,
+                                          BtsTechnology.LTE,
+                                          reset=False)
 
         # Create an empty array for new configuration objects. Elements will be
         # added to this list after parsing the CA configuration from the band
@@ -214,7 +188,7 @@
 
             if ca_class.upper() == 'A':
 
-                if bts_index >= len(self.bts):
+                if bts_index >= self.simulator.LTE_MAX_CARRIERS:
                     raise ValueError("This callbox model doesn't allow the "
                                      "requested CA configuration")
 
@@ -228,7 +202,7 @@
 
             elif ca_class.upper() == 'C':
 
-                if bts_index + 1 >= len(self.bts):
+                if bts_index + 1 >= self.simulator.LTE_MAX_CARRIERS:
                     raise ValueError("This callbox model doesn't allow the "
                                      "requested CA configuration")
 
@@ -355,7 +329,7 @@
                      for elem in LteSimulation.MimoMode})
 
             if (requested_mimo == LteSimulation.MimoMode.MIMO_4x4
-                    and self.anritsu._md8475_version == 'A'):
+                    and not self.simulator.LTE_SUPPORTS_4X4_MIMO):
                 raise ValueError("The test requires 4x4 MIMO, but that is not "
                                  "supported by the MD8475A callbox.")
 
@@ -436,16 +410,16 @@
                     "The '{}' parameter was not set, using 100% RBs for both "
                     "DL and UL. To set the percentages of total RBs include "
                     "the '{}' parameter followed by two ints separated by an "
-                    "underscore indicating downlink and uplink percentages."
-                    .format(self.PARAM_PATTERN, self.PARAM_PATTERN))
+                    "underscore indicating downlink and uplink percentages.".
+                    format(self.PARAM_PATTERN, self.PARAM_PATTERN))
                 dl_pattern = 100
                 ul_pattern = 100
             else:
                 dl_pattern = int(values[1])
                 ul_pattern = int(values[2])
 
-            if (dl_pattern, ul_pattern) not in [(0, 100), (100, 0), (100,
-                                                                     100)]:
+            if (dl_pattern, ul_pattern) not in [(0, 100), (100, 0),
+                                                (100, 100)]:
                 raise ValueError(
                     "Only full RB allocation for DL or UL is supported in CA "
                     "sims. The allowed combinations are 100/0, 0/100 and "
@@ -469,58 +443,30 @@
 
                 dl_rbs, ul_rbs = self.allocation_percentages_to_rbs(
                     new_configs[bts_index].bandwidth,
-                    new_configs[bts_index].transmission_mode,
-                    dl_pattern, ul_pattern)
+                    new_configs[bts_index].transmission_mode, dl_pattern,
+                    ul_pattern)
 
                 new_configs[bts_index].dl_rbs = dl_rbs
                 new_configs[bts_index].ul_rbs = ul_rbs
                 new_configs[bts_index].dl_mcs = mcs_dl
                 new_configs[bts_index].ul_mcs = mcs_ul
 
+        # Enable the configured base stations for CA
+        for bts_config in new_configs:
+            bts_config.dl_cc_enabled = True
+
         # Setup the base stations with the obtained configurations and then save
         # these parameters in the current configuration objects
         for bts_index in range(len(new_configs)):
-            self.configure_bts(self.bts[bts_index], new_configs[bts_index])
+            self.simulator.configure_bts(new_configs[bts_index], bts_index)
             self.bts_configs[bts_index].incorporate(new_configs[bts_index])
 
-    def start_test_case(self):
-        """ Attaches the phone to all the other basestations.
-
-        Starts the CA test case. Requires being attached to
-        basestation 1 first.
-
-        """
-
         # Trigger UE capability enquiry from network to get
         # UE supported CA band combinations. Here freq_bands is a hex string.
-
         self.anritsu.trigger_ue_capability_enquiry(self.freq_bands)
 
-        testcase = self.anritsu.get_AnritsuTestCases()
-        # Setting the procedure to selection is needed because of a bug in the
-        # instrument's software (b/139547391).
-        testcase.procedure = TestProcedure.PROCEDURE_SELECTION
-        testcase.procedure = TestProcedure.PROCEDURE_MULTICELL
-        testcase.power_control = TestPowerControl.POWER_CONTROL_DISABLE
-        testcase.measurement_LTE = TestMeasurement.MEASUREMENT_DISABLE
-
-        for bts_index in range(1, self.num_carriers):
-            self.bts[bts_index].dl_cc_enabled = True
-
-        self.anritsu.start_testcase()
-
-        retry_counter = 0
-        self.log.info("Waiting for the test case to start...")
-        time.sleep(5)
-
-        while self.anritsu.get_testcase_status() == "0":
-            retry_counter += 1
-            if retry_counter == 3:
-                raise RuntimeError("The test case failed to start after {} "
-                                   "retries. The connection between the phone "
-                                   "and the basestation might be unstable."
-                                   .format(retry_counter))
-            time.sleep(10)
+        # Now that the band is set, calibrate the link for the PCC if necessary
+        self.load_pathloss_if_required()
 
     def maximum_downlink_throughput(self):
         """ Calculates maximum downlink throughput as the sum of all the active
@@ -533,17 +479,20 @@
     def start(self):
         """ Set the signal level for the secondary carriers, as the base class
         implementation of this method will only set up downlink power for the
-        primary carrier component. """
+        primary carrier component.
+
+        After that, attaches the secondary carriers."""
 
         super().start()
 
-        if not self.sim_dl_power:
-            return
+        if self.sim_dl_power:
+            self.log.info('Setting DL power for secondary carriers.')
 
-        for bts_index in range(1, self.num_carriers):
-            self.log.info("Setting DL power for BTS{}.".format(bts_index + 1))
-            new_config = self.BtsConfig()
-            new_config.output_power = self.calibrated_downlink_rx_power(
-                self.bts_configs[bts_index], self.sim_dl_power)
-            self.configure_bts(self.bts[bts_index], new_config)
-            self.bts_configs[bts_index].incorporate(new_config)
+            for bts_index in range(1, self.num_carriers):
+                new_config = self.BtsConfig()
+                new_config.output_power = self.calibrated_downlink_rx_power(
+                    self.bts_configs[bts_index], self.sim_dl_power)
+                self.simulator.configure_bts(new_config, bts_index)
+                self.bts_configs[bts_index].incorporate(new_config)
+
+        self.simulator.lte_attach_secondary_carriers()
diff --git a/acts/framework/acts/test_utils/power/tel_simulations/LteImsSimulation.py b/acts/framework/acts/test_utils/power/tel_simulations/LteImsSimulation.py
index ecbd4b9..7110246 100644
--- a/acts/framework/acts/test_utils/power/tel_simulations/LteImsSimulation.py
+++ b/acts/framework/acts/test_utils/power/tel_simulations/LteImsSimulation.py
@@ -17,6 +17,7 @@
 import acts.test_utils.tel.anritsu_utils as anritsu_utils
 import acts.controllers.anritsu_lib.md8475a as md8475a
 
+
 class LteImsSimulation(LteSimulation):
 
     LTE_BASIC_SIM_FILE = 'VoLTE_ATT_Sim.wnssp'
diff --git a/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py b/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py
index 224cb67..c0cf063 100644
--- a/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py
+++ b/acts/framework/acts/test_utils/power/tel_simulations/LteSimulation.py
@@ -14,28 +14,46 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import time
 import math
-import ntpath
 from enum import Enum
 
 from acts.controllers.anritsu_lib import md8475_cellular_simulator as anritsusim
-from acts.controllers.anritsu_lib.md8475a import BtsBandwidth
-from acts.controllers.anritsu_lib.md8475a import BtsPacketRate
 from acts.test_utils.power.tel_simulations.BaseSimulation import BaseSimulation
 from acts.test_utils.tel.tel_defines import NETWORK_MODE_LTE_ONLY
 
 
+class TransmissionMode(Enum):
+    """ Transmission modes for LTE (e.g., TM1, TM4, ...) """
+    TM1 = "TM1"
+    TM2 = "TM2"
+    TM3 = "TM3"
+    TM4 = "TM4"
+    TM7 = "TM7"
+    TM8 = "TM8"
+    TM9 = "TM9"
+
+
+class MimoMode(Enum):
+    """ Mimo modes """
+    MIMO_1x1 = "1x1"
+    MIMO_2x2 = "2x2"
+    MIMO_4x4 = "4x4"
+
+
+class SchedulingMode(Enum):
+    """ Traffic scheduling modes (e.g., STATIC, DYNAMIC) """
+    DYNAMIC = "DYNAMIC"
+    STATIC = "STATIC"
+
+
+class DuplexMode(Enum):
+    """ DL/UL Duplex mode """
+    FDD = "FDD"
+    TDD = "TDD"
+
+
 class LteSimulation(BaseSimulation):
-    """ Simple LTE simulation with only one basestation.
-
-    """
-
-    # Simulation config files in the callbox computer.
-    # These should be replaced in the future by setting up
-    # the same configuration manually.
-    LTE_BASIC_SIM_FILE = 'SIM_default_LTE.wnssp'
-    LTE_BASIC_CELL_FILE = 'CELL_LTE_config.wnscp'
+    """ Single-carrier LTE simulation. """
 
     # Simulation config keywords contained in the test name
     PARAM_FRAME_CONFIG = "tddconfig"
@@ -56,39 +74,6 @@
     KEY_DL_256_QAM = "256_qam_dl"
     KEY_UL_64_QAM = "64_qam_ul"
 
-    class TransmissionMode(Enum):
-        ''' Transmission modes for LTE (e.g., TM1, TM4, ..)
-
-        '''
-        TM1 = "TM1"
-        TM2 = "TM2"
-        TM3 = "TM3"
-        TM4 = "TM4"
-        TM7 = "TM7"
-        TM8 = "TM8"
-        TM9 = "TM9"
-
-    class MimoMode(Enum):
-        """ Mimo modes """
-
-        MIMO_1x1 = "1x1"
-        MIMO_2x2 = "2x2"
-        MIMO_4x4 = "4x4"
-
-    class SchedulingMode(Enum):
-        ''' Traffic scheduling modes (e.g., STATIC, DYNAMIC)
-
-        '''
-        DYNAMIC = "DYNAMIC"
-        STATIC = "STATIC"
-
-    class DuplexMode(Enum):
-        ''' DL/UL Duplex mode
-
-        '''
-        FDD = "FDD"
-        TDD = "TDD"
-
     # Units in which signal level is defined in DOWNLINK_SIGNAL_LEVEL_DICTIONARY
     DOWNLINK_SIGNAL_LEVEL_UNITS = "RSRP"
 
@@ -407,7 +392,6 @@
                 should be used or not
             dl_channel: an integer indicating the downlink channel number
         """
-
         def __init__(self):
             """ Initialize the base station config by setting all its
             parameters to None. """
@@ -426,9 +410,10 @@
             self.ul_modulation_order = None
             self.tbs_pattern_on = None
             self.dl_channel = None
+            self.dl_cc_enabled = None
 
     def __init__(self, simulator, log, dut, test_config, calibration_table):
-        """ Configures Anritsu system for LTE simulation with 1 basetation
+        """ Initializes the simulator for a single-carrier LTE simulation.
 
         Loads a simple LTE simulation enviroment with 1 basestation.
 
@@ -449,8 +434,6 @@
             raise ValueError('The LTE simulation relies on the simulator to '
                              'be an Anritsu MD8475 A/B instrument.')
 
-        self.anritsu = self.simulator.anritsu
-
         if not dut.droid.telephonySetPreferredNetworkTypesForSubscription(
                 NETWORK_MODE_LTE_ONLY,
                 dut.droid.subscriptionGetDefaultSubId()):
@@ -475,9 +458,9 @@
         self.dl_256_qam = test_config.get(self.KEY_DL_256_QAM, False)
 
         if self.dl_256_qam:
-            if self.anritsu._md8475_version == 'A':
-                self.log.warning("The key '{}' is set to true but MD8475A "
-                                 "callbox doesn't support that modulation "
+            if not self.simulator.LTE_SUPPORTS_DL_256QAM:
+                self.log.warning("The key '{}' is set to true but the "
+                                 "simulator doesn't support that modulation "
                                  "order.".format(self.KEY_DL_256_QAM))
                 self.dl_256_qam = False
             else:
@@ -492,103 +475,19 @@
         self.ul_64_qam = test_config.get(self.KEY_UL_64_QAM, False)
 
         if self.ul_64_qam:
-            if self.anritsu._md8475_version == 'A':
-                self.log.warning("The key '{}' is set to true but MD8475A "
-                                 "callbox doesn't support that modulation "
+            if not self.simulator.LTE_SUPPORTS_UL_64QAM:
+                self.log.warning("The key '{}' is set to true but the "
+                                 "simulator doesn't support that modulation "
                                  "order.".format(self.KEY_UL_64_QAM))
                 self.ul_64_qam = False
             else:
                 self.primary_config.ul_modulation_order = "64QAM"
 
-        self.configure_bts(self.bts1, self.primary_config)
+        self.simulator.configure_bts(self.primary_config)
 
-    def configure_bts(self, bts_handle, config):
-        """ Configures LTE parameters in the base station handle. See parent
-        class implementation for more details.
-
-        Args:
-            bts_handle: a handle to the Anritsu base station controller.
-            config: a BtsConfig object containing the desired configuration.
-        """
-
-        super().configure_bts(bts_handle, config)
-
-        if config.band:
-            # Only calibrate for the primary carrier band
-            isPcc = bts_handle == self.bts1
-            self.set_band(bts_handle,
-                          config.band,
-                          calibrate_if_necessary=isPcc)
-
-        if config.dlul_config:
-            self.set_dlul_configuration(bts_handle, config.dlul_config)
-
-        if config.bandwidth:
-            self.set_channel_bandwidth(bts_handle, config.bandwidth)
-
-        if config.dl_channel:
-            # Temporarily adding this line to workaround a bug in the
-            # Anritsu callbox in which the channel number needs to be set
-            # to a different value before setting it to the final one.
-            bts_handle.dl_channel = str(config.dl_channel)
-            time.sleep(8)
-            bts_handle.dl_channel = str(config.dl_channel - 1)
-
-        if config.mimo_mode:
-            self.set_mimo_mode(bts_handle, config.mimo_mode)
-
-        if config.transmission_mode:
-            self.set_transmission_mode(bts_handle, config.transmission_mode)
-
-        if config.scheduling_mode:
-
-            if (config.scheduling_mode == LteSimulation.SchedulingMode.STATIC
-                    and not all([
-                        config.dl_rbs, config.ul_rbs, config.dl_mcs,
-                        config.ul_mcs
-                    ])):
-                raise ValueError('When the scheduling mode is set to manual, '
-                                 'the RB and MCS parameters are required.')
-
-            # If scheduling mode is set to Dynamic, the RB and MCS parameters
-            # will be ignored by set_scheduling_mode.
-            self.set_scheduling_mode(bts_handle,
-                                     config.scheduling_mode,
-                                     packet_rate=BtsPacketRate.LTE_MANUAL,
-                                     nrb_dl=config.dl_rbs,
-                                     nrb_ul=config.ul_rbs,
-                                     mcs_ul=config.ul_mcs,
-                                     mcs_dl=config.dl_mcs)
-
-        if config.dl_modulation_order:
-            bts_handle.lte_dl_modulation_order = config.dl_modulation_order
-
-        if config.ul_modulation_order:
-            bts_handle.lte_u_modulation_order = config.ul_modulation_order
-
-        # This variable stores a boolean value so the following is needed to
-        # differentiate False from None
-        if config.tbs_pattern_on is not None:
-            if config.tbs_pattern_on:
-                bts_handle.tbs_pattern = "FULLALLOCATION"
-            else:
-                bts_handle.tbs_pattern = "OFF"
-
-    def load_config_files(self):
-        """ Loads configuration files for the simulation. """
-
-        cell_file_name = self.LTE_BASIC_CELL_FILE
-        sim_file_name = self.LTE_BASIC_SIM_FILE
-
-        if self.anritsu._md8475_version == 'B':
-            cell_file_name += '2'
-            sim_file_name += '2'
-
-        cell_file_path = ntpath.join(self.callbox_config_path, cell_file_name)
-        sim_file_path = ntpath.join(self.callbox_config_path, sim_file_name)
-
-        self.anritsu.load_simulation_paramfile(sim_file_path)
-        self.anritsu.load_cell_paramfile(cell_file_path)
+    def setup_simulator(self):
+        """ Do initial configuration in the simulator. """
+        self.simulator.setup_lte_scenario()
 
     def parse_parameters(self, parameters):
         """ Configs an LTE simulation using a list of parameters.
@@ -614,15 +513,16 @@
         new_config.band = values[1]
 
         # Set DL/UL frame configuration
-        if self.get_duplex_mode(new_config.band) == self.DuplexMode.TDD:
+        if self.get_duplex_mode(new_config.band) == DuplexMode.TDD:
 
             values = self.consume_parameter(parameters,
                                             self.PARAM_FRAME_CONFIG, 1)
             if not values:
-                raise ValueError("When a TDD band is selected the frame "
-                                 "structure has to be indicated with the '{}' "
-                                 "parameter followed by a number from 0 to 6."
-                                 .format(self.PARAM_FRAME_CONFIG))
+                raise ValueError(
+                    "When a TDD band is selected the frame "
+                    "structure has to be indicated with the '{}' "
+                    "parameter followed by a number from 0 to 6.".format(
+                        self.PARAM_FRAME_CONFIG))
 
             new_config.dlul_config = int(values[1])
 
@@ -652,7 +552,7 @@
                 "The test name needs to include parameter '{}' followed by the "
                 "mimo mode.".format(self.PARAM_MIMO))
 
-        for mimo_mode in LteSimulation.MimoMode:
+        for mimo_mode in MimoMode:
             if values[1] == mimo_mode.value:
                 new_config.mimo_mode = mimo_mode
                 break
@@ -660,10 +560,10 @@
             raise ValueError("The {} parameter needs to be followed by either "
                              "1x1, 2x2 or 4x4.".format(self.PARAM_MIMO))
 
-        if (new_config.mimo_mode == LteSimulation.MimoMode.MIMO_4x4
-                and self.anritsu._md8475_version == 'A'):
+        if (new_config.mimo_mode == MimoMode.MIMO_4x4
+                and not self.simulator.LTE_SUPPORTS_4X4_MIMO):
             raise ValueError("The test requires 4x4 MIMO, but that is not "
-                             "supported by the MD8475A callbox.")
+                             "supported by the cellular simulator.")
 
         # Setup transmission mode
 
@@ -675,7 +575,7 @@
                 "int value from 1 to 4 indicating transmission mode.".format(
                     self.PARAM_TM))
 
-        for tm in LteSimulation.TransmissionMode:
+        for tm in TransmissionMode:
             if values[1] == tm.value[2:]:
                 new_config.transmission_mode = tm
                 break
@@ -689,20 +589,20 @@
         values = self.consume_parameter(parameters, self.PARAM_SCHEDULING, 1)
 
         if not values:
-            new_config.scheduling_mode = LteSimulation.SchedulingMode.STATIC
+            new_config.scheduling_mode = SchedulingMode.STATIC
             self.log.warning(
                 "The test name does not include the '{}' parameter. Setting to "
                 "static by default.".format(self.PARAM_SCHEDULING))
         elif values[1] == self.PARAM_SCHEDULING_DYNAMIC:
-            new_config.scheduling_mode = LteSimulation.SchedulingMode.DYNAMIC
+            new_config.scheduling_mode = SchedulingMode.DYNAMIC
         elif values[1] == self.PARAM_SCHEDULING_STATIC:
-            new_config.scheduling_mode = LteSimulation.SchedulingMode.STATIC
+            new_config.scheduling_mode = SchedulingMode.STATIC
         else:
             raise ValueError(
                 "The test name parameter '{}' has to be followed by either "
                 "'dynamic' or 'static'.".format(self.PARAM_SCHEDULING))
 
-        if new_config.scheduling_mode == LteSimulation.SchedulingMode.STATIC:
+        if new_config.scheduling_mode == SchedulingMode.STATIC:
 
             values = self.consume_parameter(parameters, self.PARAM_PATTERN, 2)
 
@@ -711,8 +611,8 @@
                     "The '{}' parameter was not set, using 100% RBs for both "
                     "DL and UL. To set the percentages of total RBs include "
                     "the '{}' parameter followed by two ints separated by an "
-                    "underscore indicating downlink and uplink percentages."
-                    .format(self.PARAM_PATTERN, self.PARAM_PATTERN))
+                    "underscore indicating downlink and uplink percentages.".
+                    format(self.PARAM_PATTERN, self.PARAM_PATTERN))
                 dl_pattern = 100
                 ul_pattern = 100
             else:
@@ -725,10 +625,9 @@
                     "positive numbers between 0 and 100.")
 
             new_config.dl_rbs, new_config.ul_rbs = (
-                self.allocation_percentages_to_rbs(new_config.bandwidth,
-                                                   new_config.transmission_mode,
-                                                   dl_pattern,
-                                                   ul_pattern))
+                self.allocation_percentages_to_rbs(
+                    new_config.bandwidth, new_config.transmission_mode,
+                    dl_pattern, ul_pattern))
 
             if self.dl_256_qam and new_config.bandwidth == 1.4:
                 new_config.dl_mcs = 26
@@ -744,18 +643,20 @@
                 new_config.ul_mcs = 23
 
         # Setup LTE RRC status change function and timer for LTE idle test case
-
+        # TODO (b/141838145): setting RRC timer parameters requires unwrapping
+        # the simulator class as it still doesn't support these methods.
         values = self.consume_parameter(parameters,
                                         self.PARAM_RRC_STATUS_CHANGE_TIMER, 1)
         if not values:
             self.log.info(
                 "The test name does not include the '{}' parameter. Disabled "
                 "by default.".format(self.PARAM_RRC_STATUS_CHANGE_TIMER))
-            self.anritsu.set_lte_rrc_status_change(False)
+            self.simulator.anritsu.set_lte_rrc_status_change(False)
         else:
             self.rrc_sc_timer = int(values[1])
-            self.anritsu.set_lte_rrc_status_change(True)
-            self.anritsu.set_lte_rrc_status_change_timer(self.rrc_sc_timer)
+            self.simulator.anritsu.set_lte_rrc_status_change(True)
+            self.simulator.anritsu.set_lte_rrc_status_change_timer(
+                self.rrc_sc_timer)
 
         # Get uplink power
 
@@ -775,9 +676,12 @@
 
         # Setup the base station with the obtained configuration and then save
         # these parameters in the current configuration object
-        self.configure_bts(self.bts1, new_config)
+        self.simulator.configure_bts(new_config)
         self.primary_config.incorporate(new_config)
 
+        # Now that the band is set, calibrate the link if necessary
+        self.load_pathloss_if_required()
+
     def calibrated_downlink_rx_power(self, bts_config, rsrp):
         """ LTE simulation overrides this method so that it can convert from
         RSRP to total signal power transmitted from the basestation.
@@ -796,9 +700,7 @@
         # Use parent method to calculate signal level
         return super().calibrated_downlink_rx_power(bts_config, power)
 
-    def downlink_calibration(self,
-                             rat=None,
-                             power_units_conversion_func=None):
+    def downlink_calibration(self, rat=None, power_units_conversion_func=None):
         """ Computes downlink path loss and returns the calibration value.
 
         See base class implementation for details.
@@ -871,11 +773,11 @@
             Maximum throughput in mbps.
 
         """
-        if bts_config.mimo_mode == LteSimulation.MimoMode.MIMO_1x1:
+        if bts_config.mimo_mode == MimoMode.MIMO_1x1:
             streams = 1
-        elif bts_config.mimo_mode == LteSimulation.MimoMode.MIMO_2x2:
+        elif bts_config.mimo_mode == MimoMode.MIMO_2x2:
             streams = 1
-        elif bts_config.mimo_mode == LteSimulation.MimoMode.MIMO_4x4:
+        elif bts_config.mimo_mode == MimoMode.MIMO_4x4:
             streams = 1
         else:
             raise ValueError('Unable to calculate maximum downlink throughput '
@@ -890,25 +792,29 @@
         tdd_subframe_config = bts_config.dlul_config
         duplex_mode = self.get_duplex_mode(bts_config.band)
 
-        if duplex_mode == self.DuplexMode.TDD.value:
+        if duplex_mode == DuplexMode.TDD.value:
             if self.dl_256_qam:
                 if mcs == "27":
                     if bts_config.tbs_pattern_on:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG3'][tdd_subframe_config][bandwidth]['DL']
+                            'TDD_CONFIG3'][tdd_subframe_config][bandwidth][
+                                'DL']
                     else:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG2'][tdd_subframe_config][bandwidth]['DL']
+                            'TDD_CONFIG2'][tdd_subframe_config][bandwidth][
+                                'DL']
             else:
                 if mcs == "28":
                     if bts_config.tbs_pattern_on:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG4'][tdd_subframe_config][bandwidth]['DL']
+                            'TDD_CONFIG4'][tdd_subframe_config][bandwidth][
+                                'DL']
                     else:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG1'][tdd_subframe_config][bandwidth]['DL']
+                            'TDD_CONFIG1'][tdd_subframe_config][bandwidth][
+                                'DL']
 
-        elif duplex_mode == self.DuplexMode.FDD.value:
+        elif duplex_mode == DuplexMode.FDD.value:
             if (not self.dl_256_qam and bts_config.tbs_pattern_on
                     and mcs == "28"):
                 max_rate_per_stream = {
@@ -919,7 +825,7 @@
                     20: 72.2
                 }.get(bandwidth, None)
             if (not self.dl_256_qam and bts_config.tbs_pattern_on
-                    and mcs ==  "27"):
+                    and mcs == "27"):
                 max_rate_per_stream = {
                     1.4: 2.94,
                 }.get(bandwidth, None)
@@ -1001,25 +907,29 @@
         tdd_subframe_config = bts_config.dlul_config
         duplex_mode = self.get_duplex_mode(bts_config.band)
 
-        if duplex_mode == self.DuplexMode.TDD.value:
+        if duplex_mode == DuplexMode.TDD.value:
             if self.ul_64_qam:
                 if mcs == "28":
                     if bts_config.tbs_pattern_on:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG3'][tdd_subframe_config][bandwidth]['UL']
+                            'TDD_CONFIG3'][tdd_subframe_config][bandwidth][
+                                'UL']
                     else:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG2'][tdd_subframe_config][bandwidth]['UL']
+                            'TDD_CONFIG2'][tdd_subframe_config][bandwidth][
+                                'UL']
             else:
                 if mcs == "23":
                     if bts_config.tbs_pattern_on:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG4'][tdd_subframe_config][bandwidth]['UL']
+                            'TDD_CONFIG4'][tdd_subframe_config][bandwidth][
+                                'UL']
                     else:
                         max_rate_per_stream = self.tdd_config_tput_lut_dict[
-                            'TDD_CONFIG1'][tdd_subframe_config][bandwidth]['UL']
+                            'TDD_CONFIG1'][tdd_subframe_config][bandwidth][
+                                'UL']
 
-        elif duplex_mode == self.DuplexMode.FDD.value:
+        elif duplex_mode == DuplexMode.FDD.value:
             if mcs == "23" and not self.ul_64_qam:
                 max_rate_per_stream = {
                     1.4: 2.85,
@@ -1047,132 +957,6 @@
 
         return max_rate_per_stream * rb_ratio
 
-    def set_transmission_mode(self, bts, tmode):
-        """ Sets the transmission mode for the LTE basetation
-
-        Args:
-            bts: basestation handle
-            tmode: Enum list from class 'TransmissionModeLTE'
-        """
-
-        # If the selected transmission mode does not support the number of DL
-        # antennas, throw an exception.
-        if (tmode in [self.TransmissionMode.TM1, self.TransmissionMode.TM7]
-                and bts.dl_antenna != '1'):
-            # TM1 and TM7 only support 1 DL antenna
-            raise ValueError("{} allows only one DL antenna. Change the "
-                             "number of DL antennas before setting the "
-                             "transmission mode.".format(tmode.value))
-        elif tmode == self.TransmissionMode.TM8 and bts.dl_antenna != '2':
-            # TM8 requires 2 DL antennas
-            raise ValueError("TM2 requires two DL antennas. Change the "
-                             "number of DL antennas before setting the "
-                             "transmission mode.")
-        elif (tmode in [
-                self.TransmissionMode.TM2, self.TransmissionMode.TM3,
-                self.TransmissionMode.TM4, self.TransmissionMode.TM9
-        ] and bts.dl_antenna == '1'):
-            # TM2, TM3, TM4 and TM9 require 2 or 4 DL antennas
-            raise ValueError("{} requires at least two DL atennas. Change the "
-                             "number of DL antennas before setting the "
-                             "transmission mode.".format(tmode.value))
-
-        # The TM mode is allowed for the current number of DL antennas, so it
-        # is safe to change this setting now
-        bts.transmode = tmode.value
-
-        time.sleep(5)  # It takes some time to propagate the new settings
-
-    def set_mimo_mode(self, bts, mimo):
-        """ Sets the number of DL antennas for the desired MIMO mode.
-
-        Args:
-            bts: basestation handle
-            mimo: object of class MimoMode
-        """
-
-        # If the requested mimo mode is not compatible with the current TM,
-        # warn the user before changing the value.
-
-        if mimo == self.MimoMode.MIMO_1x1:
-            if bts.transmode not in [
-                    self.TransmissionMode.TM1, self.TransmissionMode.TM7
-            ]:
-                self.log.warning(
-                    "Using only 1 DL antennas is not allowed with "
-                    "the current transmission mode. Changing the "
-                    "number of DL antennas will override this "
-                    "setting.")
-            bts.dl_antenna = 1
-        elif mimo == self.MimoMode.MIMO_2x2:
-            if bts.transmode not in [
-                    self.TransmissionMode.TM2, self.TransmissionMode.TM3,
-                    self.TransmissionMode.TM4, self.TransmissionMode.TM8,
-                    self.TransmissionMode.TM9
-            ]:
-                self.log.warning("Using two DL antennas is not allowed with "
-                                 "the current transmission mode. Changing the "
-                                 "number of DL antennas will override this "
-                                 "setting.")
-            bts.dl_antenna = 2
-        elif mimo == self.MimoMode.MIMO_4x4:
-            if bts.transmode not in [
-                    self.TransmissionMode.TM2, self.TransmissionMode.TM3,
-                    self.TransmissionMode.TM4, self.TransmissionMode.TM9
-            ]:
-                self.log.warning("Using four DL antennas is not allowed with "
-                                 "the current transmission mode. Changing the "
-                                 "number of DL antennas will override this "
-                                 "setting.")
-
-            bts.dl_antenna = 4
-        else:
-            RuntimeError("The requested MIMO mode is not supported.")
-
-    def set_scheduling_mode(self,
-                            bts,
-                            scheduling,
-                            packet_rate=None,
-                            mcs_dl=None,
-                            mcs_ul=None,
-                            nrb_dl=None,
-                            nrb_ul=None):
-        """ Sets the scheduling mode for LTE
-
-        Args:
-            bts: basestation handle
-            scheduling: DYNAMIC or STATIC scheduling (Enum list)
-            mcs_dl: Downlink MCS (only for STATIC scheduling)
-            mcs_ul: Uplink MCS (only for STATIC scheduling)
-            nrb_dl: Number of RBs for downlink (only for STATIC scheduling)
-            nrb_ul: Number of RBs for uplink (only for STATIC scheduling)
-        """
-
-        bts.lte_scheduling_mode = scheduling.value
-
-        if scheduling == self.SchedulingMode.STATIC:
-
-            if not packet_rate:
-                raise RuntimeError("Packet rate needs to be indicated when "
-                                   "selecting static scheduling.")
-
-            bts.packet_rate = packet_rate
-
-            if packet_rate == BtsPacketRate.LTE_MANUAL:
-
-                if not (mcs_dl and mcs_ul and nrb_dl and nrb_ul):
-                    raise RuntimeError("When using manual packet rate the "
-                                       "number of dl/ul RBs and the dl/ul "
-                                       "MCS needs to be indicated with the "
-                                       "optional arguments.")
-
-                bts.lte_mcs_dl = mcs_dl
-                bts.lte_mcs_ul = mcs_ul
-                bts.nrb_dl = nrb_dl
-                bts.nrb_ul = nrb_ul
-
-        time.sleep(5)  # It takes some time to propagate the new settings
-
     def allocation_percentages_to_rbs(self, bw, tm, dl, ul):
         """ Converts usage percentages to number of DL/UL RBs
 
@@ -1217,10 +1001,11 @@
 
         # Get the number of DL RBs that corresponds to
         #  the required percentage.
-        desired_dl_rbs = percentage_to_amount(
-            min_val=min_dl_rbs, max_val=max_rbs, percentage=dl)
+        desired_dl_rbs = percentage_to_amount(min_val=min_dl_rbs,
+                                              max_val=max_rbs,
+                                              percentage=dl)
 
-        if tm == self.TransmissionMode.TM3 or tm == self.TransmissionMode.TM4:
+        if tm == TransmissionMode.TM3 or tm == TransmissionMode.TM4:
 
             # For TM3 and TM4 the number of DL RBs needs to be max_rbs or a
             # multiple of the RBG size
@@ -1239,8 +1024,9 @@
 
         # Get the number of UL RBs that corresponds
         # to the required percentage
-        desired_ul_rbs = percentage_to_amount(
-            min_val=min_ul_rbs, max_val=max_rbs, percentage=ul)
+        desired_ul_rbs = percentage_to_amount(min_val=min_ul_rbs,
+                                              max_val=max_rbs,
+                                              percentage=ul)
 
         # Create a list of all possible UL RBs assignment
         # The standard allows any number that can be written as
@@ -1270,60 +1056,6 @@
 
         return dl_rbs, ul_rbs
 
-    def set_channel_bandwidth(self, bts, bandwidth):
-        """ Sets the LTE channel bandwidth (MHz)
-
-        Args:
-            bts: basestation handle
-            bandwidth: desired bandwidth (MHz)
-        """
-        if bandwidth == 20:
-            bts.bandwidth = BtsBandwidth.LTE_BANDWIDTH_20MHz
-        elif bandwidth == 15:
-            bts.bandwidth = BtsBandwidth.LTE_BANDWIDTH_15MHz
-        elif bandwidth == 10:
-            bts.bandwidth = BtsBandwidth.LTE_BANDWIDTH_10MHz
-        elif bandwidth == 5:
-            bts.bandwidth = BtsBandwidth.LTE_BANDWIDTH_5MHz
-        elif bandwidth == 3:
-            bts.bandwidth = BtsBandwidth.LTE_BANDWIDTH_3MHz
-        elif bandwidth == 1.4:
-            bts.bandwidth = BtsBandwidth.LTE_BANDWIDTH_1dot4MHz
-        else:
-            msg = "Bandwidth = {} MHz is not valid for LTE".format(bandwidth)
-            self.log.error(msg)
-            raise ValueError(msg)
-        time.sleep(5)  # It takes some time to propagate the new settings
-
-    def get_bandwidth(self, bts):
-        """ Obtains bandwidth in MHz for a base station. The Anritsu callbox
-        returns a string that needs to be mapped to the right number.
-
-        Args:
-            bts: base station handle
-
-        Returns:
-            the channel bandwidth in MHz
-        """
-
-        return BtsBandwidth.get_float_value(bts.bandwidth)
-
-    def set_dlul_configuration(self, bts, config):
-        """ Sets the frame structure for TDD bands.
-
-        Args:
-            config: the desired frame structure. An int between 0 and 6.
-        """
-
-        if not 0 <= config <= 6:
-            raise ValueError("The frame structure configuration has to be a "
-                             "number between 0 and 6")
-
-        bts.uldl_configuration = config
-
-        # Wait for the setting to propagate
-        time.sleep(5)
-
     def calibrate(self, band):
         """ Calculates UL and DL path loss if it wasn't done before
 
@@ -1342,17 +1074,17 @@
 
         # Set up a temporary calibration configuration.
         temporary_config = self.BtsConfig()
-        temporary_config.mimo_mode = LteSimulation.MimoMode.MIMO_1x1
-        temporary_config.transmission_mode = LteSimulation.TransmissionMode.TM1
+        temporary_config.mimo_mode = MimoMode.MIMO_1x1
+        temporary_config.transmission_mode = TransmissionMode.TM1
         temporary_config.bandwidth = max(
             self.allowed_bandwidth_dictionary[int(band)])
-        self.configure_bts(self.bts1, temporary_config)
+        self.simulator.configure_bts(temporary_config)
         self.primary_config.incorporate(temporary_config)
 
         super().calibrate(band)
 
         # Restore values as they were before changing them for calibration.
-        self.configure_bts(self.bts1, restore_config)
+        self.simulator.configure_bts(restore_config)
         self.primary_config.incorporate(restore_config)
 
     def start_traffic_for_calibration(self):
@@ -1380,19 +1112,6 @@
         """
 
         if 33 <= int(band) <= 46:
-            return self.DuplexMode.TDD
+            return DuplexMode.TDD
         else:
-            return self.DuplexMode.FDD
-
-    def set_band(self, bts, band, calibrate_if_necessary=True):
-        """ Sets the right duplex mode before switching to a new band.
-
-        Args:
-            bts: basestation handle
-            band: desired band
-            calibrate_if_necessary: if False calibration will be skipped
-        """
-
-        bts.duplex_mode = self.get_duplex_mode(band).value
-
-        super().set_band(bts, band, calibrate_if_necessary)
+            return DuplexMode.FDD
diff --git a/acts/framework/acts/test_utils/power/tel_simulations/UmtsSimulation.py b/acts/framework/acts/test_utils/power/tel_simulations/UmtsSimulation.py
index d136f84..4d4aeeb 100644
--- a/acts/framework/acts/test_utils/power/tel_simulations/UmtsSimulation.py
+++ b/acts/framework/acts/test_utils/power/tel_simulations/UmtsSimulation.py
@@ -15,17 +15,17 @@
 #   limitations under the License.
 
 import ntpath
+import time
 
 from acts.controllers.anritsu_lib import md8475_cellular_simulator as anritsusim
+from acts.controllers.anritsu_lib.md8475a import BtsNumber
 from acts.controllers.anritsu_lib.md8475a import BtsPacketRate
 from acts.test_utils.power.tel_simulations.BaseSimulation import BaseSimulation
 from acts.test_utils.tel.tel_defines import NETWORK_MODE_WCDMA_ONLY
 
 
 class UmtsSimulation(BaseSimulation):
-    """ Simple UMTS simulation with only one basestation.
-
-    """
+    """ Single base station simulation. """
 
     # Simulation config files in the callbox computer.
     # These should be replaced in the future by setting up
@@ -90,7 +90,7 @@
     }
 
     def __init__(self, simulator, log, dut, test_config, calibration_table):
-        """ Configures Anritsu system for UMTS simulation with 1 basetation
+        """ Initializes the cellular simulator for a UMTS simulation.
 
         Loads a simple UMTS simulation enviroment with 1 basestation. It also
         creates the BTS handle so we can change the parameters as desired.
@@ -104,15 +104,18 @@
                 different bands.
 
         """
-
-        super().__init__(simulator, log, dut, test_config, calibration_table)
-
         # The UMTS simulation relies on the cellular simulator to be a MD8475
         if not isinstance(self.simulator, anritsusim.MD8475CellularSimulator):
             raise ValueError('The UMTS simulation relies on the simulator to '
                              'be an Anritsu MD8475 A/B instrument.')
 
+        # The Anritsu controller needs to be unwrapped before calling
+        # super().__init__ because setup_simulator() requires self.anritsu and
+        # will be called during the parent class initialization.
         self.anritsu = self.simulator.anritsu
+        self.bts1 = self.anritsu.get_BTS(BtsNumber.BTS1)
+
+        super().__init__(simulator, log, dut, test_config, calibration_table)
 
         if not dut.droid.telephonySetPreferredNetworkTypesForSubscription(
                 NETWORK_MODE_WCDMA_ONLY,
@@ -124,11 +127,18 @@
         self.release_version = None
         self.packet_rate = None
 
-    def load_config_files(self):
-        """ Loads configuration files for the simulation. """
+    def setup_simulator(self):
+        """ Do initial configuration in the simulator. """
+
+        # Load callbox config files
+        callbox_config_path = self.CALLBOX_PATH_FORMAT_STR.format(
+            self.anritsu._md8475_version)
 
         self.anritsu.load_simulation_paramfile(
-            ntpath.join(self.callbox_config_path, self.UMTS_BASIC_SIM_FILE))
+            ntpath.join(callbox_config_path, self.UMTS_BASIC_SIM_FILE))
+
+        # Start simulation if it wasn't started
+        self.anritsu.start_simulation()
 
     def parse_parameters(self, parameters):
         """ Configs an UMTS simulation using a list of parameters.
@@ -149,6 +159,7 @@
                 "the required band number.".format(self.PARAM_BAND))
 
         self.set_band(self.bts1, values[1])
+        self.load_pathloss_if_required()
 
         # Setup release version
 
@@ -279,3 +290,14 @@
 
         # Stop IP traffic after setting the signal level
         self.stop_traffic_for_calibration()
+
+    def set_band(self, bts, band):
+        """ Sets the band used for communication.
+
+        Args:
+            bts: basestation handle
+            band: desired band
+        """
+
+        bts.band = band
+        time.sleep(5)  # It takes some time to propagate the new band
diff --git a/acts/framework/acts/test_utils/power/tel_simulations/__init__.py b/acts/framework/acts/test_utils/power/tel_simulations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/framework/acts/test_utils/power/tel_simulations/__init__.py
diff --git a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
index dbdcf7f..b4b40ed 100644
--- a/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
+++ b/acts/framework/acts/test_utils/tel/TelephonyBaseTest.py
@@ -103,7 +103,7 @@
             self.test_id = test_id
             self.result_detail = ""
             self.testsignal_details = ""
-            self.testsignal_extras = ""
+            self.testsignal_extras = {}
             tries = int(self.user_params.get("telephony_auto_rerun", 1))
             for ad in self.android_devices:
                 ad.log_path = self.log_path
diff --git a/acts/framework/acts/test_utils/tel/tel_test_utils.py b/acts/framework/acts/test_utils/tel/tel_test_utils.py
index 0c26235..a4cb3f2 100644
--- a/acts/framework/acts/test_utils/tel/tel_test_utils.py
+++ b/acts/framework/acts/test_utils/tel/tel_test_utils.py
@@ -1567,6 +1567,9 @@
         else:
             return True
     finally:
+        if hasattr(ad, "sdm_log") and getattr(ad, "sdm_log"):
+            ad.adb.shell("i2cset -fy 3 64 6 1 b", ignore_status=True)
+            ad.adb.shell("i2cset -fy 3 65 6 1 b", ignore_status=True)
         ad.droid.telephonyStopTrackingCallStateChangeForSubscription(sub_id)
         if incall_ui_display == INCALL_UI_DISPLAY_FOREGROUND:
             ad.droid.telecomShowInCallScreen()
@@ -3216,7 +3219,7 @@
 def phone_switch_to_msim_mode(ad, retries=3, timeout=60):
     result = False
     if not ad.is_apk_installed("com.google.mdstest"):
-        raise signals.TestSkipClass("mdstest is not installed")
+        raise signals.TestAbortClass("mdstest is not installed")
     mode = ad.droid.telephonyGetPhoneCount()
     if mode == 2:
         ad.log.info("Device already in MSIM mode")
@@ -3259,7 +3262,7 @@
 def phone_switch_to_ssim_mode(ad, retries=3, timeout=30):
     result = False
     if not ad.is_apk_installed("com.google.mdstest"):
-        raise signals.TestSkipClass("mdstest is not installed")
+        raise signals.TestAbortClass("mdstest is not installed")
     mode = ad.droid.telephonyGetPhoneCount()
     if mode == 1:
         ad.log.info("Device already in SSIM mode")
@@ -3738,17 +3741,18 @@
             If None, opposite of the current state.
 
     """
-    # TODO: b/26293960 No framework API available to set IMS by SubId.
-    if not ad.droid.imsIsEnhanced4gLteModeSettingEnabledByPlatform():
-        ad.log.info("Enhanced 4G Lte Mode Setting is not enabled by platform.")
-        return False
-    current_state = ad.droid.imsIsEnhanced4gLteModeSettingEnabledByUser()
+    current_state = ad.droid.imsMmTelIsAdvancedCallingEnabled(sub_id)
     if new_state is None:
         new_state = not current_state
     if new_state != current_state:
-        ad.log.info("Toggle Enhanced 4G LTE Mode from %s to %s", current_state,
-                    new_state)
-        ad.droid.imsSetEnhanced4gMode(new_state)
+        ad.log.info("Toggle Enhanced 4G LTE Mode from %s to %s on sub_id %s", current_state,
+                    new_state, sub_id)
+        ad.droid.imsMmTelSetAdvancedCallingEnabled(sub_id, new_state)
+    check_state = ad.droid.imsMmTelIsAdvancedCallingEnabled(sub_id)
+    if check_state != new_state:
+        ad.log.error("Failed to toggle Enhanced 4G LTE Mode to %s, still set to %s on sub_id %s",
+                     new_state, check_state, sub_id)
+        return False
     return True
 
 
@@ -6660,7 +6664,7 @@
             if ad.is_sl4a_installed():
                 break
             ad.log.info("Re-install sl4a")
-            ad.adb.shell("settings put global package_verifier_enable 0")
+            ad.adb.shell("settings put global verifier_verify_adb_installs 0")
             ad.adb.install("-r /tmp/base.apk")
             time.sleep(10)
             break
diff --git a/acts/framework/acts/test_utils/wifi/wifi_performance_test_utils.py b/acts/framework/acts/test_utils/wifi/wifi_performance_test_utils.py
index e71748e..18c50a5 100644
--- a/acts/framework/acts/test_utils/wifi/wifi_performance_test_utils.py
+++ b/acts/framework/acts/test_utils/wifi/wifi_performance_test_utils.py
@@ -16,6 +16,8 @@
 
 import bokeh, bokeh.plotting
 import collections
+import itertools
+import json
 import logging
 import math
 import re
@@ -185,102 +187,6 @@
             self.llstats_cumulative)
 
 
-# Dashboard utilities
-class BlackboxMappedMetricLogger(MetricLogger):
-    """A MetricLogger for logging and publishing Blackbox metrics from a dict.
-
-    The dict maps the metric name to the metric value. For additional
-    information on reporting to Blackbox, see BlackBoxMetricLogger.
-
-    Attributes:
-        proto_module: The proto module for ActsBlackboxMetricResult.
-        metric_key: The metric key to use. If unset, the logger will use the
-                    context's identifier.
-    """
-
-    PROTO_FILE = '../../metrics/loggers/protos/acts_blackbox.proto'
-
-    def __init__(self, metric_key=None, event=None):
-        """Initializes a logger for Blackbox metrics.
-
-        Args:
-            metric_key: The metric key to use. If unset, the logger will use
-                        the context's identifier.
-            event: The event triggering the creation of this logger.
-        """
-        super().__init__(event=event)
-        self.proto_module = self._compile_proto(self.PROTO_FILE)
-        self.metric_key = metric_key
-        self._metric_map = {}
-
-    def _get_metric_key(self, metric_name):
-        """Gets the metric key to use.
-
-        If the metric_key is explicitly set, returns that value. Otherwise,
-        extracts an identifier from the context.
-
-        Args:
-            metric_name: The name of the metric to report.
-        """
-        if self.metric_key:
-            key = self.metric_key
-        else:
-            key = self._get_blackbox_identifier()
-        key = '%s.%s' % (key, metric_name)
-        return key
-
-    def set_metric_data(self, metric_map):
-        """Sets the map of metrics to be uploaded to Blackbox. Note that
-        this will overwrite all existing added by this function or add_metric.
-
-        Args:
-            metric_map: the map of metric_name -> metric_value to publish
-                to blackbox. If the metric value is set to None, the
-                metric will not be reported.
-        """
-        self._metric_map = metric_map
-
-    def add_metric(self, metric_name, metric_value):
-        """Adds a metric value to be published later.
-
-        Note that if the metric name has already been added, the metric value
-        will be overwritten.
-
-        Args:
-            metric_name: the name of the metric.
-            metric_value: the value of the metric.
-        """
-        self._metric_map[metric_name] = metric_value
-
-    def _get_blackbox_identifier(self):
-        """Returns the testcase identifier, as expected by Blackbox."""
-        # b/119787228: Blackbox requires function names to look like Java
-        # functions.
-        identifier = self.context.identifier
-        parts = identifier.rsplit('.', 1)
-        return '#'.join(parts)
-
-    def end(self, _):
-        """Creates and publishes a ProtoMetric with blackbox data.
-
-        Builds a list of ActsBlackboxMetricResult messages from the set
-        metric data, and sends them to the publisher.
-        """
-        metrics = []
-        for metric_name, metric_value in self._metric_map.items():
-            if metric_value is None:
-                continue
-            result = self.proto_module.ActsBlackboxMetricResult()
-            result.test_identifier = self._get_blackbox_identifier()
-            result.metric_key = self._get_metric_key(metric_name)
-            result.metric_value = metric_value
-
-            metrics.append(
-                ProtoMetric(name='blackbox_%s' % metric_name, data=result))
-
-        return self.publisher.publish(metrics)
-
-
 # JSON serializer
 def serialize_dict(input_dict):
     """Function to serialize dicts to enable JSON output"""
@@ -343,23 +249,23 @@
     def __init__(self,
                  title=None,
                  x_label=None,
-                 primary_y=None,
-                 secondary_y=None,
+                 primary_y_label=None,
+                 secondary_y_label=None,
                  height=700,
                  width=1300,
-                 title_size=15,
-                 axis_label_size=12):
+                 title_size='15pt',
+                 axis_label_size='12pt'):
         self.figure_data = []
         self.fig_property = {
             'title': title,
             'x_label': x_label,
-            'primary_y_label': primary_y,
-            'secondary_y_label': secondary_y,
+            'primary_y_label': primary_y_label,
+            'secondary_y_label': secondary_y_label,
             'num_lines': 0,
             'height': height,
             'width': width,
-            'title_size': '{}pt'.format(title_size),
-            'axis_label_size': '{}pt'.format(axis_label_size)
+            'title_size': title_size,
+            'axis_label_size': axis_label_size
         }
         self.TOOLS = (
             'box_zoom,box_select,pan,crosshair,redo,undo,reset,hover,save')
@@ -382,6 +288,18 @@
         self.plot.add_tools(
             bokeh.models.tools.WheelZoomTool(dimensions='height'))
 
+    def _filter_line(self, x_data, y_data, hover_text=None):
+        """Function to remove NaN points from bokeh plots."""
+        x_data_filtered = []
+        y_data_filtered = []
+        hover_text_filtered = []
+        for x, y, hover in itertools.zip_longest(x_data, y_data, hover_text):
+            if not math.isnan(y):
+                x_data_filtered.append(x)
+                y_data_filtered.append(y)
+                hover_text_filtered.append(hover)
+        return x_data_filtered, y_data_filtered, hover_text_filtered
+
     def add_line(self,
                  x_data,
                  y_data,
@@ -417,18 +335,20 @@
             style = [5, 5]
         if not hover_text:
             hover_text = ['y={}'.format(y) for y in y_data]
+        x_data_filter, y_data_filter, hover_text_filter = self._filter_line(
+            x_data, y_data, hover_text)
         self.figure_data.append({
-            'x_data': x_data,
-            'y_data': y_data,
+            'x_data': x_data_filter,
+            'y_data': y_data_filter,
             'legend': legend,
-            'hover_text': hover_text,
+            'hover_text': hover_text_filter,
             'color': color,
             'width': width,
             'style': style,
             'marker': marker,
             'marker_size': marker_size,
             'shaded_region': shaded_region,
-            'y_range_name': y_axis
+            'y_axis': y_axis
         })
         self.fig_property['num_lines'] += 1
 
@@ -473,7 +393,7 @@
             'marker': marker,
             'marker_size': marker_size,
             'shaded_region': None,
-            'y_range_name': y_axis
+            'y_axis': y_axis
         })
         self.fig_property['num_lines'] += 1
 
@@ -499,8 +419,8 @@
                     line_width=line['width'],
                     color=line['color'],
                     line_dash=line['style'],
-                    name=line['y_range_name'],
-                    y_range_name=line['y_range_name'],
+                    name=line['y_axis'],
+                    y_range_name=line['y_axis'],
                     source=source)
             if line['shaded_region']:
                 band_x = line['shaded_region']['x_vector']
@@ -522,10 +442,10 @@
                     legend=line['legend'],
                     line_color=line['color'],
                     fill_color=line['color'],
-                    name=line['y_range_name'],
-                    y_range_name=line['y_range_name'],
+                    name=line['y_axis'],
+                    y_range_name=line['y_axis'],
                     source=source)
-            if line['y_range_name'] == 'secondary':
+            if line['y_axis'] == 'secondary':
                 two_axes = True
 
         #x-axis formatting
@@ -554,10 +474,20 @@
         self.plot.title.text_font_size = self.fig_property['title_size']
 
         if output_file is not None:
-            bokeh.plotting.output_file(output_file)
-            bokeh.plotting.save(self.plot)
+            self.save_figure(output_file)
         return self.plot
 
+    def _save_figure_json(self, output_file):
+        """Function to save a json format of a figure"""
+        figure_dict = collections.OrderedDict(
+            fig_property=self.fig_property,
+            figure_data=self.figure_data,
+            tools=self.TOOLS,
+            tooltips=self.TOOLTIPS)
+        output_file = output_file.replace('.html', '_plot_data.json')
+        with open(output_file, 'w') as outfile:
+            json.dump(figure_dict, outfile, indent=4)
+
     def save_figure(self, output_file):
         """Function to save BokehFigure.
 
@@ -566,6 +496,7 @@
         """
         bokeh.plotting.output_file(output_file)
         bokeh.plotting.save(self.plot)
+        self._save_figure_json(output_file)
 
     @staticmethod
     def save_figures(figure_array, output_file_path):
@@ -575,8 +506,11 @@
             figure_array: list of BokehFigure object to be plotted
             output_file: string specifying output file path
         """
-        for figure in figure_array:
+        for idx, figure in enumerate(figure_array):
             figure.generate_figure()
+            json_file_path = output_file_path.replace(
+                '.html', '{}-plot_data.json'.format(idx))
+            figure._save_figure_json(json_file_path)
         plot_array = [figure.plot for figure in figure_array]
         all_plots = bokeh.layouts.column(children=plot_array)
         bokeh.plotting.output_file(output_file_path)
@@ -709,23 +643,22 @@
     Returns:
         ping_result: dict containing ping results and other meta data
     """
-    ping_count = int(ping_duration/ping_interval)
-    ping_deadline = int(ping_count*ping_interval)+1
+    ping_count = int(ping_duration / ping_interval)
+    ping_deadline = int(ping_count * ping_interval) + 1
     ping_cmd = 'ping -c {} -w {} -i {} -s {} -D'.format(
         ping_count,
         ping_deadline,
         ping_interval,
         ping_size,
     )
-    start_time = time.time()
     if isinstance(src_device, AndroidDevice):
         ping_cmd = '{} {}'.format(ping_cmd, dest_address)
         ping_output = src_device.adb.shell(
-            ping_cmd, timeout=ping_deadline+SHORT_SLEEP, ignore_status=True)
+            ping_cmd, timeout=ping_deadline + SHORT_SLEEP, ignore_status=True)
     elif isinstance(src_device, ssh.connection.SshConnection):
         ping_cmd = 'sudo {} {}'.format(ping_cmd, dest_address)
         ping_output = src_device.run(
-            ping_cmd, timeout=ping_deadline+SHORT_SLEEP,
+            ping_cmd, timeout=ping_deadline + SHORT_SLEEP,
             ignore_status=True).stdout
     else:
         raise TypeError(
diff --git a/acts/framework/tests/acts_test_decorators_test.py b/acts/framework/tests/acts_test_decorators_test.py
index 7d98d49..f491302 100644
--- a/acts/framework/tests/acts_test_decorators_test.py
+++ b/acts/framework/tests/acts_test_decorators_test.py
@@ -11,6 +11,7 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import shutil
 import tempfile
 import unittest
 
@@ -90,26 +91,26 @@
 
 class TestDecoratorIntegrationTests(unittest.TestCase):
 
-    MOCK_CONFIG = {
-        "testbed": {
-            "name": "SampleTestBed",
-        },
-        "logpath": tempfile.mkdtemp(),
-        "cli_args": None,
-        "testpaths": ["./"],
-    }
+    @classmethod
+    def setUpClass(cls):
+        cls.MOCK_CONFIG = {
+            "testbed": {
+                "name": "SampleTestBed",
+            },
+            "logpath": tempfile.mkdtemp(),
+            "cli_args": None,
+            "testpaths": ["./"],
+        }
 
-    MOCK_TEST_RUN_LIST = [(MockTest.__name__, [MockTest.TEST_CASE_LIST])]
-
-    def setUp(self):
-        pass
+        cls.MOCK_TEST_RUN_LIST = [(MockTest.__name__,
+                                   [MockTest.TEST_CASE_LIST])]
 
     def _run_with_test_logic(self, func):
         if hasattr(MockTest, MockTest.TEST_LOGIC_ATTR):
             delattr(MockTest, MockTest.TEST_LOGIC_ATTR)
         setattr(MockTest, MockTest.TEST_LOGIC_ATTR, func)
-        self.test_runner = test_runner.TestRunner(TestDecoratorIntegrationTests.MOCK_CONFIG,
-                                                  TestDecoratorIntegrationTests.MOCK_TEST_RUN_LIST)
+        self.test_runner = test_runner.TestRunner(self.MOCK_CONFIG,
+                                                  self.MOCK_TEST_RUN_LIST)
         self.test_runner.run(MockTest)
 
     def _validate_results_has_extra(self, result, extra_key, extra_value):
@@ -127,6 +128,10 @@
         self._run_with_test_logic(raise_generic)
         self._validate_results_has_extra(self.test_runner.results, UUID_KEY, TEST_TRACKER_UUID)
 
+    @classmethod
+    def tearDownClass(cls):
+        shutil.rmtree(cls.MOCK_CONFIG['logpath'])
+
 
 if __name__ == "__main__":
-    unittest.main()
\ No newline at end of file
+    unittest.main()
diff --git a/acts/framework/tests/acts_test_runner_test.py b/acts/framework/tests/acts_test_runner_test.py
index e8599a7..4769fd7 100755
--- a/acts/framework/tests/acts_test_runner_test.py
+++ b/acts/framework/tests/acts_test_runner_test.py
@@ -15,16 +15,17 @@
 #   limitations under the License.
 
 import mock
+import os
 import shutil
 import tempfile
 import unittest
 
 from acts import keys
-from acts import signals
 from acts import test_runner
 
 import acts_android_device_test
 import mock_controller
+import IntegrationTest
 
 
 class ActsTestRunnerTest(unittest.TestCase):
@@ -35,14 +36,14 @@
     def setUp(self):
         self.tmp_dir = tempfile.mkdtemp()
         self.base_mock_test_config = {
-            "testbed": {
-                "name": "SampleTestBed",
+            'testbed': {
+                'name': 'SampleTestBed',
             },
-            "logpath": self.tmp_dir,
-            "cli_args": None,
-            "testpaths": ["./"],
-            "icecream": 42,
-            "extra_param": "haha"
+            'logpath': self.tmp_dir,
+            'cli_args': None,
+            'testpaths': [os.path.dirname(IntegrationTest.__file__)],
+            'icecream': 42,
+            'extra_param': 'haha'
         }
         self.mock_run_list = [('SampleTest', None)]
 
@@ -59,11 +60,11 @@
         tb_key = keys.Config.key_testbed.value
         mock_ctrlr_config_name = mock_controller.ACTS_CONTROLLER_CONFIG_NAME
         my_config = [{
-            "serial": "xxxx",
-            "magic": "Magic1"
+            'serial': 'xxxx',
+            'magic': 'Magic1'
         }, {
-            "serial": "xxxx",
-            "magic": "Magic2"
+            'serial': 'xxxx',
+            'magic': 'Magic2'
         }]
         mock_test_config[tb_key][mock_ctrlr_config_name] = my_config
         tr = test_runner.TestRunner(mock_test_config,
@@ -73,9 +74,9 @@
         tr.run()
         tr.stop()
         results = tr.results.summary_dict()
-        self.assertEqual(results["Requested"], 2)
-        self.assertEqual(results["Executed"], 2)
-        self.assertEqual(results["Passed"], 2)
+        self.assertEqual(results['Requested'], 2)
+        self.assertEqual(results['Executed'], 2)
+        self.assertEqual(results['Passed'], 2)
 
     @mock.patch(
         'acts.controllers.adb.AdbProxy',
@@ -84,7 +85,7 @@
         'acts.controllers.fastboot.FastbootProxy',
         return_value=acts_android_device_test.MockFastbootProxy(1))
     @mock.patch(
-        'acts.controllers.android_device.list_adb_devices', return_value=["1"])
+        'acts.controllers.android_device.list_adb_devices', return_value=['1'])
     @mock.patch(
         'acts.controllers.android_device.get_all_instances',
         return_value=acts_android_device_test.get_mock_ads(1))
@@ -107,16 +108,16 @@
         tb_key = keys.Config.key_testbed.value
         mock_ctrlr_config_name = mock_controller.ACTS_CONTROLLER_CONFIG_NAME
         my_config = [{
-            "serial": "xxxx",
-            "magic": "Magic1"
+            'serial': 'xxxx',
+            'magic': 'Magic1'
         }, {
-            "serial": "xxxx",
-            "magic": "Magic2"
+            'serial': 'xxxx',
+            'magic': 'Magic2'
         }]
         mock_test_config[tb_key][mock_ctrlr_config_name] = my_config
-        mock_test_config[tb_key]["AndroidDevice"] = [{
-            "serial": "1",
-            "skip_sl4a": True
+        mock_test_config[tb_key]['AndroidDevice'] = [{
+            'serial': '1',
+            'skip_sl4a': True
         }]
         tr = test_runner.TestRunner(mock_test_config,
                                     [('IntegrationTest', None),
@@ -124,10 +125,10 @@
         tr.run()
         tr.stop()
         results = tr.results.summary_dict()
-        self.assertEqual(results["Requested"], 2)
-        self.assertEqual(results["Executed"], 2)
-        self.assertEqual(results["Passed"], 2)
+        self.assertEqual(results['Requested'], 2)
+        self.assertEqual(results['Executed'], 2)
+        self.assertEqual(results['Passed'], 2)
 
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     unittest.main()
diff --git a/acts/framework/tests/config/unittest_bundle.py b/acts/framework/tests/config/unittest_bundle.py
index 8e26b93..87d5bc5 100755
--- a/acts/framework/tests/config/unittest_bundle.py
+++ b/acts/framework/tests/config/unittest_bundle.py
@@ -14,13 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/config/', pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py b/acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py
index 3a40d31..cf8e81e 100755
--- a/acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py
+++ b/acts/framework/tests/controllers/android_lib/android_lib_unittest_bundle.py
@@ -14,14 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/controllers/android_lib',
-        pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/event/event_bus_integration_test.py b/acts/framework/tests/event/event_bus_integration_test.py
index 7dadf40..c58727d 100755
--- a/acts/framework/tests/event/event_bus_integration_test.py
+++ b/acts/framework/tests/event/event_bus_integration_test.py
@@ -13,6 +13,7 @@
 #   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 shutil
 import tempfile
 import unittest
 from threading import RLock
@@ -65,6 +66,9 @@
         TestClass.instance_event_received = []
         TestClass.static_event_received = []
 
+    def tearDown(self):
+        shutil.rmtree(self.tmp_dir)
+
     def test_test_class_subscribed_fn_receives_event(self):
         """Tests that TestClasses have their subscribed functions called."""
         TestRunner(self.config, [('TestClass', [])]).run(TestClass)
diff --git a/acts/framework/tests/event/event_unittest_bundle.py b/acts/framework/tests/event/event_unittest_bundle.py
index b310cb5..56c3e09 100755
--- a/acts/framework/tests/event/event_unittest_bundle.py
+++ b/acts/framework/tests/event/event_unittest_bundle.py
@@ -14,13 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/event', pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/libs/logging/logging_unittest_bundle.py b/acts/framework/tests/libs/logging/logging_unittest_bundle.py
index 6f384ca..cf8e81e 100755
--- a/acts/framework/tests/libs/logging/logging_unittest_bundle.py
+++ b/acts/framework/tests/libs/logging/logging_unittest_bundle.py
@@ -14,13 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/libs/logging', pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/libs/metrics/unittest_bundle.py b/acts/framework/tests/libs/metrics/unittest_bundle.py
index 8bf7411..87d5bc5 100755
--- a/acts/framework/tests/libs/metrics/unittest_bundle.py
+++ b/acts/framework/tests/libs/metrics/unittest_bundle.py
@@ -14,13 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/libs/metrics', pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/libs/ota/unittest_bundle.py b/acts/framework/tests/libs/ota/unittest_bundle.py
index e0019f1..87d5bc5 100755
--- a/acts/framework/tests/libs/ota/unittest_bundle.py
+++ b/acts/framework/tests/libs/ota/unittest_bundle.py
@@ -14,13 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/libs/ota', pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/libs/proc/proc_unittest_bundle.py b/acts/framework/tests/libs/proc/proc_unittest_bundle.py
index 2a25587..cf8e81e 100755
--- a/acts/framework/tests/libs/proc/proc_unittest_bundle.py
+++ b/acts/framework/tests/libs/proc/proc_unittest_bundle.py
@@ -14,13 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/libs/proc', pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/metrics/loggers/blackbox_test.py b/acts/framework/tests/metrics/loggers/blackbox_test.py
index bda9cc3..d40b00d 100644
--- a/acts/framework/tests/metrics/loggers/blackbox_test.py
+++ b/acts/framework/tests/metrics/loggers/blackbox_test.py
@@ -13,18 +13,19 @@
 #   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 warnings
-
-from mock import Mock
-from mock import patch
+import shutil
 import tempfile
 import unittest
+import warnings
 from unittest import TestCase
+
 from acts.base_test import BaseTestClass
 from acts.metrics.loggers.blackbox import BlackboxMetricLogger
 from acts.test_runner import TestRunner
+from mock import Mock
+from mock import patch
 
-COMPILE_IMPORT_PROTO = 'acts.metrics.logger.compile_import_proto'
+COMPILE_PROTO = 'acts.metrics.logger.MetricLogger._compile_proto'
 GET_CONTEXT_FOR_EVENT = 'acts.metrics.logger.get_context_for_event'
 PROTO_METRIC_PUBLISHER = 'acts.metrics.logger.ProtoMetricPublisher'
 
@@ -42,10 +43,10 @@
         self.publisher = Mock()
         self._get_blackbox_identifier = lambda: str(id(self.context))
 
-    @patch(COMPILE_IMPORT_PROTO)
-    def test_default_init_attributes(self, compile_import_proto):
+    @patch(COMPILE_PROTO)
+    def test_default_init_attributes(self, compile_proto):
         metric_name = Mock()
-        compile_import_proto.return_value = self.proto_module
+        compile_proto.return_value = self.proto_module
 
         logger = BlackboxMetricLogger(metric_name)
 
@@ -53,8 +54,8 @@
         self.assertEqual(logger.proto_module, self.proto_module)
         self.assertIsNone(logger.metric_key)
 
-    @patch(COMPILE_IMPORT_PROTO)
-    def test_init_with_params(self, compile_import_proto):
+    @patch(COMPILE_PROTO)
+    def test_init_with_params(self, compile_proto):
         metric_name = Mock()
         metric_key = Mock()
 
@@ -64,9 +65,8 @@
 
     @patch(PROTO_METRIC_PUBLISHER)
     @patch(GET_CONTEXT_FOR_EVENT)
-    @patch(COMPILE_IMPORT_PROTO)
-    def test_init_with_event(self, compile_import_proto, get_context,
-                             publisher_cls):
+    @patch(COMPILE_PROTO)
+    def test_init_with_event(self, compile_proto, get_context, publisher_cls):
         metric_name = Mock()
 
         logger = BlackboxMetricLogger(metric_name, event=self.event)
@@ -74,10 +74,10 @@
         self.assertIsNotNone(logger.context)
         self.assertIsNotNone(logger.publisher)
 
-    @patch(COMPILE_IMPORT_PROTO)
-    def test_end_populates_result(self, compile_import_proto):
+    @patch(COMPILE_PROTO)
+    def test_end_populates_result(self, compile_proto):
         result = Mock()
-        compile_import_proto.return_value = self.proto_module
+        compile_proto.return_value = self.proto_module
         self.proto_module.ActsBlackboxMetricResult.return_value = result
 
         logger = BlackboxMetricLogger(self.TEST_METRIC_NAME)
@@ -93,12 +93,12 @@
                          '%s.%s' % ('Class#test', self.TEST_METRIC_NAME))
         self.assertEqual(result.metric_value, logger.metric_value)
 
-    @patch(COMPILE_IMPORT_PROTO)
+    @patch(COMPILE_PROTO)
     def test_end_uses_metric_value_on_metric_value_not_none(
-            self, compile_import_proto):
+            self, compile_proto):
         result = Mock()
         expected_result = Mock()
-        compile_import_proto.return_value = self.proto_module
+        compile_proto.return_value = self.proto_module
         self.proto_module.ActsBlackboxMetricResult.return_value = result
 
         logger = BlackboxMetricLogger(self.TEST_METRIC_NAME)
@@ -110,10 +110,10 @@
 
         self.assertEqual(result.metric_value, expected_result)
 
-    @patch(COMPILE_IMPORT_PROTO)
-    def test_end_uses_custom_metric_key(self, compile_import_proto):
+    @patch(COMPILE_PROTO)
+    def test_end_uses_custom_metric_key(self, compile_proto):
         result = Mock()
-        compile_import_proto.return_value = self.proto_module
+        compile_proto.return_value = self.proto_module
         self.proto_module.ActsBlackboxMetricResult.return_value = result
         metric_key = 'metric_key'
 
@@ -122,6 +122,7 @@
         logger.context = self.context
         logger.publisher = self.publisher
         logger._get_blackbox_identifier = self._get_blackbox_identifier
+        logger.metric_value = 'foo'
 
         logger.end(self.event)
 
@@ -129,10 +130,10 @@
         self.assertEqual(result.metric_key, expected_metric_key)
 
     @patch('acts.metrics.loggers.blackbox.ProtoMetric')
-    @patch(COMPILE_IMPORT_PROTO)
-    def test_end_does_publish(self, compile_import_proto, proto_metric_cls):
+    @patch(COMPILE_PROTO)
+    def test_end_does_publish(self, compile_proto, proto_metric_cls):
         result = Mock()
-        compile_import_proto.return_value = self.proto_module
+        compile_proto.return_value = self.proto_module
         self.proto_module.ActsBlackboxMetricResult.return_value = result
         metric_key = 'metric_key'
 
@@ -141,13 +142,26 @@
         logger.context = self.context
         logger.publisher = self.publisher
         logger._get_blackbox_identifier = self._get_blackbox_identifier
+        logger.metric_value = 'foo'
 
         logger.end(self.event)
 
         proto_metric_cls.assert_called_once_with(
             name=self.TEST_FILE_NAME, data=result)
         self.publisher.publish.assert_called_once_with(
-            proto_metric_cls.return_value)
+            [proto_metric_cls.return_value])
+
+
+class _BaseTestClassWithCleanup(BaseTestClass):
+    """Subclass of ACTS base test that generates a temp directory for
+    proto compiler output and cleans up upon exit.
+    """
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.proto_dir = tempfile.mkdtemp()
+
+    def __del__(self):
+        shutil.rmtree(self.proto_dir)
 
 
 class BlackboxMetricLoggerIntegrationTest(TestCase):
@@ -179,17 +193,19 @@
 
         runner.run()
         runner.stop()
+        shutil.rmtree(config['logpath'])
         return runner
 
     @patch('acts.metrics.logger.ProtoMetricPublisher')
     def test_test_case_metric(self, publisher_cls):
         result = 5.0
 
-        class MyTest(BaseTestClass):
+        class MyTest(_BaseTestClassWithCleanup):
             def __init__(self, controllers):
-                BaseTestClass.__init__(self, controllers)
+                super().__init__(controllers)
                 self.tests = ('test_case', )
-                self.metric = BlackboxMetricLogger.for_test_case('my_metric')
+                self.metric = BlackboxMetricLogger.for_test_case(
+                    'my_metric', compiler_out=self.proto_dir)
 
             def test_case(self):
                 self.metric.metric_value = result
@@ -198,7 +214,7 @@
 
         args_list = publisher_cls().publish.call_args_list
         self.assertEqual(len(args_list), 1)
-        metric = self.__get_only_arg(args_list[0])
+        metric = self.__get_only_arg(args_list[0])[0]
         self.assertEqual(metric.name, 'blackbox_my_metric')
         self.assertEqual(metric.data.test_identifier, 'MyTest#test_case')
         self.assertEqual(metric.data.metric_key, 'MyTest#test_case.my_metric')
@@ -208,14 +224,16 @@
     def test_multiple_test_case_metrics(self, publisher_cls):
         result = 5.0
 
-        class MyTest(BaseTestClass):
+        class MyTest(_BaseTestClassWithCleanup):
             def __init__(self, controllers):
-                BaseTestClass.__init__(self, controllers)
+                super().__init__(controllers)
                 self.tests = ('test_case', )
                 self.metric_1 = (
-                    BlackboxMetricLogger.for_test_case('my_metric_1'))
+                    BlackboxMetricLogger.for_test_case(
+                        'my_metric_1', compiler_out=self.proto_dir))
                 self.metric_2 = (
-                    BlackboxMetricLogger.for_test_case('my_metric_2'))
+                    BlackboxMetricLogger.for_test_case(
+                        'my_metric_2', compiler_out=self.proto_dir))
 
             def test_case(self):
                 self.metric_1.metric_value = result
@@ -225,7 +243,7 @@
 
         args_list = publisher_cls().publish.call_args_list
         self.assertEqual(len(args_list), 2)
-        metrics = [self.__get_only_arg(args) for args in args_list]
+        metrics = [self.__get_only_arg(args)[0] for args in args_list]
         self.assertEqual({metric.name
                           for metric in metrics},
                          {'blackbox_my_metric_1', 'blackbox_my_metric_2'})
@@ -242,21 +260,22 @@
     def test_test_case_metric_with_custom_key(self, publisher_cls):
         result = 5.0
 
-        class MyTest(BaseTestClass):
+        class MyTest(_BaseTestClassWithCleanup):
             def __init__(self, controllers):
-                BaseTestClass.__init__(self, controllers)
+                super().__init__(controllers)
                 self.tests = ('test_case', )
-                BlackboxMetricLogger.for_test_case(
-                    'my_metric', metric_key='my_metric_key')
+                self.metrics = BlackboxMetricLogger.for_test_case(
+                    'my_metric', metric_key='my_metric_key',
+                    compiler_out=self.proto_dir)
 
             def test_case(self):
-                self.result = result
+                self.metrics.metric_value = result
 
         self.run_acts_test(MyTest)
 
         args_list = publisher_cls().publish.call_args_list
         self.assertEqual(len(args_list), 1)
-        metric = self.__get_only_arg(args_list[0])
+        metric = self.__get_only_arg(args_list[0])[0]
         self.assertEqual(metric.data.metric_key, 'my_metric_key.my_metric')
 
     @patch('acts.metrics.logger.ProtoMetricPublisher')
@@ -265,14 +284,15 @@
         result_1 = 5.0
         result_2 = 8.0
 
-        class MyTest(BaseTestClass):
+        class MyTest(_BaseTestClassWithCleanup):
             def __init__(self, controllers):
-                BaseTestClass.__init__(self, controllers)
+                super().__init__(controllers)
                 self.tests = (
                     'test_case_1',
                     'test_case_2',
                 )
-                self.metric = BlackboxMetricLogger.for_test_class('my_metric')
+                self.metric = BlackboxMetricLogger.for_test_class(
+                    'my_metric', compiler_out=self.proto_dir)
 
             def setup_class(self):
                 self.metric.metric_value = 0
@@ -287,7 +307,7 @@
 
         args_list = publisher_cls().publish.call_args_list
         self.assertEqual(len(args_list), 1)
-        metric = self.__get_only_arg(args_list[0])
+        metric = self.__get_only_arg(args_list[0])[0]
         self.assertEqual(metric.data.metric_value, result_1 + result_2)
         self.assertEqual(metric.data.test_identifier, MyTest.__name__)
 
diff --git a/acts/framework/tests/metrics/unittest_bundle.py b/acts/framework/tests/metrics/unittest_bundle.py
index 9ba0bc5..56c3e09 100755
--- a/acts/framework/tests/metrics/unittest_bundle.py
+++ b/acts/framework/tests/metrics/unittest_bundle.py
@@ -14,13 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/metrics/', pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/framework/tests/test_utils/instrumentation/instrumentation_base_test_test.py b/acts/framework/tests/test_utils/instrumentation/instrumentation_base_test_test.py
index 04ac8e8..0a3ddde 100755
--- a/acts/framework/tests/test_utils/instrumentation/instrumentation_base_test_test.py
+++ b/acts/framework/tests/test_utils/instrumentation/instrumentation_base_test_test.py
@@ -29,11 +29,13 @@
         'lvl2': {'file1': 'FILE'}
     },
     'MockController': {
-        'param1': 1
+        'param1': 1,
+        'param2': 4
     },
     'MockInstrumentationBaseTest': {
         'MockController': {
-            'param2': 2
+            'param2': 2,
+            'param3': 5
         },
         'test_case': {
             'MockController': {
@@ -83,21 +85,21 @@
         controller config for the current test case.
         """
         self.instrumentation_test.current_test_name = 'test_case'
-        config = self.instrumentation_test._get_controller_config(
+        config = self.instrumentation_test._get_merged_config(
             'MockController')
-        self.assertNotIn('param1', config)
-        self.assertNotIn('param2', config)
-        self.assertIn('param3', config)
+        self.assertEqual(config.get('param1'), 1)
+        self.assertEqual(config.get('param2'), 2)
+        self.assertEqual(config.get('param3'), 3)
 
     def test_get_controller_config_for_test_class(self):
         """Test that _get_controller_config returns the controller config for
         the current test class (while no test case is running).
         """
-        config = self.instrumentation_test._get_controller_config(
+        config = self.instrumentation_test._get_merged_config(
             'MockController')
-        self.assertIn('param1', config)
-        self.assertIn('param2', config)
-        self.assertNotIn('param3', config)
+        self.assertEqual(config.get('param1'), 1)
+        self.assertEqual(config.get('param2'), 2)
+        self.assertEqual(config.get('param3'), 5)
 
 
 if __name__ == '__main__':
diff --git a/acts/framework/tests/test_utils/instrumentation/unit_test_suite.py b/acts/framework/tests/test_utils/instrumentation/unit_test_suite.py
index c929b65..d253cb3 100755
--- a/acts/framework/tests/test_utils/instrumentation/unit_test_suite.py
+++ b/acts/framework/tests/test_utils/instrumentation/unit_test_suite.py
@@ -14,14 +14,14 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import os
 import sys
 import unittest
 
 
 def main():
     suite = unittest.TestLoader().discover(
-        start_dir='./acts/framework/tests/test_utils/instrumentation',
-        pattern='*_test.py')
+        start_dir=os.path.dirname(__file__), pattern='*_test.py')
     return suite
 
 
diff --git a/acts/tests/google/ble/api/BleAdvertiseApiTest.py b/acts/tests/google/ble/api/BleAdvertiseApiTest.py
index 2a6cb26..26c459f 100644
--- a/acts/tests/google/ble/api/BleAdvertiseApiTest.py
+++ b/acts/tests/google/ble/api/BleAdvertiseApiTest.py
@@ -35,8 +35,8 @@
 
 
 class BleAdvertiseApiTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.ad_dut = self.android_devices[0]
 
     @BluetoothBaseTest.bt_test_wrap
diff --git a/acts/tests/google/ble/api/BleScanApiTest.py b/acts/tests/google/ble/api/BleScanApiTest.py
index 1398715..06f2362 100644
--- a/acts/tests/google/ble/api/BleScanApiTest.py
+++ b/acts/tests/google/ble/api/BleScanApiTest.py
@@ -47,8 +47,8 @@
 
 
 class BleScanApiTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.ad_dut = self.android_devices[0]
 
     def _format_defaults(self, input):
diff --git a/acts/tests/google/ble/api/GattApiTest.py b/acts/tests/google/ble/api/GattApiTest.py
index 5444e51..cc87979 100644
--- a/acts/tests/google/ble/api/GattApiTest.py
+++ b/acts/tests/google/ble/api/GattApiTest.py
@@ -24,11 +24,10 @@
 
 
 class GattApiTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.ad = self.android_devices[0]
 
-    def setup_class(self):
         return setup_multiple_devices_for_bt_test(self.android_devices)
 
     def setup_test(self):
diff --git a/acts/tests/google/ble/beacon_tests/BeaconSwarmTest.py b/acts/tests/google/ble/beacon_tests/BeaconSwarmTest.py
index 9ebfe1e..0df9a7b 100644
--- a/acts/tests/google/ble/beacon_tests/BeaconSwarmTest.py
+++ b/acts/tests/google/ble/beacon_tests/BeaconSwarmTest.py
@@ -41,10 +41,6 @@
     advertising_device_name_list = []
     discovered_mac_address_list = []
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
-        self.scn_ad = self.android_devices[0]
-
     def setup_test(self):
         self.discovered_mac_address_list = []
         for a in self.android_devices:
@@ -56,6 +52,7 @@
         return True
 
     def setup_class(self):
+        self.scn_ad = self.android_devices[0]
         if not setup_multiple_devices_for_bt_test(self.android_devices):
             return False
         return self._start_special_advertisements()
diff --git a/acts/tests/google/ble/bt5/AdvertisingSetTest.py b/acts/tests/google/ble/bt5/AdvertisingSetTest.py
index 66b0028..de4192f 100644
--- a/acts/tests/google/ble/bt5/AdvertisingSetTest.py
+++ b/acts/tests/google/ble/bt5/AdvertisingSetTest.py
@@ -62,14 +62,12 @@
         ]
     }
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
-        self.adv_ad = self.android_devices[0]
-
     def setup_class(self):
         super(AdvertisingSetTest, self).setup_class()
+        self.adv_ad = self.android_devices[0]
+
         if not self.adv_ad.droid.bluetoothIsLeExtendedAdvertisingSupported():
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Advertiser does not support LE Extended Advertising")
 
     def teardown_test(self):
diff --git a/acts/tests/google/ble/bt5/Bt5ScanTest.py b/acts/tests/google/ble/bt5/Bt5ScanTest.py
index e4cf4c5..e2c9c83 100644
--- a/acts/tests/google/ble/bt5/Bt5ScanTest.py
+++ b/acts/tests/google/ble/bt5/Bt5ScanTest.py
@@ -60,19 +60,17 @@
         ]
     }
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super(Bt5ScanTest, self).setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
 
-    def setup_class(self):
-        super(Bt5ScanTest, self).setup_class()
         if not self.scn_ad.droid.bluetoothIsLeExtendedAdvertisingSupported():
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Scanner does not support LE Extended Advertising")
 
         if not self.adv_ad.droid.bluetoothIsLeExtendedAdvertisingSupported():
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Advertiser does not support LE Extended Advertising")
 
     def teardown_test(self):
diff --git a/acts/tests/google/ble/bt5/PhyTest.py b/acts/tests/google/ble/bt5/PhyTest.py
index 9b95ead..0b1ecfa 100644
--- a/acts/tests/google/ble/bt5/PhyTest.py
+++ b/acts/tests/google/ble/bt5/PhyTest.py
@@ -42,11 +42,11 @@
     def setup_class(self):
         super(PhyTest, self).setup_class()
         if not self.cen_ad.droid.bluetoothIsLe2MPhySupported():
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Central device does not support LE 2M PHY")
 
         if not self.per_ad.droid.bluetoothIsLe2MPhySupported():
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Peripheral device does not support LE 2M PHY")
 
     # Some controllers auto-update PHY to 2M, and both client and server
diff --git a/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisementDiscoveryTest.py b/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisementDiscoveryTest.py
index 950a370..2af4c05 100644
--- a/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisementDiscoveryTest.py
+++ b/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisementDiscoveryTest.py
@@ -46,8 +46,8 @@
     max_advertisements = -1
     advertise_callback_list = []
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.droid_list = get_advanced_droid_list(self.android_devices)
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
diff --git a/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisingTest.py b/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisingTest.py
index 62f06d1..94ee0f7 100644
--- a/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisingTest.py
+++ b/acts/tests/google/ble/concurrency/ConcurrentBleAdvertisingTest.py
@@ -45,8 +45,8 @@
     default_timeout = 10
     max_advertisements = -1
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.droid_list = get_advanced_droid_list(self.android_devices)
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
diff --git a/acts/tests/google/ble/concurrency/ConcurrentBleScanningTest.py b/acts/tests/google/ble/concurrency/ConcurrentBleScanningTest.py
index c3e71f2..512aed8 100644
--- a/acts/tests/google/ble/concurrency/ConcurrentBleScanningTest.py
+++ b/acts/tests/google/ble/concurrency/ConcurrentBleScanningTest.py
@@ -39,8 +39,8 @@
     default_timeout = 20
     max_concurrent_scans = 27
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
 
diff --git a/acts/tests/google/ble/concurrency/ConcurrentGattConnectTest.py b/acts/tests/google/ble/concurrency/ConcurrentGattConnectTest.py
index 96cdf0a..ec5e09c 100644
--- a/acts/tests/google/ble/concurrency/ConcurrentGattConnectTest.py
+++ b/acts/tests/google/ble/concurrency/ConcurrentGattConnectTest.py
@@ -79,12 +79,10 @@
     advertisement_names = []
     list_of_arguments_list = []
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
-        self.pri_dut = self.android_devices[0]
-
     def setup_class(self):
         super(BluetoothBaseTest, self).setup_class()
+        self.pri_dut = self.android_devices[0]
+
 
         # Create 5 advertisements from different android devices
         for i in range(1, self.max_connections + 1):
diff --git a/acts/tests/google/ble/conn_oriented_chan/BleCoc2ConnTest.py b/acts/tests/google/ble/conn_oriented_chan/BleCoc2ConnTest.py
index 4229630..353f507 100644
--- a/acts/tests/google/ble/conn_oriented_chan/BleCoc2ConnTest.py
+++ b/acts/tests/google/ble/conn_oriented_chan/BleCoc2ConnTest.py
@@ -42,8 +42,8 @@
 
 
 class BleCoc2ConnTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.client_ad = self.android_devices[0]
         # The client which is scanning will need location to be enabled in order to
         # start scan and get scan results.
@@ -53,7 +53,6 @@
         if len(self.android_devices) > 2:
             self.server2_ad = self.android_devices[2]
 
-    def setup_class(self):
         return setup_multiple_devices_for_bt_test(self.android_devices)
 
     def teardown_test(self):
diff --git a/acts/tests/google/ble/conn_oriented_chan/BleCocTest.py b/acts/tests/google/ble/conn_oriented_chan/BleCocTest.py
index 29bbe1a..97ace9b 100644
--- a/acts/tests/google/ble/conn_oriented_chan/BleCocTest.py
+++ b/acts/tests/google/ble/conn_oriented_chan/BleCocTest.py
@@ -46,8 +46,8 @@
         "strange new worlds, to seek out new life and new civilizations,"
         " to boldly go where no man has gone before.")
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.client_ad = self.android_devices[0]
         # The client which is scanning will need location to be enabled in order to
         # start scan and get scan results.
@@ -57,7 +57,6 @@
         if len(self.android_devices) > 2:
             self.server2_ad = self.android_devices[2]
 
-    def setup_class(self):
         return setup_multiple_devices_for_bt_test(self.android_devices)
 
     def teardown_test(self):
diff --git a/acts/tests/google/ble/examples/BleExamplesTest.py b/acts/tests/google/ble/examples/BleExamplesTest.py
index e84e201..1ced2db 100644
--- a/acts/tests/google/ble/examples/BleExamplesTest.py
+++ b/acts/tests/google/ble/examples/BleExamplesTest.py
@@ -34,8 +34,8 @@
     scn_droid = None
     adv_droid = None
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.scn_droid, self.scn_ed = (self.android_devices[0].droid,
                                        self.android_devices[0].ed)
         self.adv_droid, self.adv_ed = (self.android_devices[1].droid,
diff --git a/acts/tests/google/ble/examples/GattServerExampleTest.py b/acts/tests/google/ble/examples/GattServerExampleTest.py
index 6a741a7..e1f6476 100644
--- a/acts/tests/google/ble/examples/GattServerExampleTest.py
+++ b/acts/tests/google/ble/examples/GattServerExampleTest.py
@@ -55,8 +55,8 @@
 
 
 class GattServerExampleTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.android_devices[0]
 
     @BluetoothBaseTest.bt_test_wrap
diff --git a/acts/tests/google/ble/filtering/FilteringTest.py b/acts/tests/google/ble/filtering/FilteringTest.py
index 3a3aaac..d1bdc39 100644
--- a/acts/tests/google/ble/filtering/FilteringTest.py
+++ b/acts/tests/google/ble/filtering/FilteringTest.py
@@ -64,8 +64,8 @@
     service_uuid_2 = "FFFFFFFF-0000-1000-8000-00805f9b34fb"
     service_uuid_3 = "3846D7A0-69C8-11E4-BA00-0002A5D5C51B"
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
         self.log.info("Scanner device model: {}".format(
diff --git a/acts/tests/google/ble/filtering/UniqueFilteringTest.py b/acts/tests/google/ble/filtering/UniqueFilteringTest.py
index 35945e5..c2e837c 100644
--- a/acts/tests/google/ble/filtering/UniqueFilteringTest.py
+++ b/acts/tests/google/ble/filtering/UniqueFilteringTest.py
@@ -38,8 +38,8 @@
 class UniqueFilteringTest(BluetoothBaseTest):
     default_timeout = 10
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
 
diff --git a/acts/tests/google/ble/gatt/GattConnectTest.py b/acts/tests/google/ble/gatt/GattConnectTest.py
index c276c0e..52f3601 100644
--- a/acts/tests/google/ble/gatt/GattConnectTest.py
+++ b/acts/tests/google/ble/gatt/GattConnectTest.py
@@ -58,8 +58,8 @@
     default_timeout = 10
     default_discovery_timeout = 3
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.cen_ad = self.android_devices[0]
         self.per_ad = self.android_devices[1]
 
diff --git a/acts/tests/google/ble/gatt/GattToolTest.py b/acts/tests/google/ble/gatt/GattToolTest.py
index fc61cb5..8e7a9f0 100644
--- a/acts/tests/google/ble/gatt/GattToolTest.py
+++ b/acts/tests/google/ble/gatt/GattToolTest.py
@@ -49,8 +49,8 @@
     adv_instances = []
     timer_list = []
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         # Central role Android device
         self.cen_ad = self.android_devices[0]
         self.ble_mac_address = self.user_params['ble_mac_address']
diff --git a/acts/tests/google/ble/power/GattPowerTest.py b/acts/tests/google/ble/power/GattPowerTest.py
index c4239bd..2817d74 100644
--- a/acts/tests/google/ble/power/GattPowerTest.py
+++ b/acts/tests/google/ble/power/GattPowerTest.py
@@ -50,14 +50,12 @@
     PMC_GATT_CMD = ("am broadcast -a com.android.pmc.GATT ")
     GATT_SERVER_MSG = "%s--es GattServer 1" % (PMC_GATT_CMD)
 
-    def __init__(self, controllers):
-        PowerBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super(GattPowerTest, self).setup_class()
 
         self.cen_ad = self.android_devices[0]
         self.per_ad = self.android_devices[1]
 
-    def setup_class(self):
-        super(GattPowerTest, self).setup_class()
         if not bluetooth_enabled_check(self.per_ad):
             self.log.error("Failed to turn on Bluetooth on peripheral")
 
diff --git a/acts/tests/google/ble/scan/BleBackgroundScanTest.py b/acts/tests/google/ble/scan/BleBackgroundScanTest.py
index 3a10793..6839602 100644
--- a/acts/tests/google/ble/scan/BleBackgroundScanTest.py
+++ b/acts/tests/google/ble/scan/BleBackgroundScanTest.py
@@ -45,13 +45,11 @@
     active_scan_callback_list = []
     active_adv_callback_list = []
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super(BluetoothBaseTest, self).setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
 
-    def setup_class(self):
-        super(BluetoothBaseTest, self).setup_class()
         utils.set_location_service(self.scn_ad, True)
         utils.set_location_service(self.adv_ad, True)
         return True
diff --git a/acts/tests/google/ble/scan/BleOnLostOnFoundTest.py b/acts/tests/google/ble/scan/BleOnLostOnFoundTest.py
index dd40b6a..01f7976 100644
--- a/acts/tests/google/ble/scan/BleOnLostOnFoundTest.py
+++ b/acts/tests/google/ble/scan/BleOnLostOnFoundTest.py
@@ -39,13 +39,11 @@
     active_scan_callback_list = []
     active_adv_callback_list = []
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super(BluetoothBaseTest, self).setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
 
-    def setup_class(self):
-        super(BluetoothBaseTest, self).setup_class()
         utils.set_location_service(self.scn_ad, True)
         utils.set_location_service(self.adv_ad, True)
         return True
diff --git a/acts/tests/google/ble/scan/BleOpportunisticScanTest.py b/acts/tests/google/ble/scan/BleOpportunisticScanTest.py
index 7b64cfe..9e59128 100644
--- a/acts/tests/google/ble/scan/BleOpportunisticScanTest.py
+++ b/acts/tests/google/ble/scan/BleOpportunisticScanTest.py
@@ -45,13 +45,11 @@
     active_scan_callback_list = []
     active_adv_callback_list = []
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super(BluetoothBaseTest, self).setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
 
-    def setup_class(self):
-        super(BluetoothBaseTest, self).setup_class()
         utils.set_location_service(self.scn_ad, True)
         utils.set_location_service(self.adv_ad, True)
         return True
diff --git a/acts/tests/google/ble/scan/BleScanScreenStateTest.py b/acts/tests/google/ble/scan/BleScanScreenStateTest.py
index b6d128a..07ae898 100644
--- a/acts/tests/google/ble/scan/BleScanScreenStateTest.py
+++ b/acts/tests/google/ble/scan/BleScanScreenStateTest.py
@@ -42,13 +42,11 @@
     scan_callback = -1
     shorter_scan_timeout = 4
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super(BluetoothBaseTest, self).setup_class()
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
 
-    def setup_class(self):
-        super(BluetoothBaseTest, self).setup_class()
         utils.set_location_service(self.scn_ad, True)
         utils.set_location_service(self.adv_ad, True)
         return True
diff --git a/acts/tests/google/ble/system_tests/BleStressTest.py b/acts/tests/google/ble/system_tests/BleStressTest.py
index be0a9fe..f97907f 100644
--- a/acts/tests/google/ble/system_tests/BleStressTest.py
+++ b/acts/tests/google/ble/system_tests/BleStressTest.py
@@ -38,8 +38,8 @@
     default_timeout = 10
     PAIRING_TIMEOUT = 20
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.droid_list = get_advanced_droid_list(self.android_devices)
         self.scn_ad = self.android_devices[0]
         self.adv_ad = self.android_devices[1]
diff --git a/acts/tests/google/bt/AkXB10PairingTest.py b/acts/tests/google/bt/AkXB10PairingTest.py
index 51e1e59..10e7335 100644
--- a/acts/tests/google/bt/AkXB10PairingTest.py
+++ b/acts/tests/google/bt/AkXB10PairingTest.py
@@ -28,8 +28,8 @@
 class AkXB10PairingTest(BluetoothBaseTest):
     DISCOVERY_TIME = 5
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.android_devices[0]
         # Do factory reset and then do delay for 3-seconds
         self.dut.droid.bluetoothFactoryReset()
diff --git a/acts/tests/google/bt/BtAirplaneModeTest.py b/acts/tests/google/bt/BtAirplaneModeTest.py
index bb90b10..e2dd7eb 100644
--- a/acts/tests/google/bt/BtAirplaneModeTest.py
+++ b/acts/tests/google/bt/BtAirplaneModeTest.py
@@ -31,8 +31,8 @@
     grace_timeout = 4
     WAIT_TIME_ANDROID_STATE_SETTLING = 5
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.android_devices[0]
 
     def setup_test(self):
diff --git a/acts/tests/google/bt/BtBasicFunctionalityTest.py b/acts/tests/google/bt/BtBasicFunctionalityTest.py
index 0cb4ea8..3194d76 100644
--- a/acts/tests/google/bt/BtBasicFunctionalityTest.py
+++ b/acts/tests/google/bt/BtBasicFunctionalityTest.py
@@ -36,12 +36,11 @@
     default_timeout = 10
     scan_discovery_time = 5
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.droid_ad = self.android_devices[0]
         self.droid1_ad = self.android_devices[1]
 
-    def setup_class(self):
         return setup_multiple_devices_for_bt_test(self.android_devices)
 
     def setup_test(self):
diff --git a/acts/tests/google/bt/BtFactoryResetTest.py b/acts/tests/google/bt/BtFactoryResetTest.py
index 8c9d399..f33b210 100644
--- a/acts/tests/google/bt/BtFactoryResetTest.py
+++ b/acts/tests/google/bt/BtFactoryResetTest.py
@@ -25,8 +25,8 @@
     default_timeout = 10
     grace_timeout = 4
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.pri_dut = self.android_devices[0]
         self.sec_dut = self.android_devices[1]
 
diff --git a/acts/tests/google/bt/BtKillProcessTest.py b/acts/tests/google/bt/BtKillProcessTest.py
index 2b17d28..ed53e20 100644
--- a/acts/tests/google/bt/BtKillProcessTest.py
+++ b/acts/tests/google/bt/BtKillProcessTest.py
@@ -25,8 +25,8 @@
 
 
 class BtKillProcessTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.android_devices[0]
 
     def _get_bt_pid(self):
diff --git a/acts/tests/google/bt/RfcommTest.py b/acts/tests/google/bt/RfcommTest.py
index 272fc89..58de1be 100644
--- a/acts/tests/google/bt/RfcommTest.py
+++ b/acts/tests/google/bt/RfcommTest.py
@@ -47,12 +47,11 @@
         "strange new worlds, to seek out new life and new civilizations,"
         " to boldly go where no man has gone before.")
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.client_ad = self.android_devices[0]
         self.server_ad = self.android_devices[1]
 
-    def setup_class(self):
         return setup_multiple_devices_for_bt_test(self.android_devices)
 
     def teardown_test(self):
diff --git a/acts/tests/google/bt/SonyXB2PairingTest.py b/acts/tests/google/bt/SonyXB2PairingTest.py
index 3907135..4dcf863 100644
--- a/acts/tests/google/bt/SonyXB2PairingTest.py
+++ b/acts/tests/google/bt/SonyXB2PairingTest.py
@@ -28,8 +28,8 @@
 class SonyXB2PairingTest(BluetoothBaseTest):
     DISCOVERY_TIME = 5
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.android_devices[0]
         # Do factory reset and then do delay for 3-seconds
         self.dut.droid.bluetoothFactoryReset()
diff --git a/acts/tests/google/bt/audio_lab/BtChameleonTest.py b/acts/tests/google/bt/audio_lab/BtChameleonTest.py
index 4a43cd3..d268344 100644
--- a/acts/tests/google/bt/audio_lab/BtChameleonTest.py
+++ b/acts/tests/google/bt/audio_lab/BtChameleonTest.py
@@ -47,8 +47,8 @@
     audio_file_2k1k_300_sec = "audio_file_2k1k_300_sec.wav"
     android_sdcard_music_path = "/sdcard/Music"
 
-    def __init__(self, controllers):
-        BtFunhausBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.chameleon = self.chameleon_devices[0]
         self.dut = self.android_devices[0]
         self.raw_audio_dest = "{}/{}".format(self.android_devices[0].log_path,
diff --git a/acts/tests/google/bt/audio_lab/BtFunhausTest.py b/acts/tests/google/bt/audio_lab/BtFunhausTest.py
index e82c955..42cbc22 100644
--- a/acts/tests/google/bt/audio_lab/BtFunhausTest.py
+++ b/acts/tests/google/bt/audio_lab/BtFunhausTest.py
@@ -26,8 +26,8 @@
     music_file_to_play = ""
     device_fails_to_connect_list = []
 
-    def __init__(self, controllers):
-        BtFunhausBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
 
     @test_tracker_info(uuid='80a4cc4c-7c2a-428d-9eaf-46239a7926df')
     def test_run_bt_audio_12_hours(self):
diff --git a/acts/tests/google/bt/audio_lab/ThreeButtonDongleTest.py b/acts/tests/google/bt/audio_lab/ThreeButtonDongleTest.py
index cf9add5..abf97be 100644
--- a/acts/tests/google/bt/audio_lab/ThreeButtonDongleTest.py
+++ b/acts/tests/google/bt/audio_lab/ThreeButtonDongleTest.py
@@ -26,8 +26,8 @@
 class ThreeButtonDongleTest(BluetoothBaseTest):
     iterations = 10
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.android_devices[0]
         self.dongle = self.relay_devices[0]
         self.log.info("Target dongle is {}".format(self.dongle.name))
diff --git a/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py b/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py
index 431e49e..99f2851 100644
--- a/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarBasicFunctionalityTest.py
@@ -36,11 +36,10 @@
     default_timeout = 10
     scan_discovery_time = 5
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.car_ad = self.android_devices[0]
 
-    def setup_class(self):
         return setup_multiple_devices_for_bt_test(self.android_devices)
 
     @test_tracker_info(uuid='b52a032a-3438-4b84-863f-c46a969882a4')
diff --git a/acts/tests/google/bt/car_bt/BtCarPairingTest.py b/acts/tests/google/bt/car_bt/BtCarPairingTest.py
index b4fde56..603f1a8 100644
--- a/acts/tests/google/bt/car_bt/BtCarPairingTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarPairingTest.py
@@ -33,8 +33,8 @@
 
 
 class BtCarPairingTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.car = self.android_devices[0]
         self.ph = self.android_devices[1]
 
diff --git a/acts/tests/google/bt/car_bt/BtCarPbapTest.py b/acts/tests/google/bt/car_bt/BtCarPbapTest.py
index ab605a2..6d5bccb 100644
--- a/acts/tests/google/bt/car_bt/BtCarPbapTest.py
+++ b/acts/tests/google/bt/car_bt/BtCarPbapTest.py
@@ -40,16 +40,14 @@
 class BtCarPbapTest(BluetoothBaseTest):
     contacts_destination_path = ""
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        if not super(BtCarPbapTest, self).setup_class():
+            return False
         self.pce = self.android_devices[0]
         self.pse = self.android_devices[1]
         self.pse2 = self.android_devices[2]
         self.contacts_destination_path = self.log_path + "/"
 
-    def setup_class(self):
-        if not super(BtCarPbapTest, self).setup_class():
-            return False
         permissions_list = [
             "android.permission.READ_CONTACTS",
             "android.permission.WRITE_CONTACTS",
diff --git a/acts/tests/google/bt/gatt/GattOverBrEdrTest.py b/acts/tests/google/bt/gatt/GattOverBrEdrTest.py
index dcf916d..7985540 100644
--- a/acts/tests/google/bt/gatt/GattOverBrEdrTest.py
+++ b/acts/tests/google/bt/gatt/GattOverBrEdrTest.py
@@ -47,13 +47,11 @@
     default_discovery_timeout = 3
     per_droid_mac_address = None
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super(BluetoothBaseTest, self).setup_class()
         self.cen_ad = self.android_devices[0]
         self.per_ad = self.android_devices[1]
 
-    def setup_class(self):
-        super(BluetoothBaseTest, self).setup_class()
         self.per_droid_mac_address = self.per_ad.droid.bluetoothGetLocalAddress(
         )
         if not self.per_droid_mac_address:
diff --git a/acts/tests/google/bt/hid/HidDeviceTest.py b/acts/tests/google/bt/hid/HidDeviceTest.py
index cdd1094..e7e1778 100644
--- a/acts/tests/google/bt/hid/HidDeviceTest.py
+++ b/acts/tests/google/bt/hid/HidDeviceTest.py
@@ -34,8 +34,8 @@
     tests = None
     default_timeout = 10
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.host_ad = self.android_devices[0]
         self.device_ad = self.android_devices[1]
 
diff --git a/acts/tests/google/bt/ota/BtOtaTest.py b/acts/tests/google/bt/ota/BtOtaTest.py
index 91e51bb..86e3098 100644
--- a/acts/tests/google/bt/ota/BtOtaTest.py
+++ b/acts/tests/google/bt/ota/BtOtaTest.py
@@ -32,14 +32,14 @@
 
         # Pairing devices
         if not pair_pri_to_sec(self.dut, self.android_devices[1]):
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed to bond devices prior to update")
 
         #Run OTA below, if ota fails then abort all tests
         try:
             ota_updater.update(self.dut)
         except Exception as err:
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed up apply OTA update. Aborting tests")
 
     @BluetoothBaseTest.bt_test_wrap
diff --git a/acts/tests/google/bt/pan/BtPanTest.py b/acts/tests/google/bt/pan/BtPanTest.py
index 0539851..01f6078 100644
--- a/acts/tests/google/bt/pan/BtPanTest.py
+++ b/acts/tests/google/bt/pan/BtPanTest.py
@@ -32,8 +32,8 @@
 
 
 class BtPanTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.pan_dut = self.android_devices[0]
         self.panu_dut = self.android_devices[1]
 
diff --git a/acts/tests/google/bt/performance/BtCodecSweepTest.py b/acts/tests/google/bt/performance/BtCodecSweepTest.py
index c8e6819..a6504a5 100644
--- a/acts/tests/google/bt/performance/BtCodecSweepTest.py
+++ b/acts/tests/google/bt/performance/BtCodecSweepTest.py
@@ -28,8 +28,8 @@
 
 class BtCodecSweepTest(A2dpCodecBaseTest):
 
-    def __init__(self, configs):
-        super().__init__(configs)
+    def setup_class(self):
+        super().setup_class()
         self.bt_logger = BluetoothMetricLogger.for_test_case()
         self.start_time = time.time()
 
diff --git a/acts/tests/google/bt/performance/BtInterferenceRSSITest.py b/acts/tests/google/bt/performance/BtInterferenceRSSITest.py
index dbc1e9e..6c92c61 100644
--- a/acts/tests/google/bt/performance/BtInterferenceRSSITest.py
+++ b/acts/tests/google/bt/performance/BtInterferenceRSSITest.py
@@ -26,8 +26,8 @@
             module) terminates the sequence and keeps it from looping.
     """
 
-    def __init__(self, configs):
-        super().__init__(configs)
+    def setup_class(self):
+        super().setup_class()
         req_params = ["bt_atten_sequences", "RelayDevice", "codec"]
         opt_params = ["audio_params"]
         self.unpack_userparams(req_params, opt_params)
diff --git a/acts/tests/google/bt/performance/BtRangeCodecTest.py b/acts/tests/google/bt/performance/BtRangeCodecTest.py
old mode 100755
new mode 100644
index 8070997..e20b864
--- a/acts/tests/google/bt/performance/BtRangeCodecTest.py
+++ b/acts/tests/google/bt/performance/BtRangeCodecTest.py
@@ -31,8 +31,8 @@
 
 class BtRangeCodecTest(A2dpCodecBaseTest):
 
-    def __init__(self, configs):
-        super().__init__(configs)
+    def setup_class(self):
+        super().setup_class()
         self.bt_logger = log.BluetoothMetricLogger.for_test_case()
         self.start_time = time.time()
         self.attenuator = self.attenuators[0]
diff --git a/acts/tests/google/bt/pts/A2dpPtsTest.py b/acts/tests/google/bt/pts/A2dpPtsTest.py
index 1b16c2a..25ed3ce 100644
--- a/acts/tests/google/bt/pts/A2dpPtsTest.py
+++ b/acts/tests/google/bt/pts/A2dpPtsTest.py
@@ -28,8 +28,8 @@
     ble_advertise_interval = 100
     pts_action_mapping = None
 
-    def __init__(self, controllers):
-        super(A2dpPtsTest, self).__init__(controllers)
+    def setup_class(self):
+        super(A2dpPtsTest, self).setup_class()
         self.dut.initialize_bluetooth_controller()
         # self.dut.set_bluetooth_local_name(self.dut_bluetooth_local_name)
         local_dut_mac_address = self.dut.get_local_bluetooth_address()
@@ -58,8 +58,6 @@
 
         self.pts.set_ics_and_ixit(ics, ixit)
 
-    def setup_class(self):
-        super(A2dpPtsTest, self).setup_class()
         self.dut.unbond_all_known_devices()
         self.dut.start_pairing_helper()
 
diff --git a/acts/tests/google/bt/pts/BtCmdLineTest.py b/acts/tests/google/bt/pts/BtCmdLineTest.py
index c75aa35..8d925ed 100644
--- a/acts/tests/google/bt/pts/BtCmdLineTest.py
+++ b/acts/tests/google/bt/pts/BtCmdLineTest.py
@@ -34,8 +34,8 @@
 class BtCmdLineTest(BluetoothBaseTest):
     target_mac_address = ""
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         if not "target_mac_address" in self.user_params.keys():
             self.log.warning("Missing user config \"target_mac_address\"!")
             self.target_mac_address = ""
@@ -71,6 +71,7 @@
             music_path = self.user_params[music_path_str]
             self._add_music_to_primary_android_device(music_path,
                                                       android_music_path)
+        return True
 
     def _add_music_to_primary_android_device(self, music_path,
                                              android_music_path):
@@ -82,9 +83,6 @@
                 self.android_devices[0].adb.push("{} {}".format(
                     file, android_music_path))
 
-    def setup_class(self):
-        return True
-
     def test_pts_cmd_line_helper(self):
         cmd_line = CmdInput()
         cmd_line.setup_vars(self.android_devices, self.target_mac_address,
diff --git a/acts/tests/google/bt/pts/GattPtsTest.py b/acts/tests/google/bt/pts/GattPtsTest.py
index 318fb51..7883f37 100644
--- a/acts/tests/google/bt/pts/GattPtsTest.py
+++ b/acts/tests/google/bt/pts/GattPtsTest.py
@@ -32,8 +32,8 @@
     ble_advertise_interval = 100
     pts_action_mapping = None
 
-    def __init__(self, controllers):
-        super(GattPtsTest, self).__init__(controllers)
+    def setup_class(self):
+        super(GattPtsTest, self).setup_class()
         self.dut_bluetooth_local_name = "fs_test"
         self.dut.initialize_bluetooth_controller()
         self.dut.set_bluetooth_local_name(self.dut_bluetooth_local_name)
@@ -74,8 +74,6 @@
 
         self.pts.set_ics_and_ixit(ics, ixit)
 
-    def setup_class(self):
-        super(GattPtsTest, self).setup_class()
         self.dut.unbond_all_known_devices()
         self.dut.start_pairing_helper()
 
diff --git a/acts/tests/google/bt/pts/SdpPtsTest.py b/acts/tests/google/bt/pts/SdpPtsTest.py
index 2535671..84c7ab7 100644
--- a/acts/tests/google/bt/pts/SdpPtsTest.py
+++ b/acts/tests/google/bt/pts/SdpPtsTest.py
@@ -97,8 +97,8 @@
 
 
 class SdpPtsTest(PtsBaseClass):
-    def __init__(self, controllers):
-        super(SdpPtsTest, self).__init__(controllers)
+    def setup_class(self):
+        super(SdpPtsTest, self).setup_class()
         self.dut.initialize_bluetooth_controller()
         # self.dut.set_bluetooth_local_name(self.dut_bluetooth_local_name)
         local_dut_mac_address = self.dut.get_local_bluetooth_address()
@@ -127,8 +127,6 @@
 
         self.pts.set_ics_and_ixit(ics, ixit)
 
-    def setup_class(self):
-        super(SdpPtsTest, self).setup_class()
         self.dut.unbond_all_known_devices()
         self.dut.set_discoverable(True)
 
diff --git a/acts/tests/google/bt/sdp/SdpSetupTest.py b/acts/tests/google/bt/sdp/SdpSetupTest.py
index cb63724..938d720 100644
--- a/acts/tests/google/bt/sdp/SdpSetupTest.py
+++ b/acts/tests/google/bt/sdp/SdpSetupTest.py
@@ -77,8 +77,8 @@
 
 
 class SdpSetupTest(BaseTestClass):
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super(SdpSetupTest, self).setup_class()
         if 'dut' in self.user_params:
             if self.user_params['dut'] == 'fuchsia_devices':
                 self.dut = create_bluetooth_device(self.fuchsia_devices[0])
@@ -92,8 +92,6 @@
             self.dut = create_bluetooth_device(self.fuchsia_devices[0])
         self.dut.initialize_bluetooth_controller()
 
-    def setup_class(self):
-        super(SdpSetupTest, self).setup_class()
 
     def setup_test(self):
         self.dut.sdp_clean_up()
diff --git a/acts/tests/google/bt/system_tests/BtStressTest.py b/acts/tests/google/bt/system_tests/BtStressTest.py
index 163ee37..0473aa0 100644
--- a/acts/tests/google/bt/system_tests/BtStressTest.py
+++ b/acts/tests/google/bt/system_tests/BtStressTest.py
@@ -33,8 +33,8 @@
     default_timeout = 20
     iterations = 100
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
 
     def teardown_test(self):
         super(BluetoothBaseTest, self).teardown_test()
diff --git a/acts/tests/google/bt/system_tests/RfcommLongevityTest.py b/acts/tests/google/bt/system_tests/RfcommLongevityTest.py
index 3e39344..d1d4fe5 100644
--- a/acts/tests/google/bt/system_tests/RfcommLongevityTest.py
+++ b/acts/tests/google/bt/system_tests/RfcommLongevityTest.py
@@ -39,8 +39,8 @@
         "strange new worlds, to seek out new life and new civilizations,"
         " to boldly go where no man has gone before.")
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.client_ad = self.android_devices[0]
         self.server_ad = self.android_devices[1]
 
diff --git a/acts/tests/google/bt/system_tests/RfcommStressTest.py b/acts/tests/google/bt/system_tests/RfcommStressTest.py
index 8e56ef0..3fac543 100644
--- a/acts/tests/google/bt/system_tests/RfcommStressTest.py
+++ b/acts/tests/google/bt/system_tests/RfcommStressTest.py
@@ -38,8 +38,8 @@
         "strange new worlds, to seek out new life and new civilizations,"
         " to boldly go where no man has gone before.")
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.client_ad = self.android_devices[0]
         self.server_ad = self.android_devices[1]
 
diff --git a/acts/tests/google/experimental/BluetoothLatencyTest.py b/acts/tests/google/experimental/BluetoothLatencyTest.py
index ebb3019..811a41c 100644
--- a/acts/tests/google/experimental/BluetoothLatencyTest.py
+++ b/acts/tests/google/experimental/BluetoothLatencyTest.py
@@ -41,8 +41,8 @@
              data_transfer_type: Data transfer protocol used for the test
         """
 
-    def __init__(self, configs):
-        BaseTestClass.__init__(self, configs)
+    def setup_class(self):
+        super().setup_class()
 
         # Sanity check of the devices under test
         # TODO(b/119051823): Investigate using a config validator to replace this.
diff --git a/acts/tests/google/experimental/BluetoothPairAndConnectTest.py b/acts/tests/google/experimental/BluetoothPairAndConnectTest.py
index f021702..e54e4e7 100644
--- a/acts/tests/google/experimental/BluetoothPairAndConnectTest.py
+++ b/acts/tests/google/experimental/BluetoothPairAndConnectTest.py
@@ -52,8 +52,8 @@
        bt_utils: BTUtils test action object
     """
 
-    def __init__(self, configs):
-        BaseTestClass.__init__(self, configs)
+    def setup_class(self):
+        super().setup_class()
         # Sanity check of the devices under test
         # TODO(b/119051823): Investigate using a config validator to replace this.
         if not self.android_devices:
diff --git a/acts/tests/google/experimental/BluetoothReconnectTest.py b/acts/tests/google/experimental/BluetoothReconnectTest.py
index 717f444..a03ec7b 100644
--- a/acts/tests/google/experimental/BluetoothReconnectTest.py
+++ b/acts/tests/google/experimental/BluetoothReconnectTest.py
@@ -46,8 +46,8 @@
        dut_bt_addr: The Bluetooth address of the Apollo earbuds
     """
 
-    def __init__(self, configs):
-        BaseTestClass.__init__(self, configs)
+    def setup_class(self):
+        super().setup_class()
         # sanity check of the dut devices.
         # TODO(b/119051823): Investigate using a config validator to replace this.
         if not self.android_devices:
diff --git a/acts/tests/google/experimental/BluetoothThroughputTest.py b/acts/tests/google/experimental/BluetoothThroughputTest.py
index 8b4fd48..3403ded 100644
--- a/acts/tests/google/experimental/BluetoothThroughputTest.py
+++ b/acts/tests/google/experimental/BluetoothThroughputTest.py
@@ -37,8 +37,8 @@
          data_transfer_type: Data transfer protocol used for the test
     """
 
-    def __init__(self, configs):
-        BaseTestClass.__init__(self, configs)
+    def setup_class(self):
+        super().setup_class()
 
         # Sanity check of the devices under test
         # TODO(b/119051823): Investigate using a config validator to replace this.
diff --git a/acts/tests/google/fuchsia/bt/BleFuchsiaAndroidTest.py b/acts/tests/google/fuchsia/bt/BleFuchsiaAndroidTest.py
index 59c5006..6c5798a 100644
--- a/acts/tests/google/fuchsia/bt/BleFuchsiaAndroidTest.py
+++ b/acts/tests/google/fuchsia/bt/BleFuchsiaAndroidTest.py
@@ -37,8 +37,8 @@
     active_adv_callback_list = []
     droid = None
 
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
 
         # Android device under test
         self.ad = self.android_devices[0]
diff --git a/acts/tests/google/fuchsia/bt/BleFuchsiaTest.py b/acts/tests/google/fuchsia/bt/BleFuchsiaTest.py
index 4597ed7..7b368ca 100644
--- a/acts/tests/google/fuchsia/bt/BleFuchsiaTest.py
+++ b/acts/tests/google/fuchsia/bt/BleFuchsiaTest.py
@@ -30,8 +30,8 @@
     active_adv_callback_list = []
     droid = None
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
 
         if (len(self.fuchsia_devices) < 2):
             self.log.error("BleFuchsiaTest Init: Not enough fuchsia devices.")
diff --git a/acts/tests/google/fuchsia/bt/FuchsiaBtMacAddressTest.py b/acts/tests/google/fuchsia/bt/FuchsiaBtMacAddressTest.py
index ec2042c..c4124b7 100644
--- a/acts/tests/google/fuchsia/bt/FuchsiaBtMacAddressTest.py
+++ b/acts/tests/google/fuchsia/bt/FuchsiaBtMacAddressTest.py
@@ -32,10 +32,9 @@
 class FuchsiaBtMacAddressTest(BaseTestClass):
     scan_timeout_seconds = 10
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
-
     def setup_class(self):
+        super().setup_class()
+
         if len(self.fuchsia_devices) < 2:
             raise signals.TestAbortAll("Need at least two Fuchsia devices")
         for device in self.fuchsia_devices:
diff --git a/acts/tests/google/fuchsia/bt/FuchsiaBtScanTest.py b/acts/tests/google/fuchsia/bt/FuchsiaBtScanTest.py
index 5e0ebc3..722a446 100644
--- a/acts/tests/google/fuchsia/bt/FuchsiaBtScanTest.py
+++ b/acts/tests/google/fuchsia/bt/FuchsiaBtScanTest.py
@@ -32,12 +32,11 @@
 class FuchsiaBtScanTest(BaseTestClass):
     scan_timeout_seconds = 30
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.pri_dut = self.fuchsia_devices[0]
         self.sec_dut = self.fuchsia_devices[1]
 
-    def setup_class(self):
         self.pri_dut.btc_lib.initBluetoothControl()
         self.sec_dut.btc_lib.initBluetoothControl()
 
diff --git a/acts/tests/google/fuchsia/bt/FuchsiaCmdLineTest.py b/acts/tests/google/fuchsia/bt/FuchsiaCmdLineTest.py
index d34dc62..63c7845 100644
--- a/acts/tests/google/fuchsia/bt/FuchsiaCmdLineTest.py
+++ b/acts/tests/google/fuchsia/bt/FuchsiaCmdLineTest.py
@@ -32,8 +32,8 @@
 class FuchsiaCmdLineTest(BaseTestClass):
     target_device_name = ""
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         if not "target_device_name" in self.user_params.keys():
             self.log.warning("Missing user config \"target_device_name\"!")
             self.target_device_name = ""
diff --git a/acts/tests/google/fuchsia/bt/gatt/GattConnectionStressTest.py b/acts/tests/google/fuchsia/bt/gatt/GattConnectionStressTest.py
index caba8f5..5c46548 100644
--- a/acts/tests/google/fuchsia/bt/gatt/GattConnectionStressTest.py
+++ b/acts/tests/google/fuchsia/bt/gatt/GattConnectionStressTest.py
@@ -42,8 +42,8 @@
     scan_timeout_seconds = 10
     default_iterations = 1000
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.fuchsia_client_dut = self.fuchsia_devices[0]
         self.fuchsia_server_dut = self.fuchsia_devices[1]
         self.default_iterations = self.user_params.get(
diff --git a/acts/tests/google/fuchsia/bt/gatt/GattServerSetupTest.py b/acts/tests/google/fuchsia/bt/gatt/GattServerSetupTest.py
index 67ef6f8..83418e4 100644
--- a/acts/tests/google/fuchsia/bt/gatt/GattServerSetupTest.py
+++ b/acts/tests/google/fuchsia/bt/gatt/GattServerSetupTest.py
@@ -31,8 +31,8 @@
 class GattServerSetupTest(BaseTestClass):
     err_message = "Setting up database failed with: {}"
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.fuchsia_dut = self.fuchsia_devices[0]
 
     def setup_database(self, database):
diff --git a/acts/tests/google/fuchsia/examples/Sl4fSanityTest.py b/acts/tests/google/fuchsia/examples/Sl4fSanityTest.py
index 7d95e2d..d2116a9 100644
--- a/acts/tests/google/fuchsia/examples/Sl4fSanityTest.py
+++ b/acts/tests/google/fuchsia/examples/Sl4fSanityTest.py
@@ -29,10 +29,9 @@
 
 class Sl4fSanityTest(BaseTestClass):
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
-
     def setup_class(self):
+        super().setup_class()
+
         success_str = ("Congratulations! Fuchsia controllers have been "
                        "initialized successfully!")
         err_str = ("Sorry, please try verifying FuchsiaDevice is in your "
@@ -40,7 +39,7 @@
         if len(self.fuchsia_devices) > 0:
             self.log.info(success_str)
         else:
-            raise signals.TestSkipClass("err_str")
+            raise signals.TestAbortClass("err_str")
 
     def test_example(self):
         self.log.info("Congratulations! You've run your first test.")
diff --git a/acts/tests/google/fuchsia/logging/FuchsiaLoggingTest.py b/acts/tests/google/fuchsia/logging/FuchsiaLoggingTest.py
index e76f274..28d6998 100644
--- a/acts/tests/google/fuchsia/logging/FuchsiaLoggingTest.py
+++ b/acts/tests/google/fuchsia/logging/FuchsiaLoggingTest.py
@@ -20,8 +20,8 @@
 
 
 class FuchsiaLoggingTest(BaseTestClass):
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.fuchsia_devices[0]
         self.message = "Logging Test"
 
diff --git a/acts/tests/google/fuchsia/wlan/ConnectionStressTest.py b/acts/tests/google/fuchsia/wlan/ConnectionStressTest.py
index 02d21af..51bc59c 100644
--- a/acts/tests/google/fuchsia/wlan/ConnectionStressTest.py
+++ b/acts/tests/google/fuchsia/wlan/ConnectionStressTest.py
@@ -44,8 +44,8 @@
     channel_2G = hostapd_constants.AP_DEFAULT_CHANNEL_2G
     channel_5G = hostapd_constants.AP_DEFAULT_CHANNEL_5G
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.ssid = rand_ascii_str(10)
         self.fd = self.fuchsia_devices[0]
         self.dut = create_wlan_device(self.fd)
diff --git a/acts/tests/google/fuchsia/wlan/DownloadStressTest.py b/acts/tests/google/fuchsia/wlan/DownloadStressTest.py
index 97d5720..f3f3803 100644
--- a/acts/tests/google/fuchsia/wlan/DownloadStressTest.py
+++ b/acts/tests/google/fuchsia/wlan/DownloadStressTest.py
@@ -52,8 +52,8 @@
     num_of_small_downloads = 5
     download_threads_result = []
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.ssid = rand_ascii_str(10)
         self.fd = self.fuchsia_devices[0]
         self.wlan_device = create_wlan_device(self.fd)
@@ -62,7 +62,6 @@
             self.user_params.get("download_stress_test_iterations",
                                  self.num_of_iterations))
 
-    def setup_class(self):
         setup_ap_and_associate(
             access_point=self.ap,
             client=self.wlan_device,
diff --git a/acts/tests/google/fuchsia/wlan/PingStressTest.py b/acts/tests/google/fuchsia/wlan/PingStressTest.py
index 911e3d7..2aa1d00 100644
--- a/acts/tests/google/fuchsia/wlan/PingStressTest.py
+++ b/acts/tests/google/fuchsia/wlan/PingStressTest.py
@@ -43,10 +43,9 @@
     google_dns_1 = '8.8.8.8'
     google_dns_2 = '8.8.4.4'
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
-
     def setup_class(self):
+        super().setup_class()
+
         self.ssid = rand_ascii_str(10)
         self.fd = self.fuchsia_devices[0]
         self.wlan_device = create_wlan_device(self.fd)
diff --git a/acts/tests/google/fuchsia/wlan/RebootAPStressTest.py b/acts/tests/google/fuchsia/wlan/RebootAPStressTest.py
index 6e37409..cbe9d12 100644
--- a/acts/tests/google/fuchsia/wlan/RebootAPStressTest.py
+++ b/acts/tests/google/fuchsia/wlan/RebootAPStressTest.py
@@ -56,8 +56,8 @@
     # after AP reboot.
     wait_after_ap_reboot_s = 1
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.ssid = rand_ascii_str(10)
         self.wlan_device = create_wlan_device(self.fuchsia_devices[0])
         self.ap = self.access_points[0]
diff --git a/acts/tests/google/fuchsia/wlan/RebootStressTest.py b/acts/tests/google/fuchsia/wlan/RebootStressTest.py
index 86bafbb..fb9a870 100644
--- a/acts/tests/google/fuchsia/wlan/RebootStressTest.py
+++ b/acts/tests/google/fuchsia/wlan/RebootStressTest.py
@@ -38,8 +38,8 @@
     # Eg: "reboot_stress_test_iterations": "10"
     num_of_iterations = 3
 
-    def __init__(self, controllers):
-        BaseTestClass.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.ssid = rand_ascii_str(10)
         self.fd = self.fuchsia_devices[0]
         self.wlan_device = create_wlan_device(self.fd)
@@ -48,7 +48,6 @@
             self.user_params.get("reboot_stress_test_iterations",
                                  self.num_of_iterations))
 
-    def setup_class(self):
         setup_ap_and_associate(
             access_point=self.ap,
             client=self.wlan_device,
diff --git a/acts/tests/google/fugu/AndroidFuguRemotePairingTest.py b/acts/tests/google/fugu/AndroidFuguRemotePairingTest.py
old mode 100755
new mode 100644
index 03b443f..a41f9fc
--- a/acts/tests/google/fugu/AndroidFuguRemotePairingTest.py
+++ b/acts/tests/google/fugu/AndroidFuguRemotePairingTest.py
@@ -22,8 +22,8 @@
 from acts.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
 
 class AndroidFuguRemotePairingTest(BluetoothBaseTest):
-    def __init__(self, controllers):
-        BluetoothBaseTest.__init__(self, controllers)
+    def setup_class(self):
+        super().setup_class()
         self.dut = self.android_devices[0]
         self.fugu_remote = self.relay_devices[0]
 
diff --git a/acts/tests/google/gnss/AGNSSPerformanceTest.py b/acts/tests/google/gnss/AGNSSPerformanceTest.py
index 60dce02..a14a1ba 100644
--- a/acts/tests/google/gnss/AGNSSPerformanceTest.py
+++ b/acts/tests/google/gnss/AGNSSPerformanceTest.py
@@ -18,7 +18,7 @@
 
 from acts import base_test
 from acts import asserts
-from acts.controllers.gnssinst_lib.rohdeschwarz import contest
+from acts.controllers.rohdeschwarz_lib import contest
 from acts.test_utils.tel import tel_test_utils
 import json
 
diff --git a/acts/tests/google/gnss/FlpTtffTest.py b/acts/tests/google/gnss/FlpTtffTest.py
index 77e8cf0..0a36923 100644
--- a/acts/tests/google/gnss/FlpTtffTest.py
+++ b/acts/tests/google/gnss/FlpTtffTest.py
@@ -56,7 +56,6 @@
         for network in self.pixel_lab_network:
             SSID = network['SSID']
             self.ssid_map[SSID] = network
-
         if int(self.ad.adb.shell("settings get global airplane_mode_on")) != 0:
             self.ad.log.info("Force airplane mode off")
             force_airplane_mode(self.ad, False)
diff --git a/acts/tests/google/gnss/GnssSanityTest.py b/acts/tests/google/gnss/GnssSanityTest.py
index 4d707d5..28c2ccf 100644
--- a/acts/tests/google/gnss/GnssSanityTest.py
+++ b/acts/tests/google/gnss/GnssSanityTest.py
@@ -69,6 +69,8 @@
 from acts.test_utils.gnss.gnss_test_utils import check_xtra_download
 from acts.test_utils.gnss.gnss_test_utils import gnss_tracking_via_gtw_gpstool
 from acts.test_utils.gnss.gnss_test_utils import parse_gtw_gpstool_log
+from acts.test_utils.gnss.gnss_test_utils import enable_supl_mode
+from acts.test_utils.gnss.gnss_test_utils import start_toggle_gnss_by_gtw_gpstool
 
 
 class GnssSanityTest(BaseTestClass):
@@ -77,8 +79,8 @@
         super().setup_class()
         self.ad = self.android_devices[0]
         req_params = ["pixel_lab_network", "standalone_cs_criteria",
-                      "supl_cs_criteria", "xtra_ws_criteria", "xtra_cs_criteria",
-                      "weak_signal_supl_cs_criteria",
+                      "supl_cs_criteria", "xtra_ws_criteria",
+                      "xtra_cs_criteria", "weak_signal_supl_cs_criteria",
                       "weak_signal_xtra_ws_criteria",
                       "weak_signal_xtra_cs_criteria",
                       "default_gnss_signal_attenuation",
@@ -98,7 +100,6 @@
         else:
             self.wifi_xtra_cs_criteria = self.xtra_cs_criteria
         self.flash_new_radio_or_mbn()
-
         set_attenuator_gnss_signal(self.ad, self.attenuators,
                                    self.default_gnss_signal_attenuation)
         _init_device(self.ad)
@@ -259,7 +260,8 @@
                     self.ad.log.error("\n%s" % error)
             else:
                 self.ad.log.info("NO \"%s\" initialization error found." % attr)
-        asserts.assert_true(error_mismatch, "Error message found after GNSS init")
+        asserts.assert_true(error_mismatch, "Error message found after GNSS "
+                                            "init")
 
     @test_tracker_info(uuid="ff318483-411c-411a-8b1a-422bd54f4a3f")
     def test_supl_capabilities(self):
@@ -436,7 +438,8 @@
             4. DUT hang up call.
 
         Expected Results:
-            All SUPL TTFF Cold Start results should be less than supl_cs_criteria.
+            All SUPL TTFF Cold Start results should be less than
+            supl_cs_criteria.
         """
         begin_time = get_current_epoch_time()
         start_qxdm_logger(self.ad, begin_time)
@@ -539,9 +542,9 @@
             start_ttff_by_gtw_gpstool(self.ad, ttff_mode="cs", iteration=3)
             ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                     self.pixel_lab_location)
-            supl_ssr_test_result = check_ttff_data(self.ad, ttff_data,
-                                                   ttff_mode="Cold Start",
-                                                   criteria=self.supl_cs_criteria)
+            supl_ssr_test_result = check_ttff_data(
+                self.ad, ttff_data, ttff_mode="Cold Start",
+                criteria=self.supl_cs_criteria)
             self.ad.log.info("SUPL after Modem SSR test %d times -> %s"
                              % (times, supl_ssr_test_result))
             supl_ssr_test_result_all.append(supl_ssr_test_result)
@@ -713,7 +716,9 @@
         start_ttff_by_gtw_gpstool(self.ad, ttff_mode="ws", iteration=10)
         ws_ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                    self.pixel_lab_location)
-        ws_result = check_ttff_data(self.ad, ws_ttff_data, ttff_mode="Warm Start",
+        ws_result = check_ttff_data(self.ad,
+                                    ws_ttff_data,
+                                    ttff_mode="Warm Start",
                                     criteria=self.xtra_ws_criteria)
         xtra_result.append(ws_result)
         begin_time = get_current_epoch_time()
@@ -721,7 +726,9 @@
         start_ttff_by_gtw_gpstool(self.ad, ttff_mode="cs", iteration=10)
         cs_ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                    self.pixel_lab_location)
-        cs_result = check_ttff_data(self.ad, cs_ttff_data, ttff_mode="Cold Start",
+        cs_result = check_ttff_data(self.ad,
+                                    cs_ttff_data,
+                                    ttff_mode="Cold Start",
                                     criteria=self.xtra_cs_criteria)
         xtra_result.append(cs_result)
         asserts.assert_true(all(xtra_result),
@@ -752,7 +759,9 @@
         start_ttff_by_gtw_gpstool(self.ad, ttff_mode="ws", iteration=10)
         ws_ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                    self.pixel_lab_location)
-        ws_result = check_ttff_data(self.ad, ws_ttff_data, ttff_mode="Warm Start",
+        ws_result = check_ttff_data(self.ad,
+                                    ws_ttff_data,
+                                    ttff_mode="Warm Start",
                                     criteria=self.weak_signal_xtra_ws_criteria)
         xtra_result.append(ws_result)
         begin_time = get_current_epoch_time()
@@ -760,7 +769,9 @@
         start_ttff_by_gtw_gpstool(self.ad, ttff_mode="cs", iteration=10)
         cs_ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                    self.pixel_lab_location)
-        cs_result = check_ttff_data(self.ad, cs_ttff_data, ttff_mode="Cold Start",
+        cs_result = check_ttff_data(self.ad,
+                                    cs_ttff_data,
+                                    ttff_mode="Cold Start",
                                     criteria=self.weak_signal_xtra_cs_criteria)
         xtra_result.append(cs_result)
         asserts.assert_true(all(xtra_result),
@@ -787,13 +798,15 @@
         self.ad.log.info("Turn airplane mode on")
         force_airplane_mode(self.ad, True)
         wifi_toggle_state(self.ad, True)
-        connect_to_wifi_network(self.ad,
-                                self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+        connect_to_wifi_network(
+            self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
         process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
         start_ttff_by_gtw_gpstool(self.ad, ttff_mode="ws", iteration=10)
         ws_ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                    self.pixel_lab_location)
-        ws_result = check_ttff_data(self.ad, ws_ttff_data, ttff_mode="Warm Start",
+        ws_result = check_ttff_data(self.ad,
+                                    ws_ttff_data,
+                                    ttff_mode="Warm Start",
                                     criteria=self.xtra_ws_criteria)
         xtra_result.append(ws_result)
         begin_time = get_current_epoch_time()
@@ -801,7 +814,9 @@
         start_ttff_by_gtw_gpstool(self.ad, ttff_mode="cs", iteration=10)
         cs_ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                    self.pixel_lab_location)
-        cs_result = check_ttff_data(self.ad, cs_ttff_data, ttff_mode="Cold Start",
+        cs_result = check_ttff_data(self.ad,
+                                    cs_ttff_data,
+                                    ttff_mode="Cold Start",
                                     criteria=self.wifi_xtra_cs_criteria)
         xtra_result.append(cs_result)
         asserts.assert_true(all(xtra_result),
@@ -833,9 +848,9 @@
             start_ttff_by_gtw_gpstool(self.ad, ttff_mode="cs", iteration=3)
             ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
                                                     self.pixel_lab_location)
-            xtra_ssr_test_result = check_ttff_data(self.ad, ttff_data,
-                                                   ttff_mode="Cold Start",
-                                                   criteria=self.xtra_cs_criteria)
+            xtra_ssr_test_result = check_ttff_data(
+                self.ad, ttff_data, ttff_mode="Cold Start",
+                criteria=self.xtra_cs_criteria)
             self.ad.log.info("XTRA after Modem SSR test %d times -> %s"
                              % (times, xtra_ssr_test_result))
             xtra_ssr_test_result_all.append(xtra_ssr_test_result)
@@ -889,8 +904,8 @@
         self.ad.log.info("Turn airplane mode on")
         force_airplane_mode(self.ad, True)
         wifi_toggle_state(self.ad, True)
-        connect_to_wifi_network(self.ad,
-                                self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+        connect_to_wifi_network(
+            self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
         for i in range(1, 6):
             begin_time = get_current_epoch_time()
             process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
@@ -901,3 +916,23 @@
             self.ad.log.info("Iteraion %d => %s" % (i, wifi_xtra_result))
         asserts.assert_true(all(wifi_xtra_result_all),
                             "Fail to Download XTRA file")
+
+    @test_tracker_info(uuid="2a9f2890-3c0a-48b8-821d-bf97e36355e9")
+    def test_quick_toggle_gnss_state(self):
+        """Verify GNSS can still work properly after quick toggle GNSS off
+        to on.
+
+        Steps:
+            1. Launch GTW_GPSTool.
+            2. Go to "Advance setting"
+            3. Set Cycle = 10 & Time-out = 60
+            4. Go to "Toggle GPS" tab
+            5. Execute "Start"
+
+        Expected Results:
+            No single Timeout is seen in 10 iterations.
+        """
+        enable_supl_mode(self.ad)
+        reboot(self.ad)
+        start_qxdm_logger(self.ad, get_current_epoch_time())
+        start_toggle_gnss_by_gtw_gpstool(self.ad, iteration=10)
diff --git a/acts/tests/google/native/NativeTest.py b/acts/tests/google/native/NativeTest.py
index be5fd2a..90ebceb 100644
--- a/acts/tests/google/native/NativeTest.py
+++ b/acts/tests/google/native/NativeTest.py
@@ -24,7 +24,6 @@
 
     def __init__(self, controllers):
         BaseTestClass.__init__(self, controllers)
-        self.droid = self.native_android_devices[0].droid
         self.tests = (
                 "test_bool_return_true",
                 "test_bool_return_false",
@@ -33,6 +32,10 @@
                 "test_max_param_size",
         )
 
+    def setup_class(self):
+        super().setup_class()
+        self.droid = self.native_android_devices[0].droid
+
     def test_bool_return_true(self):
         return self.droid.TestBoolTrueReturn()
 
diff --git a/acts/tests/google/native/bt/BtNativeTest.py b/acts/tests/google/native/bt/BtNativeTest.py
index 5315e34..55674bc 100644
--- a/acts/tests/google/native/bt/BtNativeTest.py
+++ b/acts/tests/google/native/bt/BtNativeTest.py
@@ -1,4 +1,3 @@
-mport time
 from acts.base_test import BaseTestClass
 from acts.controllers import native_android_device
 from acts.test_utils.bt.native_bt_test_utils import setup_native_bluetooth
@@ -10,13 +9,15 @@
 
     def __init__(self, controllers):
         BaseTestClass.__init__(self, controllers)
-        setup_native_bluetooth(self.native_android_devices)
-        self.droid = self.native_android_devices[0].droid
         self.tests = (
             "test_binder_get_name",
             "test_binder_get_name_invalid_parameter",
             "test_binder_set_name_get_name",
             "test_binder_get_address", )
+
+    def setup_class(self):
+        setup_native_bluetooth(self.native_android_devices)
+        self.droid = self.native_android_devices[0].droid
         if len(self.native_android_devices) > 1:
             self.droid1 = self.native_android_devices[1].droid
             self.tests = self.tests + ("test_two_devices_set_get_name", )
diff --git a/acts/tests/google/net/CoreNetworkingOTATest.py b/acts/tests/google/net/CoreNetworkingOTATest.py
index 2444971..5b350f8 100755
--- a/acts/tests/google/net/CoreNetworkingOTATest.py
+++ b/acts/tests/google/net/CoreNetworkingOTATest.py
@@ -84,7 +84,7 @@
           for ad in self.android_devices:
               ota_updater.update(ad)
         except Exception as err:
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed up apply OTA update. Aborting tests")
 
     def on_fail(self, test_name, begin_time):
diff --git a/acts/tests/google/net/DnsOverTlsTest.py b/acts/tests/google/net/DnsOverTlsTest.py
index e69a5ae..e8a883d 100644
--- a/acts/tests/google/net/DnsOverTlsTest.py
+++ b/acts/tests/google/net/DnsOverTlsTest.py
@@ -86,28 +86,27 @@
         """
         return stop_tcpdump(ad, self.tcpdump_pid, self.test_name)
 
-    def _verify_dns_queries_over_tls(self, pcap_file, dns, tls=True):
+    def _verify_dns_queries_over_tls(self, pcap_file, tls=True):
         """ Verify if DNS queries were over TLS or not
 
         Args:
             1. pcap_file: tcpdump file
-            2. dns: private DNS set in strict mode
-            3. tls: if queries should be over TLS or port 853
+            2. tls: if queries should be over TLS or port 853
         """
-        if not dns:
-            dns = cconst.DNS_GOOGLE
         try:
             packets = rdpcap(pcap_file)
-        except Scapy_Exception:
+        except Scapy_Excaption:
             asserts.fail("Not a valid pcap file")
         for pkt in packets:
             summary = "%s" % pkt.summary()
-            if tls and UDP in pkt and pkt[UDP].dport == 53 and \
-                dns not in summary and 'mtalk.google.com' not in summary:
-                  asserts.fail("Found query to port 53: %s" % summary)
-            elif not tls and TCP in pkt and pkt[TCP].dport == 853 and \
-                not pkt[TCP].flags:
-                  asserts.fail("Found query to port 853: %s" % summary)
+            for host in self.ping_hosts:
+                host = host.split('.')[-2]
+                if tls and UDP in pkt and pkt[UDP].dport == 53 and \
+                    host in summary:
+                      asserts.fail("Found query to port 53: %s" % summary)
+                elif not tls and TCP in pkt and pkt[TCP].dport == 853 and \
+                    not pkt[TCP].flags:
+                      asserts.fail("Found query to port 853: %s" % summary)
 
     def _verify_rst_packets(self, pcap_file):
         """ Verify if RST packets are found in the pcap file
@@ -155,7 +154,7 @@
         pcap_file = self._stop_tcp_dump(self.dut)
 
         # verify DNS queries
-        self._verify_dns_queries_over_tls(pcap_file, hostname, use_tls)
+        self._verify_dns_queries_over_tls(pcap_file, use_tls)
 
         # reset wifi
         wutils.reset_wifi(self.dut)
diff --git a/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py b/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py
index 59a9dc6..66aa458 100644
--- a/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py
+++ b/acts/tests/google/power/tel/lab/PowerTelHotspotTest.py
@@ -106,12 +106,14 @@
             dut.droid.wifiSetCountryCode(country_code)
 
         # Setup tethering
-        wutils.start_wifi_tethering(
-            self.dut, self.network[wutils.WifiEnums.SSID_KEY],
-            self.network[wutils.WifiEnums.PWD_KEY], self.wifi_band)
+        wutils.start_wifi_tethering(self.dut,
+                                    self.network[wutils.WifiEnums.SSID_KEY],
+                                    self.network[wutils.WifiEnums.PWD_KEY],
+                                    self.wifi_band)
 
-        wutils.wifi_connect(
-            self.android_devices[1], self.network, check_connectivity=False)
+        wutils.wifi_connect(self.android_devices[1],
+                            self.network,
+                            check_connectivity=False)
 
         # Start data traffic
         iperf_helpers = self.start_tel_traffic(self.android_devices[1])
diff --git a/acts/tests/google/power/tel/lab/PowerTelIdleTest.py b/acts/tests/google/power/tel/lab/PowerTelIdleTest.py
index 7af7cdc..934fe20 100644
--- a/acts/tests/google/power/tel/lab/PowerTelIdleTest.py
+++ b/acts/tests/google/power/tel/lab/PowerTelIdleTest.py
@@ -24,17 +24,16 @@
     cellular idle scenarios to verify the ability to set power consumption
     to a minimum during connectivity power tests.
     """
-
     def power_tel_idle_test(self):
         """ Measures power when the device is on LTE RRC idle state. """
 
         idle_wait_time = self.simulation.rrc_sc_timer + 30
 
         # Wait for RRC status change to trigger
-        self.simulation.wait_for_rrc_idle_state(idle_wait_time)
+        self.cellular_simulator.wait_until_idle_state(idle_wait_time)
 
         # Measure power
         self.collect_power_data()
 
         # Check if power measurement is below the required value
-        self.pass_fail_check()
\ No newline at end of file
+        self.pass_fail_check()
diff --git a/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py b/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py
index 9e9a85f..97094e3 100644
--- a/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py
+++ b/acts/tests/google/power/tel/lab/PowerTelTrafficTest.py
@@ -55,6 +55,11 @@
 
         super().__init__(controllers)
 
+        # Verify that at least one PacketSender controller has been initialized
+        if not hasattr(self, 'packet_senders'):
+            raise RuntimeError('At least one packet sender controller needs '
+                               'to be defined in the test config files.')
+
         # These variables are passed to iPerf when starting data
         # traffic with the -b parameter to limit throughput on
         # the application layer.
@@ -212,8 +217,9 @@
                                "current simulation class.")
             else:
 
-                self.log.info("The expected {} throughput is {} Mbit/s.".format(
-                    direction, expected_t))
+                self.log.info(
+                    "The expected {} throughput is {} Mbit/s.".format(
+                        direction, expected_t))
                 asserts.assert_true(
                     0.90 < throughput / expected_t < 1.10,
                     "{} throughput differed more than 10% from the expected "
@@ -237,7 +243,8 @@
         """
 
         # The iPerf server is hosted in this computer
-        self.iperf_server_address = scapy.get_if_addr(self.pkt_sender.interface)
+        self.iperf_server_address = scapy.get_if_addr(
+            self.packet_senders[0].interface)
 
         # Start iPerf traffic
         iperf_helpers = []
@@ -320,7 +327,8 @@
 
         config = {
             'traffic_type': 'TCP',
-            'duration': self.mon_duration + self.mon_offset + self.IPERF_MARGIN,
+            'duration':
+            self.mon_duration + self.mon_offset + self.IPERF_MARGIN,
             'start_meas_time': 4,
             'server_idx': server_idx,
             'port': self.iperf_servers[server_idx].port,
diff --git a/acts/tests/google/tel/live/TelLiveConnectivityMonitorBaseTest.py b/acts/tests/google/tel/live/TelLiveConnectivityMonitorBaseTest.py
index c9378e4..135f583 100644
--- a/acts/tests/google/tel/live/TelLiveConnectivityMonitorBaseTest.py
+++ b/acts/tests/google/tel/live/TelLiveConnectivityMonitorBaseTest.py
@@ -37,6 +37,7 @@
 from acts.test_utils.tel.tel_test_utils import get_device_epoch_time
 from acts.test_utils.tel.tel_test_utils import get_model_name
 from acts.test_utils.tel.tel_test_utils import get_operator_name
+from acts.test_utils.tel.tel_test_utils import get_outgoing_voice_sub_id
 from acts.test_utils.tel.tel_test_utils import hangup_call
 from acts.test_utils.tel.tel_test_utils import last_call_drop_reason
 from acts.test_utils.tel.tel_test_utils import reboot_device
@@ -125,9 +126,11 @@
         self.ad_reference = self.android_devices[1]
         self.dut_model = get_model_name(self.dut)
         self.dut_operator = get_operator_name(self.log, self.dut)
-        self.dut_capabilities = self.dut.telephony.get("capabilities", [])
-        self.dut_wfc_modes = self.dut.telephony.get("wfc_modes", [])
-        self.reference_capabilities = self.ad_reference.telephony.get(
+        self.dut_subID = get_outgoing_voice_sub_id(self.dut)
+        self.dut_capabilities = self.dut.telephony["subscription"][self.dut_subID].get("capabilities", [])
+        self.dut_wfc_modes = self.dut.telephony["subscription"][self.dut_subID].get("wfc_modes", [])
+        self.ad_reference_subID = get_outgoing_voice_sub_id(self.ad_reference)
+        self.reference_capabilities = self.ad_reference.telephony["subscription"][self.ad_reference_subID].get(
             "capabilities", [])
         self.dut.log.info("DUT capabilities: %s", self.dut_capabilities)
         self.skip_reset_between_cases = False
diff --git a/acts/tests/google/tel/live/TelLiveImsSettingsTest.py b/acts/tests/google/tel/live/TelLiveImsSettingsTest.py
index 64f81ca..7a6d384 100644
--- a/acts/tests/google/tel/live/TelLiveImsSettingsTest.py
+++ b/acts/tests/google/tel/live/TelLiveImsSettingsTest.py
@@ -86,9 +86,9 @@
             subid].get("capabilities", [])
         self.dut.log.info("DUT capabilities: %s", self.dut_capabilities)
         if CAPABILITY_VOLTE not in self.dut_capabilities:
-            raise signals.TestSkipClass("VoLTE is not supported")
+            raise signals.TestAbortClass("VoLTE is not supported")
         if CAPABILITY_WFC not in self.dut_capabilities:
-            raise signals.TestSkipClass("WFC is not supported")
+            raise signals.TestAbortClass("WFC is not supported")
 
         self.default_volte = (CAPABILITY_VOLTE in self.dut_capabilities) and (
             self.carrier_configs[CarrierConfigs.
diff --git a/acts/tests/google/tel/live/TelLiveNoQXDMLogTest.py b/acts/tests/google/tel/live/TelLiveNoQXDMLogTest.py
index 94c2cbf..86e9927 100644
--- a/acts/tests/google/tel/live/TelLiveNoQXDMLogTest.py
+++ b/acts/tests/google/tel/live/TelLiveNoQXDMLogTest.py
@@ -69,10 +69,12 @@
     def setup_class(self):
         super().setup_class()
         self.dut = self.android_devices[0]
-        self.ad_reference = self.android_devices[1] if len(
-            self.android_devices) > 1 else None
+        if len(self.android_devices) > 1:
+            self.ad_reference = self.android_devices[1]
+            setattr(self.ad_reference, "qxdm_log", False)
+        else:
+            self.ad_reference = None
         setattr(self.dut, "qxdm_log", False)
-        setattr(self.ad_reference, "qxdm_log", False)
         self.stress_test_number = int(
             self.user_params.get("stress_test_number", 5))
         self.skip_reset_between_cases = False
@@ -379,9 +381,8 @@
         begin_time = get_current_epoch_time()
         for i in range(3):
             try:
-                bugreport_path = os.path.join(ad.log_path, self.test_name)
-                create_dir(bugreport_path)
                 ad.take_bug_report(self.test_name, begin_time)
+                bugreport_path = ad.device_log_path
                 break
             except Exception as e:
                 ad.log.error("bugreport attempt %s error: %s", i + 1, e)
@@ -406,9 +407,11 @@
                 exe_cmd("tar -xvf %s" %
                         (bugreport_path + "/dumpstate_board.tar"))
                 os.chdir(current_dir)
-                if os.path.isfile(bugreport_path + "/power_anomaly_data.txt"):
-                    ad.log.info("Modem Power Anomaly File Exists!!")
-                    return True
+            else:
+                ad.log.info("The dumpstate_path file %s does not exist" % dumpstate_path)
+            if os.path.isfile(bugreport_path + "/power_anomaly_data.txt"):
+                ad.log.info("Modem Power Anomaly File Exists!!")
+                return True
             ad.log.info("Modem Power Anomaly File DO NOT Exist!!")
             return False
         except Exception as e:
@@ -504,7 +507,8 @@
                 ad.wait_for_boot_completion()
                 ad.root_adb()
                 ad.log.info("Re-install sl4a")
-                ad.adb.shell("settings put global package_verifier_enable 0")
+                ad.adb.shell("settings put global verifier_verify_adb_installs"
+                             " 0")
                 ad.adb.install("-r /tmp/base.apk")
                 time.sleep(10)
                 try:
diff --git a/acts/tests/google/tel/live/TelLiveRebootStressTest.py b/acts/tests/google/tel/live/TelLiveRebootStressTest.py
index 70677e1..2936d9e 100644
--- a/acts/tests/google/tel/live/TelLiveRebootStressTest.py
+++ b/acts/tests/google/tel/live/TelLiveRebootStressTest.py
@@ -95,8 +95,9 @@
         self.user_params["check_crash"] = False
         self.skip_reset_between_cases = False
 
-        self.dut_capabilities = self.dut.telephony.get("capabilities", [])
-        self.dut_wfc_modes = self.dut.telephony.get("wfc_modes", [])
+        self.dut_subID = get_outgoing_voice_sub_id(self.dut)
+        self.dut_capabilities = self.dut.telephony["subscription"][self.dut_subID].get("capabilities", [])
+        self.dut_wfc_modes = self.dut.telephony["subscription"][self.dut_subID].get("wfc_modes", [])
         self.default_testing_func_names = []
         for method in ("_check_volte", "_check_vt", "_check_csfb",
                        "_check_tethering", "_check_wfc_apm",
diff --git a/acts/tests/google/tel/live/TelLiveSettingsTest.py b/acts/tests/google/tel/live/TelLiveSettingsTest.py
index 791e9e7..10b045f 100644
--- a/acts/tests/google/tel/live/TelLiveSettingsTest.py
+++ b/acts/tests/google/tel/live/TelLiveSettingsTest.py
@@ -51,7 +51,8 @@
         self.number_of_devices = 1
         self.stress_test_number = self.get_stress_test_number()
         self.carrier_configs = dumpsys_carrier_config(self.dut)
-        self.dut_capabilities = self.dut.telephony.get("capabilities", [])
+        self.dut_subID = get_outgoing_voice_sub_id(self.dut)
+        self.dut_capabilities = self.dut.telephony["subscription"][self.dut_subID].get("capabilities", [])
 
     @test_tracker_info(uuid="c6149bd6-7080-453d-af37-1f9bd350a764")
     @TelephonyBaseTest.tel_test_wrap
diff --git a/acts/tests/google/tel/live/TelLiveStressTest.py b/acts/tests/google/tel/live/TelLiveStressTest.py
index 899e758..6a4276d 100644
--- a/acts/tests/google/tel/live/TelLiveStressTest.py
+++ b/acts/tests/google/tel/live/TelLiveStressTest.py
@@ -125,7 +125,9 @@
                 self.file_download_method = "curl"
         else:
             self.android_devices = self.android_devices[:2]
+        self.sdm_log = self.user_params.get("sdm_log", False)
         for ad in self.android_devices:
+            setattr(ad, "sdm_log", self.sdm_log)
             ad.adb.shell("setprop nfc.debug_enable 1")
             if self.user_params.get("turn_on_tcpdump", False):
                 start_adb_tcpdump(ad, interface="any", mask="all")
@@ -150,7 +152,6 @@
         self.dut_incall = False
         self.dsds_esim = self.user_params.get("dsds_esim", False)
         self.cbrs_esim = self.user_params.get("cbrs_esim", False)
-        self.sdm_log = self.user_params.get("sdm_log", False)
         telephony_info = getattr(self.dut, "telephony", {})
         self.dut_capabilities = telephony_info.get("capabilities", [])
         self.dut_wfc_modes = telephony_info.get("wfc_modes", [])
@@ -1102,7 +1103,7 @@
     def test_lte_volte_parallel_stress(self):
         """ VoLTE on stress test"""
         if CAPABILITY_VOLTE not in self.dut_capabilities:
-            raise signals.TestSkipClass("VoLTE is not supported")
+            raise signals.TestAbortClass("VoLTE is not supported")
         return self.parallel_tests(
             setup_func=self._setup_lte_volte_enabled,
             call_verification_func=is_phone_in_call_volte)
@@ -1120,7 +1121,7 @@
     def test_wfc_parallel_stress(self):
         """ Wifi calling APM mode off stress test"""
         if CAPABILITY_WFC not in self.dut_capabilities:
-            raise signals.TestSkipClass("WFC is not supported")
+            raise signals.TestAbortClass("WFC is not supported")
         if WFC_MODE_WIFI_PREFERRED not in self.dut_wfc_modes:
             raise signals.TestSkip("WFC_MODE_WIFI_PREFERRED is not supported")
         return self.parallel_tests(
@@ -1132,7 +1133,7 @@
     def test_wfc_apm_parallel_stress(self):
         """ Wifi calling in APM mode on stress test"""
         if CAPABILITY_WFC not in self.dut_capabilities:
-            raise signals.TestSkipClass("WFC is not supported")
+            raise signals.TestAbortClass("WFC is not supported")
         return self.parallel_tests(
             setup_func=self._setup_wfc_apm,
             call_verification_func=is_phone_in_call_iwlan)
@@ -1158,7 +1159,7 @@
     def test_volte_modeprefchange_parallel_stress(self):
         """ VoLTE Mode Pref call stress test"""
         if CAPABILITY_VOLTE not in self.dut_capabilities:
-            raise signals.TestSkipClass("VoLTE is not supported")
+            raise signals.TestAbortClass("VoLTE is not supported")
         return self.parallel_with_network_change_tests(
             setup_func=self._setup_lte_volte_enabled)
 
diff --git a/acts/tests/google/wifi/WifiAutoUpdateTest.py b/acts/tests/google/wifi/WifiAutoUpdateTest.py
index 04fb850..33369a2 100755
--- a/acts/tests/google/wifi/WifiAutoUpdateTest.py
+++ b/acts/tests/google/wifi/WifiAutoUpdateTest.py
@@ -93,7 +93,7 @@
         try:
             ota_updater.update(self.dut)
         except Exception as err:
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed up apply OTA update. Aborting tests")
 
     def setup_test(self):
diff --git a/acts/tests/google/wifi/WifiCrashStressTest.py b/acts/tests/google/wifi/WifiCrashStressTest.py
index bf17ada..837112a 100644
--- a/acts/tests/google/wifi/WifiCrashStressTest.py
+++ b/acts/tests/google/wifi/WifiCrashStressTest.py
@@ -41,7 +41,7 @@
         wutils.wifi_test_device_init(self.dut)
         wutils.wifi_test_device_init(self.dut_client)
         if not self.dut.is_apk_installed("com.google.mdstest"):
-            raise signals.TestSkipClass("mdstest is not installed")
+            raise signals.TestAbortClass("mdstest is not installed")
         req_params = ["dbs_supported_models", "stress_count"]
         opt_param = ["reference_networks"]
         self.unpack_userparams(
diff --git a/acts/tests/google/wifi/WifiPingTest.py b/acts/tests/google/wifi/WifiPingTest.py
index 551719d..8abe9eb 100644
--- a/acts/tests/google/wifi/WifiPingTest.py
+++ b/acts/tests/google/wifi/WifiPingTest.py
@@ -25,6 +25,7 @@
 from acts import base_test
 from acts import utils
 from acts.controllers.utils_lib import ssh
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
 from acts.test_utils.wifi import ota_chamber
 from acts.test_utils.wifi import wifi_performance_test_utils as wputils
 from acts.test_utils.wifi import wifi_retail_ap as retail_ap
@@ -57,9 +58,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = True
 
         self.tests = self.generate_test_cases(
@@ -107,6 +108,11 @@
         self.testclass_results = []
 
         # Turn WiFi ON
+        if self.testclass_params.get('airplane_mode', 1):
+            self.log.info('Turning on airplane mode.')
+            asserts.assert_true(
+                utils.force_airplane_mode(self.dut, True),
+                "Can not turn on airplane mode.")
         wutils.wifi_toggle_state(self.dut, True)
 
     def teardown_class(self):
@@ -190,15 +196,15 @@
             self.testcase_metric_logger.add_metric('ping_range',
                                                    result['range'])
         # Evaluate test pass/fail
+        test_message = ('Attenuation at range is {}dB. Golden range is {}dB. '
+                        'LLStats at Range: {}'.format(
+                            result['range'], rvr_range,
+                            result['llstats_at_range']))
         if result['range'] - rvr_range < -self.testclass_params[
                 'range_gap_threshold']:
-            asserts.fail(
-                'Attenuation at range is {}dB. Golden range is {}dB'.format(
-                    result['range'], rvr_range))
+            asserts.fail(test_message)
         else:
-            asserts.explicit_pass(
-                'Attenuation at range is {}dB. Golden range is {}dB'.format(
-                    result['range'], rvr_range))
+            asserts.explicit_pass(test_message)
 
     def pass_fail_check(self, result):
         if 'range' in result['testcase_params']['test_type']:
@@ -233,6 +239,15 @@
             ping_loss_over_att)
         ping_range_result['range'] = (ping_range_result['atten_at_range'] +
                                       ping_range_result['fixed_attenuation'])
+        ping_range_result['llstats_at_range'] = (
+            'TX MCS = {0} ({1:.1f}%). '
+            'RX MCS = {2} ({3:.1f}%)'.format(
+                ping_range_result['llstats'][range_index]['summary']
+                ['common_tx_mcs'], ping_range_result['llstats'][range_index]
+                ['summary']['common_tx_mcs_freq'] * 100,
+                ping_range_result['llstats'][range_index]['summary']
+                ['common_rx_mcs'], ping_range_result['llstats'][range_index]
+                ['summary']['common_rx_mcs_freq'] * 100))
 
         # Save results
         results_file_path = os.path.join(
@@ -241,22 +256,24 @@
             json.dump(ping_range_result, results_file, indent=4)
 
         # Plot results
-        figure = wputils.BokehFigure(
-            self.current_test_name,
-            x_label='Timestamp (s)',
-            primary_y='Round Trip Time (ms)')
-        for idx, result in enumerate(ping_range_result['ping_results']):
-            if len(result['rtt']) > 1:
-                x_data = [
-                    t - result['time_stamp'][0] for t in result['time_stamp']
-                ]
-                figure.add_line(
-                    x_data, result['rtt'],
-                    'RTT @ {}dB'.format(ping_range_result['attenuation'][idx]))
+        if 'range' not in self.current_test_name:
+            figure = wputils.BokehFigure(
+                self.current_test_name,
+                x_label='Timestamp (s)',
+                primary_y_label='Round Trip Time (ms)')
+            for idx, result in enumerate(ping_range_result['ping_results']):
+                if len(result['rtt']) > 1:
+                    x_data = [
+                        t - result['time_stamp'][0]
+                        for t in result['time_stamp']
+                    ]
+                    figure.add_line(
+                        x_data, result['rtt'], 'RTT @ {}dB'.format(
+                            ping_range_result['attenuation'][idx]))
 
-        output_file_path = os.path.join(
-            self.log_path, '{}.html'.format(self.current_test_name))
-        figure.generate_figure(output_file_path)
+            output_file_path = os.path.join(
+                self.log_path, '{}.html'.format(self.current_test_name))
+            figure.generate_figure(output_file_path)
 
     def get_range_from_rvr(self):
         """Function gets range from RvR golden results
@@ -302,6 +319,7 @@
             test_result: dict containing ping results and other meta data
         """
         # Prepare results dict
+        llstats_obj = wputils.LinkLayerStats(self.dut)
         test_result = collections.OrderedDict()
         test_result['testcase_params'] = testcase_params.copy()
         test_result['test_name'] = self.current_test_name
@@ -311,6 +329,7 @@
             'fixed_attenuation'][str(testcase_params['channel'])]
         test_result['rssi_results'] = []
         test_result['ping_results'] = []
+        test_result['llstats'] = []
         # Run ping and sweep attenuation as needed
         zero_counter = 0
         for atten in testcase_params['atten_range']:
@@ -321,12 +340,17 @@
                 int(testcase_params['ping_duration'] / 2 /
                     self.RSSI_POLL_INTERVAL), self.RSSI_POLL_INTERVAL,
                 testcase_params['ping_duration'] / 2)
+            # Refresh link layer stats
+            llstats_obj.update_stats()
             current_ping_stats = wputils.get_ping_stats(
                 self.ping_server, self.dut_ip,
                 testcase_params['ping_duration'],
                 testcase_params['ping_interval'], testcase_params['ping_size'])
             current_rssi = rssi_future.result()
             test_result['rssi_results'].append(current_rssi)
+            llstats_obj.update_stats()
+            curr_llstats = llstats_obj.llstats_incremental.copy()
+            test_result['llstats'].append(curr_llstats)
             if current_ping_stats['connected']:
                 self.log.info(
                     'Attenuation = {0}dB\tPacket Loss = {1}%\t'
@@ -395,9 +419,12 @@
         band = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
         current_network = self.dut.droid.wifiGetConnectionInfo()
-        valid_connection = wutils.validate_connection(self.dut)
-        if valid_connection and current_network['SSID'] == self.main_network[
-                band]['SSID']:
+        try:
+            connected = wutils.validate_connection(self.dut) is not None
+        except:
+            connected = False
+        if connected and current_network['SSID'] == self.main_network[band][
+                'SSID']:
             self.log.info('Already connected to desired network')
         else:
             wutils.reset_wifi(self.dut)
@@ -541,9 +568,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = False
 
     def setup_class(self):
@@ -567,10 +594,13 @@
                 range_vs_angle[curr_config]['position'].append(
                     curr_params['position'])
                 range_vs_angle[curr_config]['range'].append(test['range'])
+                range_vs_angle[curr_config]['llstats_at_range'].append(
+                    test['llstats_at_range'])
             else:
                 range_vs_angle[curr_config] = {
                     'position': [curr_params['position']],
-                    'range': [test['range']]
+                    'range': [test['range']],
+                    'llstats_at_range': [test['llstats_at_range']]
                 }
         chamber_mode = self.testclass_results[0]['testcase_params'][
             'chamber_mode']
@@ -581,11 +611,14 @@
         figure = wputils.BokehFigure(
             title='Range vs. Position',
             x_label=x_label,
-            primary_y='Range (dB)',
+            primary_y_label='Range (dB)',
         )
         for channel, channel_data in range_vs_angle.items():
-            figure.add_line(channel_data['position'], channel_data['range'],
-                            'Channel {}'.format(channel))
+            figure.add_line(
+                x_data=channel_data['position'],
+                y_data=channel_data['range'],
+                hover_text=channel_data['llstats_at_range'],
+                legend='Channel {}'.format(channel))
             average_range = sum(channel_data['range']) / len(
                 channel_data['range'])
             self.log.info('Average range for Channel {} is: {}dB'.format(
diff --git a/acts/tests/google/wifi/WifiRoamingPerformanceTest.py b/acts/tests/google/wifi/WifiRoamingPerformanceTest.py
index f05cc01..da57bf5 100644
--- a/acts/tests/google/wifi/WifiRoamingPerformanceTest.py
+++ b/acts/tests/google/wifi/WifiRoamingPerformanceTest.py
@@ -16,13 +16,13 @@
 
 import collections
 import json
-import logging
 import math
 import os
 import time
 from acts import asserts
 from acts import base_test
 from acts import context
+from acts import utils
 from acts.controllers import iperf_server as ipf
 from acts.controllers.utils_lib import ssh
 from acts.test_utils.wifi import wifi_performance_test_utils as wputils
@@ -89,6 +89,11 @@
         self.log.info("RF Map (by Atten): {}".format(self.rf_map_by_atten))
 
         #Turn WiFi ON
+        if self.testclass_params.get('airplane_mode', 1):
+            self.log.info('Turning on airplane mode.')
+            asserts.assert_true(
+                utils.force_airplane_mode(self.dut, True),
+                "Can not turn on airplane mode.")
         wutils.wifi_toggle_state(self.dut, True)
 
     def pass_fail_traffic_continuity(self, result):
@@ -206,8 +211,8 @@
             figure = wputils.BokehFigure(
                 title=self.current_test_name,
                 x_label='Time (ms)',
-                primary_y=primary_y_axis,
-                secondary_y='RSSI (dBm)')
+                primary_y_label=primary_y_axis,
+                secondary_y_label='RSSI (dBm)')
             roam_stats[secondary_atten] = collections.OrderedDict()
             for result in results_list:
                 self.detect_roam_events(result)
@@ -361,8 +366,8 @@
             figure = wputils.BokehFigure(
                 title=self.current_test_name,
                 x_label='Time (ms)',
-                primary_y='RTT (ms)',
-                secondary_y='RSSI (dBm)')
+                primary_y_label='RTT (ms)',
+                secondary_y_label='RSSI (dBm)')
         figure.add_line(
             x_data=result['ping_result']['time_stamp'],
             y_data=result['ping_result']['rtt'],
@@ -395,8 +400,8 @@
             figure = wputils.BokehFigure(
                 title=self.current_test_name,
                 x_label='Time (s)',
-                primary_y='Throughput (Mbps)',
-                secondary_y='RSSI (dBm)')
+                primary_y_label='Throughput (Mbps)',
+                secondary_y_label='RSSI (dBm)')
         iperf_time_stamps = [
             idx * IPERF_INTERVAL for idx in range(len(result['throughput']))
         ]
diff --git a/acts/tests/google/wifi/WifiRssiTest.py b/acts/tests/google/wifi/WifiRssiTest.py
index 7b1e68e..91bfa9f 100644
--- a/acts/tests/google/wifi/WifiRssiTest.py
+++ b/acts/tests/google/wifi/WifiRssiTest.py
@@ -28,6 +28,7 @@
 from acts import utils
 from acts.controllers.utils_lib import ssh
 from acts.controllers import iperf_server as ipf
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
 from acts.test_utils.wifi import ota_chamber
 from acts.test_utils.wifi import wifi_performance_test_utils as wputils
 from acts.test_utils.wifi import wifi_retail_ap as retail_ap
@@ -55,9 +56,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_test_metrics = True
 
     def setup_class(self):
@@ -92,6 +93,11 @@
         self.testclass_results = []
 
         # Turn WiFi ON
+        if self.testclass_params.get('airplane_mode', 1):
+            self.log.info('Turning on airplane mode.')
+            asserts.assert_true(
+                utils.force_airplane_mode(self.dut, True),
+                "Can not turn on airplane mode.")
         wutils.wifi_toggle_state(self.dut, True)
 
     def teardown_test(self):
@@ -301,7 +307,7 @@
         figure = wputils.BokehFigure(
             self.current_test_name,
             x_label='Attenuation (dB)',
-            primary_y='RSSI (dBm)')
+            primary_y_label='RSSI (dBm)')
         figure.add_line(
             postprocessed_results['total_attenuation'],
             postprocessed_results['signal_poll_rssi']['mean'],
@@ -345,7 +351,7 @@
         figure = wputils.BokehFigure(
             self.current_test_name,
             x_label='Time (s)',
-            primary_y=center_curves * 'Centered' + 'RSSI (dBm)',
+            primary_y_label=center_curves * 'Centered' + 'RSSI (dBm)',
         )
 
         # yapf: disable
@@ -421,8 +427,8 @@
         figure = wputils.BokehFigure(
             self.current_test_name,
             x_label='RSSI (dBm)',
-            primary_y='p(RSSI = x)',
-            secondary_y='p(RSSI <= x)')
+            primary_y_label='p(RSSI = x)',
+            secondary_y_label='p(RSSI <= x)')
         for rssi_key, rssi_data in rssi_dist.items():
             figure.add_line(
                 x_data=rssi_data['rssi_values'],
@@ -876,9 +882,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_test_metrics = False
 
     def setup_class(self):
@@ -947,7 +953,7 @@
             current_plot = wputils.BokehFigure(
                 title='Channel {} - Rssi vs. Position'.format(channel),
                 x_label=x_label,
-                primary_y='RSSI (dBm)',
+                primary_y_label='RSSI (dBm)',
             )
             for rssi_metric, rssi_metric_value in channel_data['rssi'].items():
                 legend = rssi_metric
diff --git a/acts/tests/google/wifi/WifiRvrTest.py b/acts/tests/google/wifi/WifiRvrTest.py
index 7d72c16..43c9bbb 100644
--- a/acts/tests/google/wifi/WifiRvrTest.py
+++ b/acts/tests/google/wifi/WifiRvrTest.py
@@ -25,6 +25,7 @@
 from acts import utils
 from acts.controllers import iperf_server as ipf
 from acts.controllers.utils_lib import ssh
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
 from acts.test_utils.wifi import ota_chamber
 from acts.test_utils.wifi import wifi_performance_test_utils as wputils
 from acts.test_utils.wifi import wifi_retail_ap as retail_ap
@@ -50,9 +51,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = True
 
     def setup_class(self):
@@ -99,6 +100,11 @@
         self.testclass_results = []
 
         # Turn WiFi ON
+        if self.testclass_params.get('airplane_mode', 1):
+            self.log.info('Turning on airplane mode.')
+            asserts.assert_true(
+                utils.force_airplane_mode(self.dut, True),
+                "Can not turn on airplane mode.")
         wutils.wifi_toggle_state(self.dut, True)
 
     def teardown_test(self):
@@ -124,7 +130,7 @@
                         result['testcase_params']['mode'],
                         result['testcase_params']['traffic_type']),
                     x_label='Attenuation (dB)',
-                    primary_y='Throughput (Mbps)')
+                    primary_y_label='Throughput (Mbps)')
             plots[plot_id].add_line(
                 result['total_attenuation'],
                 result['throughput_receive'],
@@ -251,7 +257,7 @@
         figure = wputils.BokehFigure(
             title=test_name,
             x_label='Attenuation (dB)',
-            primary_y='Throughput (Mbps)')
+            primary_y_label='Throughput (Mbps)')
         try:
             golden_path = next(file_name
                                for file_name in self.golden_files_list
@@ -314,11 +320,13 @@
         ]
         for idx in range(len(tput_below_limit)):
             if all(tput_below_limit[idx:]):
-                rvr_result['metrics']['high_tput_range'] = rvr_result[
-                    'total_attenuation'][max(idx, 1) - 1]
+                if idx == 0:
+                    #Throughput was never above limit
+                    rvr_result['metrics']['high_tput_range'] = -1
+                else:
+                    rvr_result['metrics']['high_tput_range'] = rvr_result[
+                        'total_attenuation'][max(idx, 1) - 1]
                 break
-        else:
-            rvr_result['metrics']['high_tput_range'] = -1
         if self.publish_testcase_metrics:
             self.testcase_metric_logger.add_metric(
                 'high_tput_range', rvr_result['metrics']['high_tput_range'])
@@ -468,15 +476,18 @@
         # Check battery level before test
         if not wputils.health_check(
                 self.dut, 20) and testcase_params['traffic_direction'] == 'UL':
-            asserts.skip('Battery level too low. Skipping test.')
+            asserts.skip('Overheating or Battery level low. Skipping test.')
         # Turn screen off to preserve battery
         self.dut.go_to_sleep()
         band = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
         current_network = self.dut.droid.wifiGetConnectionInfo()
-        valid_connection = wutils.validate_connection(self.dut)
-        if valid_connection and current_network['SSID'] == self.main_network[
-                band]['SSID']:
+        try:
+            connected = wutils.validate_connection(self.dut) is not None
+        except:
+            connected = False
+        if connected and current_network['SSID'] == self.main_network[band][
+                'SSID']:
             self.log.info('Already connected to desired network')
         else:
             wutils.reset_wifi(self.dut)
@@ -682,9 +693,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = False
 
     def setup_class(self):
@@ -726,7 +737,7 @@
                         result['testcase_params']['traffic_type'],
                         result['testcase_params']['traffic_direction']),
                     x_label='Attenuation (dB)',
-                    primary_y='Throughput (Mbps)')
+                    primary_y_label='Throughput (Mbps)')
             # Compile test id data and metrics
             compiled_data[test_id]['throughput'].append(
                 result['throughput_receive'])
diff --git a/acts/tests/google/wifi/WifiSensitivityTest.py b/acts/tests/google/wifi/WifiSensitivityTest.py
index b262679..1947684 100644
--- a/acts/tests/google/wifi/WifiSensitivityTest.py
+++ b/acts/tests/google/wifi/WifiSensitivityTest.py
@@ -27,6 +27,7 @@
 from acts import utils
 from acts.controllers import iperf_client
 from acts.controllers.utils_lib import ssh
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
 from acts.test_utils.wifi import ota_chamber
 from acts.test_utils.wifi import wifi_performance_test_utils as wputils
 from acts.test_utils.wifi import wifi_test_utils as wutils
@@ -126,9 +127,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = True
 
     def setup_class(self):
@@ -175,6 +176,11 @@
         self.testclass_results = []
 
         # Turn WiFi ON
+        if self.testclass_params.get('airplane_mode', 1):
+            self.log.info('Turning on airplane mode.')
+            asserts.assert_true(
+                utils.force_airplane_mode(self.dut, True),
+                "Can not turn on airplane mode.")
         wutils.wifi_toggle_state(self.dut, True)
 
     def teardown_class(self):
@@ -378,9 +384,12 @@
         band = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
         current_network = self.dut.droid.wifiGetConnectionInfo()
-        valid_connection = wutils.validate_connection(self.dut)
-        if valid_connection and current_network['SSID'] == self.main_network[
-                band]['SSID']:
+        try:
+            connected = wutils.validate_connection(self.dut) is not None
+        except:
+            connected = False
+        if connected and current_network['SSID'] == self.main_network[band][
+                'SSID']:
             self.log.info('Already connected to desired network')
         else:
             wutils.reset_wifi(self.dut)
@@ -615,9 +624,9 @@
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = False
 
     def setup_class(self):
@@ -680,7 +689,7 @@
             curr_plot = wputils.BokehFigure(
                 title=str(test_id_str),
                 x_label='Orientation (deg)',
-                primary_y='Sensitivity (dBm)')
+                primary_y_label='Sensitivity (dBm)')
             for channel, channel_results in test_data.items():
                 curr_plot.add_line(
                     channel_results['orientation'],
diff --git a/acts/tests/google/wifi/WifiSoftApPerformanceTest.py b/acts/tests/google/wifi/WifiSoftApPerformanceTest.py
index f764b9f..a31812f 100644
--- a/acts/tests/google/wifi/WifiSoftApPerformanceTest.py
+++ b/acts/tests/google/wifi/WifiSoftApPerformanceTest.py
@@ -21,7 +21,7 @@
 from acts import utils
 from acts.controllers import iperf_server as ipf
 from acts.controllers import iperf_client as ipc
-from acts.metrics.loggers.blackbox import BlackboxMetricLogger
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
 from acts.test_utils.wifi import wifi_test_utils as wutils
 from acts.test_utils.wifi import wifi_performance_test_utils as wputils
 from WifiRvrTest import WifiRvrTest
@@ -36,9 +36,9 @@
         self.tests = ("test_rvr_TCP_DL_2GHz", "test_rvr_TCP_UL_2GHz",
                       "test_rvr_TCP_DL_5GHz", "test_rvr_TCP_UL_5GHz")
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = True
 
     def setup_class(self):
diff --git a/acts/tests/google/wifi/WifiStressTest.py b/acts/tests/google/wifi/WifiStressTest.py
index 7ca8034..40a3108 100644
--- a/acts/tests/google/wifi/WifiStressTest.py
+++ b/acts/tests/google/wifi/WifiStressTest.py
@@ -61,7 +61,8 @@
         req_params = []
         opt_param = [
             "open_network", "reference_networks", "iperf_server_address",
-            "stress_count", "stress_hours", "attn_vals", "pno_interval"]
+            "stress_count", "stress_hours", "attn_vals", "pno_interval",
+            "iperf_server_port"]
         self.unpack_userparams(
             req_param_names=req_params, opt_param_names=opt_param)
 
@@ -76,12 +77,6 @@
         self.open_2g = self.open_network[0]["2g"]
         self.open_5g = self.open_network[0]["5g"]
         self.networks = [self.wpa_2g, self.wpa_5g, self.open_2g, self.open_5g]
-        if "iperf_server_address" in self.user_params:
-            self.iperf_server = self.iperf_servers[0]
-        if hasattr(self, 'iperf_server'):
-            self.iperf_server.start()
-            if(len(self.iperf_servers) > 1):
-                self.iperf_servers[1].start()
 
     def setup_test(self):
         self.dut.droid.wakeLockAcquireBright()
@@ -100,10 +95,6 @@
 
     def teardown_class(self):
         wutils.reset_wifi(self.dut)
-        if hasattr(self, 'iperf_server'):
-            self.iperf_server.stop()
-            if(len(self.iperf_servers) > 1):
-                self.iperf_servers[1].stop()
         if "AccessPoint" in self.user_params:
             del self.user_params["reference_networks"]
             del self.user_params["open_network"]
@@ -295,7 +286,7 @@
                 self.scan_and_connect_by_id(self.wpa_5g, net_id)
                 # Start IPerf traffic from phone to server.
                 # Upload data for 10s.
-                args = "-p {} -t {}".format(self.iperf_server.port, 10)
+                args = "-p {} -t {}".format(self.iperf_server_port, 10)
                 self.log.info("Running iperf client {}".format(args))
                 result, data = self.dut.run_iperf_client(self.iperf_server_address, args)
                 if not result:
@@ -328,7 +319,7 @@
         sec = self.stress_hours * 60 * 60
         start_time = time.time()
 
-        dl_args = "-p {} -t {} -R".format(self.iperf_server.port, sec)
+        dl_args = "-p {} -t {} -R".format(self.iperf_server_port, sec)
         dl = threading.Thread(target=self.run_long_traffic, args=(sec, dl_args, q))
         dl.start()
         if(len(self.iperf_servers) > 1):
diff --git a/acts/tests/google/wifi/WifiTethering2GOpenOTATest.py b/acts/tests/google/wifi/WifiTethering2GOpenOTATest.py
index a603e01..0716158 100755
--- a/acts/tests/google/wifi/WifiTethering2GOpenOTATest.py
+++ b/acts/tests/google/wifi/WifiTethering2GOpenOTATest.py
@@ -52,7 +52,7 @@
         try:
           ota_updater.update(self.hotspot_device)
         except Exception as err:
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed up apply OTA update. Aborting tests")
 
     def on_fail(self, test_name, begin_time):
diff --git a/acts/tests/google/wifi/WifiTethering2GPskOTATest.py b/acts/tests/google/wifi/WifiTethering2GPskOTATest.py
index e9fedcd..7399e32 100755
--- a/acts/tests/google/wifi/WifiTethering2GPskOTATest.py
+++ b/acts/tests/google/wifi/WifiTethering2GPskOTATest.py
@@ -52,7 +52,7 @@
         try:
           ota_updater.update(self.hotspot_device)
         except Exception as err:
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed up apply OTA update. Aborting tests")
 
     def on_fail(self, test_name, begin_time):
diff --git a/acts/tests/google/wifi/WifiTethering5GOpenOTATest.py b/acts/tests/google/wifi/WifiTethering5GOpenOTATest.py
index 6648d0e..985e7a7 100755
--- a/acts/tests/google/wifi/WifiTethering5GOpenOTATest.py
+++ b/acts/tests/google/wifi/WifiTethering5GOpenOTATest.py
@@ -52,7 +52,7 @@
         try:
           ota_updater.update(self.hotspot_device)
         except Exception as err:
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed up apply OTA update. Aborting tests")
 
     def on_fail(self, test_name, begin_time):
diff --git a/acts/tests/google/wifi/WifiTethering5GPskOTATest.py b/acts/tests/google/wifi/WifiTethering5GPskOTATest.py
index 7450578..9e68f22 100755
--- a/acts/tests/google/wifi/WifiTethering5GPskOTATest.py
+++ b/acts/tests/google/wifi/WifiTethering5GPskOTATest.py
@@ -52,7 +52,7 @@
         try:
           ota_updater.update(self.hotspot_device)
         except Exception as err:
-            raise signals.TestSkipClass(
+            raise signals.TestAbortClass(
                 "Failed up apply OTA update. Aborting tests")
 
     def on_fail(self, test_name, begin_time):
diff --git a/acts/tests/google/wifi/WifiThroughputStabilityTest.py b/acts/tests/google/wifi/WifiThroughputStabilityTest.py
index fd51961..f3e942f 100644
--- a/acts/tests/google/wifi/WifiThroughputStabilityTest.py
+++ b/acts/tests/google/wifi/WifiThroughputStabilityTest.py
@@ -27,6 +27,7 @@
 from acts import utils
 from acts.controllers import iperf_server as ipf
 from acts.controllers.utils_lib import ssh
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
 from acts.test_utils.wifi import ota_chamber
 from acts.test_utils.wifi import wifi_performance_test_utils as wputils
 from acts.test_utils.wifi import wifi_retail_ap as retail_ap
@@ -52,9 +53,9 @@
         base_test.BaseTestClass.__init__(self, controllers)
         # Define metrics to be uploaded to BlackBox
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = True
         # Generate test cases
         self.tests = self.generate_test_cases(
@@ -132,6 +133,11 @@
         self.testclass_results = []
 
         # Turn WiFi ON
+        if self.testclass_params.get('airplane_mode', 1):
+            self.log.info('Turning on airplane mode.')
+            asserts.assert_true(
+                utils.force_airplane_mode(self.dut, True),
+                "Can not turn on airplane mode.")
         wutils.wifi_toggle_state(self.dut, True)
 
     def teardown_test(self):
@@ -221,7 +227,7 @@
             json.dump(test_result_dict, results_file)
         # Plot and save
         figure = wputils.BokehFigure(
-            test_name, x_label='Time (s)', primary_y='Throughput (Mbps)')
+            test_name, x_label='Time (s)', primary_y_label='Throughput (Mbps)')
         time_data = list(range(0, len(instantaneous_rates_Mbps)))
         figure.add_line(
             time_data,
@@ -270,9 +276,12 @@
         band = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
         current_network = self.dut.droid.wifiGetConnectionInfo()
-        valid_connection = wutils.validate_connection(self.dut)
-        if valid_connection and current_network['SSID'] == self.main_network[
-                band]['SSID']:
+        try:
+            connected = wutils.validate_connection(self.dut) is not None
+        except:
+            connected = False
+        if connected and current_network['SSID'] == self.main_network[band][
+                'SSID']:
             self.log.info('Already connected to desired network')
         else:
             wutils.wifi_toggle_state(self.dut, True)
@@ -448,9 +457,9 @@
         base_test.BaseTestClass.__init__(self, controllers)
         # Define metrics to be uploaded to BlackBox
         self.testcase_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_case())
+            BlackboxMappedMetricLogger.for_test_case())
         self.testclass_metric_logger = (
-            wputils.BlackboxMappedMetricLogger.for_test_class())
+            BlackboxMappedMetricLogger.for_test_class())
         self.publish_testcase_metrics = False
 
     def setup_class(self):
@@ -517,7 +526,7 @@
             current_plot = wputils.BokehFigure(
                 title='Channel {} - Rate vs. Position'.format(channel),
                 x_label=x_label,
-                primary_y='Rate (Mbps)',
+                primary_y_label='Rate (Mbps)',
             )
             for test_id, test_data in channel_data.items():
                 test_id_dict = dict(test_id)
diff --git a/acts/tests/meta/ActsUnitTest.py b/acts/tests/meta/ActsUnitTest.py
new file mode 100755
index 0000000..f383ade
--- /dev/null
+++ b/acts/tests/meta/ActsUnitTest.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2019 - 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 logging
+import os
+import subprocess
+import sys
+
+import acts
+from acts import base_test
+from acts import signals
+
+# The files under acts/framework to consider as unit tests.
+UNITTEST_FILES = [
+    'tests/acts_adb_test.py',
+    'tests/acts_android_device_test.py',
+    'tests/acts_asserts_test.py',
+    'tests/acts_base_class_test.py',
+    'tests/config/unittest_bundle.py',
+    'tests/acts_context_test.py',
+    'tests/acts_error_test.py',
+    'tests/acts_host_utils_test.py',
+    'tests/acts_import_test_utils_test.py',
+    'tests/acts_import_unit_test.py',
+    'tests/acts_job_test.py',
+    'tests/libs/ota/unittest_bundle.py',
+    'tests/acts_logger_test.py',
+    'tests/libs/metrics/unittest_bundle.py',
+    'tests/acts_records_test.py',
+    'tests/acts_relay_controller_test.py',
+    'tests/acts_test_runner_test.py',
+    'tests/acts_unittest_suite.py',
+    'tests/acts_utils_test.py',
+    'tests/controllers/android_lib/android_lib_unittest_bundle.py',
+    'tests/event/event_unittest_bundle.py',
+    'tests/test_utils/instrumentation/unit_test_suite.py',
+    'tests/libs/logging/logging_unittest_bundle.py',
+    'tests/metrics/unittest_bundle.py',
+    'tests/libs/proc/proc_unittest_bundle.py',
+    'tests/controllers/sl4a_lib/test_suite.py',
+    'tests/test_runner_test.py',
+    'tests/libs/version_selector_test.py',
+]
+
+# The number of seconds to wait before considering the unit test to have timed
+# out.
+UNITTEST_TIMEOUT = 60
+
+
+class ActsUnitTest(base_test.BaseTestClass):
+    """A class to run the ACTS unit tests in parallel.
+
+    This is a hack to run the ACTS unit tests through CI. Please use the main
+    function below if you need to run these tests.
+    """
+
+    def test_units(self):
+        """Runs all the ACTS unit tests in parallel."""
+        acts_unittest_path = os.path.dirname(acts.__path__[0])
+        test_processes = []
+
+        fail_test = False
+
+        for unittest_file in UNITTEST_FILES:
+            file_path = os.path.join(acts_unittest_path, unittest_file)
+            test_processes.append(
+                subprocess.Popen(
+                    [sys.executable, file_path],
+                    stdout=subprocess.PIPE,
+                    stderr=subprocess.STDOUT))
+
+        for test_process in test_processes:
+            killed = False
+            try:
+                stdout, _ = test_process.communicate(timeout=UNITTEST_TIMEOUT)
+            except subprocess.TimeoutExpired:
+                killed = True
+                self.log.error('Unit test %s timed out after %s seconds.' %
+                               (test_process.args, UNITTEST_TIMEOUT))
+                test_process.kill()
+                stdout, _ = test_process.communicate()
+            if test_process.returncode != 0 or killed:
+                self.log.error('=' * 79)
+                self.log.error('Unit Test %s failed with error %s.' %
+                               (test_process.args, test_process.returncode))
+                self.log.error('=' * 79)
+                self.log.error('Failure for `%s`:\n%s' %
+                               (test_process.args,
+                                stdout.decode('utf-8', errors='replace')))
+                fail_test = True
+            else:
+                self.log.debug('Output for `%s`:\n%s' %
+                               (test_process.args,
+                                stdout.decode('utf-8', errors='replace')))
+
+        if fail_test:
+            raise signals.TestFailure(
+                'One or more unit tests failed. See the logs.')
+
+
+def main():
+    ActsUnitTest({'log': logging.getLogger()}).test_units()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/acts/tests/meta/__init__.py b/acts/tests/meta/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts/tests/meta/__init__.py
diff --git a/tools/create_virtualenv.sh b/tools/create_virtualenv.sh
new file mode 100755
index 0000000..3746ba2
--- /dev/null
+++ b/tools/create_virtualenv.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+python3 -m pip install virtualenv
+
+if [ $? -ne 0 ]; then
+  echo "Virtualenv must be installed to run the upload tests. Run: " >&2
+  echo "    sudo python3 -m pip install virtualenv" >&2
+  exit 1
+fi
+
+virtualenv='/tmp/acts_preupload_virtualenv'
+
+python3 -m virtualenv -p python3 $virtualenv
+cp -r acts/framework $virtualenv/
+cd $virtualenv/framework
+$virtualenv/bin/python3 setup.py develop
+cd -