Merge Android 14

Bug: 298295554
Merged-In: I5739c9d3b1ae22e06d76007bf51bddd7c71be429
Change-Id: Ia0cf62dad6a04cce046c15f8223a2a599f542e86
diff --git a/acts/framework/acts/controllers/bits.py b/acts/framework/acts/controllers/bits.py
index d89a9b3..caadeb7 100644
--- a/acts/framework/acts/controllers/bits.py
+++ b/acts/framework/acts/controllers/bits.py
@@ -1,5 +1,7 @@
 """Module managing the required definitions for using the bits power monitor"""
 
+import csv
+import json
 import logging
 import os
 import time
@@ -424,8 +426,14 @@
         In the case where there is not enough information to retrieve a
         monsoon-like file, this function will do nothing.
         """
-        available_channels = self._client.list_channels(
-            self._active_collection.name)
+        metrics = self._client.get_metrics(self._active_collection.name)
+
+        try:
+            self._save_rails_csv(metrics)
+        except Exception as e:
+            logging.warning(
+                'Could not save rails data to csv format with error {}'.format(e))
+        available_channels = [channel['name'] for channel in metrics['data']]
         milli_amps_channel = None
 
         for channel in available_channels:
@@ -447,6 +455,70 @@
             self._active_collection.name,
             milli_amps_channel)
 
+    def _save_rails_csv(self, metrics):
+        # Creates csv path for rails data
+        monsoon_path = self._active_collection.monsoon_output_path
+        dir_path = os.path.dirname(monsoon_path)
+        if dir_path.endswith('Monsoon'):
+            dir_path = os.path.join(os.path.dirname(dir_path), 'Kibble')
+            os.makedirs(dir_path, exist_ok=True)
+        rails_basename = os.path.basename(monsoon_path)
+        if rails_basename.endswith('.txt'):
+            rails_basename = os.path.splitext(rails_basename)[0]
+        json_basename = 'kibble_rails_' + rails_basename + '.json'
+        rails_basename = 'kibble_rails_' + rails_basename + '.csv'
+        root_rail_results_basename = '{}_results.csv'.format(
+            self._root_rail.split(':')[0])
+        rails_csv_path = os.path.join(dir_path, rails_basename)
+        rails_json_path = os.path.join(dir_path, json_basename)
+        root_rail_results_path = os.path.join(dir_path, root_rail_results_basename)
+
+        logging.info('dump metric to json format: {}'.format(rails_json_path))
+        with open(rails_json_path, 'w') as f:
+            json.dump(metrics['data'], f, sort_keys=True, indent=2)
+
+        # Gets all channels
+        channels = {
+            channel['name'].split('.')[-1].split(':')[0]
+            for channel in metrics['data']
+        }
+        channels = list(channels)
+        list.sort(channels)
+
+        rail_dict = {
+            channel['name'].split('.')[-1] : channel['avg']
+            for channel in metrics['data']
+        }
+
+        root_rail_key = self._root_rail.split(':')[0] + ':mW'
+        root_rail_power = 0
+        if root_rail_key in rail_dict:
+            root_rail_power = rail_dict[root_rail_key]
+        logging.info('root rail {} power is: {}'.format(root_rail_key, root_rail_power))
+
+        path_existed = os.path.exists(root_rail_results_path)
+        with open(root_rail_results_path, 'a') as f:
+            if not path_existed:
+                f.write('{},{}'.format(root_rail_key, 'power(mW)'))
+            f.write('\n{},{}'.format(self._active_collection.name, root_rail_power))
+
+        header = ['CHANNEL', 'VALUE', 'UNIT', 'VALUE', 'UNIT', 'VALUE', 'UNIT']
+        with open(rails_csv_path, 'w') as f:
+            csvwriter = csv.writer(f)
+            csvwriter.writerow(header)
+            for key in  channels:
+                if not key.startswith('C') and not key.startswith('M'):
+                    continue
+                try:
+                    row = [key, '0', 'mA', '0', 'mV', '0', 'mW']
+                    row[1] = str(rail_dict[key + ':mA'])
+                    row[3] = str(rail_dict[key + ':mV'])
+                    row[5] = str(rail_dict[key + ':mW'])
+                    csvwriter.writerow(row)
+                    logging.debug('channel {}: {}'.format(key, row))
+                except Exception as e:
+                    logging.info('channel {} fail'.format(key))
+
     def get_waveform(self, file_path=None):
         """Parses a file generated in release_resources.
 
@@ -462,6 +534,26 @@
 
         return list(power_metrics.import_raw_data(file_path))
 
+    def get_bits_root_rail_csv_export(self, file_path=None, collection_name=None):
+        """Export raw data samples for root rail in csv format.
+
+        Args:
+            file_path: Path to save the export file.
+            collection_name: Name of collection to be exported on client.
+        """
+        if file_path is None:
+            raise ValueError('file_path cannot be None')
+        if collection_name is None:
+            raise ValueError('collection_name cannot be None')
+        try:
+            key = self._root_rail.split(':')[0] + ':mW'
+            file_name = 'raw_data_' + collection_name + '.csv'
+            raw_bits_data_path = os.path.join(file_path, file_name)
+            self._client.export_as_csv([key], collection_name,
+                                       raw_bits_data_path)
+        except Exception as e:
+            logging.warning('Failed to save raw data due to :  {}'.format(e))
+
     def teardown(self):
         if self._service is None:
             return
diff --git a/acts/framework/acts/controllers/bits_lib/bits_service_config.py b/acts/framework/acts/controllers/bits_lib/bits_service_config.py
index cb2d219..a5fb2f9 100644
--- a/acts/framework/acts/controllers/bits_lib/bits_service_config.py
+++ b/acts/framework/acts/controllers/bits_lib/bits_service_config.py
@@ -144,7 +144,11 @@
             if 'serial' not in kibble:
                 raise ValueError('An individual kibble config must have a '
                                  'serial')
-
+            if 'subkibble_params' in kibble:
+                user_defined_kibble_config = kibble['subkibble_params']
+                kibble_config = copy.deepcopy(user_defined_kibble_config)
+            else:
+                kibble_config = copy.deepcopy(DEFAULT_KIBBLE_CONFIG)
             board = kibble['board']
             connector = kibble['connector']
             serial = kibble['serial']
@@ -154,7 +158,6 @@
                 self.boards_configs[board][
                     'board_file'] = kibble_board_file
                 self.boards_configs[board]['kibble_py'] = kibble_bin
-            kibble_config = copy.deepcopy(DEFAULT_KIBBLE_CONFIG)
             kibble_config['connector'] = connector
             self.boards_configs[board]['attached_kibbles'][
                 serial] = kibble_config
diff --git a/acts/framework/acts/controllers/buds_lib/dev_utils/proto/gen/apollo_qa_pb2.py b/acts/framework/acts/controllers/buds_lib/dev_utils/proto/gen/apollo_qa_pb2.py
index fefcfe4..1290491 100644
--- a/acts/framework/acts/controllers/buds_lib/dev_utils/proto/gen/apollo_qa_pb2.py
+++ b/acts/framework/acts/controllers/buds_lib/dev_utils/proto/gen/apollo_qa_pb2.py
@@ -11,7 +11,7 @@
 _sym_db = _symbol_database.Default()
 
 
-import nanopb_pb2 as nanopb__pb2
+from . import nanopb_pb2 as nanopb__pb2
 
 
 DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x61pollo_qa.proto\x12$apollo.lib.apollo_dev_util_lib.proto\x1a\x0cnanopb.proto\"t\n\rApolloQATrace\x12\x11\n\ttimestamp\x18\x01 \x02(\r\x12\x39\n\x02id\x18\x02 \x02(\x0e\x32-.apollo.lib.apollo_dev_util_lib.proto.TraceId\x12\x15\n\x04\x64\x61ta\x18\x03 \x03(\rB\x07\x10\x01\x92?\x02\x10\x05\"\xcd\x02\n\x16\x41polloQAGetVerResponse\x12\x11\n\ttimestamp\x18\x01 \x02(\r\x12\x16\n\x0e\x63sr_fw_version\x18\x02 \x02(\r\x12\x1a\n\x12\x63sr_fw_debug_build\x18\x03 \x02(\x08\x12\x17\n\x0fvm_build_number\x18\x04 \x02(\r\x12\x16\n\x0evm_debug_build\x18\x05 \x02(\x08\x12\x14\n\x0cpsoc_version\x18\x06 \x02(\r\x12\x1a\n\x0b\x62uild_label\x18\x07 \x02(\tB\x05\x92?\x02\x08 \x12Q\n\x0flast_ota_status\x18\x08 \x01(\x0e\x32\x38.apollo.lib.apollo_dev_util_lib.proto.PreviousBootStatus\x12\x17\n\x0f\x63harger_version\x18\t \x02(\r\x12\x1d\n\x15\x65xpected_psoc_version\x18\n \x01(\r\"u\n\x18\x41polloQAGetCodecResponse\x12\x11\n\ttimestamp\x18\x01 \x02(\r\x12\x46\n\x05\x63odec\x18\x02 \x01(\x0e\x32\x37.apollo.lib.apollo_dev_util_lib.proto.ApolloQAA2dpCodec\"\xa6\x01\n\x1c\x41polloQAGetDspStatusResponse\x12\x11\n\ttimestamp\x18\x01 \x02(\r\x12\x15\n\ris_dsp_loaded\x18\x02 \x02(\x08\x12\x43\n\nsink_state\x18\x03 \x02(\x0e\x32/.apollo.lib.apollo_dev_util_lib.proto.SinkState\x12\x17\n\x0f\x66\x65\x61tures_active\x18\x04 \x02(\r\"\xb9\x01\n\x18\x41polloQAFactoryPlaySound\x12Y\n\x06prompt\x18\x01 \x02(\x0e\x32I.apollo.lib.apollo_dev_util_lib.proto.ApolloQAFactoryPlaySound.PromptType\"B\n\nPromptType\x12\x1c\n\x18PROMPT_TYPE_BT_CONNECTED\x10\x01\x12\x16\n\x12PROMPT_TYPE_IN_EAR\x10\x02\"\x1c\n\x1a\x41polloQAFactoryInfoRequest\"\xb6\x01\n\x1b\x41polloQAFactoryInfoResponse\x12\x11\n\ttimestamp\x18\x01 \x02(\r\x12\x1b\n\x0c\x63rystal_trim\x18\x02 \x01(\x05\x42\x05\x92?\x02\x38\x10\x12\x19\n\x11\x63rash_dump_exists\x18\x03 \x01(\x08\x12!\n\x19is_developer_mode_enabled\x18\x04 \x01(\x08\x12\x1b\n\x13is_always_connected\x18\x05 \x01(\x08\x12\x0c\n\x04hwid\x18\x06 \x01(\r*\xb8\x01\n\x13\x41polloQAMessageType\x12\t\n\x05TRACE\x10\x01\x12\x14\n\x10GET_VER_RESPONSE\x10\x02\x12\x16\n\x12GET_CODEC_RESPONSE\x10\x03\x12\x1b\n\x17GET_DSP_STATUS_RESPONSE\x10\x04\x12\x16\n\x12\x46\x41\x43TORY_PLAY_SOUND\x10\x05\x12\x18\n\x14\x46\x41\x43TORY_INFO_REQUEST\x10\x06\x12\x19\n\x15\x46\x41\x43TORY_INFO_RESPONSE\x10\x07*\xfc\x02\n\x07TraceId\x12\x17\n\x13OTA_ERASE_PARTITION\x10\x01\x12\x1d\n\x19OTA_START_PARTITION_WRITE\x10\x02\x12 \n\x1cOTA_FINISHED_PARTITION_WRITE\x10\x03\x12\x17\n\x13OTA_SIGNATURE_START\x10\x04\x12\x19\n\x15OTA_SIGNATURE_FAILURE\x10\x05\x12\x19\n\x15OTA_TRIGGERING_LOADER\x10\x06\x12\x1c\n\x18OTA_LOADER_VERIFY_FAILED\x10\x07\x12\x10\n\x0cOTA_PROGRESS\x10\x08\x12\x0f\n\x0bOTA_ABORTED\x10\t\x12\x1c\n\x18\x41VRCP_PLAY_STATUS_CHANGE\x10\n\x12\x11\n\rVOLUME_CHANGE\x10\x0b\x12\x1a\n\x16\x43OMMANDER_RECV_COMMAND\x10\x0c\x12\x1c\n\x18\x43OMMANDER_FINISH_COMMAND\x10\r\x12\x1c\n\x18\x43OMMANDER_REJECT_COMMAND\x10\x0e*m\n\x0f\x41vrcpPlayStatus\x12\x0b\n\x07STOPPED\x10\x00\x12\x0b\n\x07PLAYING\x10\x01\x12\n\n\x06PAUSED\x10\x02\x12\x0c\n\x08\x46WD_SEEK\x10\x08\x12\x0c\n\x08REV_SEEK\x10\x10\x12\t\n\x05\x45RROR\x10\x05\x12\r\n\tSEEK_MASK\x10\x18*4\n\x12PreviousBootStatus\x12\x0f\n\x0bOTA_SUCCESS\x10\x01\x12\r\n\tOTA_ERROR\x10\x02*%\n\x11\x41polloQAA2dpCodec\x12\x07\n\x03\x41\x41\x43\x10\x01\x12\x07\n\x03SBC\x10\x02*\xd8\x02\n\tSinkState\x12\t\n\x05LIMBO\x10\x00\x12\x0f\n\x0b\x43ONNECTABLE\x10\x01\x12\x10\n\x0c\x44ISCOVERABLE\x10\x02\x12\r\n\tCONNECTED\x10\x03\x12\x1c\n\x18OUTGOING_CALLS_ESTABLISH\x10\x04\x12\x1c\n\x18INCOMING_CALLS_ESTABLISH\x10\x05\x12\x13\n\x0f\x41\x43TIVE_CALL_SCO\x10\x06\x12\r\n\tTEST_MODE\x10\x07\x12\x1a\n\x16THREE_WAY_CALL_WAITING\x10\x08\x12\x1a\n\x16THREE_WAY_CALL_ON_HOLD\x10\t\x12\x17\n\x13THREE_WAY_MULTICALL\x10\n\x12\x19\n\x15INCOMING_CALL_ON_HOLD\x10\x0b\x12\x16\n\x12\x41\x43TIVE_CALL_NO_SCO\x10\x0c\x12\x12\n\x0e\x41\x32\x44P_STREAMING\x10\r\x12\x16\n\x12\x44\x45VICE_LOW_BATTERY\x10\x0e\x42)\n\x1d\x63om.google.android.bisto.nanoB\x08\x41polloQA')
diff --git a/acts/framework/acts/controllers/cellular_lib/PresetSimulation.py b/acts/framework/acts/controllers/cellular_lib/PresetSimulation.py
index b3c2e5a..d536b09 100644
--- a/acts/framework/acts/controllers/cellular_lib/PresetSimulation.py
+++ b/acts/framework/acts/controllers/cellular_lib/PresetSimulation.py
@@ -27,6 +27,12 @@
     KEY_CELL_INFO = "cell_info"
     KEY_SCPI_FILE_NAME = "scpi_file"
 
+    NETWORK_BIT_MASK = {
+        'nr_lte': '11000001000000000000'
+    }
+    ADB_CMD_LOCK_NETWORK = 'cmd phone set-allowed-network-types-for-users -s 0 {network_bit_mask}'
+    NR_LTE_BIT_MASK_KEY = 'nr_lte'
+
     def __init__(self,
                  simulator,
                  log,
@@ -47,6 +53,8 @@
 
         super().__init__(simulator, log, dut, test_config, calibration_table,
                          nr_mode)
+        # require param for idle test case
+        self.rrc_sc_timer = 0
 
         # Set to KeySight APN
         log.info('Configuring APN.')
@@ -56,15 +64,6 @@
         # Enable roaming on the phone
         self.dut.toggle_data_roaming(True)
 
-        # Force device to LTE only so that it connects faster
-        try:
-            self.dut.set_preferred_network_type(
-                BaseCellularDut.PreferredNetworkType.NR_LTE)
-        except Exception as e:
-            # If this fails the test should be able to run anyways, even if it
-            # takes longer to find the cell.
-            self.log.warning('Setting preferred RAT failed: ' + str(e))
-
     def setup_simulator(self):
         """Do initial configuration in the simulator. """
         self.log.info('This simulation does not require initial setup.')
@@ -99,11 +98,7 @@
             RuntimeError: simulation fail to start
                 due to unable to connect dut and cells.
         """
-
-        try:
-            self.attach()
-        except Exception as exc:
-            raise RuntimeError('Simulation fail to start.') from exc
+        self.attach()
 
     def attach(self):
         """Attach UE to the callbox.
diff --git a/acts/framework/acts/controllers/spirent_lib/gss7000.py b/acts/framework/acts/controllers/spirent_lib/gss7000.py
index 961d4e8..3f463fc 100644
--- a/acts/framework/acts/controllers/spirent_lib/gss7000.py
+++ b/acts/framework/acts/controllers/spirent_lib/gss7000.py
@@ -162,7 +162,7 @@
                     Type, list.
         """
         root = ET.fromstring(xml)
-        capability_ls = list()
+        capability_ls = []
         sig_cap_list = root.find('data').find('Signal_capabilities').findall(
             'Signal')
         for signal in sig_cap_list:
@@ -203,15 +203,13 @@
         if scenario == '':
             errmsg = ('Missing scenario file')
             raise GSS7000Error(error=errmsg, command='load_scenario')
-        else:
-            self._logger.debug('Stopped the original scenario')
-            self._query('-,EN,1')
-            cmd = 'SC,' + scenario
-            self._logger.debug('Loading scenario')
-            self._query(cmd)
-            self._logger.debug('Scenario is loaded')
-            return True
-        return False
+        self._logger.debug('Stopped the original scenario')
+        self._query('-,EN,1')
+        cmd = 'SC,' + scenario
+        self._logger.debug('Loading scenario')
+        self._query(cmd)
+        self._logger.debug('Scenario is loaded')
+        return True
 
     def start_scenario(self, scenario=''):
         """Load and Start the running scenario.
@@ -223,6 +221,8 @@
         if scenario:
             if self.load_scenario(scenario):
                 self._query('RU')
+        # TODO: Need to refactor the logic design to solve the comment in ag/19222896
+        # Track the issue in b/241200605
             else:
                 infmsg = 'No scenario is loaded. Stop running scenario'
                 self._logger.debug(infmsg)
@@ -230,7 +230,7 @@
             pass
 
         if scenario:
-            infmsg = 'Started running scenario {}'.format(scenario)
+            infmsg = f'Started running scenario {scenario}'
         else:
             infmsg = 'Started running current scenario'
 
@@ -279,12 +279,12 @@
             GSS7000Error: raise when power offset level is not in [-170, -115] range.
         """
         if not -170 <= ref_dBm <= -115:
-            errmsg = ('"power_offset" must be within [-170, -115], '
-                      'current input is {}').format(str(ref_dBm))
+            errmsg = (f'"power_offset" must be within [-170, -115], '
+                      f'current input is {ref_dBm}')
             raise GSS7000Error(error=errmsg, command='set_ref_power')
-        cmd = 'REF_DBM,{}'.format(str(round(ref_dBm, 1)))
+        cmd = f'REF_DBM,{ref_dBm:.1f}'
         self._query(cmd)
-        infmsg = 'Set reference power level: {}'.format(str(round(ref_dBm, 1)))
+        infmsg = f'Set reference power level: {ref_dBm:.1f}'
         self._logger.debug(infmsg)
 
     def get_status(self, return_txt=False):
@@ -309,8 +309,7 @@
                      'Waiting for further commands.'
             }
             return status_dict.get(status)
-        else:
-            return int(status)
+        return int(status)
 
     def set_power(self, power_level=-130):
         """Set Power Level of GSS7000 Tx
@@ -331,8 +330,7 @@
         self.set_power_offset(1, power_offset)
         self.set_power_offset(2, power_offset)
 
-        infmsg = 'Set GSS7000 transmit power to "{}"'.format(
-            round(power_level, 1))
+        infmsg = f'Set GSS7000 transmit power to "{power_level:.1f}"'
         self._logger.debug(infmsg)
 
     def power_lev_offset_cal(self, power_level=-130, sat='GPS', band='L1'):
@@ -418,8 +416,8 @@
                 f'Satellite system and band ({sat_band}) are not supported.'
                 f'The GSS7000 support list: {self.capability}')
             raise GSS7000Error(error=errmsg, command='set_scenario_power')
-        else:
-            sat_band_tp = tuple(sat_band.split('_'))
+
+        sat_band_tp = tuple(sat_band.split('_'))
 
         return sat_band_tp
 
@@ -436,14 +434,14 @@
                 Default. -130
             sat_id: set power level for specific satellite identifiers
                 Type, int.
-            sat_system: to set power level for all Satellites
+            sat_system: to set power level for specific system
                 Type, str
                 Option 'GPS/GLO/GAL/BDS'
                 Type, str
                 Default, '', assumed to be GPS.
             freq_band: Frequency band to set the power level
                 Type, str
-                Option 'L1/L5/B1I/B1C/B2A/F1/E5/ALL'
+                Option 'L1/L5/B1I/B1C/B2A/F1/E5'
                 Default, '', assumed to be L1.
         Raises:
             GSS7000Error: raise when power offset is not in [-49, -15] range.
@@ -455,8 +453,7 @@
             'B1I': 1,
             'B1C': 1,
             'F1': 1,
-            'E5': 2,
-            'ALL': 3
+            'E5': 2
         }
 
         # Convert and check satellite system and band
@@ -464,12 +461,13 @@
         # Get freq band setting
         band_cmd = band_dict.get(band, 1)
 
+        # When set sat_id --> control specific SV power.
+        # When set is not set --> control all SVs of specific system power.
         if not sat_id:
-            sat_id = 0
+            sat_id = 1
             all_tx_type = 1
         else:
             all_tx_type = 0
-
         # Convert absolute power level to absolute power offset.
         power_offset = self.power_lev_offset_cal(power_level, sat, band)
 
@@ -478,13 +476,10 @@
                       f'current input is {power_offset}')
             raise GSS7000Error(error=errmsg, command='set_power_offset')
 
+        # If no specific sat_system is set, the default is GPS L1.
         if band_cmd == 1:
             cmd = f'-,POW_LEV,v1_a1,{power_offset},{sat},{sat_id},0,0,0,1,1,{all_tx_type}'
             self._query(cmd)
         elif band_cmd == 2:
             cmd = f'-,POW_LEV,v1_a2,{power_offset},{sat},{sat_id},0,0,0,1,1,{all_tx_type}'
             self._query(cmd)
-        elif band_cmd == 3:
-            cmd = f'-,POW_LEV,v1_a1,{power_offset},{sat},{sat_id},0,0,0,1,1,{all_tx_type}'
-            self._query(cmd)
-            cmd = f'-,POW_LEV,v1_a2,{power_offset},{sat},{sat_id},0,0,0,1,1,{all_tx_type}'
diff --git a/acts/framework/acts/controllers/uxm_lib/uxm_cellular_simulator.py b/acts/framework/acts/controllers/uxm_lib/uxm_cellular_simulator.py
index ffb5e16..5ef7eac 100644
--- a/acts/framework/acts/controllers/uxm_lib/uxm_cellular_simulator.py
+++ b/acts/framework/acts/controllers/uxm_lib/uxm_cellular_simulator.py
@@ -11,14 +11,81 @@
 #   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 re
+import logging
 import os
+import paramiko
 import socket
 import time
-import paramiko
-import re
 
 from acts.controllers.cellular_simulator import AbstractCellularSimulator
 
+class SocketWrapper():
+    """A wrapper for socket communicate with test equipment.
+
+    Attributes:
+        _socket: a socket object.
+        _ip: a string value for ip address
+            which we want to connect.
+        _port: an integer for port
+            which we want to connect.
+        _connecting_timeout: an integer for socket connecting timeout.
+        _encode_format: a string specify encoding format.
+        _cmd_terminator: a character indicates the end of command/data
+            which need to be sent.
+    """
+
+    def __init__(self, ip, port,
+                 connecting_timeout=120,
+                 cmd_terminator='\n',
+                 encode_format='utf-8',
+                 buff_size=1024):
+        self._socket = None
+        self._ip = ip
+        self._port = port
+        self._connecting_timeout = connecting_timeout
+        self._cmd_terminator = cmd_terminator
+        self._encode_format = encode_format
+        self._buff_size = buff_size
+        self._logger = logging.getLogger(__name__)
+
+    def _connect(self):
+        self._socket = socket.create_connection(
+            (self._ip, self._port), timeout=self._connecting_timeout
+        )
+
+    def send_command(self, cmd: str):
+        if not self._socket:
+            self._connect()
+        if cmd and cmd[-1] != self._cmd_terminator:
+            cmd = cmd + self._cmd_terminator
+        self._socket.sendall(cmd.encode(self._encode_format))
+
+    def send_command_recv(self, cmd: str) -> str:
+        """Send data and wait for response
+
+        Args:
+            cmd: a string command to be sent.
+
+        Returns:
+            a string response.
+        """
+        self.send_command(cmd)
+        response = ''
+        try:
+            response = self._socket.recv(self._buff_size).decode(
+                self._encode_format
+            )
+        except socket.timeout as e:
+            self._logger.info('Socket timeout while receiving response.')
+            self.close()
+            raise
+
+        return response
+
+    def close(self):
+        self._socket.close()
+        self._socket = None
 
 class UXMCellularSimulator(AbstractCellularSimulator):
     """A cellular simulator for UXM callbox."""
@@ -28,20 +95,26 @@
     KEY_CELL_TYPE = "cell_type"
 
     # UXM socket port
-    UXM_PORT = 5125
+    UXM_SOCKET_PORT = 5125
 
     # UXM SCPI COMMAND
     SCPI_IMPORT_STATUS_QUERY_CMD = 'SYSTem:SCPI:IMPort:STATus?'
     SCPI_SYSTEM_ERROR_CHECK_CMD = 'SYST:ERR?\n'
+    SCPI_CHECK_CONNECTION_CMD = '*IDN?\n'
+    SCPI_DEREGISTER_UE_IMS = 'SYSTem:IMS:SERVer:UE:DERegister'
     # require: path to SCPI file
     SCPI_IMPORT_SCPI_FILE_CMD = 'SYSTem:SCPI:IMPort "{}"\n'
     # require: 1. cell type (E.g. NR5G), 2. cell number (E.g CELL1)
     SCPI_CELL_ON_CMD = 'BSE:CONFig:{}:{}:ACTive 1'
-    # require: 1. cell type (E.g. NR5G), 2. cell number (E.g CELL1)
     SCPI_CELL_OFF_CMD = 'BSE:CONFig:{}:{}:ACTive 0'
-    # require: 1. cell type (E.g. NR5G), 2. cell number (E.g CELL1)
     SCPI_GET_CELL_STATUS = 'BSE:STATus:{}:{}?'
-    SCPI_CHECK_CONNECTION_CMD = '*IDN?\n'
+    SCPI_RRC_RELEASE_LTE_CMD = 'BSE:FUNCtion:{}:{}:RELease:SEND'
+    SCPI_RRC_RELEASE_NR_CMD = 'BSE:CONFig:{}:{}:RCONtrol:RRC:STARt RRELease'
+    # require cell number
+    SCPI_CREATE_DEDICATED_BEARER = 'BSE:FUNCtion:LTE:{}:NAS:EBID10:DEDicated:CREate'
+    SCPI_CHANGE_SIM_NR_CMD = 'BSE:CONFig:NR5G:CELL1:SECurity:AUTHenticate:KEY:TYPE {}'
+    SCPI_CHANGE_SIM_LTE_CMD = 'BSE:CONFig:LTE:SECurity:AUTHenticate:KEY {}'
+    SCPI_SETTINGS_PRESET_CMD = 'SYSTem:PRESet:FULL'
 
     # UXM's Test Application recovery
     TA_BOOT_TIME = 100
@@ -49,11 +122,28 @@
     # shh command
     SSH_START_GUI_APP_CMD_FORMAT = 'psexec -s -d -i 1 "{exe_path}"'
     SSH_CHECK_APP_RUNNING_CMD_FORMAT = 'tasklist | findstr /R {regex_app_name}'
+    SSH_KILL_PROCESS_BY_NAME = 'taskkill /IM {process_name} /F'
+    UXM_TEST_APP_NAME = 'TestApp.exe'
 
     # start process success regex
     PSEXEC_PROC_STARTED_REGEX_FORMAT = 'started on * with process ID {proc_id}'
 
-    def __init__(self, ip_address, custom_files, uxm_user,
+    # HCCU default value
+    HCCU_SOCKET_PORT = 4882
+    # number of digit of the length of setup name
+    HCCU_SCPI_CHANGE_SETUP_CMD = ':SYSTem:SETup:CONFig #{number_of_digit}{setup_name_len}{setup_name}'
+    HCCU_SCPI_CHANGE_SCENARIO_CMD = ':SETup:SCENe "((NE_1, {scenario_name}))"'
+    HCCU_STATUS_CHECK_CMD = ':SETup:INSTrument:STATus? 0\n'
+    HCCU_FR2_SETUP_NAME = '{Name:"TSPC_1UXM5G_HF_2RRH_M1740A"}'
+    HCCU_FR1_SETUP_NAME = '{Name:"TSPC_1UXM5G_LF"}'
+    HCCU_GET_INSTRUMENT_COUNT_CMD = ':SETup:INSTrument:COUNt?'
+    HCCU_FR2_INSTRUMENT_COUNT = 5
+    HCCU_FR1_INSTRUMENT_COUNT = 2
+    HCCU_FR2_SCENARIO = 'NR_4DL2x2_2UL2x2_LTE_4CC'
+    HCCU_FR1_SCENARIO = 'NR_1DL4x4_1UL2x2_LTE_4CC'
+
+
+    def __init__(self, ip_address, custom_files,uxm_user,
                  ssh_private_key_to_uxm, ta_exe_path, ta_exe_name):
         """Initializes the cellular simulator.
 
@@ -73,10 +163,11 @@
         self.cells = []
         self.uxm_ip = ip_address
         self.uxm_user = uxm_user
-        self.ssh_private_key_to_uxm = ssh_private_key_to_uxm
+        self.ssh_private_key_to_uxm = os.path.expanduser(
+                                        ssh_private_key_to_uxm)
         self.ta_exe_path = ta_exe_path
         self.ta_exe_name = ta_exe_name
-        self.ssh_client = self._create_ssh_client()
+        self.ssh_client = self.create_ssh_client()
 
         # get roclbottom file
         for file in self.custom_files:
@@ -85,11 +176,124 @@
 
         # connect to Keysight Test Application via socket
         self.recovery_ta()
-        self.socket = self._socket_connect(self.uxm_ip, self.UXM_PORT)
+        self.socket = self._socket_connect(self.uxm_ip, self.UXM_SOCKET_PORT)
         self.check_socket_connection()
         self.timeout = 120
 
-    def _create_ssh_client(self):
+        # hccu socket
+        self.hccu_socket_port = self.HCCU_SOCKET_PORT
+        self.hccu_socket = SocketWrapper(self.uxm_ip, self.hccu_socket_port)
+
+    def socket_connect(self):
+        self.socket = self._socket_connect(self.uxm_ip, self.UXM_SOCKET_PORT)
+
+    def switch_HCCU_scenario(self, scenario_name: str):
+        cmd = self.HCCU_SCPI_CHANGE_SCENARIO_CMD.format(
+            scenario_name=scenario_name)
+        self.hccu_socket.send_command(cmd)
+        self.log.debug(f'Sent command: {cmd}')
+        # this is require for the command to take effect
+        # because hccu's port need to be free.
+        self.hccu_socket.close()
+
+    def switch_HCCU_setup(self, setup_name: str):
+        """Change HHCU system setup.
+
+        Args:
+            setup_name: a string name
+                of the system setup will be changed to.
+        """
+        setup_name_len = str(len(setup_name))
+        number_of_digit = str(len(setup_name_len))
+        cmd = self.HCCU_SCPI_CHANGE_SETUP_CMD.format(
+            number_of_digit=number_of_digit,
+            setup_name_len=setup_name_len,
+            setup_name=setup_name
+        )
+        self.hccu_socket.send_command(cmd)
+        self.log.debug(f'Sent command: {cmd}')
+        # this is require for the command to take effect
+        # because hccu's port need to be free.
+        self.hccu_socket.close()
+
+    def wait_until_hccu_operational(self, timeout=1200):
+        """ Wait for hccu is ready to operate for a specified timeout.
+
+        Args:
+            timeout: time we are waiting for
+                hccu in opertional status.
+
+        Returns:
+            True if HCCU status is operational within timeout.
+            False otherwise.
+        """
+        # check status
+        self.log.info('Waiting for HCCU to ready to operate.')
+        cmd = self.HCCU_STATUS_CHECK_CMD
+        t = 0
+        interval = 10
+        while t < timeout:
+            response = self.hccu_socket.send_command_recv(cmd)
+            if response == 'OPER\n':
+                return True
+            time.sleep(interval)
+            t += interval
+        return False
+
+    def switch_HCCU_settings(self, is_fr2: bool):
+        """Set HCCU setup configuration.
+
+        HCCU stands for Hardware Configuration Control Utility,
+        an interface allows us to control Keysight Test Equipment.
+
+        Args:
+            is_fr2: a bool value.
+        """
+        # change HCCU configration
+        data = ''
+        scenario_name = ''
+        instrument_count_res = self.hccu_socket.send_command_recv(
+            self.HCCU_GET_INSTRUMENT_COUNT_CMD)
+        instrument_count = int(instrument_count_res)
+        # if hccu setup is correct, no need to change.
+        if is_fr2 and instrument_count == self.HCCU_FR2_INSTRUMENT_COUNT:
+            self.log.info('UXM has correct HCCU setup.')
+            return
+        if not is_fr2 and instrument_count == self.HCCU_FR1_INSTRUMENT_COUNT:
+            self.log.info('UXM has correct HCCU setup.')
+            return
+
+        self.log.info('UXM has incorrect HCCU setup, start changing setup.')
+        # terminate TA and close socket
+        self.log.info('Terminate TA before switch HCCU settings.')
+        self.terminate_process(self.UXM_TEST_APP_NAME)
+        self.socket.close()
+
+        # change hccu setup
+        if is_fr2:
+            data = self.HCCU_FR2_SETUP_NAME
+            scenario_name = self.HCCU_FR2_SCENARIO
+        else:
+            data = self.HCCU_FR1_SETUP_NAME
+            scenario_name = self.HCCU_FR1_SCENARIO
+        self.log.info('Switch HCCU setup.')
+        self.switch_HCCU_setup(data)
+        time.sleep(10)
+        if not self.wait_until_hccu_operational():
+            raise RuntimeError('Fail to switch HCCU setup.')
+
+        # change scenario
+        self.log.info('Ativate HCCU scenario.')
+        self.switch_HCCU_scenario(scenario_name)
+        time.sleep(40)
+        if not self.wait_until_hccu_operational():
+            raise RuntimeError('Fail to switch HCCU scenario.')
+
+        # start TA and reconnect socket.
+        self.recovery_ta()
+        self.socket = self._socket_connect(self.uxm_ip, self.UXM_SOCKET_PORT)
+
+    def create_ssh_client(self):
         """Create a ssh client to host."""
         ssh = paramiko.SSHClient()
         ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -99,6 +303,18 @@
         self.log.info('SSH client to %s is connected' % self.uxm_ip)
         return ssh
 
+    def terminate_process(self, process_name):
+        cmd = self.SSH_KILL_PROCESS_BY_NAME.format(
+            process_name=process_name
+        )
+        stdin, stdout, stderr = self.ssh_client.exec_command(cmd)
+        stdin.close()
+        err = ''.join(stderr.readlines())
+        out = ''.join(stdout.readlines())
+        final_output = str(out) + str(err)
+        self.log.info(final_output)
+        return out
+
     def is_ta_running(self):
         is_running_cmd = self.SSH_CHECK_APP_RUNNING_CMD_FORMAT.format(
             regex_app_name=self.ta_exe_name)
@@ -141,7 +357,7 @@
             retries = 12
             for _ in range(retries):
                 try:
-                    s = self._socket_connect(self.uxm_ip, self.UXM_PORT)
+                    s = self._socket_connect(self.uxm_ip, self.UXM_SOCKET_PORT)
                     s.close()
                     return
                 except ConnectionRefusedError as cre:
@@ -171,6 +387,22 @@
             raise ValueError('Missing cell info from configurations file')
         self.cells = cell_info
 
+    def deregister_ue_ims(self):
+        """Remove UE IMS profile from UXM."""
+        self._socket_send_SCPI_command(
+                self.SCPI_DEREGISTER_UE_IMS)
+
+    def create_dedicated_bearer(self):
+        """Create a dedicated bearer setup for ims call.
+
+        After UE connected and register on UXM IMS tab.
+        It is required to create a dedicated bearer setup
+        with EPS bearer ID 10.
+        """
+        cell_number = self.cells[0][self.KEY_CELL_NUMBER]
+        self._socket_send_SCPI_command(
+                self.SCPI_CREATE_DEDICATED_BEARER.format(cell_number))
+
     def turn_cell_on(self, cell_type, cell_number):
         """Turn UXM's cell on.
 
@@ -237,7 +469,7 @@
             host: IP address of desktop where Keysight Test Application resides.
             port: port that Keysight Test Application is listening for socket
                 communication.
-        Return:
+        Returns:
             s: socket object.
         """
         self.log.info('Establishing connection to callbox via socket')
@@ -279,7 +511,7 @@
     def check_system_error(self):
         """Query system error from Keysight Test Application.
 
-        Return:
+        Returns:
             status: a message indicate the number of errors
                 and detail of errors if any.
                 a string `0,"No error"` indicates no error.
@@ -296,6 +528,9 @@
             path: path to SCPI file.
         """
         self._socket_send_SCPI_command(
+            self.SCPI_SETTINGS_PRESET_CMD)
+        time.sleep(10)
+        self._socket_send_SCPI_command(
             self.SCPI_IMPORT_SCPI_FILE_CMD.format(path))
         time.sleep(45)
 
@@ -328,13 +563,23 @@
         # Restart SL4A
         dut.ad.start_services()
 
+    def set_sim_type(self, is_3gpp_sim):
+        sim_type = 'KEYSight'
+        if is_3gpp_sim:
+            sim_type = 'TEST3GPP'
+        self._socket_send_SCPI_command(
+            self.SCPI_CHANGE_SIM_NR_CMD.format(sim_type))
+        time.sleep(2)
+        self._socket_send_SCPI_command(
+            self.SCPI_CHANGE_SIM_LTE_CMD.format(sim_type))
+        time.sleep(2)
+
     def wait_until_attached_one_cell(self,
                                      cell_type,
                                      cell_number,
                                      dut,
                                      wait_for_camp_interval,
-                                     attach_retries,
-                                     change_dut_setting_allow=True):
+                                     attach_retries):
         """Wait until connect to given UXM cell.
 
         After turn off airplane mode, sleep for
@@ -346,54 +591,74 @@
                 which we are trying to connect to.
             cell_number: ordinal number of a cell
                 which we are trying to connect to.
-            dut: a CellularAndroid controller.
+            dut: a AndroidCellular controller.
             wait_for_camp_interval: sleep interval,
                 wait for device to camp.
             attach_retries: number of retry
                 to wait for device
                 to connect to 1 basestation.
-            change_dut_setting_allow: turn on/off APM
-                or reboot device helps with device camp time.
-                However, if we are trying to connect to second cell
-                changing APM status or reboot is not allowed.
         Raise:
-            AbstractCellularSimulator.CellularSimulatorError:
-                device unable to connect to cell.
+            RuntimeError: device unable to connect to cell.
         """
-        # airplane mode off
-        # dut.ad.adb.shell('settings put secure adaptive_connectivity_enabled 0')
-        dut.toggle_airplane_mode(False)
+        # airplane mode on
+        dut.toggle_airplane_mode(True)
         time.sleep(5)
+
         # turn cell on
         self.turn_cell_on(cell_type, cell_number)
         time.sleep(5)
 
-        # waits for connect
+        interval = 10
+        # waits for device to camp
         for index in range(1, attach_retries):
-            # airplane mode on
-            time.sleep(wait_for_camp_interval)
-            cell_state = self.get_cell_status(cell_type, cell_number)
-            self.log.info(f'cell state: {cell_state}')
-            if cell_state == 'CONN\n':
-                return True
-            if cell_state == 'OFF\n':
+            count = 0
+            # airplane mode off
+            dut.toggle_airplane_mode(False)
+            time.sleep(5)
+            # check connection in small interval
+            while count < wait_for_camp_interval:
+                time.sleep(interval)
+                cell_state = self.get_cell_status(cell_type, cell_number)
+                self.log.info(f'cell state: {cell_state}')
+                if cell_state == 'CONN\n':
+                    # wait for connection stable
+                    time.sleep(15)
+                    # check connection status again
+                    cell_state = self.get_cell_status(cell_type, cell_number)
+                    self.log.info(f'cell state: {cell_state}')
+                    if cell_state == 'CONN\n':
+                        return True
+                if cell_state == 'OFF\n':
+                    self.turn_cell_on(cell_type, cell_number)
+                    time.sleep(5)
+                count += interval
+
+            # reboot device
+            if (index % 2) == 0:
+                dut.ad.reboot()
+                if self.rockbottom_script:
+                    self.dut_rockbottom(dut)
+                else:
+                    self.log.warning(
+                        f'Rockbottom script was not executed after reboot.'
+                    )
+            # toggle APM and cell on/off
+            elif (index % 1) == 0:
+                # Toggle APM on
+                dut.toggle_airplane_mode(True)
+                time.sleep(5)
+
+                # Toggle simulator cell
+                self.turn_cell_off(cell_type, cell_number)
+                time.sleep(5)
                 self.turn_cell_on(cell_type, cell_number)
                 time.sleep(5)
-            if change_dut_setting_allow:
-                if (index % 4) == 0:
-                    dut.ad.reboot()
-                    if self.rockbottom_script:
-                        self.dut_rockbottom(dut)
-                    else:
-                        self.log.warning(
-                            f'Rockbottom script {self} was not executed after reboot'
-                        )
-                else:
-                    # airplane mode on
-                    dut.toggle_airplane_mode(True)
-                    time.sleep(5)
-                    # airplane mode off
-                    dut.toggle_airplane_mode(False)
+
+                # Toggle APM off
+                dut.toggle_airplane_mode(False)
+                time.sleep(5)
+            # increase length of small waiting interval
+            interval += 5
 
         # Phone cannot connected to basestation of callbox
         raise RuntimeError(
@@ -418,37 +683,44 @@
             second_cell_number = self.cells[1][self.KEY_CELL_NUMBER]
 
         # connect to 1st cell
-        try:
-            self.wait_until_attached_one_cell(first_cell_type,
-                                              first_cell_number, dut, timeout,
-                                              attach_retries)
-        except Exception as exc:
-            raise RuntimeError(f'Cannot connect to first cell') from exc
+        self.wait_until_attached_one_cell(first_cell_type,
+                                          first_cell_number, dut, timeout,
+                                          attach_retries)
 
-        # connect to 2nd cell
+        # aggregation to NR
         if len(self.cells) == 2:
             self.turn_cell_on(
                 second_cell_type,
                 second_cell_number,
             )
-            self._socket_send_SCPI_command(
-                'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:DL None')
-            self._socket_send_SCPI_command(
-                'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:UL None')
-            self._socket_send_SCPI_command(
-                'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:DL CELL1')
-            self._socket_send_SCPI_command(
-                'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:DL CELL1')
-            time.sleep(1)
-            self._socket_send_SCPI_command(
-                "BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:APPly")
-            try:
-                self.wait_until_attached_one_cell(second_cell_type,
-                                                  second_cell_number, dut,
-                                                  timeout, attach_retries,
-                                                  False)
-            except Exception as exc:
-                raise RuntimeError(f'Cannot connect to second cell') from exc
+
+            for _ in range(1, attach_retries):
+                self.log.info('Try to aggregate to NR.')
+                self._socket_send_SCPI_command(
+                    'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:DL None')
+                self._socket_send_SCPI_command(
+                    'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:UL None')
+                self._socket_send_SCPI_command(
+                    'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:DL CELL1')
+                self._socket_send_SCPI_command(
+                    'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:DL CELL1')
+                time.sleep(1)
+                self._socket_send_SCPI_command(
+                    "BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:APPly")
+                # wait for status stable
+                time.sleep(10)
+                cell_state = self.get_cell_status(second_cell_type, second_cell_number)
+                self.log.info(f'cell state: {cell_state}')
+                if cell_state == 'CONN\n':
+                    return
+                else:
+                    self.turn_cell_off(second_cell_type, second_cell_number)
+                    # wait for LTE cell to connect again
+                    self.wait_until_attached_one_cell(first_cell_type,
+                                            first_cell_number, dut, 120,
+                                            2)
+
+            raise RuntimeError(f'Fail to aggregate to NR from LTE.')
 
     def set_lte_rrc_state_change_timer(self, enabled, time=10):
         """Configures the LTE RRC state change timer.
@@ -677,16 +949,42 @@
             timeout: after this amount of time the method will raise a
                 CellularSimulatorError exception. Default is 120 seconds.
         """
-        raise NotImplementedError(
-            'This UXM callbox simulator does not support this feature.')
+        # turn on RRC release
+        cell_type = self.cells[0][self.KEY_CELL_TYPE]
+        cell_number = self.cells[0][self.KEY_CELL_NUMBER]
+
+        # choose cmd base on cell type
+        cmd = None
+        if cell_type == 'LTE':
+            cmd = self.SCPI_RRC_RELEASE_LTE_CMD
+        else:
+            cmd = self.SCPI_RRC_RELEASE_NR_CMD
+
+        if not cmd:
+            raise RuntimeError(f'Cell type [{cell_type}] is not supporting IDLE.')
+
+        # checking status
+        self.log.info('Wait for IDLE state.')
+        for _ in range(5):
+            cell_state = self.get_cell_status(cell_type, cell_number)
+            self.log.info(f'cell state: {cell_state}')
+            if cell_state == 'CONN\n':
+                # RRC release
+                self._socket_send_SCPI_command(cmd.format(cell_type, cell_number))
+                # wait for status stable
+                time.sleep(60)
+            elif cell_state == 'IDLE\n':
+                return
+
+        raise RuntimeError('RRC release fail.')
 
     def detach(self):
         """ Turns off all the base stations so the DUT loose connection."""
         for cell in self.cells:
             cell_type = cell[self.KEY_CELL_TYPE]
             cell_number = cell[self.KEY_CELL_NUMBER]
-            self._socket_send_SCPI_command(
-                self.SCPI_CELL_OFF_CMD.format(cell_type, cell_number))
+            self.turn_cell_off(cell_type, cell_number)
+            time.sleep(5)
 
     def stop(self):
         """Stops current simulation.
diff --git a/acts_tests/acts_contrib/test_utils/bt/BtMultiprofileBaseTest.py b/acts_tests/acts_contrib/test_utils/bt/BtMultiprofileBaseTest.py
new file mode 100644
index 0000000..f2246df
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/bt/BtMultiprofileBaseTest.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 time
+from acts_contrib.test_utils.bt.A2dpBaseTest import A2dpBaseTest
+import acts_contrib.test_utils.bt.BleBaseTest as BleBT
+from acts_contrib.test_utils.power.IperfHelper import IperfHelper
+from acts_contrib.test_utils.bt.bt_test_utils import orchestrate_rfcomm_connection
+from concurrent.futures import ThreadPoolExecutor
+from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
+from acts_contrib.test_utils.tel.tel_test_utils import WIFI_CONFIG_APBAND_2G
+from acts_contrib.test_utils.wifi import wifi_power_test_utils as wputils
+
+
+class BtMultiprofileBaseTest(A2dpBaseTest, BleBT.BleBaseTest):
+    """Base class for BT mutiprofile related tests.
+
+     Inherited from the A2DP Base class, Ble Base class
+     """
+    # Iperf waiting time (margin)
+    IPERF_MARGIN = 10
+
+    def mutiprofile_test(self,
+                         codec_config=None,
+                         mode=None,
+                         victim=None,
+                         aggressor=None,
+                         metric=None):
+
+        if victim == 'A2DP' and aggressor == 'Ble_Scan' and metric == 'range':
+            scan_callback = self.start_ble_scan(self.dut, mode)
+            self.run_a2dp_to_max_range(codec_config)
+            self.dut.droid.bleStopBleScan(scan_callback)
+            self.log.info("BLE Scan stopped successfully")
+            return True
+
+        if victim == 'Ble_Scan' and aggressor == 'A2DP' and metric == 'scan_accuracy':
+            scan_callback = self.start_ble_scan(self.dut, mode)
+            recorded_file = self.play_and_record_audio(
+                self.audio_params['duration'])
+            self.dut.droid.bleStopBleScan(scan_callback)
+            self.media.stop()
+            self.log.info("BLE Scan & A2DP streaming stopped successfully")
+            return True
+
+        if victim == 'RFCOMM' and aggressor == 'Ble_Scan' and metric == 'throughput':
+            self.remote_device = self.android_devices[2]
+            scan_callback = self.start_ble_scan(self.dut, mode)
+            if not orchestrate_rfcomm_connection(self.dut, self.remote_device):
+                return False
+            self.log.info("RFCOMM Connection established")
+            self.measure_rfcomm_throughput(100)
+            self.dut.droid.bleStopBleScan(scan_callback)
+            self.log.info("BLE Scan stopped successfully")
+
+        if victim == 'A2DP' and aggressor == 'Ble_Adv' and metric == 'range':
+            advertise_callback = self.start_ble_adv(self.dut, mode, 2)
+            self.run_a2dp_to_max_range(codec_config)
+            self.dut.droid.bleStopBleAdvertising(advertise_callback)
+            self.log.info("Advertisement stopped Successfully")
+            return True
+
+        if victim == 'A2DP' and aggressor == 'Ble_conn' and metric == 'range':
+            self.start_ble_connection(self.dut, self.android_devices[2], mode)
+            self.run_a2dp_to_max_range(codec_config)
+            return True
+
+        if victim == 'A2DP' and aggressor == 'wifi' and metric == 'range':
+            self.setup_hotspot_and_connect_client()
+            self.setup_iperf_and_run_throughput()
+            self.run_a2dp_to_max_range(codec_config)
+            self.process_iperf_results()
+            return True
+
+        if victim == 'Ble_Scan' and aggressor == 'wifi' and metric == 'scan_accuracy':
+            scan_callback = self.start_ble_scan(self.dut, mode)
+            self.setup_hotspot_and_connect_client()
+            self.setup_iperf_and_run_throughput()
+            time.sleep(self.audio_params['duration'] + self.IPERF_MARGIN + 2)
+            self.log.info("BLE Scan & iPerf started successfully")
+            self.process_iperf_results()
+            self.dut.droid.bleStopBleScan(scan_callback)
+            self.log.info("BLE Scan stopped successfully")
+            return True
+
+        if victim == 'Ble_Adv' and aggressor == 'wifi' and metric == 'periodic_adv':
+            advertise_callback = self.start_ble_adv(self.dut, mode, 2)
+            self.setup_hotspot_and_connect_client()
+            self.setup_iperf_and_run_throughput()
+            time.sleep(self.audio_params['duration'] + self.IPERF_MARGIN + 2)
+            self.log.info("BLE Advertisement & iPerf started successfully")
+            self.process_iperf_results()
+            self.dut.droid.bleStopBleAdvertising(advertise_callback)
+            self.log.info("Advertisement stopped Successfully")
+            return True
+
+        if victim == 'RFCOMM' and aggressor == 'wifi' and metric == 'throughput':
+            self.remote_device = self.android_devices[2]
+            if not orchestrate_rfcomm_connection(self.dut, self.remote_device):
+                return False
+            self.log.info("RFCOMM Connection established")
+            self.setup_hotspot_and_connect_client()
+            executor = ThreadPoolExecutor(2)
+            throughput = executor.submit(self.measure_rfcomm_throughput, 100)
+            executor.submit(self.setup_iperf_and_run_throughput, )
+            time.sleep(self.audio_params['duration'] + self.IPERF_MARGIN + 10)
+            self.process_iperf_results()
+            return True
+
+    def measure_rfcomm_throughput(self, iteration):
+        """Measures the throughput of a data transfer.
+
+        Sends data over RFCOMM from the client device that is read by the server device.
+        Calculates the throughput for the transfer.
+
+        Args:
+           iteration : An integer value that respesents number of RFCOMM data trasfer iteration
+
+        Returns:
+            The throughput of the transfer in bits per second.
+        """
+        #An integer value designating the number of buffers to be sent.
+        num_of_buffers = 1
+        #An integer value designating the size of each buffer, in bytes.
+        buffer_size = 22000
+        throughput_list = []
+        for transfer in range(iteration):
+            (self.dut.droid.bluetoothConnectionThroughputSend(
+                num_of_buffers, buffer_size))
+
+            throughput = (
+                self.remote_device.droid.bluetoothConnectionThroughputRead(
+                    num_of_buffers, buffer_size))
+            throughput = throughput * 8
+            throughput_list.append(throughput)
+            self.log.info(
+                ("RFCOMM Throughput is :{} bits/sec".format(throughput)))
+        throughput = statistics.mean(throughput_list)
+        return throughput
+
+    def setup_hotspot_and_connect_client(self):
+        """
+        Setup hotspot on the remote device and client connects to hotspot
+
+        """
+        self.network = {
+            wutils.WifiEnums.SSID_KEY: 'Pixel_2G',
+            wutils.WifiEnums.PWD_KEY: '1234567890'
+        }
+        # Setup tethering on dut
+        wutils.start_wifi_tethering(self.android_devices[1],
+                                    self.network[wutils.WifiEnums.SSID_KEY],
+                                    self.network[wutils.WifiEnums.PWD_KEY],
+                                    WIFI_CONFIG_APBAND_2G)
+
+        # Connect client device to Hotspot
+        wutils.wifi_connect(self.dut, self.network, check_connectivity=False)
+
+    def setup_iperf_and_run_throughput(self):
+        self.iperf_server_address = self.android_devices[
+            1].droid.connectivityGetIPv4Addresses('wlan2')[0]
+        # Create the iperf config
+        iperf_config = {
+            'traffic_type': 'TCP',
+            'duration': self.audio_params['duration'] + self.IPERF_MARGIN,
+            'server_idx': 0,
+            'traffic_direction': 'UL',
+            'port': self.iperf_servers[0].port,
+            'start_meas_time': 4,
+        }
+        # Start iperf traffic (dut is the client)
+        self.client_iperf_helper = IperfHelper(iperf_config)
+        self.iperf_servers[0].start()
+        wputils.run_iperf_client_nonblocking(
+            self.dut, self.iperf_server_address,
+            self.client_iperf_helper.iperf_args)
+
+    def process_iperf_results(self):
+        time.sleep(self.IPERF_MARGIN + 2)
+        self.client_iperf_helper.process_iperf_results(self.dut, self.log,
+                                                       self.iperf_servers,
+                                                       self.test_name)
+        self.iperf_servers[0].stop()
+        return True
diff --git a/acts_tests/acts_contrib/test_utils/cellular/__init__.py b/acts_tests/acts_contrib/test_utils/cellular/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/__init__.py
diff --git a/acts_tests/acts_contrib/test_utils/cellular/cellular_base_test.py b/acts_tests/acts_contrib/test_utils/cellular/cellular_base_test.py
index ea7615c..27a66e0 100644
--- a/acts_tests/acts_contrib/test_utils/cellular/cellular_base_test.py
+++ b/acts_tests/acts_contrib/test_utils/cellular/cellular_base_test.py
@@ -201,7 +201,7 @@
                 if getattr(self, param) is None:
                     raise RuntimeError('The uxm cellular simulator '
                                        'requires %s to be set in the '
-                                       'config file.' % key)
+                                       'config file.' % param)
             return uxm.UXMCellularSimulator(self.uxm_ip, self.custom_files,
                                             self.uxm_user,
                                             self.ssh_private_key_to_uxm,
diff --git a/acts_tests/acts_contrib/test_utils/cellular/keysight_5g_testapp.py b/acts_tests/acts_contrib/test_utils/cellular/keysight_5g_testapp.py
new file mode 100644
index 0000000..8e4d10a
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/keysight_5g_testapp.py
@@ -0,0 +1,864 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2021 - 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 collections
+import pyvisa
+import time
+from acts import logger
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+
+SHORT_SLEEP = 1
+VERY_SHORT_SLEEP = 0.1
+SUBFRAME_DURATION = 0.001
+VISA_QUERY_DELAY = 0.01
+
+
+class Keysight5GTestApp(object):
+    """Controller for the Keysight 5G NR Test Application.
+
+    This controller enables interacting with a Keysight Test Application
+    running on a remote test PC and implements many of the configuration
+    parameters supported in test app.
+    """
+
+    VISA_LOCATION = '/opt/keysight/iolibs/libktvisa32.so'
+
+    def __init__(self, config):
+        self.config = config
+        self.log = logger.create_tagged_trace_logger("{}{}".format(
+            self.config['brand'], self.config['model']))
+        self.resource_manager = pyvisa.ResourceManager(self.VISA_LOCATION)
+        self.test_app = self.resource_manager.open_resource(
+            'TCPIP0::{}::{}::INSTR'.format(self.config['ip_address'],
+                                           self.config['hislip_interface']))
+        self.test_app.timeout = 200000
+        self.test_app.write_termination = '\n'
+        self.test_app.read_termination = '\n'
+        self.test_app.query_delay = VISA_QUERY_DELAY
+        self.last_loaded_scpi = None
+
+        inst_id = self.send_cmd('*IDN?', 1)
+        if 'Keysight' not in inst_id[0]:
+            self.log.error(
+                'Failed to connect to Keysight Test App: {}'.format(inst_id))
+        else:
+            self.log.info("Test App ID: {}".format(inst_id))
+
+    def destroy(self):
+        self.test_app.close()
+
+    ### Programming Utilities
+    @staticmethod
+    def _format_cells(cells):
+        "Helper function to format list of cells."
+        if isinstance(cells, int):
+            return 'CELL{}'.format(cells)
+        elif isinstance(cells, str):
+            return cells
+        elif isinstance(cells, list):
+            cell_list = [
+                Keysight5GTestApp._format_cells(cell) for cell in cells
+            ]
+            cell_list = ','.join(cell_list)
+            return cell_list
+
+    @staticmethod
+    def _format_response(response):
+        "Helper function to format test app response."
+
+        def _format_response_entry(entry):
+            try:
+                formatted_entry = float(entry)
+            except:
+                formatted_entry = entry
+            return formatted_entry
+
+        if ',' not in response:
+            return _format_response_entry(response)
+        response = response.split(',')
+        formatted_response = [
+            _format_response_entry(entry) for entry in response
+        ]
+        return formatted_response
+
+    def send_cmd(self, command, read_response=0, check_errors=1):
+        "Helper function to write to or query test app."
+        if read_response:
+            try:
+                response = Keysight5GTestApp._format_response(
+                    self.test_app.query(command))
+                time.sleep(VISA_QUERY_DELAY)
+                if check_errors:
+                    error = self.test_app.query('SYSTem:ERRor?')
+                    time.sleep(VISA_QUERY_DELAY)
+                    if 'No error' not in error:
+                        self.log.warning("Command: {}. Error: {}".format(
+                            command, error))
+                return response
+            except:
+                raise RuntimeError('Lost connection to test app.')
+        else:
+            try:
+                self.test_app.write(command)
+                time.sleep(VISA_QUERY_DELAY)
+                if check_errors:
+                    error = self.test_app.query('SYSTem:ERRor?')
+                    if 'No error' not in error:
+                        self.log.warning("Command: {}. Error: {}".format(
+                            command, error))
+                self.send_cmd('*OPC?', 1)
+                time.sleep(VISA_QUERY_DELAY)
+            except:
+                raise RuntimeError('Lost connection to test app.')
+            return None
+
+    def import_scpi_file(self, file_name, check_last_loaded=0):
+        """Function to import SCPI file specified in file_name.
+
+        Args:
+            file_name: name of SCPI file to run
+            check_last_loaded: flag to check last loaded scpi and
+            only load if different.
+        """
+        if file_name == self.last_loaded_scpi and check_last_loaded:
+            self.log.info('Skipping SCPI import.')
+        self.send_cmd("SYSTem:SCPI:IMPort '{}'".format(file_name))
+        while int(self.send_cmd('SYSTem:SCPI:IMPort:STATus?', 1)):
+            self.send_cmd('*OPC?', 1)
+        self.log.info('Done with SCPI import')
+
+    ### Configure Cells
+    def assert_cell_off_decorator(func):
+        "Decorator function that ensures cells or off when configuring them"
+
+        def inner(self, *args, **kwargs):
+            if "nr" in func.__name__:
+                cell_type = 'NR5G'
+            else:
+                cell_type = kwargs.get('cell_type', args[0])
+            cell = kwargs.get('cell', args[1])
+            cell_state = self.get_cell_state(cell_type, cell)
+            if cell_state:
+                self.log.error('Cell must be off when calling {}'.format(
+                    func.__name__))
+            return (func(self, *args, **kwargs))
+
+        return inner
+
+    def assert_cell_off(self, cell_type, cell):
+        cell_state = self.get_cell_state(cell_type, cell)
+        if cell_state:
+            self.log.error('Cell must be off')
+
+    def select_cell(self, cell_type, cell):
+        """Function to select active cell.
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+        """
+        self.send_cmd('BSE:SELected:CELL {},{}'.format(
+            cell_type, Keysight5GTestApp._format_cells(cell)))
+
+    def select_display_tab(self, cell_type, cell, tab, subtab):
+        """Function to select display tab.
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            tab: tab to display for the selected cell
+        """
+        supported_tabs = {
+            'PHY': [
+                'BWP', 'HARQ', 'PDSCH', 'PDCCH', 'PRACH', 'PUSCH', 'PUCCH',
+                'SRSC'
+            ],
+            'BTHR': ['SUMMARY', 'OTAGRAPH', 'ULOTA', 'DLOTA'],
+            'CSI': []
+        }
+        if (tab not in supported_tabs) or (subtab not in supported_tabs[tab]):
+            return
+        self.select_cell(cell_type, cell)
+        self.send_cmd('DISPlay:{} {},{}'.format(cell_type, tab, subtab))
+
+    def get_cell_state(self, cell_type, cell):
+        """Function to get cell on/off state.
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+        Returns:
+            cell_state: boolean. True if cell on
+        """
+        cell_state = int(
+            self.send_cmd(
+                'BSE:CONFig:{}:{}:ACTive:STATe?'.format(
+                    cell_type, Keysight5GTestApp._format_cells(cell)), 1))
+        return cell_state
+
+    def wait_for_cell_status(self,
+                             cell_type,
+                             cell,
+                             states,
+                             timeout,
+                             polling_interval=SHORT_SLEEP):
+        """Function to wait for a specific cell status
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            states: list of acceptable states (ON, CONN, AGG, ACT, etc)
+            timeout: amount of time to wait for requested status
+        Returns:
+            True if one of the listed states is achieved
+            False if timed out waiting for acceptable state.
+        """
+        states = [states] if isinstance(states, str) else states
+        for i in range(int(timeout / polling_interval)):
+            current_state = self.send_cmd(
+                'BSE:STATus:{}:{}?'.format(
+                    cell_type, Keysight5GTestApp._format_cells(cell)), 1)
+            if current_state in states:
+                return True
+            time.sleep(polling_interval)
+        self.log.warning('Timeout waiting for {} {} {}'.format(
+            cell_type, Keysight5GTestApp._format_cells(cell), states))
+        return False
+
+    def set_cell_state(self, cell_type, cell, state):
+        """Function to set cell state
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            state: requested state
+        """
+        self.send_cmd('BSE:CONFig:{}:{}:ACTive:STATe {}'.format(
+            cell_type, Keysight5GTestApp._format_cells(cell), state))
+
+    def set_cell_type(self, cell_type, cell, sa_or_nsa):
+        """Function to set cell duplex mode
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            sa_or_nsa: SA or NSA
+        """
+        self.assert_cell_off(cell_type, cell)
+        self.send_cmd('BSE: CONFig:NR5G:{}:TYPE {}'.format(
+            Keysight5GTestApp._format_cells(cell), sa_or_nsa))
+
+    def set_cell_duplex_mode(self, cell_type, cell, duplex_mode):
+        """Function to set cell duplex mode
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            duplex_mode: TDD or FDD
+        """
+        self.assert_cell_off(cell_type, cell)
+        self.send_cmd('BSE:CONFig:{}:{}:DUPLEX:MODe {}'.format(
+            cell_type, Keysight5GTestApp._format_cells(cell), duplex_mode))
+
+    def set_cell_band(self, cell_type, cell, band):
+        """Function to set cell band
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            band: LTE or NR band (e.g. 1,3,N260, N77)
+        """
+        self.assert_cell_off(cell_type, cell)
+        self.send_cmd('BSE:CONFig:{}:{}:BAND {}'.format(
+            cell_type, Keysight5GTestApp._format_cells(cell), band))
+
+    def set_cell_channel(self, cell_type, cell, channel, arfcn=1):
+        """Function to set cell frequency/channel
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            channel: requested channel (ARFCN) or frequency in MHz
+        """
+        self.assert_cell_off(cell_type, cell)
+        if cell_type == 'NR5G' and isinstance(
+                channel, str) and channel.lower() in ['low', 'mid', 'high']:
+            self.send_cmd('BSE:CONFig:{}:{}:TESTChanLoc {}'.format(
+                cell_type, Keysight5GTestApp._format_cells(cell), channel.upper()))
+        elif arfcn == 1:
+            self.send_cmd('BSE:CONFig:{}:{}:DL:CHANnel {}'.format(
+                cell_type, Keysight5GTestApp._format_cells(cell), channel))
+        else:
+            self.send_cmd('BSE:CONFig:{}:{}:DL:FREQuency:MAIN {}'.format(
+                cell_type, Keysight5GTestApp._format_cells(cell),
+                channel * 1e6))
+
+    def toggle_contiguous_nr_channels(self, force_contiguous):
+        self.assert_cell_off('NR5G', 1)
+        self.log.warning('Forcing contiguous NR channels overrides channel config.')
+        self.send_cmd('BSE:CONFig:NR5G:PHY:OPTimize:CONTiguous:STATe 0')
+        if force_contiguous:
+            self.send_cmd('BSE:CONFig:NR5G:PHY:OPTimize:CONTiguous:STATe 1')
+
+    def configure_contiguous_nr_channels(self, cell, band, channel):
+        """Function to set cell frequency/channel
+
+        Args:
+            cell: cell/carrier number
+            band: band to set channel in (only required for preset)
+            channel_preset: frequency in MHz or preset in [low, mid, or high]
+        """
+        self.assert_cell_off('NR5G', cell)
+        self.send_cmd('BSE:CONFig:NR5G:PHY:OPTimize:CONTiguous:STATe 0')
+        if channel.lower() in ['low', 'mid', 'high']:
+            pcc_arfcn = cputils.PCC_PRESET_MAPPING[band][channel]
+            self.set_cell_channel('NR5G', cell, pcc_arfcn, 1)
+        else:
+            self.set_cell_channel('NR5G', cell, channel, 0)
+        self.send_cmd('BSE:CONFig:NR5G:PHY:OPTimize:CONTiguous:STATe 1')
+
+    def configure_noncontiguous_nr_channels(self, cells, band, channels):
+        """Function to set cell frequency/channel
+
+        Args:
+            cell: cell/carrier number
+            band: band number
+            channel: frequency in MHz
+        """
+        for cell in cells:
+            self.assert_cell_off('NR5G', cell)
+        self.send_cmd('BSE:CONFig:NR5G:PHY:OPTimize:CONTiguous:STATe 0')
+        for cell, channel in zip(cells, channels):
+            self.set_cell_channel('NR5G', cell, channel, arfcn=0)
+
+    def set_cell_bandwidth(self, cell_type, cell, bandwidth):
+        """Function to set cell bandwidth
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            bandwidth: requested bandwidth
+        """
+        self.assert_cell_off(cell_type, cell)
+        self.send_cmd('BSE:CONFig:{}:{}:DL:BW {}'.format(
+            cell_type, Keysight5GTestApp._format_cells(cell), bandwidth))
+
+    def set_nr_subcarrier_spacing(self, cell, subcarrier_spacing):
+        """Function to set cell bandwidth
+
+        Args:
+            cell: cell/carrier number
+            subcarrier_spacing: requested SCS
+        """
+        self.assert_cell_off('NR5G', cell)
+        self.send_cmd('BSE:CONFig:NR5G:{}:SUBCarrier:SPACing:COMMon {}'.format(
+            Keysight5GTestApp._format_cells(cell), subcarrier_spacing))
+
+    def set_cell_mimo_config(self, cell_type, cell, link, mimo_config):
+        """Function to set cell mimo config.
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            link: uplink or downlink
+            mimo_config: requested mimo configuration (refer to SCPI
+                         documentation for allowed range of values)
+        """
+        self.assert_cell_off(cell_type, cell)
+        if cell_type == 'NR5G':
+            self.send_cmd('BSE:CONFig:{}:{}:{}:MIMO:CONFig {}'.format(
+                cell_type, Keysight5GTestApp._format_cells(cell), link,
+                mimo_config))
+        else:
+            self.send_cmd('BSE:CONFig:{}:{}:PHY:DL:ANTenna:CONFig {}'.format(
+                cell_type, Keysight5GTestApp._format_cells(cell), mimo_config))
+
+    def set_lte_cell_transmission_mode(self, cell, transmission_mode):
+        """Function to set LTE cell transmission mode.
+
+        Args:
+            cell: cell/carrier number
+            transmission_mode: one of TM1, TM2, TM3, TM4 ...
+        """
+        self.assert_cell_off('LTE', cell)
+        self.send_cmd('BSE:CONFig:LTE:{}:RRC:TMODe {}'.format(
+            Keysight5GTestApp._format_cells(cell), transmission_mode))
+
+    def set_cell_dl_power(self, cell_type, cell, power, full_bw):
+        """Function to set cell power
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            power: requested power
+            full_bw: boolean controlling if requested power is per channel
+                     or subcarrier
+        """
+        if full_bw:
+            self.send_cmd('BSE:CONFIG:{}:{}:DL:POWer:CHANnel {}'.format(
+                cell_type, Keysight5GTestApp._format_cells(cell), power))
+        else:
+            self.send_cmd('BSE:CONFIG:{}:{}:DL:POWer:EPRE {}'.format(
+                cell_type, Keysight5GTestApp._format_cells(cell), power))
+        self.send_cmd('BSE:CONFig:{}:APPLY'.format(cell_type))
+
+    def set_cell_duplex_mode(self, cell_type, cell, duplex_mode):
+        """Function to set cell power
+
+        Args:
+            cell_type: LTE or NR5G cell
+            cell: cell/carrier number
+            duplex mode: TDD or FDD
+        """
+        self.assert_cell_off(cell_type, cell)
+        self.send_cmd('BSE:CONFig:{}:{}:DUPLEX:MODe {}'.format(
+            cell_type, Keysight5GTestApp._format_cells(cell), duplex_mode))
+
+    def set_dl_carriers(self, cells):
+        """Function to set aggregated DL NR5G carriers
+
+        Args:
+            cells: list of DL cells/carriers to aggregate with LTE (e.g. [1,2])
+        """
+        self.send_cmd('BSE:CONFig:NR5G:CELL1:CAGGregation:NRCC:DL {}'.format(
+            Keysight5GTestApp._format_cells(cells)))
+
+    def set_ul_carriers(self, cells):
+        """Function to set aggregated UL NR5G carriers
+
+        Args:
+            cells: list of DL cells/carriers to aggregate with LTE (e.g. [1,2])
+        """
+        self.send_cmd('BSE:CONFig:NR5G:CELL1:CAGGregation:NRCC:UL {}'.format(
+            Keysight5GTestApp._format_cells(cells)))
+
+    def set_nr_cell_schedule_scenario(self, cell, scenario):
+        """Function to set NR schedule to one of predefince quick configs.
+
+        Args:
+            cell: cell number to address. schedule will apply to all cells
+            scenario: one of the predefined test app schedlue quick configs
+                      (e.g. FULL_TPUT, BASIC).
+        """
+        self.assert_cell_off('NR5G', cell)
+        self.send_cmd(
+            'BSE:CONFig:NR5G:{}:SCHeduling:QCONFig:SCENario {}'.format(
+                Keysight5GTestApp._format_cells(cell), scenario))
+        self.send_cmd('BSE:CONFig:NR5G:SCHeduling:QCONFig:APPLy:ALL')
+
+    def set_nr_cell_mcs(self, cell, dl_mcs, ul_mcs):
+        """Function to set NR cell DL & UL MCS
+
+        Args:
+            cell: cell number to address. MCS will apply to all cells
+            dl_mcs: mcs index to use on DL
+            ul_mcs: mcs index to use on UL
+        """
+        self.assert_cell_off('NR5G', cell)
+        self.send_cmd(
+            'BSE:CONFig:NR5G:SCHeduling:SETParameter "CELLALL:BWPALL:FCALL:SCALL", "DL:IMCS", "{}"'
+            .format(dl_mcs))
+        self.send_cmd(
+            'BSE:CONFig:NR5G:SCHeduling:SETParameter "CELLALL:BWPALL:FCALL:SCALL", "UL:IMCS", "{}"'
+            .format(ul_mcs))
+
+    def set_lte_cell_mcs(
+        self,
+        cell,
+        dl_mcs_table,
+        dl_mcs,
+        ul_mcs_table,
+        ul_mcs,
+    ):
+        """Function to set NR cell DL & UL MCS
+
+        Args:
+            cell: cell number to address. MCS will apply to all cells
+            dl_mcs: mcs index to use on DL
+            ul_mcs: mcs index to use on UL
+        """
+        if dl_mcs_table == 'QAM256':
+            dl_mcs_table_formatted = 'ASUBframe'
+        elif dl_mcs_table == 'QAM1024':
+            dl_mcs_table_formatted = 'ASUB1024'
+        elif dl_mcs_table == 'QAM64':
+            dl_mcs_table_formatted = 'DISabled'
+        self.assert_cell_off('LTE', cell)
+        self.send_cmd(
+            'BSE:CONFig:LTE:SCHeduling:SETParameter "CELLALL", "DL:MCS:TABle", "{}"'
+            .format(dl_mcs_table_formatted))
+        self.send_cmd(
+            'BSE:CONFig:LTE:SCHeduling:SETParameter "CELLALL:SFALL:CWALL", "DL:IMCS", "{}"'
+            .format(dl_mcs))
+        self.send_cmd(
+            'BSE:CONFig:LTE:SCHeduling:SETParameter "CELLALL", "UL:MCS:TABle", "{}"'
+            .format(ul_mcs_table))
+        self.send_cmd(
+            'BSE:CONFig:LTE:SCHeduling:SETParameter "CELLALL:SFALL", "UL:IMCS", "{}"'
+            .format(ul_mcs))
+
+    def set_lte_control_region_size(self, cell, num_symbols):
+        self.assert_cell_off('LTE', cell)
+        self.send_cmd('BSE:CONFig:LTE:{}:PHY:PCFich:CFI {}'.format(
+            Keysight5GTestApp._format_cells(cell), num_symbols))
+
+    def set_lte_ul_mac_padding(self, mac_padding):
+        self.assert_cell_off('LTE', 'CELL1')
+        padding_str = 'TRUE' if mac_padding else 'FALSE'
+        self.send_cmd(
+            'BSE:CONFig:LTE:SCHeduling:SETParameter "CELLALL", "UL:MAC:PADDING", "{}"'
+            .format(padding_str))
+
+    def set_nr_ul_dft_precoding(self, cell, precoding):
+        """Function to configure DFT-precoding on uplink.
+
+        Args:
+            cell: cell number to address. MCS will apply to all cells
+            precoding: 0/1 to disable/enable precoding
+        """
+        self.assert_cell_off('NR5G', cell)
+        precoding_str = "ENABled" if precoding else "DISabled"
+        self.send_cmd(
+            'BSE:CONFig:NR5G:{}:SCHeduling:QCONFig:UL:TRANsform:PRECoding {}'.
+            format(Keysight5GTestApp._format_cells(cell), precoding_str))
+        precoding_str = "True" if precoding else "False"
+        self.send_cmd(
+            'BSE:CONFig:NR5G:SCHeduling:SETParameter "CELLALL:BWPALL", "UL:TPEnabled", "{}"'
+            .format(precoding_str))
+
+    def configure_ul_clpc(self, channel, mode, target):
+        """Function to configure UL power control on all cells/carriers
+
+        Args:
+            channel: physical channel must be PUSCh or PUCCh
+            mode: mode supported by test app (all up/down bits, target, etc)
+            target: target power if mode is set to target
+        """
+        self.send_cmd('BSE:CONFig:NR5G:UL:{}:CLPControl:MODE:ALL {}'.format(
+            channel, mode))
+        if "tar" in mode.lower():
+            self.send_cmd(
+                'BSE:CONFig:NR5G:UL:{}:CLPControl:TARGet:POWer:ALL {}'.format(
+                    channel, target))
+
+    def apply_lte_carrier_agg(self, cells):
+        """Function to start LTE carrier aggregation on already configured cells"""
+        if self.wait_for_cell_status('LTE', 'CELL1', 'CONN', 60):
+            self.send_cmd(
+                'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:SCC {}'.format(
+                    Keysight5GTestApp._format_cells(cells)))
+            self.send_cmd(
+                'BSE:CONFig:LTE:CELL1:CAGGregation:ACTivate:SCC {}'.format(
+                    Keysight5GTestApp._format_cells(cells)))
+
+    def apply_carrier_agg(self):
+        """Function to start carrier aggregation on already configured cells"""
+        if self.wait_for_cell_status('LTE', 'CELL1', 'CONN', 60):
+            self.send_cmd(
+                'BSE:CONFig:LTE:CELL1:CAGGregation:AGGRegate:NRCC:APPly')
+        else:
+            raise RuntimeError('LTE must be connected to start aggregation.')
+
+    def get_ip_throughput(self, cell_type):
+        """Function to query IP layer throughput on LTE or NR
+
+        Args:
+            cell_type: LTE or NR5G
+        Returns:
+            dict containing DL and UL IP-layer throughput
+        """
+        #Tester reply format
+        #{ report-count, total-bytes, current transfer-rate, average transfer-rate, peak transfer-rate }
+        dl_tput = self.send_cmd(
+            'BSE:MEASure:{}:BTHRoughput:DL:THRoughput:IP?'.format(cell_type),
+            1)
+        ul_tput = self.send_cmd(
+            'BSE:MEASure:{}:BTHRoughput:UL:THRoughput:IP?'.format(cell_type),
+            1)
+        return {'dl_tput': dl_tput, 'ul_tput': ul_tput}
+
+    def _get_throughput(self, cell_type, link, cell):
+        """Helper function to get PHY layer throughput on single cell"""
+        if cell_type == 'LTE':
+            tput_response = self.send_cmd(
+                'BSE:MEASure:LTE:{}:BTHRoughput:{}:THRoughput:OTA:{}?'.format(
+                    Keysight5GTestApp._format_cells(cell), link,
+                    Keysight5GTestApp._format_cells(cell)), 1)
+        elif cell_type == 'NR5G':
+            # Tester reply format
+            #progress-count, ack-count, ack-ratio, nack-count, nack-ratio,  statdtx-count,  statdtx-ratio,  pdschBlerCount,  pdschBlerRatio,  pdschTputRatio.
+            tput_response = self.send_cmd(
+                'BSE:MEASure:NR5G:BTHRoughput:{}:THRoughput:OTA:{}?'.format(
+                    link, Keysight5GTestApp._format_cells(cell)), 1)
+        tput_result = {
+            'frame_count': tput_response[0] / 1e6,
+            'current_tput': tput_response[1] / 1e6,
+            'min_tput': tput_response[2] / 1e6,
+            'max_tput': tput_response[3] / 1e6,
+            'average_tput': tput_response[4] / 1e6,
+            'theoretical_tput': tput_response[5] / 1e6,
+        }
+        return tput_result
+
+    def get_throughput(self, cell_type, cells):
+        """Function to get PHY layer throughput on on or more cells
+
+        This function returns the throughput data on the requested cells
+        during the last BLER test run, i.e., throughpt data must be fetch at
+        the end/after a BLE test run on the Keysight Test App.
+
+        Args:
+            cell_type: LTE or NR5G
+            cells: list of cells to query for throughput data
+        Returns:
+            tput_result: dict containing all throughput statistics in Mbps
+        """
+        if not isinstance(cells, list):
+            cells = [cells]
+        tput_result = collections.OrderedDict()
+        for cell in cells:
+            tput_result[cell] = {
+                'DL': self._get_throughput(cell_type, 'DL', cell),
+                'UL': self._get_throughput(cell_type, 'UL', cell)
+            }
+            frame_count = tput_result[cell]['DL']['frame_count']
+        agg_tput = {
+            'DL': {
+                'frame_count': frame_count,
+                'current_tput': 0,
+                'min_tput': 0,
+                'max_tput': 0,
+                'average_tput': 0,
+                'theoretical_tput': 0
+            },
+            'UL': {
+                'frame_count': frame_count,
+                'current_tput': 0,
+                'min_tput': 0,
+                'max_tput': 0,
+                'average_tput': 0,
+                'theoretical_tput': 0
+            }
+        }
+        for cell, cell_tput in tput_result.items():
+            for link, link_tput in cell_tput.items():
+                for key, value in link_tput.items():
+                    if 'tput' in key:
+                        agg_tput[link][key] = agg_tput[link][key] + value
+        tput_result['total'] = agg_tput
+        return tput_result
+
+    def _clear_bler_measurement(self, cell_type):
+        """Helper function to clear BLER results."""
+        if cell_type == 'LTE':
+            self.send_cmd('BSE:MEASure:LTE:CELL1:BTHRoughput:CLEar')
+        elif cell_type == 'NR5G':
+            self.send_cmd('BSE:MEASure:NR5G:BTHRoughput:CLEar')
+
+    def _configure_bler_measurement(self, cell_type, continuous, length):
+        """Helper function to configure BLER results."""
+        if continuous:
+            if cell_type == 'LTE':
+                self.send_cmd('BSE:MEASure:LTE:CELL1:BTHRoughput:CONTinuous 1')
+            elif cell_type == 'NR5G':
+                self.send_cmd('BSE:MEASure:NR5G:BTHRoughput:CONTinuous 1')
+        elif length > 1:
+            if cell_type == 'LTE':
+                self.send_cmd(
+                    'BSE:MEASure:LTE:CELL1:BTHRoughput:LENGth {}'.format(
+                        length))
+                self.send_cmd('BSE:MEASure:LTE:CELL1:BTHRoughput:CONTinuous 0')
+            elif cell_type == 'NR5G':
+                self.send_cmd(
+                    'BSE:MEASure:NR5G:BTHRoughput:LENGth {}'.format(length))
+                self.send_cmd('BSE:MEASure:NR5G:BTHRoughput:CONTinuous 0')
+
+    def _set_bler_measurement_state(self, cell_type, state):
+        """Helper function to start or stop BLER measurement."""
+        if cell_type == 'LTE':
+            self.send_cmd(
+                'BSE:MEASure:LTE:CELL1:BTHRoughput:STATe {}'.format(state))
+        elif cell_type == 'NR5G':
+            self.send_cmd(
+                'BSE:MEASure:NR5G:BTHRoughput:STATe {}'.format(state))
+
+    def start_bler_measurement(self, cell_type, cells, length):
+        """Function to kick off a BLER measurement
+
+        Args:
+            cell_type: LTE or NR5G
+            length: integer length of BLER measurements in subframes
+        """
+        self._clear_bler_measurement(cell_type)
+        self._set_bler_measurement_state(cell_type, 0)
+        self._configure_bler_measurement(cell_type, 0, length)
+        self._set_bler_measurement_state(cell_type, 1)
+        time.sleep(0.1)
+        bler_check = self.get_bler_result(cell_type, cells, length, 0)
+        if bler_check['total']['DL']['frame_count'] == 0:
+            self.log.warning('BLER measurement did not start. Retrying')
+            self.start_bler_measurement(cell_type, cells, length)
+
+    def _get_bler(self, cell_type, link, cell):
+        """Helper function to get single-cell BLER measurement results."""
+        if cell_type == 'LTE':
+            bler_response = self.send_cmd(
+                'BSE:MEASure:LTE:CELL1:BTHRoughput:{}:BLER:CELL1?'.format(
+                    link), 1)
+        elif cell_type == 'NR5G':
+            bler_response = self.send_cmd(
+                'BSE:MEASure:NR5G:BTHRoughput:{}:BLER:{}?'.format(
+                    link, Keysight5GTestApp._format_cells(cell)), 1)
+        bler_result = {
+            'frame_count': bler_response[0],
+            'ack_count': bler_response[1],
+            'ack_ratio': bler_response[2],
+            'nack_count': bler_response[3],
+            'nack_ratio': bler_response[4]
+        }
+        return bler_result
+
+    def get_bler_result(self,
+                        cell_type,
+                        cells,
+                        length,
+                        wait_for_length=1,
+                        polling_interval=SHORT_SLEEP):
+        """Function to get BLER results.
+
+        This function gets the BLER measurements results on one or more
+        requested cells. The function can either return BLER statistics
+        immediately or wait until a certain number of subframes have been
+        counted (e.g. if the BLER measurement is done)
+
+        Args:
+            cell_type: LTE or NR5G
+            cells: list of cells for which to get BLER
+            length: number of subframes to wait for (typically set to the
+                    configured length of the BLER measurements)
+            wait_for_length: boolean to block/wait till length subframes have
+            been counted.
+        Returns:
+            bler_result: dict containing per-cell and aggregate BLER results
+        """
+
+        if not isinstance(cells, list):
+            cells = [cells]
+        while wait_for_length:
+            dl_bler = self._get_bler(cell_type, 'DL', cells[0])
+            if dl_bler['frame_count'] < length:
+                time.sleep(polling_interval)
+            else:
+                break
+
+        bler_result = collections.OrderedDict()
+        for cell in cells:
+            bler_result[cell] = {
+                'DL': self._get_bler(cell_type, 'DL', cell),
+                'UL': self._get_bler(cell_type, 'UL', cell)
+            }
+        agg_bler = {
+            'DL': {
+                'frame_count': length,
+                'ack_count': 0,
+                'ack_ratio': 0,
+                'nack_count': 0,
+                'nack_ratio': 0
+            },
+            'UL': {
+                'frame_count': length,
+                'ack_count': 0,
+                'ack_ratio': 0,
+                'nack_count': 0,
+                'nack_ratio': 0
+            }
+        }
+        for cell, cell_bler in bler_result.items():
+            for link, link_bler in cell_bler.items():
+                for key, value in link_bler.items():
+                    if 'ack_count' in key:
+                        agg_bler[link][key] = agg_bler[link][key] + value
+        dl_ack_nack = agg_bler['DL']['ack_count'] + agg_bler['DL']['nack_count']
+        ul_ack_nack = agg_bler['UL']['ack_count'] + agg_bler['UL']['nack_count']
+        try:
+            agg_bler['DL'][
+                'ack_ratio'] = agg_bler['DL']['ack_count'] / dl_ack_nack
+            agg_bler['DL'][
+                'nack_ratio'] = agg_bler['DL']['nack_count'] / dl_ack_nack
+            agg_bler['UL'][
+                'ack_ratio'] = agg_bler['UL']['ack_count'] / ul_ack_nack
+            agg_bler['UL'][
+                'nack_ratio'] = agg_bler['UL']['nack_count'] / ul_ack_nack
+        except:
+            self.log.debug(bler_result)
+            agg_bler['DL']['ack_ratio'] = 0
+            agg_bler['DL']['nack_ratio'] = 1
+            agg_bler['UL']['ack_ratio'] = 0
+            agg_bler['UL']['nack_ratio'] = 1
+        bler_result['total'] = agg_bler
+        return bler_result
+
+    def measure_bler(self, cell_type, cells, length):
+        """Function to start and wait for BLER results.
+
+        This function starts a BLER test on a number of cells and waits for the
+        test to complete before returning the BLER measurements.
+
+        Args:
+            cell_type: LTE or NR5G
+            cells: list of cells for which to get BLER
+            length: number of subframes to wait for (typically set to the
+                    configured length of the BLER measurements)
+        Returns:
+            bler_result: dict containing per-cell and aggregate BLER results
+        """
+        self.start_bler_measurement(cell_type, cells, length)
+        time.sleep(length * SUBFRAME_DURATION)
+        bler_result = self.get_bler_result(cell_type, cells, length, 1)
+        return bler_result
+
+    def start_nr_rsrp_measurement(self, cells, length):
+        """Function to start 5G NR RSRP measurement.
+
+        Args:
+            cells: list of NR cells to get RSRP on
+            length: length of RSRP measurement in milliseconds
+        Returns:
+            rsrp_result: dict containing per-cell and aggregate BLER results
+        """
+        for cell in cells:
+            self.send_cmd('BSE:MEASure:NR5G:{}:L1:RSRPower:STOP'.format(
+                Keysight5GTestApp._format_cells(cell)))
+        for cell in cells:
+            self.send_cmd('BSE:MEASure:NR5G:{}:L1:RSRPower:LENGth {}'.format(
+                Keysight5GTestApp._format_cells(cell), length))
+        for cell in cells:
+            self.send_cmd('BSE:MEASure:NR5G:{}:L1:RSRPower:STARt'.format(
+                Keysight5GTestApp._format_cells(cell)))
+
+    def get_nr_rsrp_measurement_state(self, cells):
+        for cell in cells:
+            self.log.info(
+                self.send_cmd(
+                    'BSE:MEASure:NR5G:{}:L1:RSRPower:STATe?'.format(
+                        Keysight5GTestApp._format_cells(cell)), 1))
+
+    def get_nr_rsrp_measurement_results(self, cells):
+        for cell in cells:
+            self.log.info(
+                self.send_cmd(
+                    'BSE:MEASure:NR5G:{}:L1:RSRPower:REPorts:JSON?'.format(
+                        Keysight5GTestApp._format_cells(cell)), 1))
diff --git a/acts_tests/acts_contrib/test_utils/cellular/keysight_catr_chamber.py b/acts_tests/acts_contrib/test_utils/cellular/keysight_catr_chamber.py
new file mode 100644
index 0000000..bd7540d
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/keysight_catr_chamber.py
@@ -0,0 +1,181 @@
+# Copyright 2020 Google Inc. All Rights Reserved.
+# Author: oelayach@google.com
+
+import pyvisa
+import time
+from acts import logger
+from ota_chamber import Chamber
+
+
+class Chamber(Chamber):
+    """Base class implementation for signal generators.
+
+    Base class provides functions whose implementation is shared by all
+    chambers.
+    """
+    CHAMBER_SLEEP = 10
+
+    def __init__(self, config):
+        self.config = config
+        self.log = logger.create_tagged_trace_logger("{}{}".format(
+            self.config['brand'], self.config['model']))
+        self.chamber_resource = pyvisa.ResourceManager()
+        self.chamber_inst = self.chamber_resource.open_resource(
+            '{}::{}::{}::INSTR'.format(self.config['network_id'],
+                                       self.config['ip_address'],
+                                       self.config['hislip_interface']))
+        self.chamber_inst.timeout = 200000
+        self.chamber_inst.write_termination = '\n'
+        self.chamber_inst.read_termination = '\n'
+
+        self.id_check(self.config)
+        self.current_azim = 0
+        self.current_roll = 0
+        if self.config.get('reset_home', True):
+            self.find_chamber_home()
+            self.move_theta_phi_abs(self.config['chamber_home']['theta'],
+                                    self.config['chamber_home']['phi'])
+            self.set_new_home_position()
+        else:
+            self.config['chamber_home'] = {'phi': 0, 'theta': 0}
+            self.log.warning(
+                'Reset home set to false. Assumed [0,0]. Chamber angles may not be as expected.'
+            )
+
+    def id_check(self, config):
+        """ Checks Chamber ID."""
+        self.log.info("ID Check Successful.")
+        self.log.info(self.chamber_inst.query("*IDN?"))
+
+    def reset(self):
+        self.reset_phi_theta()
+
+    def disconnect(self):
+        if self.config.get('reset_home', True):
+            self.reset_phi_theta()
+        self.chamber_inst.close()
+        self.chamber_resource.close()
+
+    def find_chamber_home(self):
+        self.chamber_inst.write(f"POS:BOR:INIT")
+        self.set_new_home_position()
+        self.wait_for_move_end()
+
+    def set_new_home_position(self):
+        self.chamber_inst.write(f"POS:ZERO:RES")
+
+    def get_phi(self):
+        return self.current_azim
+
+    def get_theta(self):
+        return self.current_roll
+
+    def get_pattern_sweep_limits(self):
+        return {
+            "pattern_phi_start": -self.config['chamber_home']['phi'],
+            "pattern_phi_stop": 165 - self.config['chamber_home']['phi'],
+            "pattern_theta_start": -self.config['chamber_home']['theta'],
+            "pattern_theta_stop": 360 - self.config['chamber_home']['theta'],
+        }
+
+    def move_phi_abs(self, phi):
+        self.log.info("Moving to Phi={}".format(phi))
+        self.move_to_azim_roll(phi, self.current_roll)
+
+    def move_theta_abs(self, theta):
+        self.log.info("Moving to Theta={}".format(theta))
+        self.move_to_azim_roll(self.current_azim, theta)
+
+    def move_theta_phi_abs(self, theta, phi):
+        self.log.info("Moving chamber to [{}, {}]".format(theta, phi))
+        self.move_to_azim_roll(phi, theta)
+
+    def move_phi_rel(self, phi):
+        self.log.info("Moving Phi by {} degrees".format(phi))
+        self.move_to_azim_roll(self.current_azim + phi, self.current_roll)
+
+    def move_theta_rel(self, theta):
+        self.log.info("Moving Theta by {} degrees".format(theta))
+        self.move_to_azim_roll(self.current_azim, self.current_roll + theta)
+
+    def move_feed_roll(self, roll):
+        self.log.info("Moving feed roll to {} degrees".format(roll))
+        self.chamber_inst.write(f"POS:MOVE:ROLL:FEED {roll}")
+        self.chamber_inst.write("POS:MOVE:INIT")
+        self.wait_for_move_end()
+        self.current_feed_roll = self.chamber_inst.query("POS:MOVE:ROLL:FEED?")
+
+    def reset_phi(self):
+        self.log.info("Resetting Phi.")
+        self.move_to_azim_roll(0, self.current_roll)
+        self.phi = 0
+
+    def reset_theta(self):
+        self.log.info("Resetting Theta.")
+        self.move_to_azim_roll(self.current_azim, 0)
+        self.theta = 0
+
+    def reset_phi_theta(self):
+        """ Resets signal generator."""
+        self.log.info("Resetting to home.")
+        self.chamber_inst.write(f"POS:ZERO:GOTO")
+        self.wait_for_move_end()
+
+    # Keysight-provided functions
+    def wait_for_move_end(self):
+        moving_bitmask = 4
+        while True:
+            stat = int(self.chamber_inst.query("STAT:OPER:COND?"))
+            if (stat & moving_bitmask) == 0:
+                return
+            time.sleep(0.25)
+
+    def wait_for_sweep_end(self):
+        sweeping_bitmask = 16
+        while True:
+            stat = int(self.chamber_inst.query("STAT:OPER:COND?"))
+            if (stat & sweeping_bitmask) == 0:
+                return
+            time.sleep(0.25)
+
+    def move_to_azim_roll(self, azim, roll):
+        self.chamber_inst.write(f"POS:MOVE:AZIM {azim};ROLL {roll}")
+        self.chamber_inst.write("POS:MOVE:INIT")
+        self.wait_for_move_end()
+        curr_motor = self.chamber_inst.query("POS:CURR:MOT?")
+        curr_azim, curr_roll = map(float, (curr_motor.split(',')))
+        self.current_azim = curr_azim
+        self.current_roll = curr_roll
+        return curr_azim, curr_roll
+
+    def sweep_setup(self, azim_sss: tuple, roll_sss: tuple, sweep_type: str):
+        self.chamber_inst.write(
+            f"POS:SWE:AZIM:STAR {azim_sss[0]};STOP {azim_sss[1]};STEP {azim_sss[2]}"
+        )
+        self.chamber_inst.write(
+            f"POS:SWE:ROLL:STAR {roll_sss[0]};STOP {roll_sss[1]};STEP {roll_sss[2]}"
+        )
+        self.chamber_inst.write(f"POS:SWE:TYPE {sweep_type}")
+        self.chamber_inst.write("POS:SWE:CONT 1")
+
+    def sweep_init(self):
+
+        def query_float_list(inst, scpi):
+            resp = inst.query(scpi)
+            return list(map(float, resp.split(',')))
+
+        self.chamber_inst.write("POS:SWE:INIT")
+        self.wait_for_sweep_end()
+        azims = query_float_list(self.chamber_inst, "FETC:AZIM?")
+        rolls = query_float_list(self.chamber_inst, "FETC:ROLL?")
+        phis = query_float_list(self.chamber_inst, "FETC:DUT:PHI?")
+        thetas = query_float_list(self.chamber_inst, "FETC:DUT:THET?")
+        return zip(azims, rolls, phis, thetas)
+
+    def configure_positioner(self, pos_name, pos_visa_addr):
+        select = "True"
+        simulate = "False"
+        options = ""
+        data = f"'{pos_name}~{select}~{simulate}~{pos_visa_addr}~{options}'"
+        self.chamber_inst.write(f"EQU:CONF {data}")
+        self.chamber_inst.write("EQU:UPD")
diff --git a/acts_tests/acts_contrib/test_utils/cellular/mock_chamber.py b/acts_tests/acts_contrib/test_utils/cellular/mock_chamber.py
new file mode 100644
index 0000000..36d74c5
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/mock_chamber.py
@@ -0,0 +1,65 @@
+# Copyright 2020 Google Inc. All Rights Reserved.
+# Author: oelayach@google.com
+from acts import logger
+from ota_chamber import Chamber
+
+
+class MockChamber(Chamber):
+    """Base class implementation for signal generators.
+
+    Base class provides functions whose implementation is shared by all
+    chambers.
+    """
+
+    def __init__(self, config):
+        self.config = config
+        self.log = logger.create_tagged_trace_logger("{}{}".format(
+            self.config['brand'], self.config['model']))
+        self.id_check(self.config)
+        self.reset()
+
+    def id_check(self, config):
+        """ Check Chamber."""
+        self.log.info("ID Check Successful.")
+
+    def reset(self):
+        """ Resets Chamber."""
+        self.log.info("Resetting instrument.")
+
+    def disconnect(self):
+        """ Disconnects Chamber."""
+        self.log.info("Disconnecting instrument.")
+
+    def get_phi(self):
+        return self.phi
+
+    def get_theta(self):
+        return self.phi
+
+    def move_phi_abs(self, phi):
+        self.log.info("Moving to Phi={}".format(phi))
+        self.phi = phi
+
+    def move_theta_abs(self, theta):
+        self.log.info("Moving to Theta={}".format(theta))
+        self.theta = theta
+
+    def move_phi_rel(self, phi):
+        self.log.info("Moving Phi by {} degrees".format(phi))
+        self.phi = self.phi + phi
+
+    def move_theta_rel(self, theta):
+        self.log.info("Moving Theta by {} degrees".format(theta))
+        self.theta = self.theta + theta
+
+    def reset_phi(self):
+        self.log.info("Resetting Phi.")
+        self.phi = 0
+
+    def reset_theta(self):
+        self.log.info("Resetting Theta.")
+        self.theta = 0
+
+    def reset_phi_theta(self):
+        """ Resets signal generator."""
+        self.log.info("Resetting to home.")
diff --git a/acts_tests/acts_contrib/test_utils/cellular/ota_chamber.py b/acts_tests/acts_contrib/test_utils/cellular/ota_chamber.py
new file mode 100644
index 0000000..923f22b
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/ota_chamber.py
@@ -0,0 +1,94 @@
+# Copyright 2020 Google Inc. All Rights Reserved.
+# Author: oelayach@google.com
+
+import importlib
+from acts import logger
+
+ACTS_CONTROLLER_CONFIG_NAME = 'Chamber'
+
+
+def create(configs):
+    """Factory method for Signal Generators.
+
+    Args:
+        configs: list of dicts with signal generator settings
+    """
+    SUPPORTED_MODELS = {
+        ("Keysight", "CATR"): "lassen.controllers.chamber.KeysightCATRChamber",
+        ("Mock", "Chamber"): "lassen.controllers.chamber.MockChamber"
+    }
+    objs = []
+    for config in configs:
+        if (config["brand"], config["model"]) not in SUPPORTED_MODELS:
+            raise ValueError("Not a valid chamber model.")
+        module = importlib.import_module(SUPPORTED_MODELS[(config["brand"],
+                                                           config["model"])])
+        chamber = module.Chamber(config)
+        objs.append(chamber)
+    return objs
+
+
+def destroy(devices):
+    for device in devices:
+        device.reset()
+
+
+class Chamber():
+    """Base class implementation for signal generators.
+
+    Base class provides functions whose implementation is shared by all
+    chambers.
+    """
+
+    def __init__(self, config):
+        self.config = config
+        self.log = logger.create_tagged_trace_logger("{}{}".format(
+            self.config['brand'], self.config['model']))
+        self.id_check(self.config)
+        self.reset()
+
+    def id_check(self, config):
+        """ Resets signal generator."""
+        self.log.info("ID Check Successful.")
+
+    def reset(self):
+        """ Resets signal generator."""
+        self.log.info("Resetting instrument.")
+
+    def disconnect(self):
+        """ Disconnects Chamber."""
+        self.log.info("Disconnecting instrument.")
+
+    def get_phi(self):
+        return self.phi
+
+    def get_theta(self):
+        return self.phi
+
+    def move_phi_abs(self, phi):
+        self.log.info("Moving to Phi={}".format(phi))
+        self.phi = phi
+
+    def move_theta_abs(self, theta):
+        self.log.info("Moving to Theta={}".format(theta))
+        self.theta = theta
+
+    def move_phi_rel(self, phi):
+        self.log.info("Moving Phi by {} degrees".format(phi))
+        self.phi = self.phi + phi
+
+    def move_theta_rel(self, theta):
+        self.log.info("Moving Theta by {} degrees".format(theta))
+        self.theta = self.theta + theta
+
+    def reset_phi(self):
+        self.log.info("Resetting Phi.")
+        self.phi = 0
+
+    def reset_theta(self):
+        self.log.info("Resetting Theta.")
+        self.theta = 0
+
+    def reset_phi_theta(self):
+        """ Resets signal generator."""
+        self.log.info("Resetting to home.")
diff --git a/acts_tests/acts_contrib/test_utils/cellular/performance/CellularThroughputBaseTest.py b/acts_tests/acts_contrib/test_utils/cellular/performance/CellularThroughputBaseTest.py
new file mode 100644
index 0000000..57e66ff
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/performance/CellularThroughputBaseTest.py
@@ -0,0 +1,481 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import csv
+import itertools
+import json
+import re
+
+import numpy
+import os
+import time
+from acts import asserts
+from acts import context
+from acts import base_test
+from acts import utils
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts.controllers.utils_lib import ssh
+from acts.controllers import iperf_server as ipf
+from acts_contrib.test_utils.cellular.keysight_5g_testapp import Keysight5GTestApp
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+from functools import partial
+
+LONG_SLEEP = 10
+MEDIUM_SLEEP = 2
+IPERF_TIMEOUT = 10
+SHORT_SLEEP = 1
+SUBFRAME_LENGTH = 0.001
+STOP_COUNTER_LIMIT = 3
+
+
+class CellularThroughputBaseTest(base_test.BaseTestClass):
+    """Base class for Cellular Throughput Testing
+
+    This base class enables cellular throughput tests on a lab/callbox setup
+    with PHY layer or iperf traffic.
+    """
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.testclass_params = None
+
+    def setup_class(self):
+        """Initializes common test hardware and parameters.
+
+        This function initializes hardwares and compiles parameters that are
+        common to all tests in this class.
+        """
+        # Setup controllers
+        self.dut = self.android_devices[-1]
+        self.keysight_test_app = Keysight5GTestApp(
+            self.user_params['Keysight5GTestApp'])
+        self.iperf_server = self.iperf_servers[0]
+        self.iperf_client = self.iperf_clients[0]
+        self.remote_server = ssh.connection.SshConnection(
+            ssh.settings.from_config(
+                self.user_params['RemoteServer']['ssh_config']))
+
+        # Configure Tester
+        if self.testclass_params.get('reload_scpi', 1):
+            self.keysight_test_app.import_scpi_file(
+                self.testclass_params['scpi_file'])
+
+        # Declare testclass variables
+        self.testclass_results = collections.OrderedDict()
+
+        # Configure test retries
+        self.user_params['retry_tests'] = [self.__class__.__name__]
+
+        # Turn Airplane mode on
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+
+    def teardown_class(self):
+        self.log.info('Turning airplane mode on')
+        try:
+            asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                                'Can not turn on airplane mode.')
+        except:
+            self.log.warning('Cannot perform teardown operations on DUT.')
+        try:
+            self.keysight_test_app.set_cell_state('LTE', 1, 0)
+            self.keysight_test_app.destroy()
+        except:
+            self.log.warning('Cannot perform teardown operations on tester.')
+        self.process_testclass_results()
+
+    def setup_test(self):
+        self.retry_flag = False
+        if self.testclass_params['enable_pixel_logs']:
+            cputils.start_pixel_logger(self.dut)
+
+    def teardown_test(self):
+        self.retry_flag = False
+        self.log.info('Turing airplane mode on')
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        if self.keysight_test_app.get_cell_state('LTE', 'CELL1'):
+            self.log.info('Turning LTE off.')
+            self.keysight_test_app.set_cell_state('LTE', 'CELL1', 0)
+        log_path = os.path.join(
+            context.get_current_context().get_full_output_path(), 'pixel_logs')
+        os.makedirs(self.log_path, exist_ok=True)
+        if self.testclass_params['enable_pixel_logs']:
+            cputils.stop_pixel_logger(self.dut, log_path)
+        self.process_testcase_results()
+        self.pass_fail_check()
+
+    def on_retry(self):
+        """Function to control test logic on retried tests.
+
+        This function is automatically executed on tests that are being
+        retried. In this case the function resets wifi, toggles it off and on
+        and sets a retry_flag to enable further tweaking the test logic on
+        second attempts.
+        """
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        if self.keysight_test_app.get_cell_state('LTE', 'CELL1'):
+            self.log.info('Turning LTE off.')
+            self.keysight_test_app.set_cell_state('LTE', 'CELL1', 0)
+        self.retry_flag = True
+
+    def pass_fail_check(self):
+        pass
+
+    def process_testcase_results(self):
+        pass
+
+    def process_testclass_results(self):
+        pass
+
+    def get_per_cell_power_sweeps(self, testcase_params):
+        raise NotImplementedError(
+            'get_per_cell_power_sweeps must be implemented.')
+
+    def compile_test_params(self, testcase_params):
+        """Function that completes all test params based on the test name.
+
+        Args:
+            testcase_params: dict containing test-specific parameters
+        """
+        # Measurement Duration
+        testcase_params['bler_measurement_length'] = int(
+            self.testclass_params['traffic_duration'] / SUBFRAME_LENGTH)
+        # Cell power sweep
+        # TODO: Make this a function to support single power and sweep modes for each cell
+        testcase_params['cell_power_sweep'] = self.get_per_cell_power_sweeps(
+            testcase_params)
+        # Traffic & iperf params
+        if self.testclass_params['traffic_type'] == 'PHY':
+            return testcase_params
+        if self.testclass_params['traffic_type'] == 'TCP':
+            testcase_params['iperf_socket_size'] = self.testclass_params.get(
+                'tcp_socket_size', None)
+            testcase_params['iperf_processes'] = self.testclass_params.get(
+                'tcp_processes', 1)
+        elif self.testclass_params['traffic_type'] == 'UDP':
+            testcase_params['iperf_socket_size'] = self.testclass_params.get(
+                'udp_socket_size', None)
+            testcase_params['iperf_processes'] = self.testclass_params.get(
+                'udp_processes', 1)
+        adb_iperf_server = isinstance(self.iperf_server,
+                                      ipf.IPerfServerOverAdb)
+        if testcase_params['traffic_direction'] == 'DL':
+            reverse_direction = 0 if adb_iperf_server else 1
+            testcase_params[
+                'use_client_output'] = False if adb_iperf_server else True
+        elif testcase_params['traffic_direction'] == 'UL':
+            reverse_direction = 1 if adb_iperf_server else 0
+            testcase_params[
+                'use_client_output'] = True if adb_iperf_server else False
+        testcase_params['iperf_args'] = wputils.get_iperf_arg_string(
+            duration=self.testclass_params['traffic_duration'],
+            reverse_direction=reverse_direction,
+            traffic_type=self.testclass_params['traffic_type'],
+            socket_size=testcase_params['iperf_socket_size'],
+            num_processes=testcase_params['iperf_processes'],
+            udp_throughput=self.testclass_params['UDP_rates'].get(
+                testcase_params['num_dl_cells'],
+                self.testclass_params['UDP_rates']["default"]),
+            udp_length=1440)
+        return testcase_params
+
+    def run_iperf_traffic(self, testcase_params):
+        self.iperf_server.start(tag=0)
+        dut_ip = self.dut.droid.connectivityGetIPv4Addresses('rmnet0')[0]
+        if 'iperf_server_address' in self.testclass_params:
+            iperf_server_address = self.testclass_params[
+                'iperf_server_address']
+        elif isinstance(self.iperf_server, ipf.IPerfServerOverAdb):
+            iperf_server_address = dut_ip
+        else:
+            iperf_server_address = wputils.get_server_address(
+                self.remote_server, dut_ip, '255.255.255.0')
+        client_output_path = self.iperf_client.start(
+            iperf_server_address, testcase_params['iperf_args'], 0,
+            self.testclass_params['traffic_duration'] + IPERF_TIMEOUT)
+        server_output_path = self.iperf_server.stop()
+        # Parse and log result
+        if testcase_params['use_client_output']:
+            iperf_file = client_output_path
+        else:
+            iperf_file = server_output_path
+        try:
+            iperf_result = ipf.IPerfResult(iperf_file)
+            current_throughput = numpy.mean(iperf_result.instantaneous_rates[
+                self.testclass_params['iperf_ignored_interval']:-1]) * 8 * (
+                    1.024**2)
+        except:
+            self.log.warning(
+                'ValueError: Cannot get iperf result. Setting to 0')
+            current_throughput = 0
+        return current_throughput
+
+    def run_single_throughput_measurement(self, testcase_params):
+        result = collections.OrderedDict()
+        self.log.info('Starting BLER & throughput tests.')
+        if testcase_params['endc_combo_config']['nr_cell_count']:
+            self.keysight_test_app.start_bler_measurement(
+                'NR5G', testcase_params['endc_combo_config']['nr_dl_carriers'],
+                testcase_params['bler_measurement_length'])
+        if testcase_params['endc_combo_config']['lte_cell_count']:
+            self.keysight_test_app.start_bler_measurement(
+                'LTE', testcase_params['endc_combo_config']['lte_carriers'][0],
+                testcase_params['bler_measurement_length'])
+
+        if self.testclass_params['traffic_type'] != 'PHY':
+            result['iperf_throughput'] = self.run_iperf_traffic(
+                testcase_params)
+
+        if testcase_params['endc_combo_config']['nr_cell_count']:
+            result['nr_bler_result'] = self.keysight_test_app.get_bler_result(
+                'NR5G', testcase_params['endc_combo_config']['nr_dl_carriers'],
+                testcase_params['bler_measurement_length'])
+            result['nr_tput_result'] = self.keysight_test_app.get_throughput(
+                'NR5G', testcase_params['endc_combo_config']['nr_dl_carriers'])
+        if testcase_params['endc_combo_config']['lte_cell_count']:
+            result['lte_bler_result'] = self.keysight_test_app.get_bler_result(
+                'LTE', testcase_params['endc_combo_config']['lte_carriers'],
+                testcase_params['bler_measurement_length'])
+            result['lte_tput_result'] = self.keysight_test_app.get_throughput(
+                'LTE', testcase_params['endc_combo_config']['lte_carriers'])
+        return result
+
+    def print_throughput_result(self, result):
+        # Print Test Summary
+        if 'nr_tput_result' in result:
+            self.log.info(
+                "----NR5G STATS-------NR5G STATS-------NR5G STATS---")
+            self.log.info(
+                "DL PHY Tput (Mbps):\tMin: {:.2f},\tAvg: {:.2f},\tMax: {:.2f},\tTheoretical: {:.2f}"
+                .format(
+                    result['nr_tput_result']['total']['DL']['min_tput'],
+                    result['nr_tput_result']['total']['DL']['average_tput'],
+                    result['nr_tput_result']['total']['DL']['max_tput'],
+                    result['nr_tput_result']['total']['DL']
+                    ['theoretical_tput']))
+            self.log.info(
+                "UL PHY Tput (Mbps):\tMin: {:.2f},\tAvg: {:.2f},\tMax: {:.2f},\tTheoretical: {:.2f}"
+                .format(
+                    result['nr_tput_result']['total']['UL']['min_tput'],
+                    result['nr_tput_result']['total']['UL']['average_tput'],
+                    result['nr_tput_result']['total']['UL']['max_tput'],
+                    result['nr_tput_result']['total']['UL']
+                    ['theoretical_tput']))
+            self.log.info("DL BLER: {:.2f}%\tUL BLER: {:.2f}%".format(
+                result['nr_bler_result']['total']['DL']['nack_ratio'] * 100,
+                result['nr_bler_result']['total']['UL']['nack_ratio'] * 100))
+        if 'lte_tput_result' in result:
+            self.log.info("----LTE STATS-------LTE STATS-------LTE STATS---")
+            self.log.info(
+                "DL PHY Tput (Mbps):\tMin: {:.2f},\tAvg: {:.2f},\tMax: {:.2f},\tTheoretical: {:.2f}"
+                .format(
+                    result['lte_tput_result']['total']['DL']['min_tput'],
+                    result['lte_tput_result']['total']['DL']['average_tput'],
+                    result['lte_tput_result']['total']['DL']['max_tput'],
+                    result['lte_tput_result']['total']['DL']
+                    ['theoretical_tput']))
+            if self.testclass_params['lte_ul_mac_padding']:
+                self.log.info(
+                    "UL PHY Tput (Mbps):\tMin: {:.2f},\tAvg: {:.2f},\tMax: {:.2f},\tTheoretical: {:.2f}"
+                    .format(
+                        result['lte_tput_result']['total']['UL']['min_tput'],
+                        result['lte_tput_result']['total']['UL']
+                        ['average_tput'],
+                        result['lte_tput_result']['total']['UL']['max_tput'],
+                        result['lte_tput_result']['total']['UL']
+                        ['theoretical_tput']))
+            self.log.info("DL BLER: {:.2f}%\tUL BLER: {:.2f}%".format(
+                result['lte_bler_result']['total']['DL']['nack_ratio'] * 100,
+                result['lte_bler_result']['total']['UL']['nack_ratio'] * 100))
+            if self.testclass_params['traffic_type'] != 'PHY':
+                self.log.info("{} Tput: {:.2f} Mbps".format(
+                    self.testclass_params['traffic_type'],
+                    result['iperf_throughput']))
+
+    def setup_tester(self, testcase_params):
+        # Configure all cells
+        for cell_idx, cell in enumerate(
+                testcase_params['endc_combo_config']['cell_list']):
+            self.keysight_test_app.set_cell_duplex_mode(
+                cell['cell_type'], cell['cell_number'], cell['duplex_mode'])
+            self.keysight_test_app.set_cell_band(cell['cell_type'],
+                                                 cell['cell_number'],
+                                                 cell['band'])
+            self.keysight_test_app.set_cell_dl_power(
+                cell['cell_type'], cell['cell_number'],
+                testcase_params['cell_power_sweep'][cell_idx][0], 1)
+            if cell['cell_type'] == 'NR5G':
+                self.keysight_test_app.set_nr_subcarrier_spacing(
+                    cell['cell_number'], cell['subcarrier_spacing'])
+            if 'channel' in cell:
+                self.keysight_test_app.set_cell_channel(
+                    cell['cell_type'], cell['cell_number'], cell['channel'])
+            self.keysight_test_app.set_cell_bandwidth(cell['cell_type'],
+                                                      cell['cell_number'],
+                                                      cell['dl_bandwidth'])
+            self.keysight_test_app.set_cell_mimo_config(
+                cell['cell_type'], cell['cell_number'], 'DL',
+                cell['dl_mimo_config'])
+            if cell['cell_type'] == 'LTE':
+                self.keysight_test_app.set_lte_cell_transmission_mode(
+                    cell['cell_number'], cell['transmission_mode'])
+                self.keysight_test_app.set_lte_control_region_size(
+                    cell['cell_number'], 1)
+            if cell['ul_enabled'] and cell['cell_type'] == 'NR5G':
+                self.keysight_test_app.set_cell_mimo_config(
+                    cell['cell_type'], cell['cell_number'], 'UL',
+                    cell['ul_mimo_config'])
+
+        if testcase_params.get('force_contiguous_nr_channel', False):
+            self.keysight_test_app.toggle_contiguous_nr_channels(1)
+
+        if testcase_params['endc_combo_config']['lte_cell_count']:
+            self.keysight_test_app.set_lte_cell_mcs(
+                'CELL1', testcase_params['lte_dl_mcs_table'],
+                testcase_params['lte_dl_mcs'],
+                testcase_params['lte_ul_mcs_table'],
+                testcase_params['lte_ul_mcs'])
+            self.keysight_test_app.set_lte_ul_mac_padding(
+                self.testclass_params['lte_ul_mac_padding'])
+
+        if testcase_params['endc_combo_config']['nr_cell_count']:
+            if 'schedule_scenario' in testcase_params:
+                self.keysight_test_app.set_nr_cell_schedule_scenario(
+                    'CELL1',
+                    testcase_params['schedule_scenario'])
+            self.keysight_test_app.set_nr_ul_dft_precoding(
+                'CELL1', testcase_params['transform_precoding'])
+            self.keysight_test_app.set_nr_cell_mcs(
+                'CELL1', testcase_params['nr_dl_mcs'],
+                testcase_params['nr_ul_mcs'])
+            self.keysight_test_app.set_dl_carriers(
+                testcase_params['endc_combo_config']['nr_dl_carriers'])
+            self.keysight_test_app.set_ul_carriers(
+                testcase_params['endc_combo_config']['nr_ul_carriers'])
+
+        # Turn on LTE cells
+        for cell in testcase_params['endc_combo_config']['cell_list']:
+            if cell['cell_type'] == 'LTE' and not self.keysight_test_app.get_cell_state(
+                    cell['cell_type'], cell['cell_number']):
+                self.log.info('Turning LTE Cell {} on.'.format(
+                    cell['cell_number']))
+                self.keysight_test_app.set_cell_state(cell['cell_type'],
+                                                      cell['cell_number'], 1)
+
+        # Activate LTE aggregation
+        if testcase_params['endc_combo_config']['lte_scc_list']:
+            self.keysight_test_app.apply_lte_carrier_agg(
+                testcase_params['endc_combo_config']['lte_scc_list'])
+
+        self.log.info('Waiting for LTE connections')
+        # Turn airplane mode off
+        num_apm_toggles = 5
+        for idx in range(num_apm_toggles):
+            self.log.info('Turning off airplane mode')
+            asserts.assert_true(utils.force_airplane_mode(self.dut, False),
+                                'Can not turn off airplane mode.')
+            if self.keysight_test_app.wait_for_cell_status(
+                    'LTE', 'CELL1', 'CONN', 180):
+                break
+            elif idx < num_apm_toggles - 1:
+                self.log.info('Turning on airplane mode')
+                asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                                    'Can not turn on airplane mode.')
+                time.sleep(MEDIUM_SLEEP)
+            else:
+                asserts.fail('DUT did not connect to LTE.')
+
+        if testcase_params['endc_combo_config']['nr_cell_count']:
+            self.keysight_test_app.apply_carrier_agg()
+            self.log.info('Waiting for 5G connection')
+            connected = self.keysight_test_app.wait_for_cell_status(
+                'NR5G', testcase_params['endc_combo_config']['nr_cell_count'],
+                ['ACT', 'CONN'], 60)
+            if not connected:
+                asserts.fail('DUT did not connect to NR.')
+        time.sleep(SHORT_SLEEP)
+
+    def _test_throughput_bler(self, testcase_params):
+        """Test function to run cellular throughput and BLER measurements.
+
+        The function runs BLER/throughput measurement after configuring the
+        callbox and DUT. The test supports running PHY or TCP/UDP layer traffic
+        in a variety of band/carrier/mcs/etc configurations.
+
+        Args:
+            testcase_params: dict containing test-specific parameters
+        Returns:
+            result: dict containing throughput results and meta data
+        """
+        # Prepare results dicts
+        testcase_params = self.compile_test_params(testcase_params)
+        testcase_results = collections.OrderedDict()
+        testcase_results['testcase_params'] = testcase_params
+        testcase_results['results'] = []
+
+        # Setup tester and wait for DUT to connect
+        self.setup_tester(testcase_params)
+
+        # Run throughput test loop
+        stop_counter = 0
+        if testcase_params['endc_combo_config']['nr_cell_count']:
+            self.keysight_test_app.select_display_tab('NR5G', 1, 'BTHR',
+                                                      'OTAGRAPH')
+        else:
+            self.keysight_test_app.select_display_tab('LTE', 1, 'BTHR',
+                                                      'OTAGRAPH')
+        for power_idx in range(len(testcase_params['cell_power_sweep'][0])):
+            result = collections.OrderedDict()
+            # Set DL cell power
+            result['cell_power'] = []
+            for cell_idx, cell in enumerate(
+                    testcase_params['endc_combo_config']['cell_list']):
+                current_cell_power = testcase_params['cell_power_sweep'][
+                    cell_idx][power_idx]
+                result['cell_power'].append(current_cell_power)
+                self.keysight_test_app.set_cell_dl_power(
+                    cell['cell_type'], cell['cell_number'], current_cell_power,
+                    1)
+
+            # Start BLER and throughput measurements
+            result = self.run_single_throughput_measurement(testcase_params)
+            testcase_results['results'].append(result)
+
+            self.print_throughput_result(result)
+
+            if (('lte_bler_result' in result
+                 and result['lte_bler_result']['total']['DL']['nack_ratio'] *
+                 100 > 99) or
+                ('nr_bler_result' in result
+                 and result['nr_bler_result']['total']['DL']['nack_ratio'] *
+                 100 > 99)):
+                stop_counter = stop_counter + 1
+            else:
+                stop_counter = 0
+            if stop_counter == STOP_COUNTER_LIMIT:
+                break
+
+        # Save results
+        self.testclass_results[self.current_test_name] = testcase_results
diff --git a/acts_tests/acts_contrib/test_utils/cellular/performance/__init__.py b/acts_tests/acts_contrib/test_utils/cellular/performance/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/performance/__init__.py
diff --git a/acts_tests/acts_contrib/test_utils/cellular/performance/cellular_performance_test_utils.py b/acts_tests/acts_contrib/test_utils/cellular/performance/cellular_performance_test_utils.py
new file mode 100644
index 0000000..4aaff91
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/performance/cellular_performance_test_utils.py
@@ -0,0 +1,230 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import logging
+import os
+import time
+
+PCC_PRESET_MAPPING = {
+    'N257': {
+        'low': 2054999,
+        'mid': 2079165,
+        'high': 2090832
+    },
+    'N258': {
+        'low': 2017499,
+        'mid': 2043749,
+        'high': 2057499
+    },
+    'N260': {
+        'low': 2229999,
+        'mid': 2254165,
+        'high': 2265832
+    },
+    'N261': {
+        'low': 2071667
+    }
+}
+
+DUPLEX_MODE_TO_BAND_MAPPING = {
+    'LTE': {
+        'FDD': [
+            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 17, 18, 19, 20, 21,
+            22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 65, 66, 67, 68, 69, 70,
+            71, 72, 73, 74, 75, 76, 85, 252, 255
+        ],
+        'TDD': [
+            33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 45, 46, 47, 48,
+            50, 51, 53
+        ]
+    },
+    'NR5G': {
+        'FDD': [
+            'N1', 'N2', 'N3', 'N5', 'N7', 'N8', 'N12', 'N13', 'N14', 'N18',
+            'N20', 'N25', 'N26', 'N28', 'N30', 'N65', 'N66', 'N70', 'N71',
+            'N74'
+        ],
+        'TDD': [
+            'N34', 'N38', 'N39', 'N40', 'N41', 'N48', 'N50', 'N51', 'N53',
+            'N77', 'N78', 'N79', 'N90', 'N257', 'N258', 'N259', 'N260', 'N261'
+        ]
+    },
+}
+
+
+def extract_test_id(testcase_params, id_fields):
+    test_id = collections.OrderedDict(
+        (param, testcase_params[param]) for param in id_fields)
+    return test_id
+
+
+def start_pixel_logger(ad):
+    """Function to start pixel logger with default log mask.
+
+    Args:
+        ad: android device on which to start logger
+    """
+
+    try:
+        ad.adb.shell(
+            'rm -R /storage/emulated/0/Android/data/com.android.pixellogger/files/logs/logs/'
+        )
+    except:
+        pass
+    ad.adb.shell(
+        'am startservice -a com.android.pixellogger.service.logging.LoggingService.ACTION_START_LOGGING'
+    )
+
+
+def stop_pixel_logger(ad, log_path, tag=None):
+    """Function to stop pixel logger and retrieve logs
+
+    Args:
+        ad: android device on which to start logger
+        log_path: location of saved logs
+    """
+    ad.adb.shell(
+        'am startservice -a com.android.pixellogger.service.logging.LoggingService.ACTION_STOP_LOGGING'
+    )
+    logging.info('Waiting for Pixel log file')
+    file_name = None
+    file_size = 0
+    previous_file_size = 0
+    for idx in range(600):
+        try:
+            file = ad.adb.shell(
+                'ls -l /storage/emulated/0/Android/data/com.android.pixellogger/files/logs/logs/'
+            ).split(' ')
+            file_name = file[-1]
+            file_size = file[-4]
+        except:
+            file_name = None
+            file_size = 0
+        if file_name and file_size == previous_file_size:
+            logging.info('Log file found after {}s.'.format(idx))
+            break
+        else:
+            previous_file_size = file_size
+            time.sleep(1)
+    try:
+        local_file_name = '{}_{}'.format(file_name, tag) if tag else file_name
+        local_path = os.path.join(log_path, local_file_name)
+        ad.pull_files(
+            '/storage/emulated/0/Android/data/com.android.pixellogger/files/logs/logs/{}'
+            .format(file_name), log_path)
+        return local_path
+    except:
+        logging.error('Could not pull pixel logs.')
+
+
+def log_system_power_metrics(ad, verbose=1):
+    # Log temperature sensors
+    if verbose:
+        temp_sensors = ad.adb.shell(
+            'ls -1 /dev/thermal/tz-by-name/').splitlines()
+    else:
+        temp_sensors = ['BIG', 'battery', 'quiet_therm', 'usb_pwr_therm']
+    temp_measurements = collections.OrderedDict()
+    for sensor in temp_sensors:
+        try:
+            temp_measurements[sensor] = ad.adb.shell(
+                'cat /dev/thermal/tz-by-name/{}/temp'.format(sensor))
+        except:
+            temp_measurements[sensor] = float('nan')
+    logging.debug('Temperature sensor readings: {}'.format(temp_measurements))
+
+    # Log mitigation items
+    if verbose:
+        mitigation_points = [
+            "batoilo",
+            "ocp_cpu1",
+            "ocp_cpu2",
+            "ocp_gpu",
+            "ocp_tpu",
+            "smpl_warn",
+            "soft_ocp_cpu1",
+            "soft_ocp_cpu2",
+            "soft_ocp_gpu",
+            "soft_ocp_tpu",
+            "vdroop1",
+            "vdroop2",
+        ]
+    else:
+        mitigation_points = [
+            "batoilo",
+            "smpl_warn",
+            "vdroop1",
+            "vdroop2",
+        ]
+
+    parameters_f = ['count', 'capacity', 'timestamp', 'voltage']
+    parameters_v = ['count', 'cap', 'time', 'volt']
+    mitigation_measurements = collections.OrderedDict()
+    for mp in mitigation_points:
+        mitigation_measurements[mp] = collections.OrderedDict()
+        for par_f, par_v in zip(parameters_f, parameters_v):
+            mitigation_measurements[mp][par_v] = ad.adb.shell(
+                'cat /sys/devices/virtual/pmic/mitigation/last_triggered_{}/{}_{}'
+                .format(par_f, mp, par_v))
+    logging.debug('Mitigation readings: {}'.format(mitigation_measurements))
+
+    # Log power meter items
+    power_meter_measurements = collections.OrderedDict()
+    for device in ['device0', 'device1']:
+        power_str = ad.adb.shell(
+            'cat /sys/bus/iio/devices/iio:{}/lpf_power'.format(
+                device)).splitlines()
+        power_meter_measurements[device] = collections.OrderedDict()
+        for line in power_str:
+            if line.startswith('CH'):
+                try:
+                    line_split = line.split(', ')
+                    power_meter_measurements[device][line_split[0]] = int(
+                        line_split[1])
+                except (IndexError, ValueError):
+                    continue
+            elif line.startswith('t='):
+                try:
+                    power_meter_measurements[device]['t_pmeter'] = int(
+                        line[2:])
+                except (IndexError, ValueError):
+                    continue
+            else:
+                continue
+        logging.debug(
+            'Power Meter readings: {}'.format(power_meter_measurements))
+
+        # Log battery items
+        if verbose:
+            battery_parameters = [
+                "act_impedance", "capacity", "charge_counter", "charge_full",
+                "charge_full_design", "current_avg", "current_now",
+                "cycle_count", "health", "offmode_charger", "present",
+                "rc_switch_enable", "resistance", "status", "temp",
+                "voltage_avg", "voltage_now", "voltage_ocv"
+            ]
+        else:
+            battery_parameters = [
+                "capacity", "current_avg", "current_now", "voltage_avg",
+                "voltage_now", "voltage_ocv"
+            ]
+
+        battery_meaurements = collections.OrderedDict()
+        for par in battery_parameters:
+            battery_meaurements['bat_{}'.format(par)] = ad.adb.shell(
+                'cat /sys/class/power_supply/maxfg/{}'.format(par))
+        logging.debug('Battery readings: {}'.format(battery_meaurements))
diff --git a/acts_tests/acts_contrib/test_utils/cellular/performance/shannon_log_parser.py b/acts_tests/acts_contrib/test_utils/cellular/performance/shannon_log_parser.py
new file mode 100644
index 0000000..89c2adf
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/cellular/performance/shannon_log_parser.py
@@ -0,0 +1,808 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the 'License');
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an 'AS IS' BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import datetime
+import gzip
+import itertools
+import logging
+import numpy
+import os
+import re
+import shutil
+import subprocess
+import zipfile
+from acts import context
+from pathlib import Path
+
+_DIGITS_REGEX = re.compile(r'-?\d+')
+_TX_PWR_MAX = 100
+_TX_PWR_MIN = -100
+
+
+class LastNrPower:
+    last_time = 0
+    last_pwr = 0
+
+
+class LastLteValue:
+    last_time = 0
+    last_pwr = 0
+
+
+def _string_to_float(input_string):
+    """Convert string to float value."""
+    try:
+        tmp = float(input_string)
+    except ValueError:
+        print(input_string)
+        tmp = float('nan')
+    return tmp
+
+
+def to_time(time_str):
+    """Convert time string to data time."""
+    return datetime.datetime.strptime(time_str, '%H:%M:%S.%f')
+
+
+def to_time_sec(time_str, start_time):
+    """"Converts time string to seconds elapsed since start time."""
+    return (to_time(time_str) - start_time).total_seconds()
+
+
+class LogParser(object):
+    """Base class to parse log csv files."""
+
+    def __init__(self):
+        self.timestamp_col = -1
+        self.message_col = -1
+        self.start_time = None
+
+        # Dictionary of {log_item_header: log_time_parser} elements
+        self.PARSER_INFO = {
+            r'###[AS] RSRP[': self._parse_lte_rsrp,
+            r'|CC0| PCell RSRP ': self._parse_lte_rsrp2,
+            r'|CC0 Mx0 Sc0| PCell RSRP ': self._parse_lte_rsrp2,
+            r'LT12 PUSCH_Power': self._parse_lte_power,
+            r'UL_PWR(PUSCH)=>pwr_val:': self._parse_lte_power,
+            r'[BM]SSB_RSRP(1)': self._parse_nr_rsrp,
+            r'[BM]SSB_RSRP(0)': self._parse_nr_rsrp,
+            r'###[AS] CurAnt': self._parse_nr_rsrp2,
+            r'[PHY] RF module(': self._parse_fr2_rsrp,
+            #r'[RF] PD : CC0 (monitoring) target_pwr': _parse_fr2_power,
+            #r'[NrPhyTxScheduler][PuschCalcPower] Po_nominal_pusch': _parse_nr_power,
+            r'[NrPhyTxPuschScheduler][PuschCalcPower] Po_nominal_pusch':
+            self._parse_fr2_power,
+            r'[RF NR SUB6] PD : CC0 (monitoring) target_pwr':
+            self._parse_nr_power2,
+            r'[RF NR SUB6] PD : CC1 (monitoring) target_pwr':
+            self._parse_nr_power2,
+            r'[AS] RFAPI_ChangeAntennaSwitch': self._parse_lte_ant_sel,
+            r'[AS] Ant switching': self._parse_lte_ant_sel2,
+            r'###[AS] Select Antenna': self._parse_nr_ant_sel,
+            r'###[AS] CurAnt(': self._parse_nr_ant_sel2,
+            r'[SAR][RESTORE]': self._parse_sar_mode,
+            r'[SAR][NORMAL]': self._parse_sar_mode,
+            r'[SAR][LIMITED-TAPC]': self._parse_tapc_sar_mode,
+            r'###[TAS] [0] ProcessRestoreStage:: [RESTORE]':
+            self._parse_nr_sar_mode,
+            r'###[TAS] [0] ProcessNormalStage:: [NORMAL]':
+            self._parse_nr_sar_mode,
+            r'|CC0| UL Power : PRACH ': self._parse_lte_avg_power,
+            r'activeStackId=0\, [Monitor] txPower ': self._parse_wcdma_power,
+            r'[SAR][DYNAMIC] EN-DC(2) UsedAvgSarLTE': self._parse_sar_values,
+            #r'[SAR][DYNAMIC] UsedAvgSarLTE_100s': self._parse_sar_values2,
+            r'[SAR][DYNAMIC] EN-DC(0) UsedAvgSarLTE':
+            self._parse_lte_sar_values,
+            r'###[TAS] [0] CalcGain:: TotalUsedSar': self._parse_nr_sar_values,
+            r'[SAR][DYNAMIC] IsLTEOn(1) IsNROn(0) ':
+            self._parse_lte_sar_values,
+            r'[SAR][DYNAMIC] IsLTEOn(1) IsNROn(1) ': self._parse_sar_values,
+            r'[MAIN][VolteStatusInd] Volte status ': self._parse_volte_status,
+            r'[PHY] CC0 SLP : dlGrantRatio(3)/ulGrantRatio(3)/RbRatio(3)':
+            self._parse_df_value,
+            r'CC0 AVG: CELLGROUP(0) DCI(D/U):': self._parse_df_value,
+            r'[OL-AIT] band': self._parse_ul_mimo,
+        }
+
+        self.SAR_MODES = [
+            'none', 'MIN', 'SAV_1', 'SAV_2', 'MAIN', 'PRE_SAV', 'LIMITED-TAPC',
+            'MAX', 'none'
+        ]
+        self.SAR_MODES_DESC = [
+            '', 'Minimum', 'Slope Saving1', 'Slope Saving2', 'Maintenance',
+            'Pre-Save', 'Limited TAPC', 'Maximum', ''
+        ]
+
+    def parse_header(self, header_line):
+        header = header_line.split(',')
+        try:
+            self.timestamp_col = header.index('PC Time')
+            self.message_col = header.index('Message')
+        except ValueError:
+            print('Error: PC Time and Message fields are not present')
+        try:
+            self.core_id_col = header.index('Core ID')
+        except:
+            self.core_id_col = self.timestamp_col
+
+    def parse_log(self, log_file, gap_options=0):
+        """Extract required data from the exported CSV file."""
+
+        log_data = LogData()
+        log_data.gap_options = gap_options
+        # Fr-1 as default
+        fr_id = 0
+
+        with open(log_file, 'r') as file:
+            # Read header line
+            header = file.readline()
+            try:
+                self.parse_header(header)
+            except:
+                print('Could not parse header')
+                return log_data
+
+            # Use first message for start time
+            line = file.readline()
+            print(line)
+            line_data = line[1:-2].split('","')
+            if len(line_data) < self.message_col:
+                print('Error: Empty exported file')
+                return log_data
+
+            start_time = to_time(line_data[self.timestamp_col])
+
+            print('Parsing log file ... ', end='', flush=True)
+            for line in file:
+                line_data = line[1:-2].split('","')
+                if len(line_data) < self.message_col + 1:
+                    continue
+
+                message = line_data[self.message_col]
+                if "frIdx 1 " in message:
+                    fr_id = 1
+                elif "frIdx 0 " in message:
+                    fr_id = 0
+                for line_prefix, line_parser in self.PARSER_INFO.items():
+                    if message.startswith(line_prefix):
+                        timestamp = to_time_sec(line_data[self.timestamp_col],
+                                                start_time)
+                        if self.core_id_col == self.timestamp_col:
+                            line_parser(timestamp, message[len(line_prefix):],
+                                        'L1', log_data, fr_id)
+                        else:
+                            if " CC1 " in message:
+                                line_parser(timestamp,
+                                            message[len(line_prefix):], 'L2',
+                                            log_data, fr_id)
+                            else:
+                                line_parser(timestamp,
+                                            message[len(line_prefix):],
+                                            line_data[self.core_id_col],
+                                            log_data, fr_id)
+                        break
+
+            if log_data.nr.tx_pwr_time:
+                if log_data.nr.tx_pwr_time[1] > log_data.nr.tx_pwr_time[0] + 50:
+                    log_data.nr.tx_pwr_time = log_data.nr.tx_pwr_time[1:]
+                    log_data.nr.tx_pwr = log_data.nr.tx_pwr[1:]
+
+            self._find_cur_ant(log_data.lte)
+            self._find_cur_ant(log_data.nr)
+        return log_data
+
+    def get_file_start_time(self, log_file):
+        # Fr-1 as default
+
+        with open(log_file, 'r') as file:
+            # Read header line
+            header = file.readline()
+            try:
+                self.parse_header(header)
+            except:
+                print('Could not parse header')
+                return None
+
+            # Use first message for start time
+            line = file.readline()
+            line_data = line[1:-2].split('","')
+            if len(line_data) < self.message_col:
+                print('Error: Empty exported file')
+                return None
+
+            start_time = to_time(line_data[self.timestamp_col])
+            return start_time
+
+    def set_start_time(self, line):
+        """Set start time of logs to the time in the line."""
+        if len(line) == 0:
+            print("Empty Line")
+            return
+        line_data = line[1:-2].split('","')
+        self.start_time = to_time(line_data[self.timestamp_col])
+
+    def get_message(self, line):
+        """Returns message and timestamp for the line."""
+        line_data = line[1:-2].split('","')
+        if len(line_data) < self.message_col + 1:
+            return None
+
+        self.line_data = line_data
+        return line_data[self.message_col]
+
+    def get_time(self, line):
+        """Convert time string to time in seconds from the start time."""
+        line_data = line[1:-2].split('","')
+        if len(line_data) < self.timestamp_col + 1:
+            return 0
+
+        return to_time_sec(line_data[self.timestamp_col], self.start_time)
+
+    def _feed_nr_power(self, timestamp, tx_pwr, option, lte_nr, window,
+                       default, interval):
+        if option < 101 and LastNrPower.last_time > 0 and timestamp - LastNrPower.last_time > interval:
+            #print ('window=',window, ' interval=',interval, ' gap=',timestamp-LastNrPower.last_time)
+            ti = LastNrPower.last_time
+            while ti < timestamp:
+                ti += (timestamp - LastNrPower.last_time) / window
+                lte_nr.tx_pwr_time.append(ti)
+                if option == 0:
+                    lte_nr.tx_pwr.append(tx_pwr / default)
+                elif option == 1:
+                    lte_nr.tx_pwr.append(LastNrPower.last_pwr)
+                elif option == 2:
+                    lte_nr.tx_pwr.append((tx_pwr + LastNrPower.last_pwr) / 2)
+                elif option == 3:
+                    lte_nr.tx_pwr.append(0)
+                else:
+                    lte_nr.tx_pwr.append(option)
+        else:
+            lte_nr.tx_pwr_time.append(timestamp)
+            lte_nr.tx_pwr.append(tx_pwr)
+        LastNrPower.last_time = timestamp
+        LastNrPower.last_pwr = tx_pwr
+
+    def _feed_lte_power(self, timestamp, tx_pwr, log_data, lte_nr, window,
+                        default, interval):
+        if log_data.gap_options <= 100 and LastLteValue.last_time > 0 and timestamp - LastLteValue.last_time > interval:
+            #print ('window=',window, ' interval=',interval, ' gap=',timestamp-LastLteValue.last_time)
+            ti = LastLteValue.last_time
+            while ti < timestamp:
+                ti += (timestamp - LastLteValue.last_time) / window
+                lte_nr.tx_pwr_time.append(ti)
+                if log_data.gap_options == 0:
+                    lte_nr.tx_pwr.append(tx_pwr / default)
+                elif log_data.gap_options == 1:
+                    lte_nr.tx_pwr.append(LastLteValue.last_pwr)
+                elif log_data.gap_options == 2:
+                    lte_nr.tx_pwr.append((tx_pwr + LastLteValue.last_pwr) / 2)
+                elif log_data.gap_options == 3:
+                    lte_nr.tx_pwr.append(0)
+                else:
+                    lte_nr.tx_pwr.append(log_data.gap_options)
+        else:
+            lte_nr.tx_pwr_time.append(timestamp)
+            lte_nr.tx_pwr.append(tx_pwr)
+        LastLteValue.last_time = timestamp
+        LastLteValue.last_pwr = tx_pwr
+
+    def _parse_lte_power(self, timestamp, message, core_id, log_data, fr_id):
+        match = re.search(r'-?\d+', message)
+        if match:
+            tx_pwr = _string_to_float(match.group())
+            if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX:
+                self._feed_lte_power(timestamp, tx_pwr, log_data, log_data.lte,
+                                     20, 1, 1)
+
+    def _parse_lte_rsrp(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        rsrp0 = _string_to_float(data[0]) / 100.0
+        rsrp1 = _string_to_float(data[1]) / 100.0
+        if rsrp0 != 0.0 and rsrp1 != 0.0:
+            log_data.lte.rsrp_time.append(timestamp)
+            log_data.lte.rsrp_rx0.append(rsrp0)
+            log_data.lte.rsrp_rx1.append(rsrp1)
+
+    def _parse_lte_rsrp2(self, timestamp, message, core_id, log_data, fr_id):
+        m = re.search('^\[ ?-?\d+ \((.*?)\)', message)
+        if not m:
+            return
+        data = _DIGITS_REGEX.findall(m.group(1))
+        if len(data) < 2:
+            return
+        rsrp0 = _string_to_float(data[0])
+        rsrp1 = _string_to_float(data[1])
+        if rsrp0 != 0.0 and rsrp1 != 0.0:
+            log_data.lte.rsrp2_time.append(timestamp)
+            log_data.lte.rsrp2_rx0.append(rsrp0)
+            log_data.lte.rsrp2_rx1.append(rsrp1)
+
+    def _parse_nr_rsrp(self, timestamp, message, core_id, log_data, fr_id):
+        index = message.find('rx0/rx1/rx2/rx3')
+        if index != -1:
+            data = _DIGITS_REGEX.findall(message[index:])
+            log_data.nr.rsrp_time.append(timestamp)
+            log_data.nr.rsrp_rx0.append(_string_to_float(data[4]) / 100)
+            log_data.nr.rsrp_rx1.append(_string_to_float(data[5]) / 100)
+
+    def _parse_nr_rsrp2(self, timestamp, message, core_id, log_data, fr_id):
+        index = message.find('Rsrp')
+        if index != -1:
+            data = _DIGITS_REGEX.findall(message[index:])
+            log_data.nr.rsrp2_time.append(timestamp)
+            log_data.nr.rsrp2_rx0.append(_string_to_float(data[0]) / 100)
+            log_data.nr.rsrp2_rx1.append(_string_to_float(data[1]) / 100)
+
+    def _parse_fr2_rsrp(self, timestamp, message, core_id, log_data, fr_id):
+        index = message.find('rsrp')
+        data = _DIGITS_REGEX.search(message)
+        module_index = _string_to_float(data.group(0))
+        data = _DIGITS_REGEX.findall(message[index:])
+        rsrp = _string_to_float(data[0])
+
+        if rsrp == 0:
+            return
+        if module_index == 0:
+            log_data.fr2.rsrp0_time.append(timestamp)
+            log_data.fr2.rsrp0.append(rsrp if rsrp < 999 else float('nan'))
+        elif module_index == 1:
+            log_data.fr2.rsrp1_time.append(timestamp)
+            log_data.fr2.rsrp1.append(rsrp if rsrp < 999 else float('nan'))
+
+    def _parse_fr2_power(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        tx_pwr = _string_to_float(data[-1]) / 10
+        if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX:
+            log_data.fr2.tx_pwr_time.append(timestamp)
+            log_data.fr2.tx_pwr.append(tx_pwr)
+
+    def _parse_nr_power(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        tx_pwr = _string_to_float(data[-1]) / 10
+        if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX:
+            if core_id == 'L2':
+                self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options,
+                                    log_data.nr2, 5, 1, 1)
+            else:
+                self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options,
+                                    log_data.nr, 5, 1, 1)
+
+    def _parse_nr_power2(self, timestamp, message, core_id, log_data, fr_id):
+        if fr_id != 0:
+            return
+        data = _DIGITS_REGEX.findall(message)
+        tx_pwr = _string_to_float(data[0]) / 10
+        if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX:
+            if core_id == 'L2':
+                self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options,
+                                    log_data.nr2, 5, 1, 1)
+            else:
+                self._feed_nr_power(timestamp, tx_pwr, log_data.gap_options,
+                                    log_data.nr, 5, 1, 1)
+
+    def _parse_lte_ant_sel(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        new_ant = _string_to_float(data[1])
+        old_ant = _string_to_float(data[2])
+        log_data.lte.ant_sel_time.append(timestamp)
+        log_data.lte.ant_sel_old.append(old_ant)
+        log_data.lte.ant_sel_new.append(new_ant)
+
+    def _parse_lte_ant_sel2(self, timestamp, message, core_id, log_data,
+                            fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        if data[0] == '0':
+            log_data.lte.cur_ant_time.append(timestamp)
+            log_data.lte.cur_ant.append(0)
+        elif data[0] == '1':
+            log_data.lte.cur_ant_time.append(timestamp)
+            log_data.lte.cur_ant.append(1)
+        elif data[0] == '10':
+            log_data.lte.cur_ant_time.append(timestamp)
+            log_data.lte.cur_ant.append(1)
+            log_data.lte.cur_ant_time.append(timestamp)
+            log_data.lte.cur_ant.append(0)
+        elif data[0] == '01':
+            log_data.lte.cur_ant_time.append(timestamp)
+            log_data.lte.cur_ant.append(0)
+            log_data.lte.cur_ant_time.append(timestamp)
+            log_data.lte.cur_ant.append(1)
+
+    def _parse_nr_ant_sel(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        log_data.nr.ant_sel_time.append(timestamp)
+        log_data.nr.ant_sel_new.append(_string_to_float(data[1]))
+        log_data.nr.ant_sel_old.append(_string_to_float(data[0]))
+
+    def _parse_nr_ant_sel2(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        log_data.nr.ant_sel_time.append(timestamp)
+        sel_ant = _string_to_float(data[0])
+        log_data.nr.ant_sel_new.append(sel_ant)
+        if log_data.nr.ant_sel_new:
+            log_data.nr.ant_sel_old.append(log_data.nr.ant_sel_new[-1])
+
+    def _parse_sar_mode(self, timestamp, message, core_id, log_data, fr_id):
+        sar_mode = len(self.SAR_MODES) - 1
+        for i, mode in enumerate(self.SAR_MODES):
+            if message.startswith('[' + mode + ']'):
+                sar_mode = i
+        log_data.lte.sar_mode_time.append(timestamp)
+        log_data.lte.sar_mode.append(sar_mode)
+
+    def _parse_tapc_sar_mode(self, timestamp, message, core_id, log_data,
+                             fr_id):
+        log_data.lte.sar_mode_time.append(timestamp)
+        log_data.lte.sar_mode.append(self.SAR_MODES.index('LIMITED-TAPC'))
+
+    def _parse_nr_sar_mode(self, timestamp, message, core_id, log_data, fr_id):
+        sar_mode = len(self.SAR_MODES) - 1
+        for i, mode in enumerate(self.SAR_MODES):
+            if message.startswith('[' + mode + ']'):
+                sar_mode = i
+
+        log_data.nr.sar_mode_time.append(timestamp)
+        log_data.nr.sar_mode.append(sar_mode)
+
+    def _parse_lte_avg_power(self, timestamp, message, core_id, log_data,
+                             fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        tx_pwr = _string_to_float(data[2])
+        if _TX_PWR_MIN < tx_pwr < _TX_PWR_MAX:
+            log_data.lte.tx_avg_pwr_time.append(timestamp)
+            log_data.lte.tx_avg_pwr.append(tx_pwr)
+
+    def _parse_wcdma_power(self, timestamp, message, core_id, log_data, fr_id):
+        match = re.search(r'-?\d+', message)
+        if match:
+            tx_pwr = _string_to_float(match.group()) / 10
+            if tx_pwr < _TX_PWR_MAX and tx_pwr > _TX_PWR_MIN:
+                log_data.wcdma.tx_pwr_time.append(timestamp)
+                log_data.wcdma.tx_pwr.append(tx_pwr)
+
+    def _parse_sar_values(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        log_data.endc_sar_time.append(timestamp)
+        log_data.endc_sar_lte.append(_string_to_float(data[0]) / 1000.0)
+        log_data.endc_sar_nr.append(_string_to_float(data[1]) / 1000.0)
+
+    def _parse_sar_values2(self, timestamp, message, core_id, log_data, fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        log_data.endc_sar_time.append(timestamp)
+        log_data.endc_sar_lte.append(_string_to_float(data[-3]) / 1000.0)
+        log_data.endc_sar_nr.append(_string_to_float(data[-1]) / 1000.0)
+
+    def _parse_lte_sar_values(self, timestamp, message, core_id, log_data,
+                              fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        log_data.lte_sar_time.append(timestamp)
+        log_data.lte_sar.append(_string_to_float(data[0]) / 1000.0)
+
+    def _parse_nr_sar_values(self, timestamp, message, core_id, log_data,
+                             fr_id):
+        data = _DIGITS_REGEX.findall(message)
+        log_data.nr_sar_time.append(timestamp)
+        log_data.nr_sar.append(_string_to_float(data[0]) / 1000.0)
+
+    def _parse_df_value(self, timestamp, message, core_id, log_data, fr_id):
+        match = re.search(r' \d+', message)
+        if match:
+            nr_df = _string_to_float(match.group())
+            log_data.nr.df = (nr_df / 1000 % 1000) / 100
+            log_data.nr.duty_cycle_time.append(timestamp)
+            log_data.nr.duty_cycle.append(log_data.nr.df * 100)
+        else:
+            match = re.search(r'\d+\\,\d+', message)
+            if match:
+                lte_df = match.group(0).split(",")
+                log_data.lte.df = _string_to_float(lte_df[1]) / 100
+                log_data.lte.duty_cycle_time.append(timestamp)
+                log_data.lte.duty_cycle.append(log_data.nr.df * 100)
+
+    def _parse_volte_status(self, timestamp, message, core_id, log_data,
+                            fr_id):
+        if message.startswith('[0 -> 1]'):
+            log_data.volte_time.append(timestamp)
+            log_data.volte_status.append(1)
+        elif message.startswith('[3 -> 0]'):
+            log_data.volte_time.append(timestamp)
+            log_data.volte_status.append(0)
+
+    def _parse_ul_mimo(self, timestamp, message, core_id, log_data, fr_id):
+        match = re.search(r'UL-MIMO', message)
+        if match:
+            log_data.ul_mimo = 1
+
+    def _find_cur_ant(self, log_data):
+        """Interpolate antenna selection from antenna switching data."""
+        if not log_data.cur_ant_time and log_data.ant_sel_time:
+            if log_data.rsrp_time:
+                start_time = log_data.rsrp_time[0]
+                end_time = log_data.rsrp_time[-1]
+            elif log_data.tx_pwr:
+                start_time = log_data.tx_pwr_time[0]
+                end_time = log_data.tx_pwr_time[-1]
+            else:
+                start_time = log_data.ant_sel_time[0]
+                end_time = log_data.ant_sel_time[-1]
+
+            [sel_time,
+             sel_ant] = self.get_ant_selection(log_data.ant_sel_time,
+                                               log_data.ant_sel_old,
+                                               log_data.ant_sel_new,
+                                               start_time, end_time)
+
+            log_data.cur_ant_time = sel_time
+            log_data.cur_ant = sel_ant
+
+    def get_ant_selection(self, config_time, old_antenna_config,
+                          new_antenna_config, start_time, end_time):
+        """Generate antenna selection data from antenna switching information."""
+        sel_time = []
+        sel_ant = []
+        if not config_time:
+            return [sel_time, sel_ant]
+
+        # Add data point for the start time
+        if config_time[0] > start_time:
+            sel_time = [start_time]
+            sel_ant = [old_antenna_config[0]]
+
+        # Add one data point before the switch and one data point after the switch.
+        for i in range(len(config_time)):
+            if not (i > 0
+                    and old_antenna_config[i - 1] == old_antenna_config[i]
+                    and new_antenna_config[i - 1] == new_antenna_config[i]):
+                sel_time.append(config_time[i])
+                sel_ant.append(old_antenna_config[i])
+            sel_time.append(config_time[i])
+            sel_ant.append(new_antenna_config[i])
+
+        # Add data point for the end time
+        if end_time > config_time[-1]:
+            sel_time.append(end_time)
+            sel_ant.append(new_antenna_config[-1])
+
+        return [sel_time, sel_ant]
+
+
+class RatLogData:
+    """Log data structure for each RAT (LTE/NR)."""
+
+    def __init__(self, label):
+
+        self.label = label
+
+        self.rsrp_time = []  # RSRP time
+        self.rsrp_rx0 = []  # RSRP for receive antenna 0
+        self.rsrp_rx1 = []  # RSRP for receive antenna 1
+
+        # second set of RSRP logs
+        self.rsrp2_time = []  # RSRP time
+        self.rsrp2_rx0 = []  # RSRP for receive antenna 0
+        self.rsrp2_rx1 = []  # RSRP for receive antenna 1
+
+        self.ant_sel_time = []  # Antenna selection/switch time
+        self.ant_sel_old = []  # Previous antenna selection
+        self.ant_sel_new = []  # New antenna selection
+
+        self.cur_ant_time = []  # Antenna selection/switch time
+        self.cur_ant = []  # Previous antenna selection
+
+        self.tx_pwr_time = []  # TX power time
+        self.tx_pwr = []  # TX power
+
+        self.tx_avg_pwr_time = []
+        self.tx_avg_pwr = []
+
+        self.sar_mode = []
+        self.sar_mode_time = []
+
+        self.df = 1.0  # Duty factor for UL transmission
+        self.duty_cycle = []  # Duty factors for UL transmission
+        self.duty_cycle_time = []  # Duty factors for UL transmission
+        self.initial_power = 0
+        self.sar_limit_dbm = None
+        self.avg_window_size = 100
+
+
+class LogData:
+    """Log data structure."""
+
+    def __init__(self):
+        self.lte = RatLogData('LTE')
+        self.lte.avg_window_size = 100
+
+        self.nr = RatLogData('NR CC0')
+        self.nr.avg_window_size = 100
+
+        # NR 2nd CC
+        self.nr2 = RatLogData('NR CC1')
+        self.nr2.avg_window_size = 100
+
+        self.wcdma = RatLogData('WCDMA')
+
+        self.fr2 = RatLogData('FR2')
+        self.fr2.rsrp0_time = []
+        self.fr2.rsrp0 = []
+        self.fr2.rsrp1_time = []
+        self.fr2.rsrp1 = []
+        self.fr2.avg_window_size = 4
+
+        self.lte_sar_time = []
+        self.lte_sar = []
+
+        self.nr_sar_time = []
+        self.nr_sar = []
+
+        self.endc_sar_time = []
+        self.endc_sar_lte = []
+        self.endc_sar_nr = []
+
+        self.volte_time = []
+        self.volte_status = []
+
+        # Options to handle data gaps
+        self.gap_options = 0
+
+        self.ul_mimo = 0  # Is UL_MIMO
+
+
+class ShannonLogger(object):
+
+    def __init__(self, dut=None, modem_bin=None, filter_file_path=None):
+        self.dm_app = shutil.which(r'DMConsole')
+        self.dut = dut
+        if self.dut:
+            self.modem_bin = self.pull_modem_file()
+        elif modem_bin:
+            self.modem_bin = modem_bin
+        else:
+            raise (RuntimeError,
+                   'ShannonLogger requires AndroidDevice or modem binary.')
+        self.filter_file = filter_file_path
+
+    def pull_modem_file(self):
+        local_modem_path = os.path.join(
+            context.get_current_context().get_full_output_path(), 'modem_bin')
+        os.makedirs(local_modem_path, exist_ok=True)
+        try:
+            self.dut.pull_files(
+                '/mnt/vendor/modem_img/images/default/modem.bin',
+                local_modem_path)
+            modem_bin_file = os.path.join(local_modem_path, 'modem.bin')
+        except:
+            self.dut.pull_files(
+                '/mnt/vendor/modem_img/images/default/modem.bin.gz',
+                local_modem_path)
+            modem_zip_file = os.path.join(local_modem_path, 'modem.bin.gz')
+            modem_bin_file = modem_zip_file[:-3]
+            with open(modem_zip_file, 'rb') as in_file:
+                with open(modem_bin_file, 'wb') as out_file:
+                    file_content = gzip.decompress(in_file.read())
+                    out_file.write(file_content)
+        return modem_bin_file
+
+    def _unzip_log(self, log_zip_file, in_place=1):
+        log_zip_file = Path(log_zip_file)
+        with zipfile.ZipFile(log_zip_file, 'r') as zip_ref:
+            file_names = zip_ref.namelist()
+            if in_place:
+                zip_dir = log_zip_file.parent
+            else:
+                zip_dir = log_zip_file.with_suffix('')
+            zip_ref.extractall(zip_dir)
+        unzipped_files = [
+            os.path.join(zip_dir, file_name) for file_name in file_names
+        ]
+        return unzipped_files
+
+    def unzip_modem_logs(self, log_zip_file):
+        log_files = self._unzip_log(log_zip_file, in_place=0)
+        sdm_files = []
+        for log_file in log_files:
+            if zipfile.is_zipfile(log_file):
+                sdm_files.append(self._unzip_log(log_file, in_place=1)[0])
+                os.remove(log_file)
+            elif Path(
+                    log_file
+            ).suffix == '.sdm' and 'sbuff_power_on_log.sdm' not in log_file:
+                sdm_files.append(log_file)
+        return sorted(set(sdm_files))
+
+    def _export_single_log(self, file):
+        temp_file = str(Path(file).with_suffix('.csv'))
+        if self.filter_file:
+            export_cmd = [
+                self.dm_app, 'traceexport', '-c', '-csv', '-f',
+                self.filter_file, '-b', self.modem_bin, '-o', temp_file, file
+            ]
+        else:
+            export_cmd = [
+                self.dm_app, 'traceexport', '-c', '-csv', '-b', self.modem_bin,
+                '-o', temp_file, file
+            ]
+        logging.debug('Executing: {}'.format(export_cmd))
+        subprocess.call(export_cmd)
+        return temp_file
+
+    def _export_logs(self, log_files):
+        csv_files = []
+        for file in log_files:
+            csv_files.append(self._export_single_log(file))
+        return csv_files
+
+    def _filter_log(self, input_filename, output_filename, write_header):
+        """Export log messages from input file to output file."""
+        log_parser = LogParser()
+        with open(input_filename, 'r') as input_file:
+            with open(output_filename, 'a') as output_file:
+
+                header_line = input_file.readline()
+                log_parser.parse_header(header_line)
+                if log_parser.message_col == -1:
+                    return
+
+                if write_header:
+                    output_file.write(header_line)
+                    # Write next line for time reference.
+                    output_file.write(input_file.readline())
+
+                for line in input_file:
+                    message = log_parser.get_message(line)
+                    if message:
+                        for filter_str in log_parser.PARSER_INFO:
+                            if message.startswith(filter_str):
+                                output_file.write(line)
+                                break
+
+    def _export_filtered_logs(self, csv_files):
+        start_times = []
+        log_parser = LogParser()
+        reordered_csv_files = []
+        for file in csv_files:
+            start_time = log_parser.get_file_start_time(file)
+            if start_time:
+                start_times.append(start_time)
+                reordered_csv_files.append(file)
+        print(reordered_csv_files)
+        print(start_times)
+        file_order = numpy.argsort(start_times)
+        print(file_order)
+        reordered_csv_files = [reordered_csv_files[i] for i in file_order]
+        print(reordered_csv_files)
+        log_directory = Path(reordered_csv_files[0]).parent
+        exported_file = os.path.join(log_directory, 'modem_log.csv')
+        write_header = True
+        for file in reordered_csv_files:
+            self._filter_log(file, exported_file, write_header)
+            write_header = False
+        return exported_file
+
+    def _parse_log(self, log_file, gap_options=0):
+        """Extract required data from the exported CSV file."""
+        log_parser = LogParser()
+        log_data = log_parser.parse_log(log_file, gap_options=0)
+        return log_data
+
+    def process_log(self, log_zip_file):
+        sdm_log_files = self.unzip_modem_logs(log_zip_file)
+        csv_log_files = self._export_logs(sdm_log_files)
+        exported_log = self._export_filtered_logs(csv_log_files)
+        log_data = self._parse_log(exported_log, 0)
+        for file in itertools.chain(sdm_log_files, csv_log_files):
+            os.remove(file)
+        return log_data
diff --git a/acts_tests/acts_contrib/test_utils/gnss/GnssBlankingBase.py b/acts_tests/acts_contrib/test_utils/gnss/GnssBlankingBase.py
index 9fe9fa4..f6d962f 100644
--- a/acts_tests/acts_contrib/test_utils/gnss/GnssBlankingBase.py
+++ b/acts_tests/acts_contrib/test_utils/gnss/GnssBlankingBase.py
@@ -13,40 +13,39 @@
 #   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.
+'''GNSS Base Class for Blanking and Hot Start Sensitivity Search'''
 
 import os
-from glob import glob
+import re
 from time import sleep
 from collections import namedtuple
+from itertools import product
 from numpy import arange
-from pandas import DataFrame
+from pandas import DataFrame, merge
 from acts.signals import TestError
 from acts.signals import TestFailure
 from acts.logger import epoch_to_log_line_timestamp
 from acts.context import get_current_context
 from acts_contrib.test_utils.gnss import LabTtffTestBase as lttb
+from acts_contrib.test_utils.gnss.LabTtffTestBase import glob_re
 from acts_contrib.test_utils.gnss.gnss_test_utils import launch_eecoexer
-from acts_contrib.test_utils.gnss.gnss_test_utils import excute_eecoexer_function
+from acts_contrib.test_utils.gnss.gnss_test_utils import execute_eecoexer_function
 from acts_contrib.test_utils.gnss.gnss_test_utils import start_gnss_by_gtw_gpstool
 from acts_contrib.test_utils.gnss.gnss_test_utils import get_current_epoch_time
 from acts_contrib.test_utils.gnss.gnss_test_utils import check_current_focus_app
 from acts_contrib.test_utils.gnss.gnss_test_utils import process_ttff_by_gtw_gpstool
 from acts_contrib.test_utils.gnss.gnss_test_utils import check_ttff_data
 from acts_contrib.test_utils.gnss.gnss_test_utils import process_gnss_by_gtw_gpstool
-from acts_contrib.test_utils.gnss.gnss_test_utils import start_pixel_logger
-from acts_contrib.test_utils.gnss.gnss_test_utils import stop_pixel_logger
-from acts_contrib.test_utils.gnss.dut_log_test_utils import start_diagmdlog_background
 from acts_contrib.test_utils.gnss.dut_log_test_utils import get_gpstool_logs
-from acts_contrib.test_utils.gnss.dut_log_test_utils import stop_background_diagmdlog
-from acts_contrib.test_utils.gnss.dut_log_test_utils import get_pixellogger_bcm_log
 from acts_contrib.test_utils.gnss.gnss_testlog_utils import parse_gpstool_ttfflog_to_df
 
 
-def range_wi_end(ad, start, stop, step):
+def range_wi_end(dut, start, stop, step):
     """
     Generate a list of data from start to stop with the step. The list includes start and stop value
     and also supports floating point.
     Args:
+        dut: An AndroidDevice object.
         start: start value.
             Type, int or float.
         stop: stop value.
@@ -57,7 +56,7 @@
         range_ls: the list of data.
     """
     if step == 0:
-        ad.log.warn('Step is 0. Return empty list')
+        dut.log.warn('Step is 0. Return empty list')
         range_ls = []
     else:
         if start == stop:
@@ -68,44 +67,50 @@
                 if (step < 0 and range_ls[-1] > stop) or (step > 0 and
                                                           range_ls[-1] < stop):
                     range_ls.append(stop)
+    dut.log.debug(f'The range list is: {range_ls}')
     return range_ls
 
 
-def check_ttff_pe(ad, ttff_data, ttff_mode, pe_criteria):
+def check_ttff_pe(dut, ttff_data, ttff_mode, pe_criteria):
     """Verify all TTFF results from ttff_data.
 
     Args:
-        ad: An AndroidDevice object.
+        dut: An AndroidDevice object.
         ttff_data: TTFF data of secs, position error and signal strength.
         ttff_mode: TTFF Test mode for current test item.
         pe_criteria: Criteria for current test item.
 
     """
     ret = True
-    ad.log.info("%d iterations of TTFF %s tests finished." %
-                (len(ttff_data.keys()), ttff_mode))
-    ad.log.info("%s PASS criteria is %f meters" % (ttff_mode, pe_criteria))
-    ad.log.debug("%s TTFF data: %s" % (ttff_mode, ttff_data))
+    no_iteration = len(ttff_data.keys())
+    dut.log.info(
+        f'{no_iteration} iterations of TTFF {ttff_mode} tests finished.')
+    dut.log.info(f'{ttff_mode} PASS criteria is {pe_criteria} meters')
+    dut.log.debug(f'{ttff_mode} TTFF data: {ttff_data}')
 
     if len(ttff_data.keys()) == 0:
-        ad.log.error("GTW_GPSTool didn't process TTFF properly.")
+        dut.log.error("GTW_GPSTool didn't process TTFF properly.")
         raise TestFailure("GTW_GPSTool didn't process TTFF properly.")
 
     if any(
             float(ttff_data[key].ttff_pe) >= pe_criteria
             for key in ttff_data.keys()):
-        ad.log.error("One or more TTFF %s are over test criteria %f meters" %
-                     (ttff_mode, pe_criteria))
+        dut.log.error(
+            f'One or more TTFF {ttff_mode} are over test criteria {pe_criteria} meters'
+        )
         ret = False
     else:
-        ad.log.info("All TTFF %s are within test criteria %f meters." %
-                    (ttff_mode, pe_criteria))
+        dut.log.info(
+            f'All TTFF {ttff_mode} are within test criteria {pe_criteria} meters.'
+        )
         ret = True
     return ret
 
 
 class GnssBlankingBase(lttb.LabTtffTestBase):
     """ LAB GNSS Cellular Coex Tx Power Sweep TTFF/FFPE Tests"""
+    GNSS_PWR_SWEEP = 'gnss_pwr_sweep'
+    CELL_PWR_SWEEP = 'cell_pwr_sweep'
 
     def __init__(self, controllers):
         """ Initializes class attributes. """
@@ -118,22 +123,28 @@
         self.gsm_sweep_params = None
         self.lte_tdd_pc3_sweep_params = None
         self.lte_tdd_pc2_sweep_params = None
-        self.sa_sensitivity = -150
-        self.gnss_pwr_lvl_offset = -5
-        self.maskfile = None
+        self.coex_stop_cmd = None
+        self.scen_sweep = False
+        self.gnss_pwr_sweep_init_ls = []
+        self.gnss_pwr_sweep_fine_sweep_ls = []
 
     def setup_class(self):
         super().setup_class()
-        req_params = ['sa_sensitivity', 'gnss_pwr_lvl_offset']
+
+        # Required parameters
+        req_params = [self.GNSS_PWR_SWEEP]
         self.unpack_userparams(req_param_names=req_params)
-        cell_sweep_params = self.user_params.get('cell_pwr_sweep', [])
-        self.gsm_sweep_params = cell_sweep_params.get("GSM", [10, 33, 1])
-        self.lte_tdd_pc3_sweep_params = cell_sweep_params.get(
-            "LTE_TDD_PC3", [10, 24, 1])
-        self.lte_tdd_pc2_sweep_params = cell_sweep_params.get(
-            "LTE_TDD_PC2", [10, 26, 1])
-        self.sa_sensitivity = self.user_params.get('sa_sensitivity', -150)
-        self.gnss_pwr_lvl_offset = self.user_params.get('gnss_pwr_lvl_offset', -5)
+        self.unpack_gnss_pwr_sweep()
+
+        # Optional parameters
+        cell_sweep_params = self.user_params.get(self.CELL_PWR_SWEEP, [])
+
+        if cell_sweep_params:
+            self.gsm_sweep_params = cell_sweep_params.get("GSM", [10, 33, 1])
+            self.lte_tdd_pc3_sweep_params = cell_sweep_params.get(
+                "LTE_TDD_PC3", [10, 24, 1])
+            self.lte_tdd_pc2_sweep_params = cell_sweep_params.get(
+                "LTE_TDD_PC2", [10, 26, 1])
 
     def setup_test(self):
         super().setup_test()
@@ -148,11 +159,8 @@
         self.gnss_log_path = os.path.join(self.log_path, cur_test_item_dir)
         os.makedirs(self.gnss_log_path, exist_ok=True)
 
-        # Start GNSS chip log
-        if self.diag_option == "QCOM":
-            start_diagmdlog_background(self.dut, maskfile=self.maskfile)
-        else:
-            start_pixel_logger(self.dut)
+        ## Start GNSS chip log
+        self.start_dut_gnss_log()
 
     def teardown_test(self):
         super().teardown_test()
@@ -161,35 +169,69 @@
                                             self.diag_option)
         os.makedirs(gnss_vendor_log_path, exist_ok=True)
 
-        # Stop GNSS chip log and pull the logs to local file system.
-        if self.diag_option == "QCOM":
-            stop_background_diagmdlog(self.dut,
-                                      gnss_vendor_log_path,
-                                      keep_logs=False)
-        else:
-            stop_pixel_logger(self.dut)
-            self.log.info('Getting Pixel BCM Log!')
-            get_pixellogger_bcm_log(self.dut,
-                                    gnss_vendor_log_path,
-                                    keep_logs=False)
+        # Stop GNSS chip log and pull the logs to local file system
+        self.stop_and_pull_dut_gnss_log(gnss_vendor_log_path)
 
         # Stop cellular Tx and close GPStool and EEcoexer APPs.
-        self.stop_cell_tx()
+        self.stop_coex_tx()
         self.log.debug('Close GPStool APP')
         self.dut.force_stop_apk("com.android.gpstool")
         self.log.debug('Close EEcoexer APP')
         self.dut.force_stop_apk("com.google.eecoexer")
 
-    def stop_cell_tx(self):
+    def derive_sweep_list(self, data):
+        """
+        Derive sweep list from config
+        Args:
+            data: GNSS simulator scenario power setting.
+                type, dictionary.
+        """
+        match_tag = r'(?P<sat>[a-z]+)_(?P<band>[a-z]+\d\S*)'
+        sweep_all_ls = []
+        set_all_ls = []
+        regex_match = re.compile(match_tag)
+        method = data.get('method')
+        for key, value in data.items():
+            result = regex_match.search(key)
+            if result:
+                set_all_ls.append(result.groupdict())
+                sweep_all_ls.append(
+                    range_wi_end(self.dut, value[0], value[1], value[2]))
+        if method == 'product':
+            swp_result_ls = list(product(*sweep_all_ls))
+        else:
+            swp_result_ls = list(zip(*sweep_all_ls))
+
+        self.log.debug(f'set_all_ls: {set_all_ls}')
+        self.log.debug(f'swp_result_ls: {swp_result_ls}')
+        return set_all_ls, swp_result_ls
+
+    def unpack_gnss_pwr_sweep(self):
+        """ Unpack gnss_pwr_sweep and construct sweep parameters
+        """
+
+        for key, value in self.gnss_pwr_sweep.items():
+            if key == 'init':
+                self.gnss_pwr_sweep_init_ls = []
+                self.log.info(f'Sweep: {value}')
+                result = self.derive_sweep_list(value)
+                self.gnss_pwr_sweep_init_ls.append(result)
+            elif key == 'fine_sweep':
+                self.gnss_pwr_sweep_fine_sweep_ls = []
+                self.log.info(f'Sweep: {value}')
+                result = self.derive_sweep_list(value)
+                self.gnss_pwr_sweep_fine_sweep_ls.append(result)
+            else:
+                self.log.error(f'{key} is a unsupported key in gnss_pwr_sweep.')
+
+    def stop_coex_tx(self):
         """
         Stop EEcoexer Tx power.
         """
-        # EEcoexer cellular stop Tx command.
-        stop_cell_tx_cmd = 'CELLR,19'
-
         # Stop cellular Tx by EEcoexer.
-        self.log.info('Stop EEcoexer Test Command: {}'.format(stop_cell_tx_cmd))
-        excute_eecoexer_function(self.dut, stop_cell_tx_cmd)
+        if self.coex_stop_cmd:
+            self.log.info(f'Stop EEcoexer Test Command: {self.coex_stop_cmd}')
+            execute_eecoexer_function(self.dut, self.coex_stop_cmd)
 
     def analysis_ttff_ffpe(self, ttff_data, json_tag=''):
         """
@@ -208,45 +250,81 @@
         get_gpstool_logs(self.dut, gps_log_path, False)
 
         # Parsing the log of GTW GPStool into pandas dataframe.
-        target_log_name_regx = os.path.join(gps_log_path, 'GPSLogs', 'files',
-                                            'GNSS_*')
-        self.log.info('Get GPStool logs from: {}'.format(target_log_name_regx))
-        gps_api_log_ls = glob(target_log_name_regx)
+        target_dir = os.path.join(gps_log_path, 'GPSLogs', 'files')
+        gps_api_log_ls = glob_re(self.dut, target_dir, r'GNSS_\d+')
         latest_gps_api_log = max(gps_api_log_ls, key=os.path.getctime)
-        self.log.info(
-            'Get latest GPStool log is: {}'.format(latest_gps_api_log))
+        self.log.info(f'Get latest GPStool log is: {latest_gps_api_log}')
+        df_ttff_ffpe = DataFrame(
+            parse_gpstool_ttfflog_to_df(latest_gps_api_log))
+        # Add test case, TTFF and FFPE data into the dataframe.
+        ttff_dict = {}
+        for i in ttff_data:
+            data = ttff_data[i]._asdict()
+            ttff_dict[i] = dict(data)
+
+        ttff_data_df = DataFrame(ttff_dict).transpose()
+        ttff_data_df = ttff_data_df[[
+            'ttff_loop', 'ttff_sec', 'ttff_pe', 'ttff_haccu'
+        ]]
         try:
-            df_ttff_ffpe = DataFrame(
-                parse_gpstool_ttfflog_to_df(latest_gps_api_log))
+            df_ttff_ffpe = merge(df_ttff_ffpe,
+                                 ttff_data_df,
+                                 left_on='loop',
+                                 right_on='ttff_loop')
+        except:  # pylint: disable=bare-except
+            self.log.warning("Can't merge ttff_data and df.")
+        df_ttff_ffpe['test_case'] = json_tag
 
-            # Add test case, TTFF and FFPE data into the dataframe.
-            ttff_dict = {}
-            for i in ttff_data:
-                data = ttff_data[i]._asdict()
-                ttff_dict[i] = dict(data)
-            ttff_time = []
-            ttff_pe = []
-            test_case = []
-            for value in ttff_dict.values():
-                ttff_time.append(value['ttff_sec'])
-                ttff_pe.append(value['ttff_pe'])
-                test_case.append(json_tag)
-            self.log.info('test_case length {}'.format(str(len(test_case))))
+        json_file = f'gps_log_{json_tag}.json'
+        ttff_data_json_file = f'gps_log_{json_tag}_ttff_data.json'
+        json_path = os.path.join(gps_log_path, json_file)
+        ttff_data_json_path = os.path.join(gps_log_path, ttff_data_json_file)
+        # Save dataframe into json file.
+        df_ttff_ffpe.to_json(json_path, orient='table', index=False)
+        ttff_data_df.to_json(ttff_data_json_path, orient='table', index=False)
 
-            df_ttff_ffpe['test_case'] = test_case
-            df_ttff_ffpe['ttff_sec'] = ttff_time
-            df_ttff_ffpe['ttff_pe'] = ttff_pe
-            json_file = 'gps_log_{}.json'.format(json_tag)
-            json_path = os.path.join(gps_log_path, json_file)
-            # Save dataframe into json file.
-            df_ttff_ffpe.to_json(json_path, orient='table', index=False)
-        except ValueError:
-            self.log.warning('Can\'t create the parsed the log data in file.')
+    def hot_start_ttff_ffpe_process(self, iteration, wait):
+        """
+        Function to run hot start TTFF/FFPE by GTW GPSTool
+        Args:
+            iteration: TTFF/FFPE test iteration.
+                type, integer.
+            wait: wait time before the hot start TTFF/FFPE test.
+                type, integer.
+        """
+        # Start GTW GPStool.
+        self.dut.log.info("Restart GTW GPSTool")
+        start_gnss_by_gtw_gpstool(self.dut, state=True)
+        if wait > 0:
+            self.log.info(
+                f'Wait for {wait} seconds before TTFF to acquire data.')
+            sleep(wait)
+        # Get current time and convert to human readable format
+        begin_time = get_current_epoch_time()
+        log_begin_time = epoch_to_log_line_timestamp(begin_time)
+        self.dut.log.debug(f'Start time is {log_begin_time}')
+
+        # Run hot start TTFF
+        for i in range(3):
+            self.log.info(f'Start hot start attempt {i + 1}')
+            self.dut.adb.shell(
+                f'am broadcast -a com.android.gpstool.ttff_action '
+                f'--es ttff hs --es cycle {iteration} --ez raninterval False')
+            sleep(1)
+            if self.dut.search_logcat(
+                    "act=com.android.gpstool.start_test_action", begin_time):
+                self.dut.log.info("Send TTFF start_test_action successfully.")
+                break
+        else:
+            check_current_focus_app(self.dut)
+            raise TestError("Fail to send TTFF start_test_action.")
+        return begin_time
 
     def gnss_hot_start_ttff_ffpe_test(self,
                                       iteration,
                                       sweep_enable=False,
-                                      json_tag=''):
+                                      json_tag='',
+                                      wait=0):
         """
         GNSS hot start ttff ffpe tset
 
@@ -261,40 +339,26 @@
                     this as a part of file name to save TTFF and FFPE results into json file.
                     Type, str.
                     Default, ''.
+            wait: wait time before ttff test.
+                    Type, int.
+                    Default, 0.
         Raise:
             TestError: fail to send TTFF start_test_action.
         """
-        # Start GTW GPStool.
         test_type = namedtuple('Type', ['command', 'criteria'])
-        test_type_ttff = test_type('Hot Start', self.hs_ttff_criteria)
+        test_type_ttff = test_type('Hot Start', self.hs_criteria)
         test_type_pe = test_type('Hot Start', self.hs_ttff_pecriteria)
-        self.dut.log.info("Restart GTW GPSTool")
-        start_gnss_by_gtw_gpstool(self.dut, state=True)
-
-        # Get current time and convert to human readable format
-        begin_time = get_current_epoch_time()
-        log_begin_time = epoch_to_log_line_timestamp(begin_time)
-        self.dut.log.debug('Start time is {}'.format(log_begin_time))
-
-        # Run hot start TTFF
-        for i in range(3):
-            self.log.info('Start hot start attempt %d' % (i + 1))
-            self.dut.adb.shell(
-                "am broadcast -a com.android.gpstool.ttff_action "
-                "--es ttff hs --es cycle {} --ez raninterval False".format(
-                    iteration))
-            sleep(1)
-            if self.dut.search_logcat(
-                    "act=com.android.gpstool.start_test_action", begin_time):
-                self.dut.log.info("Send TTFF start_test_action successfully.")
-                break
-        else:
-            check_current_focus_app(self.dut)
-            raise TestError("Fail to send TTFF start_test_action.")
 
         # Verify hot start TTFF results
-        ttff_data = process_ttff_by_gtw_gpstool(self.dut, begin_time,
-                                                self.simulator_location)
+        begin_time = self.hot_start_ttff_ffpe_process(iteration, wait)
+        try:
+            ttff_data = process_ttff_by_gtw_gpstool(self.dut, begin_time,
+                                                    self.simulator_location)
+        except:  # pylint: disable=bare-except
+            self.log.warning('Fail to acquire TTFF data. Retry again.')
+            begin_time = self.hot_start_ttff_ffpe_process(iteration, wait)
+            ttff_data = process_ttff_by_gtw_gpstool(self.dut, begin_time,
+                                                    self.simulator_location)
 
         # Stop GTW GPSTool
         self.dut.log.info("Stop GTW GPSTool")
@@ -320,10 +384,8 @@
         return True
 
     def hot_start_gnss_power_sweep(self,
-                                   start_pwr,
-                                   stop_pwr,
-                                   offset,
-                                   wait,
+                                   sweep_ls,
+                                   wait=0,
                                    iteration=1,
                                    sweep_enable=False,
                                    title=''):
@@ -331,14 +393,11 @@
         GNSS simulator power sweep of hot start test.
 
         Args:
-            start_pwr: GNSS simulator power sweep start power level.
-                    Type, int.
-            stop_pwr: GNSS simulator power sweep stop power level.
-                    Type, int.
-            offset: GNSS simulator power sweep offset
-                    Type, int.
+            sweep_ls: list of sweep parameters.
+                    Type, tuple.
             wait: Wait time before the power sweep.
                     Type, int.
+                    Default, 0.
             iteration: The iteration times of hot start test.
                     Type, int.
                     Default, 1.
@@ -349,15 +408,15 @@
             title: the target log folder title for GNSS sensitivity search test items.
                     Type, str.
                     Default, ''.
+        Return:
+            Bool, gnss_pwr_params.
         """
 
         # Calculate loop range list from gnss_simulator_power_level and sa_sensitivity
-        range_ls = range_wi_end(self.dut, start_pwr, stop_pwr, offset)
-        sweep_range = ','.join([str(x) for x in range_ls])
 
         self.log.debug(
-            'Start the GNSS simulator power sweep. The sweep range is [{}]'.
-            format(sweep_range))
+            f'Start the GNSS simulator power sweep. The sweep tuple is [{sweep_ls}]'
+        )
 
         if sweep_enable:
             self.start_gnss_and_wait(wait)
@@ -368,21 +427,41 @@
         # Sweep GNSS simulator power level in range_ls.
         # Do hot start for every power level.
         # Check the TTFF result if it can pass the criteria.
-        gnss_pwr_lvl = -130
-        for gnss_pwr_lvl in range_ls:
-
-            # Set GNSS Simulator power level
-            self.log.info('Set GNSS simulator power level to %.1f' %
-                          gnss_pwr_lvl)
-            self.gnss_simulator.set_power(gnss_pwr_lvl)
-            json_tag = title + '_gnss_pwr_' + str(gnss_pwr_lvl)
-
+        gnss_pwr_params = None
+        previous_pwr_lvl = None
+        current_pwr_lvl = None
+        return_pwr_lvl = {}
+        for j, gnss_pwr_params in enumerate(sweep_ls[1]):
+            json_tag = f'{title}_'
+            current_pwr_lvl = gnss_pwr_params
+            if j == 0:
+                previous_pwr_lvl = current_pwr_lvl
+            for i, pwr in enumerate(gnss_pwr_params):
+                sat_sys = sweep_ls[0][i].get('sat').upper()
+                band = sweep_ls[0][i].get('band').upper()
+                # Set GNSS Simulator power level
+                self.gnss_simulator.ping_inst()
+                self.gnss_simulator.set_scenario_power(power_level=pwr,
+                                                       sat_system=sat_sys,
+                                                       freq_band=band)
+                self.log.info(f'Set {sat_sys} {band} with power {pwr}')
+                json_tag = json_tag + f'{sat_sys}_{band}_{pwr}'
+            # Wait 30 seconds if major power sweep level is changed.
+            wait = 0
+            if j > 0:
+                if current_pwr_lvl[0] != previous_pwr_lvl[0]:
+                    wait = 30
             # GNSS hot start test
             if not self.gnss_hot_start_ttff_ffpe_test(iteration, sweep_enable,
-                                                      json_tag):
-                sensitivity = gnss_pwr_lvl - offset
-                return False, sensitivity
-        return True, gnss_pwr_lvl
+                                                      json_tag, wait):
+                result = False
+                break
+            previous_pwr_lvl = current_pwr_lvl
+        result = True
+        for i, pwr in enumerate(previous_pwr_lvl):
+            key = f'{sweep_ls[0][i].get("sat").upper()}_{sweep_ls[0][i].get("band").upper()}'
+            return_pwr_lvl.setdefault(key, pwr)
+        return result, return_pwr_lvl
 
     def gnss_init_power_setting(self, first_wait=180):
         """
@@ -398,39 +477,22 @@
         """
 
         # Start and set GNSS simulator
-        self.start_and_set_gnss_simulator_power()
+        self.start_set_gnss_power()
 
         # Start 1st time cold start to obtain ephemeris
         process_gnss_by_gtw_gpstool(self.dut, self.test_types['cs'].criteria)
 
-        self.hot_start_gnss_power_sweep(self.gnss_simulator_power_level,
-                                        self.sa_sensitivity,
-                                        self.gnss_pwr_lvl_offset, first_wait)
+        # Read initial power sweep settings
+        if self.gnss_pwr_sweep_init_ls:
+            for sweep_ls in self.gnss_pwr_sweep_init_ls:
+                ret, gnss_pwr_lvl = self.hot_start_gnss_power_sweep(
+                    sweep_ls, first_wait)
+        else:
+            self.log.warning('Skip initial power sweep.')
+            ret = False
+            gnss_pwr_lvl = None
 
-        return True
-
-    def start_gnss_and_wait(self, wait=60):
-        """
-        The process of enable gnss and spend the wait time for GNSS to
-        gather enoung information that make sure the stability of testing.
-
-        Args:
-            wait: wait time between power sweep.
-                Type, int.
-                Default, 60.
-        """
-        # Create log path for waiting section logs of GPStool.
-        gnss_wait_log_dir = os.path.join(self.gnss_log_path, 'GNSS_wait')
-
-        # Enable GNSS to receive satellites' signals for "wait_between_pwr" seconds.
-        self.log.info('Enable GNSS for searching satellites')
-        start_gnss_by_gtw_gpstool(self.dut, state=True)
-        self.log.info('Wait for {} seconds'.format(str(wait)))
-        sleep(wait)
-
-        # Stop GNSS and pull the logs.
-        start_gnss_by_gtw_gpstool(self.dut, state=False)
-        get_gpstool_logs(self.dut, gnss_wait_log_dir, False)
+        return ret, gnss_pwr_lvl
 
     def cell_power_sweep(self):
         """
@@ -449,9 +511,6 @@
         power_search_ls = range_wi_end(self.dut, self.start_pwr, self.stop_pwr,
                                        self.offset)
 
-        # Set GNSS simulator power level.
-        self.gnss_simulator.set_power(self.sa_sensitivity)
-
         # Create gnss log folders for init and cellular sweep
         gnss_init_log_dir = os.path.join(self.gnss_log_path, 'GNSS_init')
 
@@ -461,8 +520,8 @@
         if power_search_ls:
             # Run the cellular and GNSS coexistence test item.
             for i, pwr_lvl in enumerate(power_search_ls):
-                self.log.info('Cellular power sweep loop: {}'.format(int(i)))
-                self.log.info('Cellular target power: {}'.format(int(pwr_lvl)))
+                self.log.info(f'Cellular power sweep loop: {i}')
+                self.log.info(f'Cellular target power: {pwr_lvl}')
 
                 # Enable GNSS to receive satellites' signals for "wait_between_pwr" seconds.
                 # Wait more time before 1st power level
@@ -473,9 +532,9 @@
                 self.start_gnss_and_wait(wait)
 
                 # Set cellular Tx power level.
-                eecoex_cmd = self.eecoex_func.format(str(pwr_lvl))
+                eecoex_cmd = self.eecoex_func.format(pwr_lvl)
                 eecoex_cmd_file_str = eecoex_cmd.replace(',', '_')
-                excute_eecoexer_function(self.dut, eecoex_cmd)
+                execute_eecoexer_function(self.dut, eecoex_cmd)
 
                 # Get the last power level that can pass hots start ttff/ffpe spec.
                 if self.gnss_hot_start_ttff_ffpe_test(ttft_iteration, True,
@@ -489,7 +548,7 @@
                         power_th = power_search_ls[i - 1]
 
                 # Stop cellular Tx after a test cycle.
-                self.stop_cell_tx()
+                self.stop_coex_tx()
 
         else:
             # Run the stand alone test item.
@@ -499,7 +558,6 @@
             self.gnss_hot_start_ttff_ffpe_test(ttft_iteration, True,
                                                eecoex_cmd_file_str)
 
-        self.log.info('The GNSS WWAN coex celluar Tx power is {}'.format(
-            str(power_th)))
+        self.log.info(f'The GNSS WWAN coex celluar Tx power is {power_th}')
 
         return power_th
diff --git a/acts_tests/acts_contrib/test_utils/gnss/LabTtffTestBase.py b/acts_tests/acts_contrib/test_utils/gnss/LabTtffTestBase.py
index 6a6bd5d..77ebcf6 100644
--- a/acts_tests/acts_contrib/test_utils/gnss/LabTtffTestBase.py
+++ b/acts_tests/acts_contrib/test_utils/gnss/LabTtffTestBase.py
@@ -13,35 +13,51 @@
 #   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.
+'''GNSS Base Class for Lab TTFF/FFPE'''
 
 import os
 import time
-import glob
 import errno
+import re
 from collections import namedtuple
-from pandas import DataFrame
-from acts import utils
-from acts import signals
-from acts.base_test import BaseTestClass
-from acts.controllers.gnss_lib import GnssSimulator
-from acts.context import get_current_context
-from acts_contrib.test_utils.gnss import dut_log_test_utils as diaglog
-from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
-from acts_contrib.test_utils.gnss import gnss_testlog_utils as glogutils
+from pandas import DataFrame, merge
 from acts_contrib.test_utils.gnss.gnss_defines import DEVICE_GPSLOG_FOLDER
 from acts_contrib.test_utils.gnss.gnss_defines import GPS_PKG_NAME
 from acts_contrib.test_utils.gnss.gnss_defines import BCM_GPS_XML_PATH
+from acts_contrib.test_utils.gnss import dut_log_test_utils as diaglog
+from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
+from acts_contrib.test_utils.gnss import gnss_testlog_utils as glogutils
+from acts import utils
+from acts import signals
+from acts.controllers.gnss_lib import GnssSimulator
+from acts.context import get_current_context
+from acts.base_test import BaseTestClass
+
+
+def glob_re(dut, directory, regex_tag):
+    """glob with regular expression method.
+    Args:
+        dut: An AndroidDevice object.
+        directory: Target directory path.
+           Type, str
+        regex_tag: regular expression format string.
+           Type, str
+    Return:
+        result_ls: list of glob result
+    """
+    all_files_in_dir = os.listdir(directory)
+    dut.log.debug(f'glob_re dir: {all_files_in_dir}')
+    target_log_name_regx = re.compile(regex_tag)
+    tmp_ls = list(filter(target_log_name_regx.match, all_files_in_dir))
+    result_ls = [os.path.join(directory, file) for file in tmp_ls]
+    dut.log.debug(f'glob_re list: {result_ls}')
+    return result_ls
 
 
 class LabTtffTestBase(BaseTestClass):
     """ LAB TTFF Tests Base Class"""
     GTW_GPSTOOL_APP = 'gtw_gpstool_apk'
-    GNSS_SIMULATOR_KEY = 'gnss_simulator'
-    GNSS_SIMULATOR_IP_KEY = 'gnss_simulator_ip'
-    GNSS_SIMULATOR_PORT_KEY = 'gnss_simulator_port'
-    GNSS_SIMULATOR_PORT_CTRL_KEY = 'gnss_simulator_port_ctrl'
-    GNSS_SIMULATOR_SCENARIO_KEY = 'gnss_simulator_scenario'
-    GNSS_SIMULATOR_POWER_LEVEL_KEY = 'gnss_simulator_power_level'
+    GNSS_SIMULATOR_KEY = 'gnss_sim_params'
     CUSTOM_FILES_KEY = 'custom_files'
     CSTTFF_CRITERIA = 'cs_criteria'
     HSTTFF_CRITERIA = 'hs_criteria'
@@ -52,76 +68,106 @@
     TTFF_ITERATION = 'ttff_iteration'
     SIMULATOR_LOCATION = 'simulator_location'
     DIAG_OPTION = 'diag_option'
+    SCENARIO_POWER = 'scenario_power'
+    MDSAPP = 'mdsapp'
+    MASKFILE = 'maskfile'
+    MODEMPARFILE = 'modemparfile'
+    NV_DICT = 'nv_dict'
+    TTFF_TIMEOUT = 'ttff_timeout'
 
     def __init__(self, controllers):
         """ Initializes class attributes. """
 
         super().__init__(controllers)
-
         self.dut = None
         self.gnss_simulator = None
         self.rockbottom_script = None
         self.gnss_log_path = self.log_path
         self.gps_xml_bk_path = BCM_GPS_XML_PATH + '.bk'
+        self.gpstool_ver = ''
+        self.test_params = None
+        self.custom_files = None
+        self.maskfile = None
+        self.mdsapp = None
+        self.modemparfile = None
+        self.nv_dict = None
+        self.scenario_power = None
+        self.ttff_timeout = None
+        self.test_types = None
+        self.simulator_location = None
+        self.gnss_simulator_scenario = None
+        self.gnss_simulator_power_level = None
 
     def setup_class(self):
         super().setup_class()
 
-        req_params = [
-            self.GNSS_SIMULATOR_KEY, self.GNSS_SIMULATOR_IP_KEY,
-            self.GNSS_SIMULATOR_PORT_KEY, self.GNSS_SIMULATOR_SCENARIO_KEY,
-            self.GNSS_SIMULATOR_POWER_LEVEL_KEY, self.CSTTFF_CRITERIA,
-            self.HSTTFF_CRITERIA, self.WSTTFF_CRITERIA, self.TTFF_ITERATION,
-            self.SIMULATOR_LOCATION, self.DIAG_OPTION
-        ]
+        # Update parameters by test case configurations.
+        test_param = self.TAG + '_params'
+        self.test_params = self.user_params.get(test_param, {})
+        if not self.test_params:
+            self.log.warning(test_param + ' was not found in the user '
+                             'parameters defined in the config file.')
 
+        # Override user_param values with test parameters
+        self.user_params.update(self.test_params)
+
+        # Unpack user_params with default values. All the usages of user_params
+        # as self attributes need to be included either as a required parameter
+        # or as a parameter with a default value.
+
+        # Required parameters
+        req_params = [
+            self.CSTTFF_PECRITERIA, self.WSTTFF_PECRITERIA, self.HSTTFF_PECRITERIA,
+            self.CSTTFF_CRITERIA, self.HSTTFF_CRITERIA, self.WSTTFF_CRITERIA,
+            self.TTFF_ITERATION, self.GNSS_SIMULATOR_KEY, self.DIAG_OPTION,
+            self.GTW_GPSTOOL_APP
+        ]
         self.unpack_userparams(req_param_names=req_params)
+
+        # Optional parameters
+        self.custom_files = self.user_params.get(self.CUSTOM_FILES_KEY,[])
+        self.maskfile = self.user_params.get(self.MASKFILE,'')
+        self.mdsapp = self.user_params.get(self.MDSAPP,'')
+        self.modemparfile = self.user_params.get(self.MODEMPARFILE,'')
+        self.nv_dict = self.user_params.get(self.NV_DICT,{})
+        self.scenario_power = self.user_params.get(self.SCENARIO_POWER, [])
+        self.ttff_timeout = self.user_params.get(self.TTFF_TIMEOUT, 60)
+
+        # Set TTFF Spec.
+        test_type = namedtuple('Type', ['command', 'criteria'])
+        self.test_types = {
+            'cs': test_type('Cold Start', self.cs_criteria),
+            'ws': test_type('Warm Start', self.ws_criteria),
+            'hs': test_type('Hot Start', self.hs_criteria)
+        }
+
         self.dut = self.android_devices[0]
-        self.gnss_simulator_scenario = self.user_params[
-            self.GNSS_SIMULATOR_SCENARIO_KEY]
-        self.gnss_simulator_power_level = self.user_params[
-            self.GNSS_SIMULATOR_POWER_LEVEL_KEY]
-        self.gtw_gpstool_app = self.user_params[self.GTW_GPSTOOL_APP]
-        custom_files = self.user_params.get(self.CUSTOM_FILES_KEY, [])
-        self.cs_ttff_criteria = self.user_params.get(self.CSTTFF_CRITERIA, [])
-        self.hs_ttff_criteria = self.user_params.get(self.HSTTFF_CRITERIA, [])
-        self.ws_ttff_criteria = self.user_params.get(self.WSTTFF_CRITERIA, [])
-        self.cs_ttff_pecriteria = self.user_params.get(self.CSTTFF_PECRITERIA,
-                                                       [])
-        self.hs_ttff_pecriteria = self.user_params.get(self.HSTTFF_PECRITERIA,
-                                                       [])
-        self.ws_ttff_pecriteria = self.user_params.get(self.WSTTFF_PECRITERIA,
-                                                       [])
-        self.ttff_iteration = self.user_params.get(self.TTFF_ITERATION, [])
-        self.simulator_location = self.user_params.get(self.SIMULATOR_LOCATION,
-                                                       [])
-        self.diag_option = self.user_params.get(self.DIAG_OPTION, [])
+
+        # GNSS Simulator Setup
+        self.simulator_location = self.gnss_sim_params.get(
+            self.SIMULATOR_LOCATION, [])
+        self.gnss_simulator_scenario = self.gnss_sim_params.get('scenario')
+        self.gnss_simulator_power_level = self.gnss_sim_params.get(
+            'power_level')
 
         # Create gnss_simulator instance
-        gnss_simulator_key = self.user_params[self.GNSS_SIMULATOR_KEY]
-        gnss_simulator_ip = self.user_params[self.GNSS_SIMULATOR_IP_KEY]
-        gnss_simulator_port = self.user_params[self.GNSS_SIMULATOR_PORT_KEY]
+        gnss_simulator_key = self.gnss_sim_params.get('type')
+        gnss_simulator_ip = self.gnss_sim_params.get('ip')
+        gnss_simulator_port = self.gnss_sim_params.get('port')
         if gnss_simulator_key == 'gss7000':
-            gnss_simulator_port_ctrl = self.user_params[
-                self.GNSS_SIMULATOR_PORT_CTRL_KEY]
+            gnss_simulator_port_ctrl = self.gnss_sim_params.get('port_ctrl')
         else:
             gnss_simulator_port_ctrl = None
         self.gnss_simulator = GnssSimulator.AbstractGnssSimulator(
             gnss_simulator_key, gnss_simulator_ip, gnss_simulator_port,
             gnss_simulator_port_ctrl)
 
-        test_type = namedtuple('Type', ['command', 'criteria'])
-        self.test_types = {
-            'cs': test_type('Cold Start', self.cs_ttff_criteria),
-            'ws': test_type('Warm Start', self.ws_ttff_criteria),
-            'hs': test_type('Hot Start', self.hs_ttff_criteria)
-        }
-
         # Unpack the rockbottom script file if its available.
-        for file in custom_files:
-            if 'rockbottom_' + self.dut.model in file:
-                self.rockbottom_script = file
-                break
+        if self.custom_files:
+            for file in self.custom_files:
+                if 'rockbottom_' + self.dut.model in file:
+                    self.rockbottom_script = file
+                    break
 
     def setup_test(self):
 
@@ -129,25 +175,43 @@
         self.gnss_simulator.stop_scenario()
         self.gnss_simulator.close()
         if self.rockbottom_script:
-            self.log.info('Running rockbottom script for this device ' +
-                          self.dut.model)
+            self.log.info(
+                f'Running rockbottom script for this device {self.dut.model}')
             self.dut_rockbottom()
         else:
-            self.log.info('Not running rockbottom for this device ' +
-                          self.dut.model)
+            self.log.info(
+                f'Not running rockbottom for this device {self.dut.model}')
 
         utils.set_location_service(self.dut, True)
         gutils.reinstall_package_apk(self.dut, GPS_PKG_NAME,
-                                     self.gtw_gpstool_app)
+                                     self.gtw_gpstool_apk)
+        gpstool_ver_cmd = f'dumpsys package {GPS_PKG_NAME} | grep versionName'
+        self.gpstool_ver = self.dut.adb.shell(gpstool_ver_cmd).split('=')[1]
+        self.log.info(f'GTW GPSTool version: {self.gpstool_ver}')
 
         # For BCM DUTs, delete gldata.sto and set IgnoreRomAlm="true" based on b/196936791#comment20
         if self.diag_option == "BCM":
             gutils.remount_device(self.dut)
             # Backup gps.xml
-            copy_cmd = "cp {} {}".format(BCM_GPS_XML_PATH, self.gps_xml_bk_path)
+            if self.dut.file_exists(BCM_GPS_XML_PATH):
+                copy_cmd = f'cp {BCM_GPS_XML_PATH} {self.gps_xml_bk_path}'
+            elif self.dut.file_exists(self.gps_xml_bk_path):
+                self.log.debug(f'{BCM_GPS_XML_PATH} is missing')
+                self.log.debug(
+                    f'Copy {self.gps_xml_bk_path} and rename to {BCM_GPS_XML_PATH}'
+                )
+                copy_cmd = f'cp {self.gps_xml_bk_path} {BCM_GPS_XML_PATH}'
+            else:
+                self.log.error(
+                    f'Missing both {BCM_GPS_XML_PATH} and {self.gps_xml_bk_path} in DUT'
+                )
+                raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT),
+                                        self.gps_xml_bk_path)
             self.dut.adb.shell(copy_cmd)
             gutils.delete_bcm_nvmem_sto_file(self.dut)
             gutils.bcm_gps_ignore_rom_alm(self.dut)
+            if self.current_test_name == "test_tracking_power_sweep":
+                gutils.bcm_gps_ignore_warmstandby(self.dut)
             # Reboot DUT to apply the setting
             gutils.reboot(self.dut)
         self.gnss_simulator.connect()
@@ -160,9 +224,9 @@
         # The rockbottom script might include a device reboot, so it is
         # necessary to stop SL4A during its execution.
         self.dut.stop_services()
-        self.log.info('Executing rockbottom script for ' + self.dut.model)
+        self.log.info(f'Executing rockbottom script for {self.dut.model}')
         os.chmod(self.rockbottom_script, 0o777)
-        os.system('{} {}'.format(self.rockbottom_script, self.dut.serial))
+        os.system(f'{self.rockbottom_script} {self.dut.serial}')
         # Make sure the DUT is in root mode after coming back
         self.dut.root_adb()
         # Restart SL4A
@@ -174,9 +238,9 @@
         # Restore the gps.xml everytime after the test.
         if self.diag_option == "BCM":
             # Restore gps.xml
-            rm_cmd = "rm -rf {}".format(BCM_GPS_XML_PATH)
-            restore_cmd = "mv {} {}".format(self.gps_xml_bk_path,
-                                            BCM_GPS_XML_PATH)
+            gutils.remount_device(self.dut)
+            rm_cmd = f'rm -rf {BCM_GPS_XML_PATH}'
+            restore_cmd = f'cp {self.gps_xml_bk_path} {BCM_GPS_XML_PATH}'
             self.dut.adb.shell(rm_cmd)
             self.dut.adb.shell(restore_cmd)
 
@@ -187,7 +251,7 @@
             self.gnss_simulator.stop_scenario()
             self.gnss_simulator.close()
 
-    def start_and_set_gnss_simulator_power(self):
+    def start_set_gnss_power(self):
         """
         Start GNSS simulator secnario and set power level.
 
@@ -195,7 +259,24 @@
 
         self.gnss_simulator.start_scenario(self.gnss_simulator_scenario)
         time.sleep(25)
-        self.gnss_simulator.set_power(self.gnss_simulator_power_level)
+        if self.scenario_power:
+            self.log.info(
+                'Set GNSS simulator power with power_level by scenario_power')
+            for setting in self.scenario_power:
+                power_level = setting.get('power_level', -130)
+                sat_system = setting.get('sat_system', '')
+                freq_band = setting.get('freq_band', 'ALL')
+                sat_id = setting.get('sat_id', '')
+                self.log.debug(f'sat: {sat_system}; freq_band: {freq_band}, '
+                               f'power_level: {power_level}, sat_id: {sat_id}')
+                self.gnss_simulator.set_scenario_power(power_level,
+                                                       sat_id,
+                                                       sat_system,
+                                                       freq_band)
+        else:
+            self.log.debug('Set GNSS simulator power '
+                           f'with power_level: {self.gnss_simulator_power_level}')
+            self.gnss_simulator.set_power(self.gnss_simulator_power_level)
 
     def get_and_verify_ttff(self, mode):
         """Retrieve ttff with designate mode.
@@ -204,14 +285,9 @@
                 mode: A string for identify gnss test mode.
         """
         if mode not in self.test_types:
-            raise signals.TestError('Unrecognized mode %s' % mode)
+            raise signals.TestError(f'Unrecognized mode {mode}')
         test_type = self.test_types.get(mode)
 
-        if mode != 'cs':
-            wait_time = 900
-        else:
-            wait_time = 300
-
         gutils.process_gnss_by_gtw_gpstool(self.dut,
                                            self.test_types['cs'].criteria)
         begin_time = gutils.get_current_epoch_time()
@@ -219,12 +295,12 @@
                                          ttff_mode=mode,
                                          iteration=self.ttff_iteration,
                                          raninterval=True,
-                                         hot_warm_sleep=wait_time)
+                                         hot_warm_sleep=3,
+                                         timeout=self.ttff_timeout)
         # Since Wear takes little longer to update the TTFF info.
         # Workround to solve the wearable timing issue
         if gutils.is_device_wearable(self.dut):
             time.sleep(20)
-
         ttff_data = gutils.process_ttff_by_gtw_gpstool(self.dut, begin_time,
                                                        self.simulator_location)
 
@@ -232,41 +308,43 @@
         gps_log_path = os.path.join(self.gnss_log_path, 'GPSLogs')
         os.makedirs(gps_log_path, exist_ok=True)
 
-        self.dut.adb.pull("{} {}".format(DEVICE_GPSLOG_FOLDER, gps_log_path))
-
-        gps_api_log = glob.glob(gps_log_path + '/*/GNSS_*.txt')
-        ttff_loop_log = glob.glob(gps_log_path +
-                                  '/*/GPS_{}_*.txt'.format(mode.upper()))
+        self.dut.adb.pull(f'{DEVICE_GPSLOG_FOLDER} {gps_log_path}')
+        local_log_dir = os.path.join(gps_log_path, 'files')
+        gps_api_log = glob_re(self.dut, local_log_dir, r'GNSS_\d+')
+        ttff_loop_log = glob_re(self.dut, local_log_dir,
+                                fr'\w+_{mode.upper()}_\d+')
 
         if not gps_api_log and ttff_loop_log:
             raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT),
                                     gps_log_path)
 
-        df = DataFrame(glogutils.parse_gpstool_ttfflog_to_df(gps_api_log[0]))
+        df_ttff_ffpe = DataFrame(glogutils.parse_gpstool_ttfflog_to_df(gps_api_log[0]))
 
         ttff_dict = {}
         for i in ttff_data:
-            d = ttff_data[i]._asdict()
-            ttff_dict[i] = dict(d)
+            data = ttff_data[i]._asdict()
+            ttff_dict[i] = dict(data)
 
-        ttff_time = []
-        ttff_pe = []
-        ttff_haccu = []
-        for i in ttff_dict.keys():
-            ttff_time.append(ttff_dict[i]['ttff_sec'])
-            ttff_pe.append(ttff_dict[i]['ttff_pe'])
-            ttff_haccu.append(ttff_dict[i]['ttff_haccu'])
-        df['ttff_sec'] = ttff_time
-        df['ttff_pe'] = ttff_pe
-        df['ttff_haccu'] = ttff_haccu
-        df.to_json(gps_log_path + '/gps_log.json', orient='table')
+        ttff_data_df = DataFrame(ttff_dict).transpose()
+        ttff_data_df = ttff_data_df[[
+            'ttff_loop', 'ttff_sec', 'ttff_pe', 'ttff_haccu'
+        ]]
+        try:
+            df_ttff_ffpe = merge(df_ttff_ffpe, ttff_data_df, left_on='loop', right_on='ttff_loop')
+        except: # pylint: disable=bare-except
+            self.log.warning("Can't merge ttff_data and df.")
+        ttff_data_df.to_json(gps_log_path + '/gps_log_ttff_data.json',
+                             orient='table',
+                             index=False)
+        df_ttff_ffpe.to_json(gps_log_path + '/gps_log.json', orient='table', index=False)
         result = gutils.check_ttff_data(self.dut,
                                         ttff_data,
                                         ttff_mode=test_type.command,
                                         criteria=test_type.criteria)
         if not result:
-            raise signals.TestFailure('%s TTFF fails to reach '
-                                      'designated criteria' % test_type.command)
+            raise signals.TestFailure(
+                f'{test_type.command} TTFF fails to reach '
+                'designated criteria')
         return ttff_data
 
     def verify_pe(self, mode):
@@ -285,7 +363,7 @@
         }
 
         if mode not in ffpe_types:
-            raise signals.TestError('Unrecognized mode %s' % mode)
+            raise signals.TestError(f'Unrecognized mode {mode}')
         test_type = ffpe_types.get(mode)
 
         ttff_data = self.get_and_verify_ttff(mode)
@@ -294,8 +372,9 @@
                                       ttff_mode=test_type.command,
                                       pe_criteria=test_type.pecriteria)
         if not result:
-            raise signals.TestFailure('%s TTFF fails to reach '
-                                      'designated criteria' % test_type.command)
+            raise signals.TestFailure(
+                f'{test_type.command} TTFF fails to reach '
+                'designated criteria')
         return ttff_data
 
     def clear_gps_log(self):
@@ -303,9 +382,77 @@
         Delete the existing GPS GTW Log from DUT.
 
         """
-        self.dut.adb.shell("rm -rf {}".format(DEVICE_GPSLOG_FOLDER))
+        self.dut.adb.shell(f'rm -rf {DEVICE_GPSLOG_FOLDER}')
 
-    def gnss_ttff_ffpe(self, mode, sub_context_path=''):
+    def start_dut_gnss_log(self):
+        """Start GNSS chip log according to different diag_option"""
+        # Start GNSS chip log
+        if self.diag_option == "QCOM":
+            diaglog.start_diagmdlog_background(self.dut, maskfile=self.maskfile)
+        else:
+            gutils.start_pixel_logger(self.dut)
+
+    def stop_and_pull_dut_gnss_log(self, gnss_vendor_log_path=None):
+        """
+        Stop DUT GNSS logger and pull log into local PC dir
+            Arg:
+                gnss_vendor_log_path: gnss log path directory.
+                    Type, str.
+                    Default, None
+        """
+        if not gnss_vendor_log_path:
+            gnss_vendor_log_path = self.gnss_log_path
+        if self.diag_option == "QCOM":
+            diaglog.stop_background_diagmdlog(self.dut,
+                                              gnss_vendor_log_path,
+                                              keep_logs=False)
+        else:
+            gutils.stop_pixel_logger(self.dut)
+            self.log.info('Getting Pixel BCM Log!')
+            diaglog.get_pixellogger_bcm_log(self.dut,
+                                            gnss_vendor_log_path,
+                                            keep_logs=False)
+
+    def start_gnss_and_wait(self, wait=60):
+        """
+        The process of enable gnss and spend the wait time for GNSS to
+        gather enoung information that make sure the stability of testing.
+
+        Args:
+            wait: wait time between power sweep.
+                Type, int.
+                Default, 60.
+        """
+        # Create log path for waiting section logs of GPStool.
+        gnss_wait_log_dir = os.path.join(self.gnss_log_path, 'GNSS_wait')
+
+        # Enable GNSS to receive satellites' signals for "wait_between_pwr" seconds.
+        self.log.info('Enable GNSS for searching satellites')
+        gutils.start_gnss_by_gtw_gpstool(self.dut, state=True)
+        self.log.info(f'Wait for {wait} seconds')
+        time.sleep(wait)
+
+        # Stop GNSS and pull the logs.
+        gutils.start_gnss_by_gtw_gpstool(self.dut, state=False)
+        diaglog.get_gpstool_logs(self.dut, gnss_wait_log_dir, False)
+
+    def exe_eecoexer_loop_cmd(self, cmd_list=None):
+        """
+        Function for execute EECoexer command list
+            Args:
+                cmd_list: a list of EECoexer function command.
+                Type, list.
+        """
+        if cmd_list:
+            for cmd in cmd_list:
+                self.log.info('Execute EEcoexer Command: {}'.format(cmd))
+                gutils.execute_eecoexer_function(self.dut, cmd)
+
+    def gnss_ttff_ffpe(self,
+                       mode,
+                       sub_context_path='',
+                       coex_cmd='',
+                       stop_coex_cmd=''):
         """
         Base ttff and ffpe function
             Args:
@@ -317,16 +464,24 @@
         full_output_path = get_current_context().get_full_output_path()
         self.gnss_log_path = os.path.join(full_output_path, sub_context_path)
         os.makedirs(self.gnss_log_path, exist_ok=True)
-        self.log.debug('Create log path: {}'.format(self.gnss_log_path))
+        self.log.debug(f'Create log path: {self.gnss_log_path}')
 
         # Start and set GNSS simulator
-        self.start_and_set_gnss_simulator_power()
+        self.start_set_gnss_power()
 
         # Start GNSS chip log
-        if self.diag_option == "QCOM":
-            diaglog.start_diagmdlog_background(self.dut, maskfile=self.maskfile)
+        self.start_dut_gnss_log()
+
+        # Wait for acquiring almanac
+        if mode != 'cs':
+            wait_time = 900
         else:
-            gutils.start_pixel_logger(self.dut)
+            wait_time = 3
+        self.start_gnss_and_wait(wait=wait_time)
+
+        # Start Coex if available
+        if coex_cmd and stop_coex_cmd:
+            self.exe_eecoexer_loop_cmd(coex_cmd)
 
         # Start verifying TTFF and FFPE
         self.verify_pe(mode)
@@ -337,13 +492,8 @@
         os.makedirs(gnss_vendor_log_path, exist_ok=True)
 
         # Stop GNSS chip log and pull the logs to local file system
-        if self.diag_option == "QCOM":
-            diaglog.stop_background_diagmdlog(self.dut,
-                                              gnss_vendor_log_path,
-                                              keep_logs=False)
-        else:
-            gutils.stop_pixel_logger(self.dut)
-            self.log.info('Getting Pixel BCM Log!')
-            diaglog.get_pixellogger_bcm_log(self.dut,
-                                            gnss_vendor_log_path,
-                                            keep_logs=False)
+        self.stop_and_pull_dut_gnss_log(gnss_vendor_log_path)
+
+        # Stop Coex if available
+        if coex_cmd and stop_coex_cmd:
+            self.exe_eecoexer_loop_cmd(stop_coex_cmd)
diff --git a/acts_tests/acts_contrib/test_utils/gnss/gnss_constant.py b/acts_tests/acts_contrib/test_utils/gnss/gnss_constant.py
new file mode 100644
index 0000000..c7835ac
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/gnss/gnss_constant.py
@@ -0,0 +1 @@
+TTFF_MODE = {"cs": "Cold Start", "ws": "Warm Start", "hs": "Hot Start", "csa": "CSWith Assist"}
diff --git a/acts_tests/acts_contrib/test_utils/gnss/gnss_measurement.py b/acts_tests/acts_contrib/test_utils/gnss/gnss_measurement.py
new file mode 100644
index 0000000..a9f9579
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/gnss/gnss_measurement.py
@@ -0,0 +1,208 @@
+import os
+import pathlib
+import re
+import shutil
+import tempfile
+from collections import defaultdict
+
+
+class AdrInfo:
+    """Represent one ADR value
+    An ADR value is a decimal number range from 0 - 31
+
+    How to parse the ADR value:
+        First, transform the decimal number to binary then we will get a 5 bit number
+        The meaning of each bit is as follow:
+                 0                  0               0         0       0
+        HalfCycleReported   HalfCycleResolved   CycleSlip   Reset   Valid
+    Special rule:
+        For an ADR value in binary fits the pattern: * * 0 0 1, we call it a usable ADR
+    More insight of ADR value:
+        go/adrstates
+
+    Attributes:
+        is_valid: (bool)
+        is_reset: (bool)
+        is_cycle_slip: (bool)
+        is_half_cycle_resolved: (bool)
+        is_half_cycle_reported: (bool)
+        is_usable: (bool)
+    """
+    def __init__(self, adr_value: int, count: int):
+        src = bin(int(adr_value))
+        self._valid = int(src[-1])
+        self._reset = int(src[-2])
+        self._cycle_slip = int(src[-3])
+        self._half_cycle_resolved = int(src[-4])
+        self._half_cycle_reported = int(src[-5])
+        self.count = count
+
+    @property
+    def is_usable(self):
+        return self.is_valid and not self.is_reset and not self.is_cycle_slip
+
+    @property
+    def is_valid(self):
+        return bool(self._valid)
+
+    @property
+    def is_reset(self):
+        return bool(self._reset)
+
+    @property
+    def is_cycle_slip(self):
+        return bool(self._cycle_slip)
+
+    @property
+    def is_half_cycle_resolved(self):
+        return bool(self._half_cycle_resolved)
+
+    @property
+    def is_half_cycle_reported(self):
+        return bool(self._half_cycle_reported)
+
+
+class AdrStatistic:
+    """Represent the ADR statistic
+
+    Attributes:
+        usable_count: (int)
+        valid_count: (int)
+        reset_count: (int)
+        cycle_slip_count: (int)
+        half_cycle_resolved_count: (int)
+        half_cycle_reported_count: (int)
+        total_count: (int)
+        usable_rate: (float)
+            usable_count / total_count
+        valid_rate: (float)
+            valid_count / total_count
+    """
+    def __init__(self):
+        self.usable_count = 0
+        self.valid_count = 0
+        self.reset_count = 0
+        self.cycle_slip_count = 0
+        self.half_cycle_resolved_count = 0
+        self.half_cycle_reported_count = 0
+        self.total_count = 0
+
+    @property
+    def usable_rate(self):
+        denominator = max(1, self.total_count)
+        result = self.usable_count / denominator
+        return round(result, 3)
+
+    @property
+    def valid_rate(self):
+        denominator = max(1, self.total_count)
+        result = self.valid_count / denominator
+        return round(result, 3)
+
+    def add_adr_info(self, adr_info: AdrInfo):
+        """Add ADR info record to increase the statistic
+
+        Args:
+            adr_info: AdrInfo object
+        """
+        if adr_info.is_valid:
+            self.valid_count += adr_info.count
+        if adr_info.is_reset:
+            self.reset_count += adr_info.count
+        if adr_info.is_cycle_slip:
+            self.cycle_slip_count += adr_info.count
+        if adr_info.is_half_cycle_resolved:
+            self.half_cycle_resolved_count += adr_info.count
+        if adr_info.is_half_cycle_reported:
+            self.half_cycle_reported_count += adr_info.count
+        if adr_info.is_usable:
+            self.usable_count += adr_info.count
+        self.total_count += adr_info.count
+
+
+
+class GnssMeasurement:
+    """Represent the content of measurement file generated by gps tool"""
+
+    FILE_PATTERN = "/storage/emulated/0/Android/data/com.android.gpstool/files/MEAS*.txt"
+
+    def __init__(self, ad):
+        self.ad = ad
+
+    def _generate_local_temp_path(self, file_name="file.txt"):
+        """Generate a file path for temporarily usage
+
+        Returns:
+            string: local file path
+        """
+        folder = tempfile.mkdtemp()
+        file_path = os.path.join(folder, file_name)
+        return file_path
+
+    def _get_latest_measurement_file_path(self):
+        """Get the latest measurement file path on device
+
+        Returns:
+            string: file path on device
+        """
+        command = f"ls -tr {self.FILE_PATTERN} | tail -1"
+        result = self.ad.adb.shell(command)
+        return result
+
+    def get_latest_measurement_file(self):
+        """Pull the latest measurement file from device to local
+
+        Returns:
+            string: local file path to the measurement file
+
+        Raise:
+            FileNotFoundError: can't get measurement file from device
+        """
+        self.ad.log.info("Get measurement file from device")
+        dest = self._generate_local_temp_path(file_name="measurement.txt")
+        src = self._get_latest_measurement_file_path()
+        if not src:
+            raise FileNotFoundError(f"Can not find measurement file: pattern {self.FILE_PATTERN}")
+        self.ad.pull_files(src, dest)
+        return dest
+
+    def _get_adr_src_value(self):
+        """Get ADR value from measurement file
+
+        Returns:
+            dict: {<ADR_value>: count, <ADR_value>: count...}
+        """
+        try:
+            file_path = self.get_latest_measurement_file()
+            adr_src = defaultdict(int)
+            adr_src_regex = re.compile("=\s(\d*)")
+            with open(file_path) as f:
+                for line in f:
+                    if "AccumulatedDeltaRangeState" in line:
+                        value = re.search(adr_src_regex, line)
+                        if not value:
+                            self.ad.log.warn("Can't get ADR value %s" % line)
+                            continue
+                        key = value.group(1)
+                        adr_src[key] += 1
+            return adr_src
+        finally:
+            folder = pathlib.PurePosixPath(file_path).parent
+            shutil.rmtree(folder, ignore_errors=True)
+
+    def get_adr_static(self):
+        """Get ADR statistic
+
+        Summarize ADR value from measurement file
+
+        Returns:
+            AdrStatistic object
+        """
+        self.ad.log.info("Get ADR statistic")
+        adr_src = self._get_adr_src_value()
+        adr_static = AdrStatistic()
+        for key, value in adr_src.items():
+            self.ad.log.debug("ADR value: %s - count: %s" % (key, value))
+            adr_info = AdrInfo(key, value)
+            adr_static.add_adr_info(adr_info)
+        return adr_static
diff --git a/acts_tests/acts_contrib/test_utils/gnss/gnss_test_utils.py b/acts_tests/acts_contrib/test_utils/gnss/gnss_test_utils.py
index 5efa817..ec9ba61 100644
--- a/acts_tests/acts_contrib/test_utils/gnss/gnss_test_utils.py
+++ b/acts_tests/acts_contrib/test_utils/gnss/gnss_test_utils.py
@@ -13,31 +13,39 @@
 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
-
 import time
 import re
 import os
+import pathlib
 import math
 import shutil
 import fnmatch
 import posixpath
+import subprocess
 import tempfile
-import zipfile
+from retry import retry
 from collections import namedtuple
 from datetime import datetime
 from xml.etree import ElementTree
+from contextlib import contextmanager
+from statistics import median
 
 from acts import utils
 from acts import asserts
 from acts import signals
 from acts.libs.proc import job
+from acts.controllers.adb_lib.error import AdbCommandError
 from acts.controllers.android_device import list_adb_devices
 from acts.controllers.android_device import list_fastboot_devices
 from acts.controllers.android_device import DEFAULT_QXDM_LOG_PATH
 from acts.controllers.android_device import SL4A_APK_NAME
+from acts_contrib.test_utils.gnss.gnss_measurement import GnssMeasurement
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.tel import tel_logging_utils as tlutils
 from acts_contrib.test_utils.tel import tel_test_utils as tutils
+from acts_contrib.test_utils.gnss import gnssstatus_utils
+from acts_contrib.test_utils.gnss import gnss_constant
+from acts_contrib.test_utils.gnss import supl
 from acts_contrib.test_utils.instrumentation.device.command.instrumentation_command_builder import InstrumentationCommandBuilder
 from acts_contrib.test_utils.instrumentation.device.command.instrumentation_command_builder import InstrumentationTestCommandBuilder
 from acts.utils import get_current_epoch_time
@@ -46,6 +54,8 @@
 from acts_contrib.test_utils.gnss.gnss_defines import BCM_NVME_STO_PATH
 
 WifiEnums = wutils.WifiEnums
+FIRST_FIXED_MAX_WAITING_TIME = 60
+UPLOAD_TO_SPONGE_PREFIX = "TestResult "
 PULL_TIMEOUT = 300
 GNSSSTATUS_LOG_PATH = (
     "/storage/emulated/0/Android/data/com.android.gpstool/files/")
@@ -54,7 +64,7 @@
     "TTFF_REPORT", "utc_time ttff_loop ttff_sec ttff_pe ttff_ant_cn "
                    "ttff_base_cn ttff_haccu")
 TRACK_REPORT = namedtuple(
-    "TRACK_REPORT", "l5flag pe ant_top4cn ant_cn base_top4cn base_cn")
+    "TRACK_REPORT", "l5flag pe ant_top4cn ant_cn base_top4cn base_cn device_time report_time")
 LOCAL_PROP_FILE_CONTENTS = """\
 log.tag.LocationManagerService=VERBOSE
 log.tag.GnssLocationProvider=VERBOSE
@@ -70,6 +80,11 @@
 log.tag.GnssPsdsDownloader=VERBOSE
 log.tag.Gnss=VERBOSE
 log.tag.GnssConfiguration=VERBOSE"""
+LOCAL_PROP_FILE_CONTENTS_FOR_WEARABLE = """\
+log.tag.ImsPhone=VERBOSE
+log.tag.GsmCdmaPhone=VERBOSE
+log.tag.Phone=VERBOSE
+log.tag.GCoreFlp=VERBOSE"""
 TEST_PACKAGE_NAME = "com.google.android.apps.maps"
 LOCATION_PERMISSIONS = [
     "android.permission.ACCESS_FINE_LOCATION",
@@ -99,6 +114,7 @@
 XTRA_SERVER_2="http://"
 XTRA_SERVER_3="http://"
 """
+_BRCM_DUTY_CYCLE_PATTERN = re.compile(r".*PGLOR,\d+,STA.*")
 
 
 class GnssTestUtilsError(Exception):
@@ -129,7 +145,8 @@
         ad: An AndroidDevice object.
     """
     ad.log.info("Reboot device to make changes take effect.")
-    ad.reboot()
+    # TODO(diegowchung): remove the timeout setting after p23 back to normal
+    ad.reboot(timeout=600)
     ad.unlock_screen(password=None)
     if not is_mobile_data_on(ad):
         set_mobile_data(ad, True)
@@ -149,7 +166,11 @@
     else:
         ad.adb.shell("echo LogEnabled=true >> /data/vendor/gps/libgps.conf")
         ad.adb.shell("chown gps.system /data/vendor/gps/libgps.conf")
-    ad.adb.shell("echo %r >> /data/local.prop" % LOCAL_PROP_FILE_CONTENTS)
+    if is_device_wearable(ad):
+       PROP_CONTENTS = LOCAL_PROP_FILE_CONTENTS + LOCAL_PROP_FILE_CONTENTS_FOR_WEARABLE
+    else:
+        PROP_CONTENTS = LOCAL_PROP_FILE_CONTENTS
+    ad.adb.shell("echo %r >> /data/local.prop" % PROP_CONTENTS)
     ad.adb.shell("chmod 644 /data/local.prop")
     ad.adb.shell("setprop persist.logd.logpersistd.size 20000")
     ad.adb.shell("setprop persist.logd.size 16777216")
@@ -221,12 +242,6 @@
     remount_device(ad)
     ad.log.info("Enable SUPL mode.")
     ad.adb.shell("echo -e '\nSUPL_MODE=1' >> /etc/gps_debug.conf")
-    if is_device_wearable(ad):
-        lto_mode_wearable(ad, True)
-    elif not check_chipset_vendor_by_qualcomm(ad):
-        lto_mode(ad, True)
-    else:
-        reboot(ad)
 
 
 def disable_supl_mode(ad):
@@ -238,16 +253,33 @@
     remount_device(ad)
     ad.log.info("Disable SUPL mode.")
     ad.adb.shell("echo -e '\nSUPL_MODE=0' >> /etc/gps_debug.conf")
+    if not check_chipset_vendor_by_qualcomm(ad):
+        supl.set_supl_over_wifi_state(ad, False)
+
+
+def enable_vendor_orbit_assistance_data(ad):
+    """Enable vendor assistance features.
+        For Qualcomm: Enable XTRA
+        For Broadcom: Enable LTO
+
+    Args:
+        ad: An AndroidDevice object.
+    """
+    ad.root_adb()
     if is_device_wearable(ad):
         lto_mode_wearable(ad, True)
-    elif not check_chipset_vendor_by_qualcomm(ad):
-        lto_mode(ad, True)
-    else:
+    elif check_chipset_vendor_by_qualcomm(ad):
+        disable_xtra_throttle(ad)
         reboot(ad)
+    else:
+        lto_mode(ad, True)
 
 
-def kill_xtra_daemon(ad):
-    """Kill XTRA daemon to test SUPL only test item.
+def disable_vendor_orbit_assistance_data(ad):
+    """Disable vendor assistance features.
+
+    For Qualcomm: disable XTRA
+    For Broadcom: disable LTO
 
     Args:
         ad: An AndroidDevice object.
@@ -256,11 +288,41 @@
     if is_device_wearable(ad):
         lto_mode_wearable(ad, False)
     elif check_chipset_vendor_by_qualcomm(ad):
-        ad.log.info("Disable XTRA-daemon until next reboot.")
-        ad.adb.shell("killall xtra-daemon", ignore_status=True)
+        disable_qualcomm_orbit_assistance_data(ad)
     else:
         lto_mode(ad, False)
 
+def gla_mode(ad, state: bool):
+    """Enable or disable Google Location Accuracy feature.
+
+    Args:
+        ad: An AndroidDevice object.
+        state: True to enable GLA, False to disable GLA.
+    """
+    ad.root_adb()
+    if state:
+        ad.adb.shell('settings put global assisted_gps_enabled 1')
+        ad.log.info("Modify current GLA Mode to MS_BASED mode")
+    else:
+        ad.adb.shell('settings put global assisted_gps_enabled 0')
+        ad.log.info("Modify current GLA Mode to standalone mode")
+
+    out = int(ad.adb.shell("settings get global assisted_gps_enabled"))
+    if out == 1:
+        ad.log.info("GLA is enabled, MS_BASED mode")
+    else:
+        ad.log.info("GLA is disabled, standalone mode")
+
+
+def disable_qualcomm_orbit_assistance_data(ad):
+    """Disable assiatance features for Qualcomm project.
+
+    Args:
+        ad: An AndroidDevice object.
+    """
+    ad.log.info("Disable XTRA-daemon until next reboot.")
+    ad.adb.shell("killall xtra-daemon", ignore_status=True)
+
 
 def disable_private_dns_mode(ad):
     """Due to b/118365122, it's better to disable private DNS mode while
@@ -282,26 +344,17 @@
     Args:
         ad: An AndroidDevice object.
     """
+    check_location_service(ad)
     enable_gnss_verbose_logging(ad)
-    enable_compact_and_particle_fusion_log(ad)
     prepare_gps_overlay(ad)
-    if check_chipset_vendor_by_qualcomm(ad):
-        disable_xtra_throttle(ad)
-    enable_supl_mode(ad)
-    if is_device_wearable(ad):
-        ad.adb.shell("settings put global stay_on_while_plugged_in 7")
-    else:
-        ad.adb.shell("settings put system screen_off_timeout 1800000")
-    wutils.wifi_toggle_state(ad, False)
+    set_screen_always_on(ad)
     ad.log.info("Setting Bluetooth state to False")
     ad.droid.bluetoothToggleState(False)
-    check_location_service(ad)
     set_wifi_and_bt_scanning(ad, True)
     disable_private_dns_mode(ad)
-    reboot(ad)
     init_gtw_gpstool(ad)
-    if not is_mobile_data_on(ad):
-        set_mobile_data(ad, True)
+    if is_device_wearable(ad):
+        disable_battery_defend(ad)
 
 
 def prepare_gps_overlay(ad):
@@ -368,8 +421,14 @@
     ad.ed.clear_all_events()
     wutils.reset_wifi(ad)
     wutils.start_wifi_connection_scan_and_ensure_network_found(ad, SSID)
-    wutils.wifi_connect(ad, network, num_of_tries=5)
-
+    for i in range(5):
+        wutils.wifi_connect(ad, network, check_connectivity=False)
+        # Validates wifi connection with ping_gateway=False to avoid issue like
+        # b/254913994.
+        if wutils.validate_connection(ad, ping_gateway=False):
+            ad.log.info("WiFi connection is validated")
+            return
+    raise signals.TestError("Failed to connect WiFi")
 
 def set_wifi_and_bt_scanning(ad, state=True):
     """Set Wi-Fi and Bluetooth scanning on/off in Settings -> Location
@@ -406,6 +465,22 @@
         raise signals.TestError("Failed to turn Location on")
 
 
+def delete_device_folder(ad, folder):
+    ad.log.info("Folder to be deleted: %s" % folder)
+    folder_contents = ad.adb.shell(f"ls {folder}", ignore_status=True)
+    ad.log.debug("Contents to be deleted: %s" % folder_contents)
+    ad.adb.shell("rm -rf %s" % folder, ignore_status=True)
+
+
+def remove_pixel_logger_folder(ad):
+    if check_chipset_vendor_by_qualcomm(ad):
+        folder = "/sdcard/Android/data/com.android.pixellogger/files/logs/diag_logs"
+    else:
+        folder = "/sdcard/Android/data/com.android.pixellogger/files/logs/gps/"
+
+    delete_device_folder(ad, folder)
+
+
 def clear_logd_gnss_qxdm_log(ad):
     """Clear /data/misc/logd,
     /storage/emulated/0/Android/data/com.android.gpstool/files and
@@ -416,20 +491,21 @@
     """
     remount_device(ad)
     ad.log.info("Clear Logd, GNSS and PixelLogger Log from previous test item.")
-    ad.adb.shell("rm -rf /data/misc/logd", ignore_status=True)
+    folders_should_be_removed = ["/data/misc/logd"]
     ad.adb.shell(
         'find %s -name "*.txt" -type f -delete' % GNSSSTATUS_LOG_PATH,
         ignore_status=True)
     if check_chipset_vendor_by_qualcomm(ad):
-        diag_logs = (
-            "/sdcard/Android/data/com.android.pixellogger/files/logs/diag_logs")
-        ad.adb.shell("rm -rf %s" % diag_logs, ignore_status=True)
         output_path = posixpath.join(DEFAULT_QXDM_LOG_PATH, "logs")
+        folders_should_be_removed += [output_path]
     else:
-        output_path = ("/sdcard/Android/data/com.android.pixellogger/files"
-                       "/logs/gps/")
-    ad.adb.shell("rm -rf %s" % output_path, ignore_status=True)
-    reboot(ad)
+        always_on_logger_log_path = ("/data/vendor/gps/logs")
+        folders_should_be_removed += [always_on_logger_log_path]
+    for folder in folders_should_be_removed:
+        delete_device_folder(ad, folder)
+    remove_pixel_logger_folder(ad)
+    if not is_device_wearable(ad):
+        reboot(ad)
 
 
 def get_gnss_qxdm_log(ad, qdb_path=None):
@@ -589,14 +665,13 @@
             ad.log.info("XTRA downloaded and injected successfully.")
             return True
         ad.log.error("XTRA downloaded FAIL.")
-    elif is_device_wearable(ad):
-        lto_results = ad.adb.shell("ls -al /data/vendor/gps/lto*")
-        if "lto2.dat" in lto_results:
-            ad.log.info("LTO downloaded and injected successfully.")
-            return True
     else:
-        lto_results = ad.search_logcat("GnssPsdsAidl: injectPsdsData: "
-                                       "psdsType: 1", begin_time)
+        if is_device_wearable(ad):
+            lto_results = ad.search_logcat("GnssLocationProvider: "
+                                           "calling native_inject_psds_data", begin_time)
+        else:
+            lto_results = ad.search_logcat("GnssPsdsAidl: injectPsdsData: "
+                                           "psdsType: 1", begin_time)
         if lto_results:
             ad.log.debug("%s" % lto_results[-1]["log_message"])
             ad.log.info("LTO downloaded and injected successfully.")
@@ -776,7 +851,7 @@
 
 def start_gnss_by_gtw_gpstool(ad,
                               state,
-                              type="gnss",
+                              api_type="gnss",
                               bgdisplay=False,
                               freq=0,
                               lowpower=False,
@@ -786,7 +861,7 @@
     Args:
         ad: An AndroidDevice object.
         state: True to start GNSS. False to Stop GNSS.
-        type: Different API for location fix. Use gnss/flp/nmea
+        api_type: Different API for location fix. Use gnss/flp/nmea
         bgdisplay: true to run GTW when Display off. false to not run GTW when
           Display off.
         freq: An integer to set location update frequency.
@@ -795,36 +870,42 @@
     """
     cmd = "am start -S -n com.android.gpstool/.GPSTool --es mode gps"
     if not state:
-        ad.log.info("Stop %s on GTW_GPSTool." % type)
+        ad.log.info("Stop %s on GTW_GPSTool." % api_type)
         cmd = "am broadcast -a com.android.gpstool.stop_gps_action"
     else:
         options = ("--es type {} --ei freq {} --ez BG {} --ez meas {} --ez "
-                   "lowpower {}").format(type, freq, bgdisplay, meas, lowpower)
+                   "lowpower {}").format(api_type, freq, bgdisplay, meas, lowpower)
         cmd = cmd + " " + options
-    ad.adb.shell(cmd)
+    ad.adb.shell(cmd, ignore_status=True, timeout = 300)
     time.sleep(3)
 
 
 def process_gnss_by_gtw_gpstool(ad,
                                 criteria,
-                                type="gnss",
+                                api_type="gnss",
                                 clear_data=True,
-                                meas_flag=False):
+                                meas_flag=False,
+                                freq=0,
+                                bg_display=False):
     """Launch GTW GPSTool and Clear all GNSS aiding data
        Start GNSS tracking on GTW_GPSTool.
 
     Args:
         ad: An AndroidDevice object.
         criteria: Criteria for current test item.
-        type: Different API for location fix. Use gnss/flp/nmea
+        api_type: Different API for location fix. Use gnss/flp/nmea
         clear_data: True to clear GNSS aiding data. False is not to. Default
         set to True.
         meas_flag: True to enable GnssMeasurement. False is not to. Default
         set to False.
+        freq: An integer to set location update frequency. Default set to 0.
+        bg_display: To enable GPS tool bg display or not
 
     Returns:
-        True: First fix TTFF are within criteria.
-        False: First fix TTFF exceed criteria.
+        First fix datetime obj
+
+    Raises:
+        signals.TestFailure: when first fixed is over criteria or not even get first fixed
     """
     retries = 3
     for i in range(retries):
@@ -836,27 +917,28 @@
         begin_time = get_current_epoch_time()
         if clear_data:
             clear_aiding_data_by_gtw_gpstool(ad)
-        ad.log.info("Start %s on GTW_GPSTool - attempt %d" % (type.upper(),
+        ad.log.info("Start %s on GTW_GPSTool - attempt %d" % (api_type.upper(),
                                                               i+1))
-        start_gnss_by_gtw_gpstool(ad, state=True, type=type, meas=meas_flag)
+        start_gnss_by_gtw_gpstool(ad, state=True, api_type=api_type, meas=meas_flag, freq=freq,
+                                  bgdisplay=bg_display)
         for _ in range(10 + criteria):
             logcat_results = ad.search_logcat("First fixed", begin_time)
             if logcat_results:
                 ad.log.debug(logcat_results[-1]["log_message"])
                 first_fixed = int(logcat_results[-1]["log_message"].split()[-1])
                 ad.log.info("%s First fixed = %.3f seconds" %
-                            (type.upper(), first_fixed/1000))
+                            (api_type.upper(), first_fixed/1000))
                 if (first_fixed/1000) <= criteria:
-                    return True
-                start_gnss_by_gtw_gpstool(ad, state=False, type=type)
+                    return logcat_results[-1]["datetime_obj"]
+                start_gnss_by_gtw_gpstool(ad, state=False, api_type=api_type)
                 raise signals.TestFailure("Fail to get %s location fixed "
                                           "within %d seconds criteria."
-                                          % (type.upper(), criteria))
+                                          % (api_type.upper(), criteria))
             time.sleep(1)
         check_current_focus_app(ad)
-        start_gnss_by_gtw_gpstool(ad, state=False, type=type)
+        start_gnss_by_gtw_gpstool(ad, state=False, api_type=api_type)
     raise signals.TestFailure("Fail to get %s location fixed within %d "
-                              "attempts." % (type.upper(), retries))
+                              "attempts." % (api_type.upper(), retries))
 
 
 def start_ttff_by_gtw_gpstool(ad,
@@ -866,7 +948,8 @@
                               raninterval=False,
                               mininterval=10,
                               maxinterval=40,
-                              hot_warm_sleep=300):
+                              hot_warm_sleep=300,
+                              timeout=60):
     """Identify which TTFF mode for different test items.
 
     Args:
@@ -878,8 +961,12 @@
         mininterval: Minimum value of random interval pool. The unit is second.
         maxinterval: Maximum value of random interval pool. The unit is second.
         hot_warm_sleep: Wait time for acquiring Almanac.
+        timeout: TTFF time out. The unit is second.
+    Returns:
+        latest_start_time: (Datetime) the start time of latest successful TTFF
     """
     begin_time = get_current_epoch_time()
+    ad.log.debug("[start_ttff] Search logcat start time: %s" % begin_time)
     if (ttff_mode == "hs" or ttff_mode == "ws") and not aid_data:
         ad.log.info("Wait {} seconds to start TTFF {}...".format(
             hot_warm_sleep, ttff_mode.upper()))
@@ -891,16 +978,32 @@
         ad.log.info("Start TTFF CSWith Assist...")
         time.sleep(3)
     for i in range(1, 4):
-        ad.adb.shell("am broadcast -a com.android.gpstool.ttff_action "
-                     "--es ttff {} --es cycle {}  --ez raninterval {} "
-                     "--ei mininterval {} --ei maxinterval {}".format(
+        try:
+            ad.log.info(f"Before sending TTFF gms version is {get_gms_version(ad)}")
+            ad.adb.shell("am broadcast -a com.android.gpstool.ttff_action "
+                         "--es ttff {} --es cycle {}  --ez raninterval {} "
+                         "--ei mininterval {} --ei maxinterval {}".format(
                          ttff_mode, iteration, raninterval, mininterval,
                          maxinterval))
+        except job.TimeoutError:
+            # If this is the last retry and we still get timeout error, raises the timeoutError.
+            if i == 3:
+                raise
+            # Currently we encounter lots of timeout issue in Qualcomm devices. But so far we don't
+            # know the root cause yet. In order to continue the test, we ignore the timeout for
+            # retry.
+            ad.log.warn("Send TTFF command timeout.")
+            ad.log.info(f"Current gms version is {get_gms_version(ad)}")
+            # Wait 2 second to retry
+            time.sleep(2)
+            continue
         time.sleep(1)
-        if ad.search_logcat("act=com.android.gpstool.start_test_action",
-                            begin_time):
+        result = ad.search_logcat("act=com.android.gpstool.start_test_action", begin_time)
+        if result:
+            ad.log.debug("TTFF start log %s" % result)
+            latest_start_time = max(list(map(lambda x: x['datetime_obj'], result)))
             ad.log.info("Send TTFF start_test_action successfully.")
-            break
+            return latest_start_time
     else:
         check_current_focus_app(ad)
         raise signals.TestError("Fail to send TTFF start_test_action.")
@@ -908,31 +1011,73 @@
 
 def gnss_tracking_via_gtw_gpstool(ad,
                                   criteria,
-                                  type="gnss",
+                                  api_type="gnss",
                                   testtime=60,
-                                  meas_flag=False):
+                                  meas_flag=False,
+                                  freq=0,
+                                  is_screen_off=False):
     """Start GNSS/FLP tracking tests for input testtime on GTW_GPSTool.
 
     Args:
         ad: An AndroidDevice object.
         criteria: Criteria for current TTFF.
-        type: Different API for location fix. Use gnss/flp/nmea
+        api_type: Different API for location fix. Use gnss/flp/nmea
         testtime: Tracking test time for minutes. Default set to 60 minutes.
         meas_flag: True to enable GnssMeasurement. False is not to. Default
         set to False.
+        freq: An integer to set location update frequency. Default set to 0.
+        is_screen_off: whether to turn off during tracking
     """
     process_gnss_by_gtw_gpstool(
-        ad, criteria=criteria, type=type, meas_flag=meas_flag)
-    ad.log.info("Start %s tracking test for %d minutes" % (type.upper(),
+        ad, criteria=criteria, api_type=api_type, meas_flag=meas_flag, freq=freq,
+        bg_display=is_screen_off)
+    ad.log.info("Start %s tracking test for %d minutes" % (api_type.upper(),
                                                            testtime))
     begin_time = get_current_epoch_time()
+    with set_screen_status(ad, off=is_screen_off):
+        wait_n_mins_for_gnss_tracking(ad, begin_time, testtime, api_type)
+        ad.log.info("Successfully tested for %d minutes" % testtime)
+    start_gnss_by_gtw_gpstool(ad, state=False, api_type=api_type)
+
+
+def wait_n_mins_for_gnss_tracking(ad, begin_time, testtime, api_type="gnss",
+                                  ignore_hal_crash=False):
+    """Waits for GNSS tracking to finish and detect GNSS crash during the waiting time.
+
+    Args:
+        ad: An AndroidDevice object.
+        begin_time: The start time of tracking.
+        api_type: Different API for location fix. Use gnss/flp/nmea
+        testtime: Tracking test time for minutes.
+        ignore_hal_crash: To ignore HAL crash error no not.
+    """
     while get_current_epoch_time() - begin_time < testtime * 60 * 1000:
-        detect_crash_during_tracking(ad, begin_time, type)
-    ad.log.info("Successfully tested for %d minutes" % testtime)
-    start_gnss_by_gtw_gpstool(ad, state=False, type=type)
+        detect_crash_during_tracking(ad, begin_time, api_type, ignore_hal_crash)
+        # add sleep here to avoid too many request and cause device not responding
+        time.sleep(1)
 
+def run_ttff_via_gtw_gpstool(ad, mode, criteria, test_cycle, true_location):
+    """Run GNSS TTFF test with selected mode and parse the results.
 
-def parse_gtw_gpstool_log(ad, true_position, type="gnss"):
+    Args:
+        mode: "cs", "ws" or "hs"
+        criteria: Criteria for the TTFF.
+
+    Returns:
+        ttff_data: A dict of all TTFF data.
+    """
+    # Before running TTFF, we will run tracking and try to get first fixed.
+    # But the TTFF before TTFF doesn't apply to any criteria, so we set a maximum value.
+    process_gnss_by_gtw_gpstool(ad, criteria=FIRST_FIXED_MAX_WAITING_TIME)
+    ttff_start_time = start_ttff_by_gtw_gpstool(ad, mode, test_cycle)
+    ttff_data = process_ttff_by_gtw_gpstool(ad, ttff_start_time, true_location)
+    result = check_ttff_data(ad, ttff_data, gnss_constant.TTFF_MODE.get(mode), criteria)
+    asserts.assert_true(
+        result, "TTFF %s fails to reach designated criteria: %d "
+                "seconds." % (gnss_constant.TTFF_MODE.get(mode), criteria))
+    return ttff_data
+
+def parse_gtw_gpstool_log(ad, true_position, api_type="gnss", validate_gnssstatus=False):
     """Process GNSS/FLP API logs from GTW GPSTool and output track_data to
     test_run_info for ACTS plugin to parse and display on MobileHarness as
     Property.
@@ -941,8 +1086,14 @@
         ad: An AndroidDevice object.
         true_position: Coordinate as [latitude, longitude] to calculate
         position error.
-        type: Different API for location fix. Use gnss/flp/nmea
+        api_type: Different API for location fix. Use gnss/flp/nmea
+        validate_gnssstatus: Validate gnssstatus or not
+
+    Returns:
+        A dict of location reported from GPSTool
+            {<utc_time>: TRACK_REPORT, ...}
     """
+    gnssstatus_count = 0
     test_logfile = {}
     track_data = {}
     ant_top4_cn = 0
@@ -952,11 +1103,13 @@
     track_lat = 0
     track_long = 0
     l5flag = "false"
+    gps_datetime_pattern = re.compile("(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}\.\d{0,5})")
+    gps_datetime_format = "%Y/%m/%d %H:%M:%S.%f"
     file_count = int(ad.adb.shell("find %s -type f -iname *.txt | wc -l"
                                   % GNSSSTATUS_LOG_PATH))
     if file_count != 1:
-        ad.log.error("%d API logs exist." % file_count)
-    dir_file = ad.adb.shell("ls %s" % GNSSSTATUS_LOG_PATH).split()
+        ad.log.warn("%d API logs exist." % file_count)
+    dir_file = ad.adb.shell("ls -tr %s" % GNSSSTATUS_LOG_PATH).split()
     for path_key in dir_file:
         if fnmatch.fnmatch(path_key, "*.txt"):
             logpath = posixpath.join(GNSSSTATUS_LOG_PATH, path_key)
@@ -970,7 +1123,20 @@
     if not test_logfile:
         raise signals.TestError("Failed to get test log file in device.")
     lines = ad.adb.shell("cat %s" % test_logfile).split("\n")
+    gnss_svid_container = gnssstatus_utils.GnssSvidContainer()
     for line in lines:
+        if line.startswith('Fix'):
+            try:
+                gnss_status = gnssstatus_utils.GnssStatus(line)
+                gnssstatus_count += 1
+            except gnssstatus_utils.RegexParseException as e:
+                ad.log.warn(e)
+                continue
+
+            gnss_svid_container.add_satellite(gnss_status)
+            if validate_gnssstatus:
+                gnss_status.validate_gnssstatus()
+
         if "Antenna_History Avg Top4" in line:
             ant_top4_cn = float(line.split(":")[-1].strip())
         elif "Antenna_History Avg" in line:
@@ -985,8 +1151,13 @@
             track_lat = float(line.split(":")[-1].strip())
         elif "Longitude" in line:
             track_long = float(line.split(":")[-1].strip())
+        elif "Read:" in line:
+            target = re.search(gps_datetime_pattern, line)
+            device_time = datetime.strptime(target.group(1), gps_datetime_format)
         elif "Time" in line:
-            track_utc = line.split("Time:")[-1].strip()
+            target = re.search(gps_datetime_pattern, line)
+            track_utc = target.group(1)
+            report_time = datetime.strptime(track_utc, gps_datetime_format)
             if track_utc in track_data.keys():
                 continue
             pe = calculate_position_error(track_lat, track_long, true_position)
@@ -995,9 +1166,13 @@
                                                  ant_top4cn=ant_top4_cn,
                                                  ant_cn=ant_cn,
                                                  base_top4cn=base_top4_cn,
-                                                 base_cn=base_cn)
+                                                 base_cn=base_cn,
+                                                 device_time=device_time,
+                                                 report_time=report_time,
+                                                 )
+    ad.log.info("Total %d gnssstatus samples verified" %gnssstatus_count)
     ad.log.debug(track_data)
-    prop_basename = "TestResult %s_tracking_" % type.upper()
+    prop_basename = UPLOAD_TO_SPONGE_PREFIX + f"{api_type.upper()}_tracking_"
     time_list = sorted(track_data.keys())
     l5flag_list = [track_data[key].l5flag for key in time_list]
     pe_list = [float(track_data[key].pe) for key in time_list]
@@ -1016,9 +1191,84 @@
     ad.log.info(prop_basename+"Ant_AvgSignal %.1f" % ant_cn_list[-1])
     ad.log.info(prop_basename+"Base_AvgTop4Signal %.1f" % base_top4cn_list[-1])
     ad.log.info(prop_basename+"Base_AvgSignal %.1f" % base_cn_list[-1])
+    _log_svid_info(gnss_svid_container, prop_basename, ad)
+    return track_data
 
 
-def process_ttff_by_gtw_gpstool(ad, begin_time, true_position, type="gnss"):
+def verify_gps_time_should_be_close_to_device_time(ad, tracking_result):
+    """Check the time gap between GPS time and device time.
+
+    In normal cases, the GPS time should be close to device time. But if GPS week rollover happens,
+    the GPS time may goes back to 20 years ago. In order to capture this issue, we assert the time
+    diff between the GPS time and device time.
+
+    Args:
+        ad: The device under test.
+        tracking_result: The result we get from GNSS tracking.
+    """
+    ad.log.info("Validating GPS/Device time difference")
+    max_time_diff_in_seconds = 2.0
+    exceed_report = []
+    for report in tracking_result.values():
+        time_diff_in_seconds = abs((report.report_time - report.device_time).total_seconds())
+        if time_diff_in_seconds > max_time_diff_in_seconds:
+            message = (f"GPS time: {report.report_time}  Device time: {report.device_time} "
+                       f"diff: {time_diff_in_seconds}")
+            exceed_report.append(message)
+    fail_message = (f"The following items exceed {max_time_diff_in_seconds}s\n" +
+                     "\n".join(exceed_report))
+    asserts.assert_false(exceed_report, msg=fail_message)
+
+
+def validate_location_fix_rate(ad, location_reported, run_time, fix_rate_criteria):
+    """Check location reported count
+
+    The formula is "total_fix_points / (run_time * 60)"
+    When the result is lower than fix_rate_criteria, fail the test case
+
+    Args:
+        ad: AndroidDevice object
+        location_reported: (Enumerate) Contains the reported location
+        run_time: (int) How many minutes do we need to verify
+        fix_rate_criteria: The threshold of the pass criteria
+            if we expect fix rate to be 99%, then fix_rate_criteria should be 0.99
+    """
+    ad.log.info("Validating fix rate")
+    pass_criteria = run_time * 60 * fix_rate_criteria
+    actual_location_count = len(location_reported)
+
+    # The fix rate may exceed 100% occasionally, to standardlize the result
+    # set maximum fix rate to 100%
+    actual_fix_rate = min(1, (actual_location_count / (run_time * 60)))
+    actual_fix_rate_percentage = f"{actual_fix_rate:.0%}"
+
+    log_prefix = UPLOAD_TO_SPONGE_PREFIX + f"FIX_RATE_"
+    ad.log.info("%sresult %s" % (log_prefix, actual_fix_rate_percentage))
+    ad.log.debug("Actual location count %s" % actual_location_count)
+
+    fail_message = (f"Fail to meet criteria. Expect to have at least {pass_criteria} location count"
+                    f" Actual: {actual_location_count}")
+    asserts.assert_true(pass_criteria <= actual_location_count, msg=fail_message)
+
+
+def _log_svid_info(container, log_prefix, ad):
+    """Write GnssSvidContainer svid information into logger
+    Args:
+        container: A GnssSvidContainer object
+        log_prefix:
+            A prefix used to specify the log will be upload to dashboard
+        ad: An AndroidDevice object
+    """
+    for sv_type, svids in container.used_in_fix.items():
+        message = f"{log_prefix}{sv_type} {len(svids)}"
+        ad.log.info(message)
+        ad.log.debug("Satellite used in fix %s ids are: %s", sv_type, svids)
+
+    for sv_type, svids in container.not_used_in_fix.items():
+        ad.log.debug("Satellite not used in fix %s ids are: %s", sv_type, svids)
+
+
+def process_ttff_by_gtw_gpstool(ad, begin_time, true_position, api_type="gnss"):
     """Process TTFF and record results in ttff_data.
 
     Args:
@@ -1026,7 +1276,7 @@
         begin_time: test begin time.
         true_position: Coordinate as [latitude, longitude] to calculate
         position error.
-        type: Different API for location fix. Use gnss/flp/nmea
+        api_type: Different API for location fix. Use gnss/flp/nmea
 
     Returns:
         ttff_data: A dict of all TTFF data.
@@ -1051,7 +1301,7 @@
             if ttff_sec != 0.0:
                 ttff_ant_cn = float(ttff_log[18].strip("]"))
                 ttff_base_cn = float(ttff_log[25].strip("]"))
-                if type == "gnss":
+                if api_type == "gnss":
                     gnss_results = ad.search_logcat("GPSService: Check item",
                                                     begin_time)
                     if gnss_results:
@@ -1067,7 +1317,7 @@
                         utc_time = epoch_to_human_time(loc_time)
                         ttff_haccu = float(
                             gnss_location_log[11].split("=")[-1].strip(","))
-                elif type == "flp":
+                elif api_type == "flp":
                     flp_results = ad.search_logcat("GPSService: FLP Location",
                                                    begin_time)
                     if flp_results:
@@ -1139,11 +1389,13 @@
                 % (len(ttff_data.keys()), ttff_mode))
     ad.log.info("%s PASS criteria is %d seconds" % (ttff_mode, criteria))
     ad.log.debug("%s TTFF data: %s" % (ttff_mode, ttff_data))
-    ttff_property_key_and_value(ad, ttff_data, ttff_mode)
     if len(ttff_data.keys()) == 0:
         ad.log.error("GTW_GPSTool didn't process TTFF properly.")
-        return False
-    elif any(float(ttff_data[key].ttff_sec) == 0.0 for key in ttff_data.keys()):
+        raise ValueError("No ttff loop is done")
+
+    ttff_property_key_and_value(ad, ttff_data, ttff_mode)
+
+    if 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
@@ -1165,6 +1417,7 @@
         ttff_data: TTFF data of secs, position error and signal strength.
         ttff_mode: TTFF Test mode for current test item.
     """
+    timeout_ttff = 61
     prop_basename = "TestResult "+ttff_mode.replace(" ", "_")+"_TTFF_"
     sec_list = [float(ttff_data[key].ttff_sec) for key in ttff_data.keys()]
     pe_list = [float(ttff_data[key].ttff_pe) for key in ttff_data.keys()]
@@ -1175,12 +1428,14 @@
     haccu_list = [float(ttff_data[key].ttff_haccu) for key in
                     ttff_data.keys()]
     timeoutcount = sec_list.count(0.0)
+    sec_list = sorted(sec_list)
     if len(sec_list) == timeoutcount:
-        avgttff = 9527
+        median_ttff = avgttff = timeout_ttff
     else:
         avgttff = sum(sec_list)/(len(sec_list) - timeoutcount)
+        median_ttff = median(sec_list)
     if timeoutcount != 0:
-        maxttff = 9527
+        maxttff = timeout_ttff
     else:
         maxttff = max(sec_list)
     avgdis = sum(pe_list)/len(pe_list)
@@ -1189,6 +1444,7 @@
     base_avgcn = sum(base_cn_list)/len(base_cn_list)
     avg_haccu = sum(haccu_list)/len(haccu_list)
     ad.log.info(prop_basename+"AvgTime %.1f" % avgttff)
+    ad.log.info(prop_basename+"MedianTime %.1f" % median_ttff)
     ad.log.info(prop_basename+"MaxTime %.1f" % maxttff)
     ad.log.info(prop_basename+"TimeoutCount %d" % timeoutcount)
     ad.log.info(prop_basename+"AvgDis %.1f" % avgdis)
@@ -1249,11 +1505,14 @@
 
     Args:
         ad: An AndroidDevice object.
+    Returns:
+        string: the current focused window / app
     """
     time.sleep(1)
     current = ad.adb.shell(
         "dumpsys window | grep -E 'mCurrentFocus|mFocusedApp'")
     ad.log.debug("\n"+current)
+    return current
 
 
 def check_location_api(ad, retries):
@@ -1300,7 +1559,8 @@
     criteria = criteria * 1000
     search_pattern = ("GPSTool : networkLocationType = %s" % location_type)
     for i in range(retries):
-        begin_time = get_current_epoch_time()
+        # Capture the begin time 1 seconds before due to time gap.
+        begin_time = get_current_epoch_time() - 1000
         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")
@@ -1404,6 +1664,11 @@
                             "but audio is not in MUSIC state")
 
 
+def get_gms_version(ad):
+    cmd = "dumpsys package com.google.android.gms | grep versionName"
+    return ad.adb.shell(cmd).split("\n")[0].split("=")[1]
+
+
 def get_baseband_and_gms_version(ad, extra_msg=""):
     """Get current radio baseband and GMSCore version of AndroidDevice object.
 
@@ -1417,9 +1682,7 @@
     try:
         build_version = ad.adb.getprop("ro.build.id")
         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 = get_gms_version(ad)
         if check_chipset_vendor_by_qualcomm(ad):
             mpss_version = ad.adb.shell(
                 "cat /sys/devices/soc0/images | grep MPSS | cut -d ':' -f 3")
@@ -1947,7 +2210,7 @@
         raise signals.TestError("Failed to launch EEcoexer.")
 
 
-def excute_eecoexer_function(ad, eecoexer_args):
+def execute_eecoexer_function(ad, eecoexer_args):
     """Execute EEcoexer commands.
 
     Args:
@@ -1970,6 +2233,21 @@
     ad.adb.shell(wait_for_cmd)
 
 
+def get_process_pid(ad, process_name):
+    """Gets the process PID
+
+    Args:
+        ad: The device under test
+        process_name: The name of the process
+
+    Returns:
+        The PID of the process
+    """
+    command = f"ps -A | grep {process_name} |  awk '{{print $2}}'"
+    pid = ad.adb.shell(command)
+    return pid
+
+
 def restart_gps_daemons(ad):
     """Restart GPS daemons by killing services of gpsd, lhd and scd.
 
@@ -1979,22 +2257,22 @@
     gps_daemons_list = ["gpsd", "lhd", "scd"]
     ad.root_adb()
     for service in gps_daemons_list:
-        begin_time = get_current_epoch_time()
         time.sleep(3)
         ad.log.info("Kill GPS daemon \"%s\"" % service)
-        ad.adb.shell("killall %s" % service)
+        service_pid = get_process_pid(ad, service)
+        ad.log.debug("%s PID: %s" % (service, service_pid))
+        ad.adb.shell(f"kill -9 {service_pid}")
         # Wait 3 seconds for daemons and services to start.
         time.sleep(3)
-        restart_services = ad.search_logcat("starting service", begin_time)
-        for restart_service in restart_services:
-            if service in restart_service["log_message"]:
-                ad.log.info(restart_service["log_message"])
-                ad.log.info(
-                    "GPS daemon \"%s\" restarts successfully." % service)
-                break
-        else:
+
+        new_pid = get_process_pid(ad, service)
+        ad.log.debug("%s new PID: %s" % (service, new_pid))
+        if not new_pid or service_pid == new_pid:
             raise signals.TestError("Unable to restart \"%s\"" % service)
 
+        ad.log.info("GPS daemon \"%s\" restarts successfully. PID from %s to %s" % (
+            service, service_pid, new_pid))
+
 
 def is_device_wearable(ad):
     """Check device is wearable project or not.
@@ -2044,6 +2322,21 @@
         return None
 
 
+def _get_dpo_info_from_logcat(ad, begin_time):
+    """Gets the DPO info from logcat.
+
+    Args:
+        ad: The device under test.
+        begin_time: The start time of the log.
+    """
+    dpo_results = ad.search_logcat("HardwareClockDiscontinuityCount",
+                                   begin_time)
+    if not dpo_results:
+        raise signals.TestError(
+            "No \"HardwareClockDiscontinuityCount\" is found in logs.")
+    return dpo_results
+
+
 def check_dpo_rate_via_gnss_meas(ad, begin_time, dpo_threshold):
     """Check DPO engage rate through "HardwareClockDiscontinuityCount" in
     GnssMeasurement callback.
@@ -2054,11 +2347,7 @@
         dpo_threshold: The value to set threshold. (Ex: dpo_threshold = 60)
     """
     time_regex = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3})'
-    dpo_results = ad.search_logcat("HardwareClockDiscontinuityCount",
-                                   begin_time)
-    if not dpo_results:
-        raise signals.TestError(
-            "No \"HardwareClockDiscontinuityCount\" is found in logs.")
+    dpo_results = _get_dpo_info_from_logcat(ad, begin_time)
     ad.log.info(dpo_results[0]["log_message"])
     ad.log.info(dpo_results[-1]["log_message"])
     start_time = re.compile(
@@ -2083,13 +2372,14 @@
                                            threshold))
 
 
-def parse_brcm_nmea_log(ad, nmea_pattern, brcm_error_log_allowlist):
+def parse_brcm_nmea_log(ad, nmea_pattern, brcm_error_log_allowlist, stop_logger=True):
     """Parse specific NMEA pattern out of BRCM NMEA log.
 
     Args:
         ad: An AndroidDevice object.
         nmea_pattern: Specific NMEA pattern to parse.
         brcm_error_log_allowlist: Benign error logs to exclude.
+        stop_logger: To stop pixel logger or not.
 
     Returns:
         brcm_log_list: A list of specific NMEA pattern logs.
@@ -2097,58 +2387,68 @@
     brcm_log_list = []
     brcm_log_error_pattern = ["lhd: FS: Start Failsafe dump", "E slog"]
     brcm_error_log_list = []
-    stop_pixel_logger(ad)
     pixellogger_path = (
         "/sdcard/Android/data/com.android.pixellogger/files/logs/gps/.")
-    tmp_log_path = tempfile.mkdtemp()
-    ad.pull_files(pixellogger_path, tmp_log_path)
-    for path_key in os.listdir(tmp_log_path):
-        zip_path = posixpath.join(tmp_log_path, path_key)
-        if path_key.endswith(".zip"):
-            ad.log.info("Processing zip file: {}".format(zip_path))
-            with zipfile.ZipFile(zip_path, "r") as zip_file:
-                zip_file.extractall(tmp_log_path)
-                gl_logs = zip_file.namelist()
-                # b/214145973 check if hidden exists in pixel logger zip file
-                tmp_file = [name for name in gl_logs if 'tmp' in name]
-                if tmp_file:
-                    ad.log.warn(f"Hidden file {tmp_file} exists in pixel logger zip file")
-            break
-        elif os.path.isdir(zip_path):
-            ad.log.info("BRCM logs didn't zip properly. Log path is directory.")
-            tmp_log_path = zip_path
-            gl_logs = os.listdir(tmp_log_path)
-            ad.log.info("Processing BRCM log files: {}".format(gl_logs))
-            break
-    else:
-        raise signals.TestError(
-            "No BRCM logs found in {}".format(os.listdir(tmp_log_path)))
-    gl_logs = [log for log in gl_logs
-               if log.startswith("gl") and log.endswith(".log")]
-    for file in gl_logs:
-        nmea_log_path = posixpath.join(tmp_log_path, file)
-        ad.log.info("Parsing log pattern of \"%s\" in %s" % (nmea_pattern,
-                                                             nmea_log_path))
-        brcm_log = open(nmea_log_path, "r", encoding="UTF-8", errors="ignore")
-        lines = brcm_log.readlines()
-        for line in lines:
-            if nmea_pattern in line:
-                brcm_log_list.append(line)
-            for attr in brcm_log_error_pattern:
-                if attr in line:
-                    benign_log = False
-                    for allow_log in brcm_error_log_allowlist:
-                        if allow_log in line:
-                            benign_log = True
-                            ad.log.info("\"%s\" is in allow-list and removed "
-                                        "from error." % allow_log)
-                    if not benign_log:
-                        brcm_error_log_list.append(line)
+    if not isinstance(nmea_pattern, re.Pattern):
+        nmea_pattern = re.compile(nmea_pattern)
+
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        try:
+            ad.pull_files(pixellogger_path, tmp_dir)
+        except AdbCommandError:
+            raise FileNotFoundError("No pixel logger folders found")
+
+        # Although we don't rely on the zip file, stop pixel logger here to avoid
+        # wasting resources.
+        if stop_logger:
+            stop_pixel_logger(ad)
+
+        tmp_path = pathlib.Path(tmp_dir)
+        log_folders = sorted([x for x in tmp_path.iterdir() if x.is_dir()])
+        if not log_folders:
+            raise FileNotFoundError("No BRCM logs found.")
+        # The folder name is a string of datetime, the latest one will be in the last index.
+        gl_logs = log_folders[-1].glob("**/gl*.log")
+
+        for nmea_log_path in gl_logs:
+            ad.log.info("Parsing log pattern of \"%s\" in %s" % (nmea_pattern,
+                                                                 nmea_log_path))
+            with open(nmea_log_path, "r", encoding="UTF-8", errors="ignore") as lines:
+                for line in lines:
+                    line = line.strip()
+                    if nmea_pattern.fullmatch(line):
+                        brcm_log_list.append(line)
+                    for attr in brcm_log_error_pattern:
+                        if attr in line:
+                            benign_log = False
+                            for regex_pattern in brcm_error_log_allowlist:
+                                if re.search(regex_pattern, line):
+                                    benign_log = True
+                                    ad.log.debug("\"%s\" is in allow-list and removed "
+                                                "from error." % line)
+                            if not benign_log:
+                                brcm_error_log_list.append(line)
+
     brcm_error_log = "".join(brcm_error_log_list)
-    shutil.rmtree(tmp_log_path, ignore_errors=True)
     return brcm_log_list, brcm_error_log
 
 
+def _get_power_mode_log_from_pixel_logger(ad, brcm_error_log_allowlist, stop_pixel_logger=True):
+    """Gets the power log from pixel logger.
+
+    Args:
+        ad: The device under test.
+        brcm_error_log_allow_list: The allow list to ignore certain error in pixel logger.
+        stop_pixel_logger: To disable pixel logger when getting the log.
+    """
+    pglor_list, brcm_error_log = parse_brcm_nmea_log(
+        ad, _BRCM_DUTY_CYCLE_PATTERN, brcm_error_log_allowlist, stop_pixel_logger)
+    if not pglor_list:
+        raise signals.TestFailure("Fail to get DPO logs from pixel logger")
+
+    return pglor_list, brcm_error_log
+
+
 def check_dpo_rate_via_brcm_log(ad, dpo_threshold, brcm_error_log_allowlist):
     """Check DPO engage rate through "$PGLOR,11,STA" in BRCM Log.
     D - Disabled, Always full power.
@@ -2164,10 +2464,7 @@
     always_full_power_count = 0
     full_power_count = 0
     power_save_count = 0
-    pglor_list, brcm_error_log = parse_brcm_nmea_log(
-        ad, "$PGLOR,11,STA", brcm_error_log_allowlist)
-    if not pglor_list:
-        raise signals.TestFailure("Fail to get DPO logs from pixel logger")
+    pglor_list, brcm_error_log = _get_power_mode_log_from_pixel_logger(ad, brcm_error_log_allowlist)
 
     for pglor in pglor_list:
         power_res = re.compile(r',P,(\w),').search(pglor).group(1)
@@ -2199,74 +2496,77 @@
                                   brcm_error_log))
 
 
-def pair_to_wearable(ad, ad1):
-    """Pair phone to watch via Bluetooth.
+def process_pair(watch, phone):
+    """Pair phone to watch via Bluetooth in OOBE.
 
     Args:
-        ad: A pixel phone.
-        ad1: A wearable project.
+        watch: A wearable project.
+        phone: A pixel phone.
     """
-    check_location_service(ad1)
-    utils.sync_device_time(ad1)
-    bt_model_name = ad.adb.getprop("ro.product.model")
-    bt_sn_name = ad.adb.getprop("ro.serialno")
+    check_location_service(phone)
+    utils.sync_device_time(phone)
+    bt_model_name = watch.adb.getprop("ro.product.model")
+    bt_sn_name = watch.adb.getprop("ro.serialno")
     bluetooth_name = bt_model_name +" " + bt_sn_name[10:]
-    fastboot_factory_reset(ad, False)
-    ad.log.info("Wait 1 min for wearable system busy time.")
+    fastboot_factory_reset(watch, False)
+    # TODO (chenstanley)Need to re-structure for better code and test flow instead of simply waiting
+    watch.log.info("Wait 1 min for wearable system busy time.")
     time.sleep(60)
-    ad.adb.shell("input keyevent 4")
+    watch.adb.shell("input keyevent 4")
     # Clear Denali paired data in phone.
-    ad1.adb.shell("pm clear com.google.android.gms")
-    ad1.adb.shell("pm clear com.google.android.apps.wear.companion")
-    ad1.adb.shell("am start -S -n com.google.android.apps.wear.companion/"
+    phone.adb.shell("pm clear com.google.android.gms")
+    phone.adb.shell("pm clear com.google.android.apps.wear.companion")
+    phone.adb.shell("am start -S -n com.google.android.apps.wear.companion/"
                         "com.google.android.apps.wear.companion.application.RootActivity")
-    uia_click(ad1, "Next")
-    uia_click(ad1, "I agree")
-    uia_click(ad1, bluetooth_name)
-    uia_click(ad1, "Pair")
-    uia_click(ad1, "Skip")
-    uia_click(ad1, "Skip")
-    uia_click(ad1, "Finish")
-    ad.log.info("Wait 3 mins for complete pairing process.")
+    uia_click(phone, "Continue")
+    uia_click(phone, "More")
+    uia_click(phone, "I agree")
+    uia_click(phone, "I accept")
+    uia_click(phone, bluetooth_name)
+    uia_click(phone, "Pair")
+    uia_click(phone, "Skip")
+    uia_click(phone, "Next")
+    uia_click(phone, "Skip")
+    uia_click(phone, "Done")
+    # TODO (chenstanley)Need to re-structure for better code and test flow instead of simply waiting
+    watch.log.info("Wait 3 mins for complete pairing process.")
     time.sleep(180)
-    ad.adb.shell("settings put global stay_on_while_plugged_in 7")
-    check_location_service(ad)
-    enable_gnss_verbose_logging(ad)
-    if is_bluetooth_connected(ad, ad1):
-        ad.log.info("Pairing successfully.")
-    else:
-        raise signals.TestFailure("Fail to pair watch and phone successfully.")
+    set_screen_always_on(watch)
+    check_location_service(watch)
+    enable_gnss_verbose_logging(watch)
 
 
-def is_bluetooth_connected(ad, ad1):
+def is_bluetooth_connected(watch, phone):
     """Check if device's Bluetooth status is connected or not.
 
     Args:
-    ad: A wearable project
-    ad1: A pixel phone.
+    watch: A wearable project
+    phone: A pixel phone.
     """
-    return ad.droid.bluetoothIsDeviceConnected(ad1.droid.bluetoothGetLocalAddress())
+    return watch.droid.bluetoothIsDeviceConnected(phone.droid.bluetoothGetLocalAddress())
 
 
-def detect_crash_during_tracking(ad, begin_time, type):
+def detect_crash_during_tracking(ad, begin_time, api_type, ignore_hal_crash=False):
     """Check if GNSS or GPSTool crash happened druing GNSS Tracking.
 
     Args:
     ad: An AndroidDevice object.
     begin_time: Start Time to check if crash happened in logs.
-    type: Using GNSS or FLP reading method in GNSS tracking.
+    api_type: Using GNSS or FLP reading method in GNSS tracking.
+    ignore_hal_crash: In BRCM devices, once the HAL is being killed, it will write error/fatal logs.
+      Ignore this error if the error logs are expected.
     """
     gnss_crash_list = [".*Fatal signal.*gnss",
-                       ".*Fatal signal.*xtra",
-                       ".*F DEBUG.*gnss",
-                       ".*Fatal signal.*gpsd"]
+                       ".*Fatal signal.*xtra"]
+    if not ignore_hal_crash:
+        gnss_crash_list += [".*Fatal signal.*gpsd", ".*F DEBUG.*gnss"]
     if not ad.is_adb_logcat_on:
         ad.start_adb_logcat()
     for attr in gnss_crash_list:
         gnss_crash_result = ad.adb.shell(
-            "logcat -d | grep -E -i '%s'" % attr)
+            "logcat -d | grep -E -i '%s'" % attr, ignore_status=True, timeout = 300)
         if gnss_crash_result:
-            start_gnss_by_gtw_gpstool(ad, state=False, type=type)
+            start_gnss_by_gtw_gpstool(ad, state=False, api_type=api_type)
             raise signals.TestFailure(
                 "Test failed due to GNSS HAL crashed. \n%s" %
                 gnss_crash_result)
@@ -2381,57 +2681,103 @@
     ad.log.info("Delete BCM's NVMEM ephemeris files.\n%s" % status)
 
 
-def bcm_gps_xml_add_option(ad,
+def bcm_gps_xml_update_option(ad,
+                           option,
                            search_line=None,
                            append_txt=None,
+                           update_txt=None,
+                           delete_txt=None,
                            gps_xml_path=BCM_GPS_XML_PATH):
     """Append parameter setting in gps.xml for BCM solution
 
     Args:
+        option: A str to identify the operation (add/update/delete).
         ad: An AndroidDevice object.
         search_line: Pattern matching of target
         line for appending new line data.
-        append_txt: New line that will be appended after the search_line.
+        append_txt: New lines that will be appended after the search_line.
+        update_txt: New line to update the original file.
+        delete_txt: lines to delete from the original file.
         gps_xml_path: gps.xml file location of DUT
     """
     remount_device(ad)
     #Update gps.xml
-    if not search_line or not append_txt:
-        ad.log.info("Nothing for update.")
-    else:
-        tmp_log_path = tempfile.mkdtemp()
-        ad.pull_files(gps_xml_path, tmp_log_path)
-        gps_xml_tmp_path = os.path.join(tmp_log_path, "gps.xml")
-        gps_xml_file = open(gps_xml_tmp_path, "r")
-        lines = gps_xml_file.readlines()
-        gps_xml_file.close()
-        fout = open(gps_xml_tmp_path, "w")
-        append_txt_tag = append_txt.strip()
+    tmp_log_path = tempfile.mkdtemp()
+    ad.pull_files(gps_xml_path, tmp_log_path)
+    gps_xml_tmp_path = os.path.join(tmp_log_path, "gps.xml")
+    gps_xml_file = open(gps_xml_tmp_path, "r")
+    lines = gps_xml_file.readlines()
+    gps_xml_file.close()
+    fout = open(gps_xml_tmp_path, "w")
+    if option == "add":
         for line in lines:
-            if append_txt_tag in line:
-                ad.log.info('{} is already in the file. Skip'.format(append_txt))
+            if line.strip() in append_txt:
+                ad.log.info("{} is already in the file. Skip".format(append_txt))
                 continue
             fout.write(line)
             if search_line in line:
-                fout.write(append_txt)
-                ad.log.info("Update new line: '{}' in gps.xml.".format(append_txt))
-        fout.close()
+                for add_txt in append_txt:
+                    fout.write(add_txt)
+                    ad.log.info("Add new line: '{}' in gps.xml.".format(add_txt))
+    elif option == "update":
+        for line in lines:
+            if search_line in line:
+                ad.log.info(line)
+                fout.write(update_txt)
+                ad.log.info("Update line: '{}' in gps.xml.".format(update_txt))
+                continue
+            fout.write(line)
+    elif option == "delete":
+        for line in lines:
+            if delete_txt in line:
+                ad.log.info("Delete line: '{}' in gps.xml.".format(line.strip()))
+                continue
+            fout.write(line)
+    fout.close()
 
-        # Update gps.xml with gps_new.xml
-        ad.push_system_file(gps_xml_tmp_path, gps_xml_path)
+    # Update gps.xml with gps_new.xml
+    ad.push_system_file(gps_xml_tmp_path, gps_xml_path)
 
-        # remove temp folder
-        shutil.rmtree(tmp_log_path, ignore_errors=True)
+    # remove temp folder
+    shutil.rmtree(tmp_log_path, ignore_errors=True)
 
+def bcm_gps_ignore_warmstandby(ad):
+    """ remove warmstandby setting in BCM gps.xml to reset tracking filter
+    Args:
+        ad: An AndroidDevice object.
+    """
+    search_line_tag = '<gll\n'
+    delete_line_str = 'WarmStandbyTimeout1Seconds'
+    bcm_gps_xml_update_option(ad,
+                              "delete",
+                              search_line_tag,
+                              append_txt=None,
+                              update_txt=None,
+                              delete_txt=delete_line_str)
+
+    search_line_tag = '<gll\n'
+    delete_line_str = 'WarmStandbyTimeout2Seconds'
+    bcm_gps_xml_update_option(ad,
+                              "delete",
+                              search_line_tag,
+                              append_txt=None,
+                              update_txt=None,
+                              delete_txt=delete_line_str)
 
 def bcm_gps_ignore_rom_alm(ad):
     """ Update BCM gps.xml with ignoreRomAlm="True"
     Args:
         ad: An AndroidDevice object.
     """
+    search_line_tag = '<hal\n'
+    append_line_str = ['       IgnoreJniTime=\"true\"\n']
+    bcm_gps_xml_update_option(ad, "add", search_line_tag, append_line_str)
+
     search_line_tag = '<gll\n'
-    append_line_str = '       IgnoreRomAlm=\"true\"\n'
-    bcm_gps_xml_add_option(ad, search_line_tag, append_line_str)
+    append_line_str = ['       IgnoreRomAlm=\"true\"\n',
+                       '       AutoColdStartSignal=\"SIMULATED\"\n',
+                       '       IgnoreJniTime=\"true\"\n']
+    bcm_gps_xml_update_option(ad, "add", search_line_tag, append_line_str)
 
 
 def check_inject_time(ad):
@@ -2449,35 +2795,477 @@
             return True
     raise signals.TestFailure("Fail to get time injected within %s attempts." % i)
 
+def recover_paired_status(watch, phone):
+    """Recover Bluetooth paired status if not paired.
 
-def enable_framework_log(ad):
-    """Enable framework log for wearable to check UTC time download.
+    Args:
+        watch: A wearable project.
+        phone: A pixel phone.
+    """
+    for _ in range(3):
+        watch.log.info("Switch Bluetooth Off-On to recover paired status.")
+        for status in (False, True):
+            watch.droid.bluetoothToggleState(status)
+            phone.droid.bluetoothToggleState(status)
+            # TODO (chenstanley)Need to re-structure for better code and test flow instead of simply waiting
+            watch.log.info("Wait for Bluetooth auto re-connect.")
+            time.sleep(10)
+        if is_bluetooth_connected(watch, phone):
+            watch.log.info("Success to recover paired status.")
+            return True
+    raise signals.TestFailure("Fail to recover BT paired status in 3 attempts.")
+
+def push_lhd_overlay(ad):
+    """Push lhd_overlay.conf to device in /data/vendor/gps/overlay/
+
+    ad:
+        ad: An AndroidDevice object.
+    """
+    overlay_name = "lhd_overlay.conf"
+    overlay_asset = ad.adb.shell("ls /data/vendor/gps/overlay/")
+    if overlay_name in overlay_asset:
+        ad.log.info(f"{overlay_name} already in device, skip.")
+        return
+
+    temp_path = tempfile.mkdtemp()
+    file_path = os.path.join(temp_path, overlay_name)
+    lhd_content = 'Lhe477xDebugFlags=RPC:FACILITY=2097151:LOG_INFO:STDOUT_PUTS:STDOUT_LOG\n'\
+                  'LogLevel=*:E\nLogLevel=*:W\nLogLevel=*:I\nLog=LOGCAT\nLogEnabled=true\n'
+    overlay_path = "/data/vendor/gps/overlay/"
+    with open(file_path, "w") as f:
+        f.write(lhd_content)
+    ad.log.info("Push lhd_overlay to device")
+    ad.adb.push(file_path, overlay_path)
+
+
+def disable_ramdump(ad):
+    """Disable ramdump so device will reboot when about to enter ramdump
+
+    Once device enter ramdump, it will take a while to generate dump file
+    The process may take a while and block all the tests.
+    By disabling the ramdump mode, device will reboot instead of entering ramdump mode
 
     Args:
         ad: An AndroidDevice object.
     """
-    remount_device(ad)
-    time.sleep(3)
-    ad.log.info("Start to enable framwork log for wearable.")
-    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.GpsNetInitiatedHandler=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.GnssNetInitiatedHandler=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.GnssNetworkConnectivityHandler=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.NtpTimeHelper=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.ConnectivityService=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.GnssPsdsDownloader=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.GnssVisibilityControl=VERBOSE'  >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.Gnss=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.GnssConfiguration=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.ImsPhone=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.GsmCdmaPhone=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.Phone=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("echo 'log.tag.GCoreFlp=VERBOSE' >> /data/local.prop")
-    ad.adb.shell("chmod 644 /data/local.prop")
-    ad.adb.shell("echo 'LogEnabled=true' > /data/vendor/gps/libgps.conf")
-    ad.adb.shell("chown gps.system /data/vendor/gps/libgps.conf")
-    ad.adb.shell("sync")
-    reboot(ad)
-    ad.log.info("Wait 2 mins for Wearable booting system busy")
-    time.sleep(120)
+    ad.log.info("Enter bootloader mode")
+    ad.stop_services()
+    ad.adb.reboot("bootloader")
+    for _ in range(1,9):
+        if ad.is_bootloader:
+            break
+        time.sleep(1)
+    else:
+        raise signals.TestFailure("can't enter bootloader mode")
+    ad.log.info("Disable ramdump")
+    ad.fastboot.oem("ramdump disable")
+    ad.fastboot.reboot()
+    ad.wait_for_boot_completion()
+    ad.root_adb()
+    tutils.bring_up_sl4a(ad)
+    ad.start_adb_logcat()
+
+
+def deep_suspend_device(ad):
+    """Force DUT to enter deep suspend mode
+
+    When DUT is connected to PCs, it won't enter deep suspend mode
+    by pressing power button.
+
+    To force DUT enter deep suspend mode, we need to send the
+    following command to DUT  "echo mem >/sys/power/state"
+
+    To make sure the DUT stays in deep suspend mode for a while,
+    it will send the suspend command 3 times with 15s interval
+
+    Args:
+        ad: An AndroidDevice object.
+    """
+    ad.log.info("Ready to go to deep suspend mode")
+    begin_time = get_device_time(ad)
+    ad.droid.goToSleepNow()
+    ensure_power_manager_is_dozing(ad, begin_time)
+    ad.stop_services()
+    try:
+        command = "echo deep > /sys/power/mem_sleep && echo mem > /sys/power/state"
+        for i in range(1, 4):
+            ad.log.debug(f"Send deep suspend command round {i}")
+            ad.adb.shell(command, ignore_status=True)
+            # sleep here to ensure the device stays enough time in deep suspend mode
+            time.sleep(15)
+            if not _is_device_enter_deep_suspend(ad):
+                raise signals.TestFailure("Device didn't enter deep suspend mode")
+        ad.log.info("Wake device up now")
+    except Exception:
+        # when exception happen, it's very likely the device is rebooting
+        # to ensure the test can go on, wait for the device is ready
+        ad.log.warn("Device may be rebooting, wait for it")
+        ad.wait_for_boot_completion()
+        ad.root_adb()
+        raise
+    finally:
+        tutils.bring_up_sl4a(ad)
+        ad.start_adb_logcat()
+        ad.droid.wakeUpNow()
+
+
+def get_device_time(ad):
+    """Get current datetime from device
+
+    Args:
+        ad: An AndroidDevice object.
+
+    Returns:
+        datetime object
+    """
+    result = ad.adb.shell("date +\"%Y-%m-%d %T.%3N\"")
+    return datetime.strptime(result, "%Y-%m-%d %H:%M:%S.%f")
+
+
+def ensure_power_manager_is_dozing(ad, begin_time):
+    """Check if power manager is in dozing
+    When device is sleeping, power manager should goes to doze mode.
+    To ensure that, we check the log every 1 second (maximum to 3 times)
+
+    Args:
+        ad: An AndroidDevice object.
+        begin_time: datetime, used as the starting point to search log
+    """
+    keyword = "PowerManagerService: Dozing"
+    ad.log.debug("Log search start time: %s" % begin_time)
+    for i in range(0,3):
+        result = ad.search_logcat(keyword, begin_time)
+        if result:
+            break
+        ad.log.debug("Power manager is not dozing... retry in 1 second")
+        time.sleep(1)
+    else:
+        ad.log.warn("Power manager didn't enter dozing")
+
+
+
+def _is_device_enter_deep_suspend(ad):
+    """Check device has been enter deep suspend mode
+
+    If device has entered deep suspend mode, we should be able to find keyword
+    "suspend entry (deep)"
+
+    Args:
+        ad: An AndroidDevice object.
+
+    Returns:
+        bool: True / False -> has / has not entered deep suspend
+    """
+    cmd = f"adb -s {ad.serial} logcat -d|grep \"suspend entry (deep)\""
+    process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
+                               stderr=subprocess.PIPE, shell=True)
+    result, _ = process.communicate()
+    ad.log.debug(f"suspend result = {result}")
+
+    return bool(result)
+
+
+def check_location_report_interval(ad, location_reported_time_src, total_seconds, tolerance):
+    """Validate the interval between two location reported time
+    Normally the interval should be around 1 second but occasionally it may up to nearly 2 seconds
+    So we set up a tolerance - 99% of reported interval should be less than 1.3 seconds
+
+    We validate the interval backward, because the wrong interval mostly happened at the end
+    Args:
+        ad: An AndroidDevice object.
+        location_reported_time_src: A list of reported time(in string) from GPS tool
+        total_seconds: (int) how many seconds has the GPS been enabled
+        tolerance: (float) set how many ratio of error should be accepted
+                   if we want to set tolerance to be 1% then pass 0.01 as tolerance value
+    """
+    ad.log.info("Checking location report frequency")
+    error_count = 0
+    error_tolerance = max(1, int(total_seconds * tolerance))
+    expected_longest_interval = 1.3
+    location_reported_time = list(map(lambda x: datetime.strptime(x, "%Y/%m/%d %H:%M:%S.%f"),
+                                      location_reported_time_src))
+    location_reported_time = sorted(location_reported_time)
+    last_gps_report_time = location_reported_time[-1]
+    ad.log.debug("Location report time: %s" % location_reported_time)
+
+    for reported_time in reversed(location_reported_time):
+        time_diff = last_gps_report_time - reported_time
+        if time_diff.total_seconds() > expected_longest_interval:
+            error_count += 1
+        last_gps_report_time = reported_time
+
+    if error_count > error_tolerance:
+        fail_message = (f"Interval longer than {expected_longest_interval}s "
+                        f"exceed tolerance count: {error_tolerance}, error count: {error_count}")
+        ad.log.error(fail_message)
+
+
+@contextmanager
+def set_screen_status(ad, off=True):
+    """Set screen on / off
+
+    A context manager function, can be used with "with" statement.
+    example:
+        with set_screen_status(ad, off=True):
+            do anything you want during screen is off
+    Once the function end, it will turn on the screen
+    Args:
+        ad: AndroidDevice object
+        off: (bool) True -> turn off screen / False -> leave screen as it is
+    """
+    try:
+        if off:
+            ad.droid.goToSleepNow()
+        yield ad
+    finally:
+        ad.droid.wakeUpNow()
+        ensure_device_screen_is_on(ad)
+
+
+@contextmanager
+def full_gnss_measurement(ad):
+    """Context manager function to enable full gnss measurement"""
+    try:
+        ad.adb.shell("settings put global enable_gnss_raw_meas_full_tracking 1")
+        yield ad
+    finally:
+        ad.adb.shell("settings put global enable_gnss_raw_meas_full_tracking 0")
+
+
+def ensure_device_screen_is_on(ad):
+    """Make sure the screen is on
+
+    Will try 3 times, each with 1 second interval
+
+    Raise:
+        GnssTestUtilsError: if screen can't be turn on after 3 tries
+    """
+    for _ in range(3):
+        # when NotificationShade appears in focus window, it indicates the screen is still off
+        if "NotificationShade" not in check_current_focus_app(ad):
+            break
+        time.sleep(1)
+    else:
+        raise GnssTestUtilsError("Device screen is not on after 3 tries")
+
+
+def start_qxdm_and_tcpdump_log(ad, enable):
+    """Start QXDM and adb tcpdump if collect_logs is True.
+    Args:
+        ad: AndroidDevice object
+        enable: (bool) True -> start collecting
+                       False -> not start collecting
+    """
+    if enable:
+        start_pixel_logger(ad)
+        tlutils.start_adb_tcpdump(ad)
+
+
+def set_screen_always_on(ad):
+    """Ensure the sceen will not turn off and display the correct app screen
+    for wearable, we also disable the charing screen,
+    otherwise the charing screen will keep popping up and block the GPS tool
+    """
+    if is_device_wearable(ad):
+        ad.adb.shell("settings put global stay_on_while_plugged_in 7")
+        ad.adb.shell("setprop persist.enable_charging_experience false")
+    else:
+        ad.adb.shell("settings put system screen_off_timeout 1800000")
+
+
+def validate_adr_rate(ad, pass_criteria):
+    """Check the ADR rate
+
+    Args:
+        ad: AndroidDevice object
+        pass_criteria: (float) the passing ratio, 1 = 100%, 0.5 = 50%
+    """
+    adr_statistic = GnssMeasurement(ad).get_adr_static()
+
+    ad.log.info("ADR threshold: {0:.1%}".format(pass_criteria))
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX + "ADR_valid_rate {0:.1%}".format(adr_statistic.valid_rate))
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX +
+                "ADR_usable_rate {0:.1%}".format(adr_statistic.usable_rate))
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX + "ADR_total_count %s" % adr_statistic.total_count)
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX + "ADR_valid_count %s" % adr_statistic.valid_count)
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX + "ADR_reset_count %s" % adr_statistic.reset_count)
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX +
+                "ADR_cycle_slip_count %s" % adr_statistic.cycle_slip_count)
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX +
+                "ADR_half_cycle_reported_count %s" % adr_statistic.half_cycle_reported_count)
+    ad.log.info(UPLOAD_TO_SPONGE_PREFIX +
+                "ADR_half_cycle_resolved_count %s" % adr_statistic.half_cycle_resolved_count)
+
+    asserts.assert_true(
+        (pass_criteria < adr_statistic.valid_rate) and (pass_criteria < adr_statistic.usable_rate),
+        f"ADR valid rate: {adr_statistic.valid_rate:.1%}, "
+        f"ADR usable rate: {adr_statistic.usable_rate:.1%} "
+        f"Lower than expected: {pass_criteria:.1%}"
+    )
+
+
+def pair_to_wearable(watch, phone):
+    """Pair watch to phone.
+
+    Args:
+        watch: A wearable project.
+        phone: A pixel phone.
+    Raise:
+        TestFailure: If pairing process could not success after 3 tries.
+    """
+    for _ in range(3):
+        process_pair(watch, phone)
+        if is_bluetooth_connected(watch, phone):
+            watch.log.info("Pairing successfully.")
+            return True
+    raise signals.TestFailure("Pairing is not successfully.")
+
+
+def disable_battery_defend(ad):
+    """Disable battery defend config to prevent battery defend message pop up
+    after connecting to the same charger for 4 days in a row.
+
+    Args:
+        ad: A wearable project.
+    """
+    for _ in range(5):
+        remount_device(ad)
+        ad.adb.shell("setprop vendor.battery.defender.disable 1")
+        # To simulate cable unplug and the status will be recover after device reboot.
+        ad.adb.shell("cmd battery unplug")
+        # Sleep 3 seconds for waiting adb commend changes config and simulates cable unplug.
+        time.sleep(3)
+        config_setting = ad.adb.shell("getprop vendor.battery.defender.state")
+        if config_setting == "DISABLED":
+            ad.log.info("Disable Battery Defend setting successfully.")
+            break
+
+
+def restart_hal_service(ad):
+    """Restart HAL service by killing the pid.
+
+    Gets the pid by ps command and pass the pid to kill command. Then we get the pid of HAL service
+    again to see if the pid changes(pid should be different after HAL restart). If not, we will
+    retry up to 4 times before raising Test Failure.
+
+    Args:
+        ad: AndroidDevice object
+    """
+    ad.log.info("Restart HAL service")
+    hal_process_name = "'android.hardware.gnss@[[:digit:]]\{1,2\}\.[[:digit:]]\{1,2\}-service'"
+    hal_pid = get_process_pid(ad, hal_process_name)
+    ad.log.info("HAL pid: %s" % hal_pid)
+
+    # Retry kill process if the PID is the same as original one
+    for _ in range(4):
+        ad.log.info("Kill HAL service")
+        ad.adb.shell(f"kill -9 {hal_pid}")
+
+        # Waits for the HAL service to restart up to 4 seconds.
+        for _ in range(4):
+            new_hal_pid = get_process_pid(ad, hal_process_name)
+            ad.log.info("New HAL pid: %s" % new_hal_pid)
+            if new_hal_pid:
+                if hal_pid != new_hal_pid:
+                    return
+                break
+            time.sleep(1)
+    else:
+        raise signals.TestFailure("HAL service can't be killed")
+
+
+def run_ttff(ad, mode, criteria, test_cycle, base_lat_long, collect_logs=False):
+    """Verify TTFF functionality with mobile data.
+
+    Args:
+        mode: "cs", "ws" or "hs"
+        criteria: Criteria for the test.
+
+    Returns:
+        ttff_data: A dict of all TTFF data.
+    """
+    start_qxdm_and_tcpdump_log(ad, collect_logs)
+    return run_ttff_via_gtw_gpstool(ad, mode, criteria, test_cycle, base_lat_long)
+
+
+def re_register_measurement_callback(dut):
+    """Send command to unregister then register measurement callback.
+
+    Args:
+        dut: The device under test.
+    """
+    dut.log.info("Reregister measurement callback")
+    dut.adb.shell("am broadcast -a com.android.gpstool.stop_meas_action")
+    time.sleep(1)
+    dut.adb.shell("am broadcast -a com.android.gpstool.start_meas_action")
+    time.sleep(1)
+
+
+def check_power_save_mode_status(ad, full_power, begin_time, brcm_error_allowlist):
+    """Checks the power save mode status.
+
+    For Broadcom:
+        Gets NEMA sentences from pixel logger and retrieve the status [F, S, D].
+        F,S => not in full power mode
+        D => in full power mode
+    For Qualcomm:
+        Gets the HardwareClockDiscontinuityCount from logcat. In full power mode, the
+        HardwareClockDiscontinuityCount should not be increased.
+
+    Args:
+        ad: The device under test.
+        full_power: The device is in full power mode or not.
+        begin_time: It is used to get the correct logcat information for qualcomm.
+        brcm_error_allowlist: It is used to ignore certain error in pixel logger.
+    """
+    if check_chipset_vendor_by_qualcomm(ad):
+        _check_qualcomm_power_save_mode(ad, full_power, begin_time)
+    else:
+        _check_broadcom_power_save_mode(ad, full_power, brcm_error_allowlist)
+
+
+def _check_qualcomm_power_save_mode(ad, full_power, begin_time):
+    dpo_results = _get_dpo_info_from_logcat(ad, begin_time)
+    first_dpo_count = int(dpo_results[0]["log_message"].split()[-1])
+    final_dpo_count = int(dpo_results[-1]["log_message"].split()[-1])
+    dpo_count_diff = final_dpo_count - first_dpo_count
+    ad.log.debug("The DPO count diff is {diff}".format(diff=dpo_count_diff))
+    if full_power:
+        asserts.assert_equal(dpo_count_diff, 0, msg="DPO count diff should be 0")
+    else:
+        asserts.assert_true(dpo_count_diff > 0, msg="DPO count diff should be more than 0")
+
+
+def _check_broadcom_power_save_mode(ad, full_power, brcm_error_allowlist):
+    power_save_log, _ = _get_power_mode_log_from_pixel_logger(
+        ad, brcm_error_allowlist, stop_pixel_logger=False)
+    power_status = re.compile(r',P,(\w),').search(power_save_log[-2]).group(1)
+    ad.log.debug("The power status is {status}".format(status=power_status))
+    if full_power:
+        asserts.assert_true(power_status == "D", msg="Should be in full power mode")
+    else:
+        asserts.assert_true(power_status in ["F", "S"], msg="Should not be in full power mode")
+
+@contextmanager
+def run_gnss_tracking(ad, criteria, meas_flag):
+    """A context manager to enable gnss tracking and stops at the end.
+
+    Args:
+        ad: The device under test.
+        criteria: The criteria for First Fixed.
+        meas_flag: A flag to turn on measurement log or not.
+    """
+    process_gnss_by_gtw_gpstool(ad, criteria=criteria, meas_flag=meas_flag)
+    try:
+        yield
+    finally:
+        start_gnss_by_gtw_gpstool(ad, state=False)
+
+def log_current_epoch_time(ad, sponge_key):
+    """Logs current epoch timestamp in second.
+
+    Args:
+        sponge_key: The key name of the sponge property.
+    """
+    current_epoch_time = get_current_epoch_time() // 1000
+    ad.log.info(f"TestResult {sponge_key} {current_epoch_time}")
\ No newline at end of file
diff --git a/acts_tests/acts_contrib/test_utils/gnss/gnss_testlog_utils.py b/acts_tests/acts_contrib/test_utils/gnss/gnss_testlog_utils.py
index 4f5df3c..5e7cf26 100644
--- a/acts_tests/acts_contrib/test_utils/gnss/gnss_testlog_utils.py
+++ b/acts_tests/acts_contrib/test_utils/gnss/gnss_testlog_utils.py
@@ -209,17 +209,17 @@
         configs=CONFIG_GPSTTFFLOG,
     )
     ttff_df = parsed_data['ttff_info']
-
-    # Data Conversion
-    ttff_df['loop'] = ttff_df['loop'].astype(int)
-    ttff_df['start_datetime'] = pds.to_datetime(ttff_df['start_datetime'])
-    ttff_df['stop_datetime'] = pds.to_datetime(ttff_df['stop_datetime'])
-    ttff_df['ttff_time'] = ttff_df['ttff'].astype(float)
-    ttff_df['ant_avg_top4_cn0'] = ttff_df['ant_avg_top4_cn0'].astype(float)
-    ttff_df['ant_avg_cn0'] = ttff_df['ant_avg_cn0'].astype(float)
-    ttff_df['bb_avg_top4_cn0'] = ttff_df['bb_avg_top4_cn0'].astype(float)
-    ttff_df['bb_avg_cn0'] = ttff_df['bb_avg_cn0'].astype(float)
-    ttff_df['satnum_for_fix'] = ttff_df['satnum_for_fix'].astype(int)
+    if not ttff_df.empty:
+        # Data Conversion
+        ttff_df['loop'] = ttff_df['loop'].astype(int)
+        ttff_df['start_datetime'] = pds.to_datetime(ttff_df['start_datetime'])
+        ttff_df['stop_datetime'] = pds.to_datetime(ttff_df['stop_datetime'])
+        ttff_df['ttff_time'] = ttff_df['ttff'].astype(float)
+        ttff_df['ant_avg_top4_cn0'] = ttff_df['ant_avg_top4_cn0'].astype(float)
+        ttff_df['ant_avg_cn0'] = ttff_df['ant_avg_cn0'].astype(float)
+        ttff_df['bb_avg_top4_cn0'] = ttff_df['bb_avg_top4_cn0'].astype(float)
+        ttff_df['bb_avg_cn0'] = ttff_df['bb_avg_cn0'].astype(float)
+        ttff_df['satnum_for_fix'] = ttff_df['satnum_for_fix'].astype(int)
 
     # return ttff dataframe
     return ttff_df
diff --git a/acts_tests/acts_contrib/test_utils/gnss/gnssstatus_utils.py b/acts_tests/acts_contrib/test_utils/gnss/gnssstatus_utils.py
new file mode 100644
index 0000000..457ba86
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/gnss/gnssstatus_utils.py
@@ -0,0 +1,200 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2020 - 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 re
+from acts import signals
+from collections import defaultdict
+
+SVID_RANGE = {
+    'GPS': [(1, 32)],
+    'SBA': [(120, 192)],
+    'GLO': [(1, 24), (93, 106)],
+    'QZS': [(193, 200)],
+    'BDS': [(1, 63)],
+    'GAL': [(1, 36)],
+    'NIC': [(1, 14)]
+}
+
+CARRIER_FREQUENCIES = {
+    'GPS': {
+        'L1': [1575.42],
+        'L5': [1176.45]
+    },
+    'SBA': {
+        'L1': [1575.42]
+    },
+    'GLO': {
+        'L1': [round((1602 + i * 0.5625), 3) for i in range(-7, 7)]
+    },
+    'QZS': {
+        'L1': [1575.42],
+        'L5': [1176.45]
+    },
+    'BDS': {
+        'B1': [1561.098],
+        'B2a': [1176.45]
+    },
+    'GAL': {
+        'E1': [1575.42],
+        'E5a': [1176.45]
+    },
+    'NIC': {
+        'L5': [1176.45]
+    }
+}
+
+
+class RegexParseException(Exception):
+    pass
+
+
+class GnssSvidContainer:
+    """A class to hold the satellite svid information
+
+    Attributes:
+        used_in_fix: A dict contains unique svid used in fixing location
+        not_used_in_fix: A dict contains unique svid not used in fixing location
+    """
+
+    def __init__(self):
+        self.used_in_fix = defaultdict(set)
+        self.not_used_in_fix = defaultdict(set)
+
+    def add_satellite(self, gnss_status):
+        """Add satellite svid into container
+
+        According to the attributes gnss_status.used_in_fix
+            True: add svid into self.used_in_fix container
+            False: add svid into self.not_used_in_fix container
+
+        Args:
+            gnss_status: A GnssStatus object
+        """
+        key = f'{gnss_status.constellation}_{gnss_status.frequency_band}'
+        if gnss_status.used_in_fix:
+            self.used_in_fix[key].add(gnss_status.svid)
+        else:
+            self.not_used_in_fix[key].add(gnss_status.svid)
+
+
+class GnssStatus:
+    """GnssStatus object, it will create an obj with a raw gnssstatus line.
+
+    Attributes:
+        raw_message: (string) The raw log from GSPTool
+            example:
+                Fix: true Type: NIC SV: 4 C/No: 45.10782, 40.9 Elevation: 78.0
+                  Azimuth: 291.0
+                Signal: L5 Frequency: 1176.45 EPH: true ALM: false
+                Fix: false Type: GPS SV: 27 C/No: 34.728134, 30.5 Elevation:
+                  76.0 Azimuth: 15.0
+                Signal: L1 Frequency: 1575.42 EPH: true ALM: true
+        used_in_fix: (boolean) Whether or not this satellite info is used to fix
+          location
+        constellation: (string) The constellation type i.e. GPS
+        svid: (int) The unique id of the constellation
+        cn: (float) The C/No value from antenna
+        base_cn: (float) The C/No value from baseband
+        elev: (float) The value of elevation
+        azim: (float) The value of azimuth
+        frequency_band: (string) The frequency_type of the constellation i.e. L1
+          / L5
+    """
+
+    gnssstatus_re = (
+        r'Fix: (.*) Type: (.*) SV: (.*) C/No: (.*), (.*) '
+        r'Elevation: (.*) Azimuth: (.*) Signal: (.*) Frequency: (.*) EPH')
+    failures = []
+
+    def __init__(self, gnssstatus_raw):
+        status_res = re.search(self.gnssstatus_re, gnssstatus_raw)
+        if not status_res:
+            raise RegexParseException(f'Gnss raw msg parse fail:\n{gnssstatus_raw}\n'
+                                      f'Please check it manually.')
+        self.raw_message = gnssstatus_raw
+        self.used_in_fix = status_res.group(1).lower() == 'true'
+        self.constellation = status_res.group(2)
+        self.svid = int(status_res.group(3))
+        self.cn = float(status_res.group(4))
+        self.base_cn = float(status_res.group(5))
+        self.elev = float(status_res.group(6))
+        self.azim = float(status_res.group(7))
+        self.frequency_band = status_res.group(8)
+        self.carrier_frequency = float(status_res.group(9))
+
+    def validate_gnssstatus(self):
+        """A validate function for each property."""
+        self._validate_sv()
+        self._validate_cn()
+        self._validate_elev()
+        self._validate_azim()
+        self._validate_carrier_frequency()
+        if self.failures:
+            failure_info = '\n'.join(self.failures)
+            raise signals.TestFailure(
+                f'Gnsstatus validate failed:\n{self.raw_message}\n{failure_info}'
+            )
+
+    def _validate_sv(self):
+        """A validate function for SV ID."""
+        if not self.constellation in SVID_RANGE.keys():
+            raise signals.TestFailure(
+                f'Satellite identify fail: {self.constellation}')
+        for id_range in SVID_RANGE[self.constellation]:
+            if id_range[0] <= self.svid <= id_range[1]:
+                break
+        else:
+            fail_details = f'{self.constellation} ID {self.svid} not in SV Range'
+            self.failures.append(fail_details)
+
+    def _validate_cn(self):
+        """A validate function for CN value."""
+        if not 0 <= self.cn <= 63:
+            self.failures.append(f'Ant CN not in range: {self.cn}')
+        if not 0 <= self.base_cn <= 63:
+            self.failures.append(f'Base CN not in range: {self.base_cn}')
+
+    def _validate_elev(self):
+        """A validate function for Elevation (should between 0-90)."""
+        if not 0 <= self.elev <= 90:
+            self.failures.append(f'Elevation not in range: {self.elev}')
+
+    def _validate_azim(self):
+        """A validate function for Azimuth (should between 0-360)."""
+        if not 0 <= self.azim <= 360:
+            self.failures.append(f'Azimuth not in range: {self.azim}')
+
+    def _validate_carrier_frequency(self):
+        """A validate function for carrier frequency (should fall in below range).
+
+           'GPS': L1:1575.42, L5:1176.45
+           'SBA': L1:1575.42
+           'GLO': L1:Between 1598.0625 and 1605.375
+           'QZS': L1:1575.42, L5:1176.45
+           'BDS': B1:1561.098, B2a:1176.45
+           'GAL': E1:1575.42, E5a:1176.45
+           'NIC': L5:1176.45
+        """
+        if self.frequency_band in CARRIER_FREQUENCIES[
+                self.constellation].keys():
+            target_freq = CARRIER_FREQUENCIES[self.constellation][
+                self.frequency_band]
+        else:
+            raise signals.TestFailure(
+                f'Carrier frequency identify fail: {self.frequency_band}')
+        if not self.carrier_frequency in target_freq:
+            self.failures.append(
+                f'{self.constellation}_{self.frequency_band} carrier'
+                f'frequency not in range: {self.carrier_frequency}')
diff --git a/acts_tests/acts_contrib/test_utils/gnss/supl.py b/acts_tests/acts_contrib/test_utils/gnss/supl.py
new file mode 100644
index 0000000..f6c1bc6
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/gnss/supl.py
@@ -0,0 +1,71 @@
+import os
+import tempfile
+from xml.etree import ElementTree
+
+
+def set_supl_over_wifi_state(ad, turn_on):
+    """Enable / Disable supl over wifi features
+
+    Modify the gps xml file: /vendor/etc/gnss/gps.xml
+    Args:
+        ad: AndroidDevice object
+        turn_on: (bool) True -> enable / False -> disable
+    """
+    ad.adb.remount()
+    folder = tempfile.mkdtemp()
+    xml_path_on_host = os.path.join(folder, "gps.xml")
+    xml_path_on_device = "/vendor/etc/gnss/gps.xml"
+    ad.pull_files(xml_path_on_device, xml_path_on_host)
+
+    # register namespance to aviod adding ns0 into xml attributes
+    ElementTree.register_namespace("", "http://www.glpals.com/")
+    xml_tree = ElementTree.parse(xml_path_on_host)
+    root = xml_tree.getroot()
+    for node in root:
+        if "hal" in node.tag:
+            if turn_on:
+                _enable_supl_over_wifi(ad, node)
+            else:
+                _disable_supl_over_wifi(ad, node)
+    xml_tree.write(xml_path_on_host, xml_declaration=True, encoding="utf-8", method="xml")
+    ad.push_system_file(xml_path_on_host, xml_path_on_device)
+
+
+def _enable_supl_over_wifi(ad, node):
+    """Enable supl over wifi
+    Detail setting:
+        <hal
+            SuplDummyCellInfo="true"
+            SuplUseApn="false"
+            SuplUseApnNI="true"
+            SuplUseFwCellInfo="false"
+        />
+    Args:
+        ad: AndroidDevice object
+        node: ElementTree node
+    """
+    ad.log.info("Enable SUPL over wifi")
+    attributes = {"SuplDummyCellInfo": "true", "SuplUseApn": "false", "SuplUseApnNI": "true",
+                  "SuplUseFwCellInfo": "false"}
+    for key, value in attributes.items():
+        node.set(key, value)
+
+
+def _disable_supl_over_wifi(ad, node):
+    """Disable supl over wifi
+    Detail setting:
+        <hal
+            SuplUseApn="true"
+        />
+    Remove following setting
+        SuplDummyCellInfo="true"
+        SuplUseApnNI="true"
+        SuplUseFwCellInfo="false"
+    Args:
+        ad: AndroidDevice object
+        node: ElementTree node
+    """
+    ad.log.info("Disable SUPL over wifi")
+    for attri in ["SuplDummyCellInfo", "SuplUseApnNI", "SuplUseFwCellInfo"]:
+        node.attrib.pop(attri, None)
+    node.set("SuplUseApn", "true")
diff --git a/acts_tests/acts_contrib/test_utils/gnss/testtracker_util.py b/acts_tests/acts_contrib/test_utils/gnss/testtracker_util.py
new file mode 100644
index 0000000..43f7920
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/gnss/testtracker_util.py
@@ -0,0 +1,88 @@
+TEST_NAME_BY_TESTTRACKER_UUID = {
+    # GnssFunctionTest
+    "test_cs_first_fixed_system_server_restart": "8169c19d-ba2a-4fef-969b-87f793f4e699",
+    "test_cs_ttff_after_gps_service_restart": "247110d9-1c9e-429e-8e73-f16dd4a1ac74",
+    "test_gnss_one_hour_tracking": "b3d20ecb-3727-48ed-8a03-19694cc726c1",
+    "test_duty_cycle_function": "0bbfb818-da93-41d7-8d83-15bc53d8d2cf",
+    "test_gnss_init_error": "c661780d-4864-4292-9988-88f64448fb78",
+    "test_sap_valid_modes": "89bb8103-a3af-4953-8f07-e43c7e829bdd",
+    "test_network_location_provider_cell": "6f59d0f5-569c-4d52-990b-0042123b70ab",
+    "test_network_location_provider_wifi": "eec8b4bd-6990-4098-ad7a-acc19574bdee",
+    "test_gmap_location_report_battery_saver": "040556bf-1ffc-4db2-b2c5-19c4da19a256",
+    "test_gnss_ttff_cs_airplane_mode_on": "bc3d509c-0392-4af1-a0d0-68fd01167573",
+    "test_gnss_ttff_ws_airplane_mode_on": "dcafc69a-095e-4d58-8afb-5276c5763f4d",
+    "test_gnss_ttff_hs_airplane_mode_on": "090ea66c-19a1-4d0b-8c7e-dbc967597764",
+    "test_cs_ttff_in_weak_gnss_signal": "1980f980-3134-47b0-8dd8-9c5af6b742a6",
+    "test_ws_ttff_in_weak_gnss_signal": "d77c8db1-687b-48c5-8101-e4267da05995",
+    "test_hs_ttff_in_weak_gnss_signal": "dd4ccd93-9e49-45c2-a3ea-40781a40b820",
+    "test_quick_toggle_gnss_state": "36c14727-5de7-4589-ad1b-9119f9d9bb52",
+    "test_gnss_init_after_reboot": "79be8ab6-26cb-4d1a-b3d3-4e5681766901",
+    "test_host_gnssstatus_validation": "767c3024-0db4-4d40-9b03-f30355d72a06",
+    "test_onchip_gnssstatus_validation": "afb08722-2c79-46a6-80fd-9ede5018e384",
+    "test_location_update_after_resuming_from_deep_suspend": "140a7763-f42c-4917-a71f-fbc0626c1609",
+    "test_location_mode_in_battery_saver_with_screen_off": "04b529f1-a99d-4b18-9bba-41e008249f7a",
+    "test_measure_adr_rate_after_10_mins_tracking": "7ebf3b52-229a-4eaf-bbff-7c527e4a1d7c",
+    "test_hal_crashing_should_resume_tracking": "0aee4450-edce-4e1a-8744-70d8c01937b0",
+    "test_power_save_mode_should_apply_latest_measurement_setting": "59a14da2-40df-4106-a190-dcbcd2e877e0",
+    # GnssConcurrencyTest
+    "test_gnss_concurrency_location_1_chre_1": "cbd9ff54-4405-44a4-ac20-de33278406d1",
+    "test_gnss_concurrency_location_1_chre_8": "ab56cb47-384e-4269-b2d8-6e80ce066de2",
+    "test_gnss_concurrency_location_15_chre_8": "e64fa984-6219-43dd-96d6-d4141b2da1cd",
+    "test_gnss_concurrency_location_61_chre_1": "217f3ab6-25c9-4092-8fe1-f4e4199d60c6",
+    "test_gnss_concurrency_location_61_chre_10": "c32ca948-0414-4529-98d0-8351b5f31bab",
+    "test_gnss_chre_1": "9dae57f3-70f9-4328-a448-925da88725ac",
+    "test_gnss_chre_8": "a8c8f7fa-4dfd-42d8-ac6a-c3e3f186e317",
+    "test_variable_interval_via_chre": "53b161e5-335e-44a7-ae2e-eae7464a2b37",
+    "test_variable_interval_via_framework": "6b525afa-1427-4a99-906f-bc0aab6d4d30",
+    "test_gps_engine_switching_host_to_onchip": "07e0e138-4966-4307-b600-0521e626b967",
+    "test_gps_engine_switching_onchip_to_host": "564a229e-e784-43af-b430-4ab14656cfdc",
+    "test_mcu_cs_ttff": "736da33a-a976-44b6-93b3-bcfd847dd03d",
+    "test_mcu_ws_ttff": "e3457e35-9872-4677-8170-bc30d84798c0",
+    "test_mcu_hs_ttff": "0e1ce60d-e257-4dc5-b927-7ae97c8386b6",
+    # GnssBroadcomConfigurationTest
+    "test_gps_logenabled_setting": "d1310171-1641-4fa2-8802-cca7ce33bbd4",
+    "test_gps_supllogenable_setting": "ebe30341-4097-4e2c-b104-0c592f1f9e83",
+    "test_lhe_setting": "099aea19-5078-447c-925f-01a702624884",
+    # GnssSuplTest
+    "test_supl_capabilities": "6c794396-46e8-4674-8985-49a7b3059372",
+    "test_supl_ttff_cs": "ae8b6d54-bdd6-44a1-b1fa-4e90e0318080",
+    "test_supl_ttff_ws": "65f25e0b-c6d0-47c5-ab1f-0b02b621411d",
+    "test_supl_ttff_hs": "a2267586-97e9-465c-8d3a-22882c8671e7",
+    "test_cs_ttff_supl_over_wifi_with_airplane_mode_on": "4b2882f8-2966-4b44-9a31-37318beb84bf",
+    "test_ws_ttff_supl_over_wifi_with_airplane_mode_on": "a7f77afe-c82e-4b1b-ae54-e3fea17bf721",
+    "test_hs_ttff_supl_over_wifi_with_airplane_mode_on": "bc9de22f-90a0-4f2b-8052-cb4529f745e3",
+    "test_ttff_gla_on": "06aa85a2-7c3a-453a-b765-dc9ea6ee6b9b",
+    "test_ttff_gla_off": "36347b6e-d03e-4773-82bf-2e12d4f4dd0d",
+    # GnssVendorFeaturesTest
+    "test_xtra_ttff_cs_mobile_data": "da0bf0a1-d635-4942-808a-30070cfb2c78",
+    "test_xtra_ttff_ws_mobile_data": "95f17477-c88e-4663-a0ab-c09dc1706f75",
+    "test_xtra_ttff_hs_mobile_data": "bf13e2e4-79c9-4769-9dfd-81b085112744",
+    "test_xtra_ttff_cs_wifi": "5c0f95d2-7c76-45ca-95c8-304c742e0c82",
+    "test_xtra_ttff_ws_wifi": "507b2da1-c58d-4bca-810b-b274082a21c4",
+    "test_xtra_ttff_hs_wifi": "69d1998a-dd78-46a9-904d-d28f07dc3ef2",
+    "test_xtra_download_mobile_data": "d260a510-941a-48c7-a545-d7239f8f03dc",
+    "test_xtra_download_wifi": "e1dec4d2-4a85-4680-92df-57c972c084aa",
+    "test_lto_download_after_reboot": "579d249e-d533-4979-915f-b3a7d847546e",
+    "test_ws_with_assist": "938cbc1f-0374-473c-b4c1-1b4af734f16a",
+    "test_cs_with_assist": "9eae7b7d-9356-4fd8-bc18-f9d93aa0a92b",
+    # GnssWearableTetherFunctionTest
+    "test_flp_ttff_cs": "6dec9502-7e74-4590-b174-be822ceefcdb",
+    "test_flp_ttff_ws": "ddb8f09d-4757-42ae-9707-1fdb38187d1f",
+    "test_flp_ttff_hs": "19a997da-7c36-4fef-bf68-497d7c21163f",
+    "test_tracking_during_bt_disconnect_resume": "c9e26620-e518-4c7d-afcc-f81f6d3971bb",
+    "test_oobe_first_fix": "432e799c-3d02-46de-84d5-d5a22feceef8",
+    "test_oobe_first_fix_with_network_connection": "66264dac-50d0-4bc0-be72-6dbe9159587b",
+    "test_far_start_ttff": "4693b424-1a24-4002-b51d-df6b6bb91830",
+}
+
+def log_testtracker_uuid(ad, current_test_name):
+    """Logs testtracker uuid for the current test case.
+
+    Args:
+        ad: Target AndroidDevice object.
+        current_test_name: Current test name used to map testtracker uuid.
+    """
+    current_test_uuid = TEST_NAME_BY_TESTTRACKER_UUID.get(
+        current_test_name, None)
+    if current_test_uuid:
+        ad.log.info(f"TestResult mobly_uid {current_test_uuid}")
\ No newline at end of file
diff --git a/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py b/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py
index 11094fb..7af1e12 100644
--- a/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py
+++ b/acts_tests/acts_contrib/test_utils/power/PowerBaseTest.py
@@ -27,6 +27,7 @@
 from acts import base_test
 from acts import utils
 from acts.metrics.loggers.blackbox import BlackboxMetricLogger
+from acts.controllers.adb_lib.error import AdbError
 from acts_contrib.test_utils.power.loggers.power_metric_logger import PowerMetricLogger
 from acts_contrib.test_utils.power import plot_utils
 
@@ -35,6 +36,8 @@
 THRESHOLD_TOLERANCE_DEFAULT = 0.2
 GET_FROM_PHONE = 'get_from_dut'
 GET_FROM_AP = 'get_from_ap'
+GET_PROPERTY_HARDWARE_PLATFORM = 'getprop ro.boot.hardware.platform'
+POWER_STATS_DUMPSYS_CMD = 'dumpsys android.hardware.power.stats.IPowerStats/default delta'
 PHONE_BATTERY_VOLTAGE_DEFAULT = 4.2
 MONSOON_MAX_CURRENT = 8.0
 DEFAULT_MONSOON_FREQUENCY = 500
@@ -76,6 +79,7 @@
         self.dut = None
         self.power_logger = PowerMetricLogger.for_test_case()
         self.power_monitor = None
+        self.odpm_folder = None
 
     @property
     def final_test(self):
@@ -150,13 +154,23 @@
                                iperf_duration=None,
                                pass_fail_tolerance=THRESHOLD_TOLERANCE_DEFAULT,
                                mon_voltage=PHONE_BATTERY_VOLTAGE_DEFAULT,
-                               ap_dtim_period=None)
+                               ap_dtim_period=None,
+                               bits_root_rail_csv_export=False)
 
         # Setup the must have controllers, phone and monsoon
         self.dut = self.android_devices[0]
         self.mon_data_path = os.path.join(self.log_path, 'Monsoon')
         os.makedirs(self.mon_data_path, exist_ok=True)
 
+        # Make odpm path for P21 or later
+        platform = self.dut.adb.shell(GET_PROPERTY_HARDWARE_PLATFORM)
+        self.log.info('The hardware platform is {}'.format(platform))
+        if platform.startswith('gs'):
+            self.odpm_folder = os.path.join(self.log_path, 'odpm')
+            os.makedirs(self.odpm_folder, exist_ok=True)
+            self.log.info('For P21 or later, create odpm folder {}'.format(
+                self.odpm_folder))
+
         # Initialize the power monitor object that will be used to measure
         self.initialize_power_monitor()
 
@@ -279,6 +293,26 @@
     def on_pass(self, test_name, begin_time):
         self.power_logger.set_pass_fail_status('PASS')
 
+    def dut_save_odpm(self, tag):
+        """Dumpsys ODPM data and save it to self.odpm_folder.
+
+        Args:
+            tag: the moment of save ODPM data
+        """
+        odpm_file_name = '{}.{}.dumpsys_odpm_{}.txt'.format(
+            self.__class__.__name__,
+            self.current_test_name,
+            tag)
+        odpm_file_path = os.path.join(self.odpm_folder, odpm_file_name)
+
+        try:
+            stats = self.dut.adb.shell(POWER_STATS_DUMPSYS_CMD)
+            with open(odpm_file_path, 'w') as f:
+                f.write(stats)
+        except AdbError as e:
+            self.log.warning('Odpm data with tag {} did not save due to adb '
+                             'error {}'.format(e))
+
     def dut_rockbottom(self):
         """Set the dut to rockbottom state
 
@@ -464,6 +498,11 @@
         # Start the power measurement using monsoon.
         self.dut.stop_services()
         time.sleep(1)
+
+        # P21 or later device, save the odpm data before power measurement
+        if self.odpm_folder:
+            self.dut_save_odpm('before')
+
         self.power_monitor.disconnect_usb()
         measurement_args = dict(duration=self.mon_info.duration,
                                 measure_after_seconds=self.mon_info.offset,
@@ -473,9 +512,15 @@
                                    start_time=device_to_host_offset,
                                    monsoon_output_path=data_path)
         self.power_monitor.release_resources()
+        self.collect_raw_data_samples()
         self.power_monitor.connect_usb()
         self.dut.wait_for_boot_completion()
         time.sleep(10)
+
+        # For P21 or later device, save the odpm data after power measurement
+        if self.odpm_folder:
+            self.dut_save_odpm('after')
+
         self.dut.start_services()
 
         return self.power_monitor.get_waveform(file_path=data_path)
@@ -510,3 +555,9 @@
             self.log.warning('Cannot get iperf result. Setting to 0')
             throughput = 0
         return throughput
+
+    def collect_raw_data_samples(self):
+        if hasattr(self, 'bitses') and self.bits_root_rail_csv_export:
+            path = os.path.join(os.path.dirname(self.mon_info.data_path),
+                                'Kibble')
+            self.power_monitor.get_bits_root_rail_csv_export(path, self.test_name)
diff --git a/acts_tests/acts_contrib/test_utils/power/PowerGTWGnssBaseTest.py b/acts_tests/acts_contrib/test_utils/power/PowerGTWGnssBaseTest.py
index baedb7e..201f17f 100644
--- a/acts_tests/acts_contrib/test_utils/power/PowerGTWGnssBaseTest.py
+++ b/acts_tests/acts_contrib/test_utils/power/PowerGTWGnssBaseTest.py
@@ -41,6 +41,7 @@
         self.set_xtra_data()
 
     def setup_test(self):
+        gutils.log_current_epoch_time(self.ad, "test_start_time")
         super().setup_test()
         # Enable DPO
         self.enable_DPO(True)
@@ -53,6 +54,7 @@
         begin_time = utils.get_current_epoch_time()
         self.ad.take_bug_report(self.test_name, begin_time)
         gutils.get_gnss_qxdm_log(self.ad, self.qdsp6m_path)
+        gutils.log_current_epoch_time(self.ad, "test_end_time")
 
     def set_xtra_data(self):
         gutils.disable_xtra_throttle(self.ad)
diff --git a/acts_tests/acts_contrib/test_utils/power/cellular/cellular_power_base_test.py b/acts_tests/acts_contrib/test_utils/power/cellular/cellular_power_base_test.py
index 7285a16..f2ebea9 100644
--- a/acts_tests/acts_contrib/test_utils/power/cellular/cellular_power_base_test.py
+++ b/acts_tests/acts_contrib/test_utils/power/cellular/cellular_power_base_test.py
@@ -19,6 +19,7 @@
 import acts_contrib.test_utils.power.PowerBaseTest as PBT
 import acts_contrib.test_utils.cellular.cellular_base_test as CBT
 from acts_contrib.test_utils.power import plot_utils
+from acts import context
 
 
 class PowerCellularLabBaseTest(CBT.CellularBaseTest, PBT.PowerBaseTest):
diff --git a/acts_tests/acts_contrib/test_utils/power/cellular/cellular_power_preset_base_test.py b/acts_tests/acts_contrib/test_utils/power/cellular/cellular_power_preset_base_test.py
new file mode 100644
index 0000000..15043d4
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/power/cellular/cellular_power_preset_base_test.py
@@ -0,0 +1,414 @@
+import os
+from typing import Optional, List
+import time
+from acts import context
+from acts import signals
+from acts.controllers.cellular_lib import AndroidCellularDut
+import acts_contrib.test_utils.power.cellular.cellular_power_base_test as PWCEL
+
+# TODO: b/261639867
+class AtUtil():
+    """Util class for sending at command.
+
+    Attributes:
+        dut: AndroidDevice controller object.
+    """
+    ADB_CMD_DISABLE_TXAS = 'am instrument -w -e request at+googtxas=2 -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+
+    def __init__(self, dut, log) -> None:
+        self.dut = dut
+        self.log = log
+
+    # TODO: to be remove when b/261639867 complete,
+    # and we are using parent method.
+    def send(self, cmd: str,) -> Optional[str]:
+        res = str(self.dut.adb.shell(cmd))
+        self.log.info(f'cmd sent: {cmd}')
+        self.log.info(f'response: {res}')
+        if 'SUCCESS' in res:
+            self.log.info('Command executed.')
+        else:
+            self.log.error('Fail to executed command.')
+        return res
+
+    def lock_LTE(self):
+        adb_enable_band_lock_lte = r'am instrument -w -e request at+GOOGSETNV=\"!SAEL3.Manual.Band.Select\ Enb\/\ Dis\",00,\"01\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+        adb_set_band_lock_mode_lte = r'am instrument -w -e request at+GOOGSETNV=\"NASU.SIPC.NetworkMode.ManualMode\",0,\"0D\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+        adb_set_band_lock_bitmap_0 = r'am instrument -w -e request at+GOOGSETNV=\"!SAEL3.Manual.Enabled.RFBands.BitMap\",0,\"09,00,00,00,00,00,00,00\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+        adb_set_band_lock_bitmap_1 = r'am instrument -w -e request at+GOOGSETNV=\"!SAEL3.Manual.Enabled.RFBands.BitMap\",1,\"00,00,00,00,00,00,00,00\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+        adb_set_band_lock_bitmap_2 = r'am instrument -w -e request at+GOOGSETNV=\"!SAEL3.Manual.Enabled.RFBands.BitMap\",2,\"00,00,00,00,00,00,00,00\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+        adb_set_band_lock_bitmap_3 = r'am instrument -w -e request at+GOOGSETNV=\"!SAEL3.Manual.Enabled.RFBands.BitMap\",3,\"00,00,00,00,00,00,00,00\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+
+        # enable lte
+        self.send(adb_enable_band_lock_lte)
+        time.sleep(2)
+
+        # OD is for NR/LTE in 4412 menu
+        self.send(adb_set_band_lock_mode_lte)
+        time.sleep(2)
+
+        # lock to B1 and B4
+        self.send(adb_set_band_lock_bitmap_0)
+        time.sleep(2)
+        self.send(adb_set_band_lock_bitmap_1)
+        time.sleep(2)
+        self.send(adb_set_band_lock_bitmap_2)
+        time.sleep(2)
+        self.send(adb_set_band_lock_bitmap_3)
+        time.sleep(2)
+
+    def clear_lock_band(self):
+        adb_set_band_lock_mode_auto = r'am instrument -w -e request at+GOOGSETNV=\"NASU.SIPC.NetworkMode.ManualMode\",0,\"03\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+        adb_disable_band_lock_lte = r'am instrument -w -e request at+GOOGSETNV=\"!SAEL3.Manual.Band.Select\ Enb\/\ Dis\",0,\"00\" -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+
+        # band lock mode auto
+        self.send(adb_set_band_lock_mode_auto)
+        time.sleep(2)
+
+        # disable band lock lte
+        self.send(adb_disable_band_lock_lte)
+        time.sleep(2)
+
+    def disable_txas(self):
+        cmd = self.ADB_CMD_DISABLE_TXAS
+        response = self.send(cmd)
+        return 'OK' in response
+
+class PowerCellularPresetLabBaseTest(PWCEL.PowerCellularLabBaseTest):
+    # Key for ODPM report
+    ODPM_ENERGY_TABLE_NAME = 'PowerStats HAL 2.0 energy meter'
+    ODPM_MODEM_CHANNEL_NAME = '[VSYS_PWR_MODEM]:Modem'
+
+    # Key for custom_property in Sponge
+    CUSTOM_PROP_KEY_BUILD_ID = 'build_id'
+    CUSTOM_PROP_KEY_INCR_BUILD_ID = 'incremental_build_id'
+    CUSTOM_PROP_KEY_BUILD_TYPE = 'build_type'
+    CUSTOM_PROP_KEY_SYSTEM_POWER = 'system_power'
+    CUSTOM_PROP_KEY_MODEM_BASEBAND = 'baseband'
+    CUSTOM_PROP_KEY_MODEM_ODPM_POWER= 'modem_odpm_power'
+    CUSTOM_PROP_KEY_DEVICE_NAME = 'device'
+    CUSTOM_PROP_KEY_DEVICE_BUILD_PHASE = 'device_build_phase'
+    CUSTOM_PROP_KEY_MODEM_KIBBLE_POWER = 'modem_kibble_power'
+    CUSTOM_PROP_KEY_TEST_NAME = 'test_name'
+    CUSTOM_PROP_KEY_MODEM_KIBBLE_WO_PCIE_POWER = 'modem_kibble_power_wo_pcie'
+    CUSTOM_PROP_KEY_MODEM_KIBBLE_PCIE_POWER = 'modem_kibble_pcie_power'
+    CUSTOM_PROP_KEY_RFFE_POWER = 'rffe_power'
+    CUSTOM_PROP_KEY_MMWAVE_POWER = 'mmwave_power'
+    # kibble report
+    KIBBLE_SYSTEM_RECORD_NAME = '- name: default_device.C10_EVT_1_1.Monsoon:mA'
+    MODEM_PCIE_RAIL_NAME_LIST = [
+        'PP1800_L2C_PCIEG3',
+        'PP1200_L9C_PCIE',
+        'PP0850_L8C_PCIE'
+    ]
+
+    MODEM_RFFE_RAIL_NAME_LIST = [
+        'PP1200_L31C_RFFE',
+        'VSYS_PWR_RFFE',
+        'PP2800_L33C_RFFE'
+    ]
+
+    MODEM_POWER_RAIL_NAME = 'VSYS_PWR_MODEM'
+
+    MODEM_MMWAVE_RAIL_NAME = 'VSYS_PWR_MMWAVE'
+
+    MONSOON_RAIL_NAME = 'Monsoon'
+
+    # params key
+    MONSOON_VOLTAGE_KEY = 'mon_voltage'
+
+    MDSTEST_APP_APK_NAME = 'mdstest.apk'
+    ADB_CMD_INSTALL = 'install {apk_path}'
+    ADB_CMD_DISABLE_TXAS = 'am instrument -w -e request at+googtxas=2 -e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"'
+    ADB_CMD_SET_NV = ('am instrument -w '
+                      '-e request at+googsetnv=\"{nv_name}\",{nv_index},\"{nv_value}\" '
+                      '-e response wait "com.google.mdstest/com.google.mdstest.instrument.ModemATCommandInstrumentation"')
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.retryable_exceptions = signals.TestFailure
+        self.power_rails = {}
+        self.pcie_power = 0
+        self.rffe_power = 0
+        self.mmwave_power = 0
+        self.modem_power = 0
+        self.monsoon_power = 0
+
+    def setup_class(self):
+        super().setup_class()
+
+        # preset callbox
+        is_fr2 = 'Fr2' in self.TAG
+        self.cellular_simulator.switch_HCCU_settings(is_fr2=is_fr2)
+
+        self.at_util = AtUtil(self.cellular_dut.ad, self.log)
+
+        # preset UE.
+        self.log.info('Installing mdstest app.')
+        self.install_apk()
+
+        self.log.info('Disable antenna switch.')
+        is_txas_disabled = self.at_util.disable_txas()
+        self.log.info('Disable txas: ' + str(is_txas_disabled))
+
+        # get sim type
+        self.unpack_userparams(has_3gpp_sim=True)
+
+    def setup_test(self):
+        self.cellular_simulator.set_sim_type(self.has_3gpp_sim)
+        try:
+            if 'LTE' in self.test_name:
+                self.at_util.lock_LTE()
+            super().setup_test()
+        except BrokenPipeError:
+            self.log.info('TA crashed test need retry.')
+            self.need_retry = True
+            self.cellular_simulator.recovery_ta()
+            self.cellular_simulator.socket_connect()
+            raise signals.TestFailure('TA crashed mid test, retry needed.')
+        # except:
+        #     # self.log.info('Waiting for device to on.')
+        #     # self.dut.adb.wait_for_device()
+        #     # self.cellular_dut = AndroidCellularDut.AndroidCellularDut(
+        #     # self.android_devices[0], self.log)
+        #     # self.dut.root_adb()
+        #     # # Restart SL4A
+        #     # self.dut.start_services()
+        #     # self.need_retry = True
+        #     raise signals.TestError('Device reboot mid test, retry needed.')
+
+    def install_apk(self):
+        sleep_time = 3
+        for file in self.custom_files:
+            if self.MDSTEST_APP_APK_NAME in file:
+                if not self.cellular_dut.ad.is_apk_installed("com.google.mdstest"):
+                    self.cellular_dut.ad.adb.install("-r -g %s" % file, timeout=300, ignore_status=True)
+        time.sleep(sleep_time)
+        if self.cellular_dut.ad.is_apk_installed("com.google.mdstest"):
+            self.log.info('mdstest installed.')
+        else:
+            self.log.warning('fail to install mdstest.')
+
+    def set_nv(self, nv_name, index, value):
+        cmd = self.ADB_CMD_SET_NV.format(
+            nv_name=nv_name,
+            nv_index=index,
+            nv_value=value
+        )
+        response = str(self.cellular_dut.ad.adb.shell(cmd))
+        self.log.info(response)
+
+    def enable_ims_nr(self):
+        # set !NRCAPA.Gen.VoiceOverNr
+        self.set_nv(
+            nv_name = '!NRCAPA.Gen.VoiceOverNr',
+            index = '0',
+            value = '01'
+        )
+        # set PSS.AIMS.Enable.NRSACONTROL
+        self.set_nv(
+            nv_name = 'PSS.AIMS.Enable.NRSACONTROL',
+            index = '0',
+            value = '00'
+        )
+        # set DS.PSS.AIMS.Enable.NRSACONTROL
+        self.set_nv(
+            nv_name = 'DS.PSS.AIMS.Enable.NRSACONTROL',
+            index = '0',
+            value = '00'
+        )
+        if self.cellular_dut.ad.model == 'oriole':
+            # For P21, NR.CONFIG.MODE/DS.NR.CONFIG.MODE
+            self.set_nv(
+                nv_name = 'NR.CONFIG.MODE',
+                index = '0',
+                value = '11'
+            )
+            # set DS.NR.CONFIG.MODE
+            self.set_nv(
+                nv_name = 'DS.NR.CONFIG.MODE',
+                index = '0',
+                value = '11'
+            )
+        else:
+            # For P22, NASU.NR.CONFIG.MODE to 11
+            self.set_nv(
+                nv_name = 'NASU.NR.CONFIG.MODE',
+                index = '0',
+                value = '11'
+            )
+
+    def get_odpm_values(self):
+        """Get power measure from ODPM.
+
+        Parsing energy table in ODPM file
+        and convert to.
+        Returns:
+            odpm_power_results: a dictionary
+                has key as channel name,
+                and value as power measurement of that channel.
+        """
+        self.log.info('Start calculating power by channel from ODPM report.')
+        odpm_power_results = {}
+
+        # device before P21 don't have ODPM reading
+        if not self.odpm_folder:
+            return odpm_power_results
+
+        # getting ODPM modem power value
+        odpm_file_name = '{}.{}.dumpsys_odpm_{}.txt'.format(
+            self.__class__.__name__,
+            self.current_test_name,
+            'after')
+        odpm_file_path = os.path.join(self.odpm_folder, odpm_file_name)
+        if os.path.exists(odpm_file_path):
+            elapsed_time = None
+            with open(odpm_file_path, 'r') as f:
+                # find energy table in ODPM report
+                for line in f:
+                    if self.ODPM_ENERGY_TABLE_NAME in line:
+                        break
+
+                # get elapse time 2 adb ODPM cmd (mS)
+                elapsed_time_str = f.readline()
+                elapsed_time = float(elapsed_time_str
+                                        .split(':')[1]
+                                        .strip()
+                                        .split(' ')[0])
+                self.log.info(elapsed_time_str)
+
+                # skip column name row
+                next(f)
+
+                # get power of different channel from odpm report
+                for line in f:
+                    if 'End' in line:
+                        break
+                    else:
+                        # parse columns
+                        # example result of line.strip().split()
+                        # ['[VSYS_PWR_DISPLAY]:Display', '1039108.42', 'mWs', '(', '344.69)']
+                        channel, _, _, _, delta_str = line.strip().split()
+                        delta = float(delta_str[:-2].strip())
+
+                        # calculate OPDM power
+                        # delta is a different in cumulative energy
+                        # between 2 adb ODPM cmd
+                        elapsed_time_s = elapsed_time / 1000
+                        power = delta / elapsed_time_s
+                        odpm_power_results[channel] = power
+                        self.log.info(
+                            channel + ' ' + str(power) + ' mW'
+                        )
+        return odpm_power_results
+
+    def _is_any_substring(self, longer_word: str, word_list: List[str]) -> bool:
+        """Check if any word in word list a substring of a longer word."""
+        return any(w in longer_word for w in word_list)
+
+    def parse_power_rails_csv(self):
+        kibble_dir = os.path.join(self.root_output_path, 'Kibble')
+        kibble_csv_path = None
+        if os.path.exists(kibble_dir):
+            for f in os.listdir(kibble_dir):
+                if self.test_name in f and '.csv' in f:
+                    kibble_csv_path = os.path.join(kibble_dir, f)
+                    self.log.info('Kibble csv file path: ' + kibble_csv_path)
+                    break
+
+        self.log.info('Parsing power rails from csv.')
+        if kibble_csv_path:
+            with open(kibble_csv_path, 'r') as f:
+                for line in f:
+                    # railname,val,mA,val,mV,val,mW
+                    railname, _, _, _, _, power, _ = line.split(',')
+                    # parse pcie power
+                    if self._is_any_substring(railname, self.MODEM_PCIE_RAIL_NAME_LIST):
+                        self.log.info(railname + ': ' + power)
+                        self.pcie_power += float(power)
+                    elif self.MODEM_POWER_RAIL_NAME in railname:
+                        self.log.info(railname + ': ' + power)
+                        self.modem_power = float(power)
+                    elif self._is_any_substring(railname, self.MODEM_RFFE_RAIL_NAME_LIST):
+                        self.log.info(railname + ': ' + power)
+                        self.rffe_power = float(power)
+                    elif self.MODEM_MMWAVE_RAIL_NAME in railname:
+                        self.log.info(railname + ': ' + power)
+                        self.mmwave_power = float(power)
+                    elif self.MONSOON_RAIL_NAME == railname:
+                        self.log.info(railname + ': ' + power)
+                        self.monsoon_power = float(power)
+        if self.modem_power:
+            self.power_results[self.test_name] = self.modem_power
+
+    def sponge_upload(self):
+        """Upload result to sponge as custom field."""
+        # test name
+        test_name_arr = self.current_test_name.split('_')
+        test_name_for_sponge = ''.join(
+            word[0].upper() + word[1:].lower()
+                for word in test_name_arr
+                    if word not in ('preset', 'test')
+        )
+
+        # build info
+        build_info = self.cellular_dut.ad.build_info
+        build_id = build_info.get('build_id', 'Unknown')
+        incr_build_id = build_info.get('incremental_build_id', 'Unknown')
+        modem_base_band = self.cellular_dut.ad.adb.getprop(
+            'gsm.version.baseband')
+        build_type = build_info.get('build_type', 'Unknown')
+
+        # device info
+        device_info = self.cellular_dut.ad.device_info
+        device_name = device_info.get('model', 'Unknown')
+        device_build_phase = self.cellular_dut.ad.adb.getprop(
+            'ro.boot.hardware.revision'
+        )
+
+        # power measurement results
+        odpm_power_results = self.get_odpm_values()
+        odpm_power = odpm_power_results.get(self.ODPM_MODEM_CHANNEL_NAME, 0)
+        system_power = 0
+
+        # if kibbles are using, get power from kibble
+        modem_kibble_power_wo_pcie = 0
+        if hasattr(self, 'bitses'):
+            self.parse_power_rails_csv()
+            modem_kibble_power_wo_pcie = self.modem_power - self.pcie_power
+            system_power = self.monsoon_power
+        else:
+            system_power = self.power_results.get(self.test_name, 0)
+
+        self.record_data({
+            'Test Name': self.test_name,
+            'sponge_properties': {
+                self.CUSTOM_PROP_KEY_SYSTEM_POWER: system_power,
+                self.CUSTOM_PROP_KEY_BUILD_ID: build_id,
+                self.CUSTOM_PROP_KEY_INCR_BUILD_ID: incr_build_id,
+                self.CUSTOM_PROP_KEY_MODEM_BASEBAND: modem_base_band,
+                self.CUSTOM_PROP_KEY_BUILD_TYPE: build_type,
+                self.CUSTOM_PROP_KEY_MODEM_ODPM_POWER: odpm_power,
+                self.CUSTOM_PROP_KEY_DEVICE_NAME: device_name,
+                self.CUSTOM_PROP_KEY_DEVICE_BUILD_PHASE: device_build_phase,
+                self.CUSTOM_PROP_KEY_MODEM_KIBBLE_POWER: self.modem_power,
+                self.CUSTOM_PROP_KEY_TEST_NAME: test_name_for_sponge,
+                self.CUSTOM_PROP_KEY_MODEM_KIBBLE_WO_PCIE_POWER: modem_kibble_power_wo_pcie,
+                self.CUSTOM_PROP_KEY_MODEM_KIBBLE_PCIE_POWER: self.pcie_power,
+                self.CUSTOM_PROP_KEY_RFFE_POWER: self.rffe_power,
+                self.CUSTOM_PROP_KEY_MMWAVE_POWER: self.mmwave_power
+            },
+        })
+
+    def teardown_test(self):
+        super().teardown_test()
+        # restore device to ready state for next test
+        self.log.info('Enable mobile data.')
+        self.dut.adb.shell('svc data enable')
+        self.cellular_simulator.detach()
+        self.cellular_dut.toggle_airplane_mode(True)
+
+        # processing result
+        self.sponge_upload()
+        if 'LTE' in self.test_name:
+            self.at_util.clear_lock_band()
\ No newline at end of file
diff --git a/acts_tests/acts_contrib/test_utils/power/cellular/ims_api_connector_utils.py b/acts_tests/acts_contrib/test_utils/power/cellular/ims_api_connector_utils.py
new file mode 100644
index 0000000..cfdc67a
--- /dev/null
+++ b/acts_tests/acts_contrib/test_utils/power/cellular/ims_api_connector_utils.py
@@ -0,0 +1,228 @@
+# TODO(hmtuan): add type annotation.
+import requests
+import time
+
+class ImsApiConnector():
+    """A wrapper class for Keysight Ims API Connector.
+
+    Keysight provided an API connector application
+    which is a HTTP server running on the same host
+    as Keysight IMS server simulator and client simulator.
+    It allows IMS simulator/app to be controlled via HTTP request.
+
+    Attributes:
+        api_connector_ip: ip of http server.
+        api_connector_port: port of http server.
+        ims_app: type of ims app (client/server).
+        api_token: an arbitrary and unique token-string
+            to identify the link between API connector
+            and ims app.
+        log: logger object.
+    """
+
+    def __init__(self, api_connector_ip,
+                 api_connector_port, ims_app,
+                 api_token, ims_app_ip,
+                 ims_app_port, log):
+        # api connector info
+        self.api_connector_ip = api_connector_ip
+        self.api_connector_port = api_connector_port
+
+        # ims app info
+        self.ims_app = ims_app
+        self.api_token = api_token
+        self.ims_app_ip = ims_app_ip
+        self.ims_app_port = ims_app_port
+
+        self.log = log
+        # construct base url
+        self.base_url = 'http://{addr}:{port}/ims/api/{app}s/{api_token}'.format(
+            addr = self.api_connector_ip,
+            port = self.api_connector_port,
+            app = self.ims_app,
+            api_token = self.api_token
+        )
+
+    def get_base_url(self):
+        return self.base_url
+
+    def create_ims_app_link(self):
+        """Create link between Keysight API Connector to ims app."""
+        self.log.info('Create ims app link: token:ip:port')
+        self.log.info('Creating ims_{app} link: {token}:{target_ip}:{target_port}'.format(
+            app = self.ims_app,
+            token = self.api_token,
+            target_ip = self.ims_app_ip,
+            target_port= self.ims_app_port)
+        )
+
+        request_data = {
+            "targetIpAddress": self.ims_app_ip,
+            "targetWcfPort": self.ims_app_port
+        }
+        self.log.debug(f'Payload to create ims app link: {request_data}')
+        r = requests.post(url = self.get_base_url(), json = request_data)
+
+        self.log.info('HTTP request sent:')
+        self.log.info('-> method: ' + str(r.request.method))
+        self.log.info('-> url: ' + str(r.url))
+        self.log.info('-> status_code: ' + str(r.status_code))
+
+        return (r.status_code == 201)
+
+    def remove_ims_app_link(self):
+        """Remove link between Keysight API Connector to ims app."""
+        self.log.info('Remove ims_{app} link: {token}'.format(
+            app = self.ims_app,
+            token = self.api_token)
+        )
+
+        r = requests.delete(url = self.get_base_url())
+
+        self.log.info('-> method: ' + str(r.request.method))
+        self.log.info('-> url: ' + str(r.url))
+        self.log.info('-> status_code: ' + str(r.status_code))
+
+        return (r.status_code == 200)
+
+    def get_ims_app_property(self, property_name):
+        """Get property value of IMS app.
+
+        Attributes:
+            property_name: name of property to get value.
+        """
+        self.log.info('Getting ims app property: ' + property_name)
+
+        request_url = self.get_base_url() + '/get_property'
+        request_params = {"propertyName": property_name}
+        r = requests.get(url = request_url, params = request_params)
+
+        self.log.info('-> method: ' + str(r.request.method))
+        self.log.info('-> url: ' + str(r.url))
+        self.log.info('-> status_code: ' + str(r.status_code))
+
+        try:
+            res_json = r.json()
+        except:
+            res_json = {'propertyValue': None }
+        prop_value = res_json['propertyValue']
+
+        return prop_value
+
+    def set_ims_app_property(self, property_name, property_value):
+        """Set property value of IMS app.
+
+        Attributes:
+            property_name: name of property to set value.
+            property_value: value to be set.
+        """
+        self.log.info('Setting ims property: ' + property_name + ' = ' + str(property_value))
+
+        request_url = self.get_base_url() + '/set_property'
+        data = {
+            'propertyName': property_name,
+            'propertyValue': property_value
+        }
+        r = requests.post(url = request_url, json = data)
+
+        self.log.info('-> method: ' + str(r.request.method))
+        self.log.info('-> url: ' + str(r.url))
+        self.log.info('-> status_code: ' + str(r.status_code))
+
+        return (r.status_code == 200)
+
+    def ims_api_call_method(self, method_name, method_args=None):
+        """
+        Attributes:
+            method_name: a name of method from Keysight API in string.
+            method_args: a python-array contains
+                arguments for the called API method.
+        Returns:
+            a tuple of (STATUS_BOOL, FUNC_RET_VAL),
+            if STATUS_BOOL is false, FUNC_RET_VAL is questionable/undefined,
+            if STATUS_BOOL is true, FUNC_RET_VAL will be the API function return value
+            or FUNC_RET_VAL is None if the called method return nothing.
+        """
+        self.log.info('Calling ims method: ' + method_name)
+
+        if (method_args == None):
+            method_args = []
+        elif (type(method_args) != list):
+            method_args = [method_args]
+        data = {
+            'methodName': method_name,
+            'arguments': method_args
+        }
+        request_url = self.get_base_url() + '/call_method'
+        r = requests.post(url = request_url, json = data)
+
+        ret_val = None
+
+        if ( ('Content-Type' in r.headers.keys()) and r.headers['Content-Type'] == 'application/json'):
+            # TODO(hmtuan): try json.loads() instead
+            response_body = r.json()
+            if ((response_body != None) and ('returnValue' in response_body.keys())) :
+                ret_val = response_body['returnValue']
+
+        self.log.info('-> method: ' + str(r.request.method))
+        self.log.info('-> url: ' + str(r.url))
+        self.log.info('-> status_code: ' + str(r.status_code))
+        self.log.info('-> ret_val: ' + str(ret_val))
+
+        return (r.status_code == 200), ret_val
+
+    def _is_line_idle(self, call_line_number):
+        is_line_idle_prop = self.get_ims_app_property(
+            f'IVoip.CallLineParams({call_line_number}).SessionState')
+        return is_line_idle_prop == 'Idle'
+
+    def _is_ims_client_app_registered(self):
+        is_registered_prop = self.get_ims_app_property('IComponentControl.IsRegistered')
+        return is_registered_prop == 'True'
+
+    def initiate_call(self, callee_number, call_line_idx=0):
+        """Dial to callee_number.
+
+        Attributes:
+            callee_number: number to be dialed to.
+        """
+        # create IMS-Client API link
+        ret_val = self.create_ims_app_link()
+
+        if not ret_val:
+            raise RuntimeError('Fail to create link to IMS app.')
+
+        # check if IMS-Client is registered, and if not, request client to perform Registration
+        self.log.info('Ensuring client registered.')
+        is_registered = self._is_ims_client_app_registered()
+        if not is_registered:
+            self.log.info('Client not currently registered - registering.')
+            self.ims_api_call_method('ISipConnection.Register()')
+
+        is_registered = self._is_ims_client_app_registered()
+        if not is_registered:
+            raise RuntimeError('Failed to register IMS-client to IMS-server.')
+
+        # switch to call-line #1 (idx = 0)
+        self.log.info('Switching to call-line #1.')
+        self.set_ims_app_property('IVoip.SelectedCallLine', call_line_idx)
+
+        # check whether the call-line #1 is ready for dialling
+        is_line1_idle = self._is_line_idle(call_line_idx)
+        if not is_line1_idle:
+            raise RuntimeError('Call-line not is not in indle state.')
+
+        # entering callee number for call-line #1
+        self.log.info(f'Enter callee number: {callee_number}.')
+        self.set_ims_app_property('IVoip.CallLineParams(0).CallLocation', callee_number)
+
+        # dial entered callee number
+        self.log.info('Dialling call.')
+        self.ims_api_call_method('IVoip.Dial()')
+
+        time.sleep(5)
+
+        # check if dial success (not idle)
+        is_line1_idle = self._is_line_idle(call_line_idx)
+        if is_line1_idle:
+            raise RuntimeError('Fail to dial.')
diff --git a/acts_tests/acts_contrib/test_utils/wifi/aware/aware_const.py b/acts_tests/acts_contrib/test_utils/wifi/aware/aware_const.py
index f0f385b..3226263 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/aware/aware_const.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/aware/aware_const.py
@@ -61,6 +61,7 @@
 DISCOVERY_KEY_RANGING_ENABLED = "RangingEnabled"
 DISCOVERY_KEY_MIN_DISTANCE_MM = "MinDistanceMm"
 DISCOVERY_KEY_MAX_DISTANCE_MM = "MaxDistanceMm"
+DISCOVERY_KEY_INSTANT_COMMUNICATION_MODE = "InstantModeEnabled"
 
 PUBLISH_TYPE_UNSOLICITED = 0
 PUBLISH_TYPE_SOLICITED = 1
@@ -150,6 +151,7 @@
 CAP_MAX_QUEUED_TRANSMIT_MESSAGES = "maxQueuedTransmitMessages"
 CAP_MAX_SUBSCRIBE_INTERFACE_ADDRESSES = "maxSubscribeInterfaceAddresses"
 CAP_SUPPORTED_CIPHER_SUITES = "supportedCipherSuites"
+CAP_SUPPORTED_INSTANT_COMMUNICATION_MODE = "isInstantCommunicationModeSupported"
 
 ######################################################
 # WifiAwareNetworkCapabilities keys
diff --git a/acts_tests/acts_contrib/test_utils/wifi/aware/aware_test_utils.py b/acts_tests/acts_contrib/test_utils/wifi/aware/aware_test_utils.py
index 4f22289..06b2e04 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/aware/aware_test_utils.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/aware/aware_test_utils.py
@@ -714,7 +714,8 @@
                             match_filter=None,
                             match_filter_list=None,
                             ttl=0,
-                            term_cb_enable=True):
+                            term_cb_enable=True,
+                            instant_mode=None):
     """Create a publish discovery configuration based on input parameters.
 
   Args:
@@ -726,6 +727,7 @@
     ttl: Time-to-live - defaults to 0 (i.e. non-self terminating)
     term_cb_enable: True (default) to enable callback on termination, False
                     means that no callback is called when session terminates.
+    instant_mode: set the band to use instant communication mode, 2G or 5G
   Returns:
     publish discovery configuration object.
   """
@@ -738,6 +740,8 @@
         config[aconsts.DISCOVERY_KEY_MATCH_FILTER] = match_filter
     if match_filter_list is not None:
         config[aconsts.DISCOVERY_KEY_MATCH_FILTER_LIST] = match_filter_list
+    if instant_mode is not None:
+        config[aconsts.DISCOVERY_KEY_INSTANT_COMMUNICATION_MODE] = instant_mode
     config[aconsts.DISCOVERY_KEY_TTL] = ttl
     config[aconsts.DISCOVERY_KEY_TERM_CB_ENABLED] = term_cb_enable
     return config
diff --git a/acts_tests/acts_contrib/test_utils/wifi/ota_chamber.py b/acts_tests/acts_contrib/test_utils/wifi/ota_chamber.py
index d74e785..280e72e 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/ota_chamber.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/ota_chamber.py
@@ -16,6 +16,7 @@
 
 import contextlib
 import io
+import requests
 import serial
 import time
 from acts import logger
@@ -52,6 +53,7 @@
     Base class provides functions whose implementation is shared by all
     chambers.
     """
+
     def reset_chamber(self):
         """Resets the chamber to its zero/home state."""
         raise NotImplementedError
@@ -83,6 +85,7 @@
 
 class MockChamber(OtaChamber):
     """Class that implements mock chamber for test development and debug."""
+
     def __init__(self, config):
         self.config = config.copy()
         self.device_id = self.config['device_id']
@@ -120,6 +123,7 @@
 
 class OctoboxChamber(OtaChamber):
     """Class that implements Octobox chamber."""
+
     def __init__(self, config):
         self.config = config.copy()
         self.device_id = self.config['device_id']
@@ -129,7 +133,9 @@
         utils.exe_cmd('sudo {} -d {} -i 0'.format(self.TURNTABLE_FILE_PATH,
                                                   self.device_id))
         self.current_mode = None
-        self.SUPPORTED_BANDS = ['2.4GHz', 'UNII-1', 'UNII-2', 'UNII-3', '6GHz']
+        self.SUPPORTED_BANDS = [
+            '2.4GHz', 'UNII-1', 'UNII-2', 'UNII-3', 'UNII-4', '6GHz'
+        ]
 
     def set_orientation(self, orientation):
         self.log.info('Setting orientation to {} degrees.'.format(orientation))
@@ -142,12 +148,49 @@
         self.set_orientation(0)
 
 
+class OctoboxChamberV2(OtaChamber):
+    """Class that implements Octobox chamber."""
+
+    def __init__(self, config):
+        self.config = config.copy()
+        self.address = config['ip_address']
+        self.data = requests.get("http://{}/api/turntable".format(
+            self.address))
+        self.vel_target = '10000'
+        self.current_mode = None
+        self.SUPPORTED_BANDS = [
+            '2.4GHz', 'UNII-1', 'UNII-2', 'UNII-3', 'UNII-4', '6GHz'
+        ]
+        self.log = logger.create_tagged_trace_logger('OtaChamber|{}'.format(
+            self.address))
+
+    def set_orientation(self, orientation):
+        self.log.info('Setting orientation to {} degrees.'.format(orientation))
+        if orientation > 720:
+            raise ValueError('Orientation may not exceed 720.')
+        set_position_submission = {
+            "action": "pos",
+            "enable": "1",
+            "pos_target": orientation,
+            "vel_target": self.vel_target
+        }
+        result = requests.post("http://{}/api/turntable".format(self.address),
+                               json=set_position_submission)
+        self.log.debug(result)
+
+    def reset_chamber(self):
+        self.log.info('Resetting chamber to home state')
+        self.set_orientation(0)
+
+
 class ChamberAutoConnect(object):
+
     def __init__(self, chamber, chamber_config):
         self._chamber = chamber
         self._config = chamber_config
 
     def __getattr__(self, item):
+
         def chamber_call(*args, **kwargs):
             self._chamber.connect(self._config['ip_address'],
                                   self._config['username'],
@@ -159,6 +202,7 @@
 
 class BluetestChamber(OtaChamber):
     """Class that implements Octobox chamber."""
+
     def __init__(self, config):
         import flow
         self.config = config.copy()
@@ -167,7 +211,9 @@
         self.chamber = ChamberAutoConnect(flow.Flow(), self.config)
         self.stirrer_ids = [0, 1, 2]
         self.current_mode = None
-        self.SUPPORTED_BANDS = ['2.4GHz', 'UNII-1', 'UNII-2', 'UNII-3']
+        self.SUPPORTED_BANDS = [
+            '2.4GHz', 'UNII-1', 'UNII-2', 'UNII-3', 'UNII-4', '6GHz'
+        ]
 
     # Capture print output decorator
     @staticmethod
@@ -248,6 +294,7 @@
 
 class EInstrumentChamber(OtaChamber):
     """Class that implements Einstrument Chamber."""
+
     def __init__(self, config):
         self.config = config.copy()
         self.device_id = self.config['device_id']
diff --git a/acts_tests/acts_contrib/test_utils/wifi/ota_sniffer.py b/acts_tests/acts_contrib/test_utils/wifi/ota_sniffer.py
index 395fed2..7cc52da 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/ota_sniffer.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/ota_sniffer.py
@@ -18,6 +18,7 @@
 import os
 import posixpath
 import time
+import zipfile
 import acts_contrib.test_utils.wifi.wifi_test_utils as wutils
 
 from acts import context
@@ -126,6 +127,7 @@
 
 class MockSniffer(OtaSnifferBase):
     """Class that implements mock sniffer for test development and debug."""
+
     def __init__(self, config):
         self.log = logger.create_tagged_trace_logger('Mock Sniffer')
 
@@ -443,6 +445,11 @@
 
         if self.sniffer_output_file_type == 'csv':
             log_file = self._process_tshark_dump(log_file)
+        if self.sniffer_output_file_type == 'pcap':
+            zip_file_path = log_file[:-4] + "zip"
+            zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED).write(
+                log_file, arcname=log_file.split('/')[-1])
+            os.remove(log_file)
 
         self.sniffer_proc_pid = None
         return log_file
@@ -450,6 +457,7 @@
 
 class TsharkSnifferOnUnix(TsharkSnifferBase):
     """Class that implements Tshark based sniffer controller on Unix systems."""
+
     def _scan_for_networks(self):
         """Scans the wireless networks on the sniffer.
 
@@ -480,6 +488,7 @@
 
 class TsharkSnifferOnLinux(TsharkSnifferBase):
     """Class that implements Tshark based sniffer controller on Linux."""
+
     def __init__(self, config):
         super().__init__(config)
         self._init_sniffer()
diff --git a/acts_tests/acts_contrib/test_utils/wifi/p2p/wifi_p2p_test_utils.py b/acts_tests/acts_contrib/test_utils/wifi/p2p/wifi_p2p_test_utils.py
index b5832d0..10f3ea0 100755
--- a/acts_tests/acts_contrib/test_utils/wifi/p2p/wifi_p2p_test_utils.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/p2p/wifi_p2p_test_utils.py
@@ -339,7 +339,6 @@
     while not p2p_find_result:
         ad2.droid.wifiP2pStopPeerDiscovery()
         ad1.droid.wifiP2pStopPeerDiscovery()
-        ad2.droid.wifiP2pDiscoverPeers()
         ad1.droid.wifiP2pDiscoverPeers()
         ad1_event = ad1.ed.pop_event(p2pconsts.PEER_AVAILABLE_EVENT,
                                      p2pconsts.P2P_FIND_TIMEOUT)
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/__init__.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/__init__.py
index 831ddc8..9da3529 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/__init__.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/__init__.py
@@ -66,7 +66,7 @@
 def detect_wifi_platform(dut):
     if hasattr(dut, 'wifi_platform'):
         return dut.wifi_platform
-    qcom_check = len(dut.get_file_names('/vendor/firmware/wlan/qca_cld/'))
+    qcom_check = len(dut.get_file_names('/vendor/firmware/wlan/'))
     if qcom_check:
         dut.wifi_platform = 'qcom'
     else:
@@ -75,6 +75,7 @@
 
 
 def detect_wifi_decorator(f):
+
     def wrap(*args, **kwargs):
         if 'dut' in kwargs:
             dut = kwargs['dut']
@@ -274,7 +275,8 @@
                          socket_size=None,
                          num_processes=1,
                          udp_throughput='1000M',
-                         ipv6=False):
+                         ipv6=False,
+                         udp_length=1470):
     """Function to format iperf client arguments.
 
     This function takes in iperf client parameters and returns a properly
@@ -296,8 +298,8 @@
     if ipv6:
         iperf_args = iperf_args + '-6 '
     if traffic_type.upper() == 'UDP':
-        iperf_args = iperf_args + '-u -b {} -l 1470 -P {} '.format(
-            udp_throughput, num_processes)
+        iperf_args = iperf_args + '-u -b {} -l {} -P {} '.format(
+            udp_throughput, udp_length, num_processes)
     elif traffic_type.upper() == 'TCP':
         iperf_args = iperf_args + '-P {} '.format(num_processes)
     if socket_size:
@@ -728,6 +730,7 @@
 
 # Link layer stats utilities
 class LinkLayerStats():
+
     def __new__(self, dut, llstats_enabled=True):
         if detect_wifi_platform(dut) == 'qcom':
             return qcom_utils.LinkLayerStats(dut, llstats_enabled)
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/bokeh_figure.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/bokeh_figure.py
index 5a8433e..d91803b 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/bokeh_figure.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/bokeh_figure.py
@@ -19,6 +19,7 @@
 import itertools
 import json
 import math
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
 
 
 # Plotting Utilities
@@ -98,8 +99,8 @@
     def init_plot(self):
         self.plot = bokeh.plotting.figure(
             sizing_mode=self.fig_property['sizing_mode'],
-            plot_width=self.fig_property['width'],
-            plot_height=self.fig_property['height'],
+            width=self.fig_property['width'],
+            height=self.fig_property['height'],
             title=self.fig_property['title'],
             tools=self.TOOLS,
             x_axis_type=self.fig_property['x_axis_type'],
@@ -326,7 +327,7 @@
                                               figure_data=self.figure_data)
         output_file = output_file.replace('.html', '_plot_data.json')
         with open(output_file, 'w') as outfile:
-            json.dump(figure_dict, outfile, indent=4)
+            json.dump(wputils.serialize_dict(figure_dict), outfile, indent=4)
 
     def save_figure(self, output_file, save_json=True):
         """Function to save BokehFigure.
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/brcm_utils.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/brcm_utils.py
index afa5f32..fe2d3e7 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/brcm_utils.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/brcm_utils.py
@@ -378,16 +378,17 @@
     LL_STATS_CLEAR_CMD = 'wl dump_clear ampdu; wl reset_cnts;'
     BW_REGEX = re.compile(r'Chanspec:.+ (?P<bandwidth>[0-9]+)MHz')
     MCS_REGEX = re.compile(r'(?P<count>[0-9]+)\((?P<percent>[0-9]+)%\)')
-    RX_REGEX = re.compile(r'RX (?P<mode>\S+)\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
-                          '\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
-    TX_REGEX = re.compile(r'TX (?P<mode>\S+)\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
-                          '\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
+    RX_REGEX = re.compile(
+        r'RX (?P<mode>MCS|VHT|HE|EHT)\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
+        '\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
+    TX_REGEX = re.compile(
+        r'TX (?P<mode>MCS|VHT|HE|EHT)\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
+        '\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
     TX_PER_REGEX = re.compile(
         r'(?P<mode>\S+) PER\s+:\s*(?P<nss1>[0-9, ,(,),%]*)'
         '\n\s*:?\s*(?P<nss2>[0-9, ,(,),%]*)')
-    RX_FCS_REGEX = re.compile(
-        r'rxbadfcs (?P<rx_bad_fcs>[0-9]*).+\n.+goodfcs (?P<rx_good_fcs>[0-9]*)'
-    )
+    RX_GOOD_FCS_REGEX = re.compile(r'goodfcs (?P<rx_good_fcs>[0-9]*)')
+    RX_BAD_FCS_REGEX = re.compile(r'rxbadfcs (?P<rx_bad_fcs>[0-9]*)')
     RX_AGG_REGEX = re.compile(r'rxmpduperampdu (?P<aggregation>[0-9]*)')
     TX_AGG_REGEX = re.compile(r' mpduperampdu (?P<aggregation>[0-9]*)')
     TX_AGG_STOP_REGEX = re.compile(
@@ -405,6 +406,7 @@
         self.llstats_enabled = llstats_enabled
         self.llstats_cumulative = self._empty_llstats()
         self.llstats_incremental = self._empty_llstats()
+        self.bandwidth = None
 
     def update_stats(self):
         if self.llstats_enabled:
@@ -414,8 +416,9 @@
                 self.dut.adb.shell_nb(self.LL_STATS_CLEAR_CMD)
 
                 wl_join = self.dut.adb.shell("wl status")
-                self.bandwidth = int(
-                    re.search(self.BW_REGEX, wl_join).group('bandwidth'))
+                if not self.bandwidth:
+                    self.bandwidth = int(
+                        re.search(self.BW_REGEX, wl_join).group('bandwidth'))
             except:
                 llstats_output = ''
         else:
@@ -494,33 +497,32 @@
         rx_agg_match = re.search(self.RX_AGG_REGEX, llstats_output)
         tx_agg_match = re.search(self.TX_AGG_REGEX, llstats_output)
         tx_agg_stop_match = re.search(self.TX_AGG_STOP_REGEX, llstats_output)
-        rx_fcs_match = re.search(self.RX_FCS_REGEX, llstats_output)
+        rx_good_fcs_match = re.search(self.RX_GOOD_FCS_REGEX, llstats_output)
+        rx_bad_fcs_match = re.search(self.RX_BAD_FCS_REGEX, llstats_output)
 
-        if rx_agg_match and tx_agg_match and tx_agg_stop_match and rx_fcs_match:
-            agg_stop_dict = collections.OrderedDict(
-                rx_aggregation=int(rx_agg_match.group('aggregation')),
-                tx_aggregation=int(tx_agg_match.group('aggregation')),
-                tx_agg_tried=int(tx_agg_stop_match.group('agg_tried')),
-                tx_agg_canceled=int(tx_agg_stop_match.group('agg_canceled')),
-                rx_good_fcs=int(rx_fcs_match.group('rx_good_fcs')),
-                rx_bad_fcs=int(rx_fcs_match.group('rx_bad_fcs')),
-                agg_stop_reason=collections.OrderedDict())
+        mpdu_stats = collections.OrderedDict(
+            rx_aggregation=int(rx_agg_match.group('aggregation'))
+            if rx_agg_match else 0,
+            tx_aggregation=int(tx_agg_match.group('aggregation'))
+            if tx_agg_match else 0,
+            tx_agg_tried=int(tx_agg_stop_match.group('agg_tried'))
+            if tx_agg_stop_match else 0,
+            tx_agg_canceled=int(tx_agg_stop_match.group('agg_canceled'))
+            if tx_agg_stop_match else 0,
+            rx_good_fcs=int(rx_good_fcs_match.group('rx_good_fcs'))
+            if rx_good_fcs_match else 0,
+            rx_bad_fcs=int(rx_bad_fcs_match.group('rx_bad_fcs'))
+            if rx_bad_fcs_match else 0,
+            agg_stop_reason=collections.OrderedDict())
+        if tx_agg_stop_match:
             agg_reason_match = re.finditer(
                 self.TX_AGG_STOP_REASON_REGEX,
                 tx_agg_stop_match.group('agg_stop_reason'))
             for reason_match in agg_reason_match:
-                agg_stop_dict['agg_stop_reason'][reason_match.group(
+                mpdu_stats['agg_stop_reason'][reason_match.group(
                     'reason')] = reason_match.group('value')
 
-        else:
-            agg_stop_dict = collections.OrderedDict(rx_aggregation=0,
-                                                    tx_aggregation=0,
-                                                    tx_agg_tried=0,
-                                                    tx_agg_canceled=0,
-                                                    rx_good_fcs=0,
-                                                    rx_bad_fcs=0,
-                                                    agg_stop_reason=None)
-        return agg_stop_dict
+        return mpdu_stats
 
     def _generate_stats_summary(self, llstats_dict):
         llstats_summary = collections.OrderedDict(common_tx_mcs=None,
@@ -547,13 +549,17 @@
         llstats_summary['common_tx_mcs_count'] = numpy.max(tx_mpdu)
         llstats_summary['common_rx_mcs'] = mcs_ids[numpy.argmax(rx_mpdu)]
         llstats_summary['common_rx_mcs_count'] = numpy.max(rx_mpdu)
-        if sum(tx_mpdu) and sum(rx_mpdu):
+        if sum(tx_mpdu):
             llstats_summary['mean_tx_phy_rate'] = numpy.average(
                 phy_rates, weights=tx_mpdu)
-            llstats_summary['mean_rx_phy_rate'] = numpy.average(
-                phy_rates, weights=rx_mpdu)
             llstats_summary['common_tx_mcs_freq'] = (
                 llstats_summary['common_tx_mcs_count'] / sum(tx_mpdu))
+        else:
+            llstats_summary['mean_tx_phy_rate'] = 0
+            llstats_summary['common_tx_mcs_freq'] = 0
+        if sum(rx_mpdu):
+            llstats_summary['mean_rx_phy_rate'] = numpy.average(
+                phy_rates, weights=rx_mpdu)
             llstats_summary['common_rx_mcs_freq'] = (
                 llstats_summary['common_rx_mcs_count'] / sum(rx_mpdu))
             total_rx_frames = llstats_dict['mpdu_stats'][
@@ -561,7 +567,11 @@
             if total_rx_frames:
                 llstats_summary['rx_per'] = (
                     llstats_dict['mpdu_stats']['rx_bad_fcs'] /
-                    (total_rx_frames)) * 100
+                    total_rx_frames) * 100
+        else:
+            llstats_summary['mean_rx_phy_rate'] = 0
+            llstats_summary['common_rx_mcs_freq'] = 0
+            llstats_summary['rx_per'] = 0
         return llstats_summary
 
     def _update_stats(self, llstats_output):
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/qcom_utils.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/qcom_utils.py
index 53321bc..f29e4fa 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/qcom_utils.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_performance_test_utils/qcom_utils.py
@@ -21,6 +21,7 @@
 import os
 import re
 import statistics
+import numpy
 import time
 from acts import asserts
 
@@ -275,14 +276,15 @@
 
 def _edit_dut_ini(dut, ini_fields):
     """Function to edit Wifi ini files."""
-    dut_ini_path = '/vendor/firmware/wlan/qca_cld/WCNSS_qcom_cfg.ini'
-    local_ini_path = os.path.expanduser('~/WCNSS_qcom_cfg.ini')
+    dut_ini_path = '/vendor/firmware/wlan/qcom_cfg.ini'
+    local_ini_path = os.path.expanduser('~/qcom_cfg.ini')
     dut.pull_files(dut_ini_path, local_ini_path)
 
     _set_ini_fields(local_ini_path, ini_fields)
 
     dut.push_system_file(local_ini_path, dut_ini_path)
-    dut.reboot()
+    # For 1x1 mode, we need to wait for sl4a to load (To avoid crashes)
+    dut.reboot(timeout=300, wait_after_reboot_complete=120)
 
 
 def set_chain_mask(dut, chain_mask):
@@ -336,6 +338,7 @@
 class LinkLayerStats():
 
     LLSTATS_CMD = 'cat /d/wlan0/ll_stats'
+    MOUNT_CMD = 'mount -t debugfs debugfs /sys/kernel/debug'
     PEER_REGEX = 'LL_STATS_PEER_ALL'
     MCS_REGEX = re.compile(
         r'preamble: (?P<mode>\S+), nss: (?P<num_streams>\S+), bw: (?P<bw>\S+), '
@@ -345,8 +348,8 @@
         'retries_long: (?P<retries_long>\S+)')
     MCS_ID = collections.namedtuple(
         'mcs_id', ['mode', 'num_streams', 'bandwidth', 'mcs', 'rate'])
-    MODE_MAP = {'0': '11a/g', '1': '11b', '2': '11n', '3': '11ac'}
-    BW_MAP = {'0': 20, '1': 40, '2': 80}
+    MODE_MAP = {'0': '11a/g', '1': '11b', '2': '11n', '3': '11ac', '4': '11ax'}
+    BW_MAP = {'0': 20, '1': 40, '2': 80, '3':160}
 
     def __init__(self, dut, llstats_enabled=True):
         self.dut = dut
@@ -356,6 +359,12 @@
 
     def update_stats(self):
         if self.llstats_enabled:
+            # Checking the files to see if the device is mounted to enable
+            # llstats capture
+            mount_check = len(self.dut.get_file_names('/d/wlan0'))
+            if not(mount_check):
+              self.dut.adb.shell(self.MOUNT_CMD, timeout=10)
+
             try:
                 llstats_output = self.dut.adb.shell(self.LLSTATS_CMD,
                                                     timeout=0.1)
@@ -400,7 +409,7 @@
             current_mcs = self.MCS_ID(self.MODE_MAP[match.group('mode')],
                                       int(match.group('num_streams')) + 1,
                                       self.BW_MAP[match.group('bw')],
-                                      int(match.group('mcs')),
+                                      int(match.group('mcs'), 16),
                                       int(match.group('rate'), 16) / 1000)
             current_stats = collections.OrderedDict(
                 txmpdu=int(match.group('txmpdu')),
@@ -427,9 +436,17 @@
                                                   common_rx_mcs_freq=0,
                                                   rx_per=float('nan'))
 
+        phy_rates=[]
+        tx_mpdu=[]
+        rx_mpdu=[]
         txmpdu_count = 0
         rxmpdu_count = 0
         for mcs_id, mcs_stats in llstats_dict['mcs_stats'].items():
+            # Extract the phy-rates
+            mcs_id_split=mcs_id.split();
+            phy_rates.append(float(mcs_id_split[len(mcs_id_split)-1].split('M')[0]))
+            rx_mpdu.append(mcs_stats['rxmpdu'])
+            tx_mpdu.append(mcs_stats['txmpdu'])
             if mcs_stats['txmpdu'] > llstats_summary['common_tx_mcs_count']:
                 llstats_summary['common_tx_mcs'] = mcs_id
                 llstats_summary['common_tx_mcs_count'] = mcs_stats['txmpdu']
@@ -438,6 +455,15 @@
                 llstats_summary['common_rx_mcs_count'] = mcs_stats['rxmpdu']
             txmpdu_count += mcs_stats['txmpdu']
             rxmpdu_count += mcs_stats['rxmpdu']
+
+        if len(tx_mpdu) == 0 or len(rx_mpdu) == 0:
+            return llstats_summary
+
+        # Calculate the average tx/rx -phy rates
+        if sum(tx_mpdu) and sum(rx_mpdu):
+            llstats_summary['mean_tx_phy_rate'] = numpy.average(phy_rates, weights=tx_mpdu)
+            llstats_summary['mean_rx_phy_rate'] = numpy.average(phy_rates, weights=rx_mpdu)
+
         if txmpdu_count:
             llstats_summary['common_tx_mcs_freq'] = (
                 llstats_summary['common_tx_mcs_count'] / txmpdu_count)
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_power_test_utils.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_power_test_utils.py
index adefa7e..3063423 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_power_test_utils.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_power_test_utils.py
@@ -33,6 +33,24 @@
 ENABLED_MODULATED_DTIM = 'gEnableModulatedDTIM='
 MAX_MODULATED_DTIM = 'gMaxLIModulatedDTIM='
 
+CHRE_WIFI_SCAN_TYPE = {
+    'active': 'active',
+    'passive': 'passive',
+    'activePassiveDfs': 'active_passive_dfs',
+    'noPreference': 'no_preference'
+}
+
+CHRE_WIFI_RADIO_CHAIN = {
+    'lowLatency': 'low_latency',
+    'lowPower': 'low_power',
+    'highAccuracy': 'high_accuracy'
+}
+
+CHRE_WIFI_CHANNEL_SET = {
+    'all': 'all',
+    'nonDfs': 'non_dfs'
+}
+
 
 def change_dtim(ad, gEnableModulatedDTIM, gMaxLIModulatedDTIM=10):
     """Function to change the DTIM setting in the phone.
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/__init__.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/__init__.py
index 214401c..da47cb8 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/__init__.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/__init__.py
@@ -18,11 +18,17 @@
 import copy
 import fcntl
 import importlib
+import logging
 import os
 import selenium
-import splinter
 import time
 from acts import logger
+from selenium.webdriver.support.ui import Select
+from selenium.webdriver.chrome.service import Service as ChromeService
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support import expected_conditions as expected_conditions
+from webdriver_manager.chrome import ChromeDriverManager
+
 
 BROWSER_WAIT_SHORT = 1
 BROWSER_WAIT_MED = 3
@@ -106,7 +112,7 @@
         obj.teardown()
 
 
-class BlockingBrowser(splinter.driver.webdriver.chrome.WebDriver):
+class BlockingBrowser(selenium.webdriver.chrome.webdriver.WebDriver):
     """Class that implements a blocking browser session on top of selenium.
 
     The class inherits from and builds upon splinter/selenium's webdriver class
@@ -123,10 +129,14 @@
             headless: boolean to control visible/headless browser operation
             timeout: maximum time allowed to launch browser
         """
+        if int(selenium.__version__[0]) < 4:
+            raise RuntimeError(
+                'BlockingBrowser now requires selenium==4.0.0 or later. ')
         self.log = logger.create_tagged_trace_logger('ChromeDriver')
-        self.chrome_options = splinter.driver.webdriver.chrome.Options()
+        self.chrome_options = selenium.webdriver.chrome.webdriver.Options()
         self.chrome_options.add_argument('--no-proxy-server')
         self.chrome_options.add_argument('--no-sandbox')
+        self.chrome_options.add_argument('--crash-dumps-dir=/tmp')
         self.chrome_options.add_argument('--allow-running-insecure-content')
         self.chrome_options.add_argument('--ignore-certificate-errors')
         self.chrome_capabilities = selenium.webdriver.common.desired_capabilities.DesiredCapabilities.CHROME.copy(
@@ -136,7 +146,8 @@
         if headless:
             self.chrome_options.add_argument('--headless')
             self.chrome_options.add_argument('--disable-gpu')
-        self.lock_file_path = '/usr/local/bin/chromedriver'
+        os.environ['WDM_LOG'] = str(logging.NOTSET)
+        self.executable_path = ChromeDriverManager().install()
         self.timeout = timeout
 
     def __enter__(self):
@@ -147,7 +158,7 @@
         session. If an exception occurs while starting the browser, the lock
         file is released.
         """
-        self.lock_file = open(self.lock_file_path, 'r')
+        self.lock_file = open(self.executable_path, 'r')
         start_time = time.time()
         while time.time() < start_time + self.timeout:
             try:
@@ -157,14 +168,11 @@
                 continue
             try:
                 self.driver = selenium.webdriver.Chrome(
+                    service=ChromeService(self.executable_path),
                     options=self.chrome_options,
                     desired_capabilities=self.chrome_capabilities)
-                self.element_class = splinter.driver.webdriver.WebDriverElement
-                self._cookie_manager = splinter.driver.webdriver.cookie_manager.CookieManager(
-                    self.driver)
-                super(splinter.driver.webdriver.chrome.WebDriver,
-                      self).__init__(2)
-                return super(BlockingBrowser, self).__enter__()
+                self.session_id = self.driver.session_id
+                return self
             except:
                 fcntl.flock(self.lock_file, fcntl.LOCK_UN)
                 self.lock_file.close()
@@ -179,8 +187,7 @@
         releases the lock file.
         """
         try:
-            super(BlockingBrowser, self).__exit__(exc_type, exc_value,
-                                                  traceback)
+            self.driver.quit()
         except:
             raise RuntimeError('Failed to quit browser. Releasing lock file.')
         finally:
@@ -189,7 +196,7 @@
 
     def restart(self):
         """Method to restart browser session without releasing lock file."""
-        self.quit()
+        self.driver.quit()
         self.__enter__()
 
     def visit_persistent(self,
@@ -215,21 +222,25 @@
         self.driver.set_page_load_timeout(page_load_timeout)
         for idx in range(num_tries):
             try:
-                self.visit(url)
+                self.driver.get(url)
             except:
                 self.restart()
 
-            page_reached = self.url.split('/')[-1] == url.split('/')[-1]
-            if check_for_element:
-                time.sleep(BROWSER_WAIT_MED)
-                element = self.find_by_id(check_for_element)
-                if not element:
-                    page_reached = 0
+            page_reached = self.driver.current_url.split('/')[-1] == url.split(
+                '/')[-1]
             if page_reached:
-                break
+                if check_for_element:
+                    time.sleep(BROWSER_WAIT_MED)
+                    if self.is_element_visible(check_for_element):
+                        break
+                    else:
+                        raise RuntimeError(
+                            'Page reached but expected element not found.')
+                else:
+                    break
             else:
                 try:
-                    self.visit(backup_url)
+                    self.driver.get(backup_url)
                 except:
                     self.restart()
 
@@ -238,6 +249,125 @@
                     self.url))
                 raise RuntimeError('URL unreachable.')
 
+    def get_element_value(self, element_name):
+        """Function to look up and get webpage element value.
+
+        Args:
+            element_name: name of element to look up
+        Returns:
+            Value of element
+        """
+        #element = self.driver.find_element_by_name(element_name)
+        element = self.driver.find_element(By.NAME, element_name)
+        element_type = self.get_element_type(element_name)
+        if element_type == 'checkbox':
+            return element.is_selected()
+        elif element_type == 'radio':
+            items = self.driver.find_elements(By.NAME, element_name)
+            for item in items:
+                if item.is_selected():
+                    return item.get_attribute('value')
+        else:
+            return element.get_attribute('value')
+
+    def get_element_type(self, element_name):
+        """Function to look up and get webpage element type.
+
+        Args:
+            element_name: name of element to look up
+        Returns:
+            Type of element
+        """
+        item = self.driver.find_element(By.NAME, element_name)
+        type = item.get_attribute('type')
+        return type
+
+    def is_element_enabled(self, element_name):
+        """Function to check if element is enabled/interactable.
+
+        Args:
+            element_name: name of element to look up
+        Returns:
+            Boolean indicating if element is interactable
+        """
+        item = self.driver.find_element(By.NAME, element_name)
+        return item.is_enabled()
+
+    def is_element_visible(self, element_name):
+        """Function to check if element is visible.
+
+        Args:
+            element_name: name of element to look up
+        Returns:
+            Boolean indicating if element is visible
+        """
+        item = self.driver.find_element(By.NAME, element_name)
+        return item.is_displayed()
+
+    def set_element_value(self, element_name, value, select_method='value'):
+        """Function to set webpage element value.
+
+        Args:
+            element_name: name of element to set
+            value: value of element
+            select_method: select method for dropdown lists (value/index/text)
+        """
+        element_type = self.get_element_type(element_name)
+        if element_type == 'text' or element_type == 'password':
+            item = self.driver.find_element(By.NAME, element_name)
+            item.clear()
+            item.send_keys(value)
+        elif element_type == 'checkbox':
+            item = self.driver.find_element(By.NAME, element_name)
+            if value != item.is_selected():
+                item.click()
+        elif element_type == 'radio':
+            items = self.driver.find_elements(By.NAME, element_name)
+            for item in items:
+                if item.get_attribute('value') == value:
+                    item.click()
+        elif element_type == 'select-one':
+            select = Select(self.driver.find_element(By.NAME, element_name))
+            if select_method == 'value':
+                select.select_by_value(str(value))
+            elif select_method == 'text':
+                select.select_by_visible_text(value)
+            elif select_method == 'index':
+                select.select_by_index(value)
+            else:
+                raise RuntimeError(
+                    '{} is not a valid select method.'.format(select_method))
+        else:
+            raise RuntimeError(
+                'Element type {} not supported.'.format(element_type))
+
+    def click_button(self, button_name):
+        """Function to click button on webpage
+
+        Args:
+            button_name: name of button to click
+        """
+        button = self.driver.find_element(By.NAME, button_name)
+        if button.get_attribute('type') == 'submit':
+            button.click()
+        else:
+            raise RuntimeError('{} is not a button.'.format(button_name))
+
+    def accept_alert_if_present(self, wait_for_alert=1):
+        """Function to check for alert and accept if present
+
+        Args:
+            wait_for_alert: time (seconds) to wait for alert
+        """
+        try:
+            selenium.webdriver.support.ui.WebDriverWait(
+                self.driver,
+                wait_for_alert).until(expected_conditions.alert_is_present())
+            alert = self.driver.switch_to.alert
+            alert.accept()
+        except selenium.common.exceptions.TimeoutException:
+            pass
+
 
 class WifiRetailAP(object):
     """Base class implementation for retail ap.
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/brcm_ref.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/brcm_ref.py
index 1b09533..c05ff40 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/brcm_ref.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/brcm_ref.py
@@ -1,5 +1,6 @@
 import collections
 import numpy
+import paramiko
 import time
 from acts_contrib.test_utils.wifi.wifi_retail_ap import WifiRetailAP
 from acts_contrib.test_utils.wifi.wifi_retail_ap import BlockingBrowser
@@ -8,6 +9,8 @@
 BROWSER_WAIT_MED = 3
 BROWSER_WAIT_LONG = 10
 BROWSER_WAIT_EXTRA_LONG = 60
+SSH_WAIT_SHORT = 0.1
+SSH_READ_BYTES = 600000
 
 
 class BrcmRefAP(WifiRetailAP):
@@ -16,13 +19,45 @@
     Since most of the class' implementation is shared with the R7000, this
     class inherits from NetgearR7000AP and simply redefines config parameters
     """
+
     def __init__(self, ap_settings):
         super().__init__(ap_settings)
         self.init_gui_data()
+        # Initialize SSH connection
+        self.init_ssh_connection()
         # Read and update AP settings
         self.read_ap_settings()
         self.update_ap_settings(ap_settings)
 
+    def teardown(self):
+        """Function to perform destroy operations."""
+        if self.ap_settings.get('lock_ap', 0):
+            self._unlock_ap()
+        self.close_ssh_connection()
+
+    def init_ssh_connection(self):
+        self.ssh_client = paramiko.SSHClient()
+        self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+        self.ssh_client.connect(hostname=self.ap_settings['ip_address'],
+                                username=self.ap_settings['admin_username'],
+                                password=self.ap_settings['admin_password'],
+                                look_for_keys=False,
+                                allow_agent=False)
+
+    def close_ssh_connection(self):
+        self.ssh_client.close()
+
+    def run_ssh_cmd(self, command):
+        with self.ssh_client.invoke_shell() as shell:
+            shell.send('sh\n')
+            time.sleep(SSH_WAIT_SHORT)
+            shell.recv(SSH_READ_BYTES)
+            shell.send('{}\n'.format(command))
+            time.sleep(SSH_WAIT_SHORT)
+            response = shell.recv(SSH_READ_BYTES).decode('utf-8').splitlines()
+            response = [line for line in response[1:] if line != '# ']
+        return response
+
     def init_gui_data(self):
         self.config_page = ('{protocol}://{username}:{password}@'
                             '{ip_address}:{port}/info.html').format(
@@ -240,4 +275,146 @@
                 config_item.first.click()
                 time.sleep(BROWSER_WAIT_LONG)
                 browser.visit_persistent(self.config_page, BROWSER_WAIT_LONG,
-                                     10)
+                                         10)
+
+    def set_power(self, interface, power):
+        """Function that sets interface transmit power.
+
+        Args:
+            interface: string containing interface identifier (2G_5G, 6G)
+            power: power level in dBm
+        """
+        wl_interface = 'wl0' if interface == '6G' else 'wl1'
+
+        if power == 'auto':
+            response = self.run_ssh_cmd(
+                'wl -i {} txpwr1 -1'.format(wl_interface))
+        else:
+            power_qdbm = int(power * 4)
+            response = self.run_ssh_cmd('wl -i {} txpwr1 -o -q {}'.format(
+                wl_interface, power_qdbm))
+
+        self.ap_settings[interface]['power'] = power_qdbm / 4
+
+    def get_power(self, interface):
+        """Function to get power used by AP
+
+        Args:
+            interface: interface to get rate on (2G_5G, 6G)
+        Returns:
+            power string returned by AP.
+        """
+        wl_interface = 'wl0' if interface == '6G' else 'wl1'
+        return self.run_ssh_cmd('wl -i {} txpwr1'.format(wl_interface))
+
+    def set_rate(self,
+                 interface,
+                 mode=None,
+                 num_streams=None,
+                 rate='auto',
+                 short_gi=0,
+                 tx_expansion=0):
+        """Function that sets rate.
+
+        Args:
+            interface: string containing interface identifier (2G, 5G_1)
+            mode: string indicating the WiFi standard to use
+            num_streams: number of MIMO streams. used only for VHT
+            rate: data rate of MCS index to use
+            short_gi: boolean controlling the use of short guard interval
+        """
+        wl_interface = 'wl0' if interface == '6G' else 'wl1'
+
+        if interface == '6G':
+            band_rate = '6g_rate'
+        elif self.ap_settings['2G_5G']['channel'] < 13:
+            band_rate = '2g_rate'
+        else:
+            band_rate = '5g_rate'
+
+        if rate == 'auto':
+            cmd_string = 'wl -i {} {} auto'.format(wl_interface, band_rate)
+        elif 'legacy' in mode.lower():
+            cmd_string = 'wl -i {} {} -r {} -x {}'.format(
+                wl_interface, band_rate, rate, tx_expansion)
+        elif 'ht' in mode.lower():
+            cmd_string = 'wl -i {} {} -h {} -x {}'.format(
+                wl_interface, band_rate, rate, tx_expansion)
+            if short_gi:
+                cmd_string = cmd_string + '--sgi'
+        elif 'vht' in mode.lower():
+            cmd_string = 'wl -i {} {} -v {}x{} -x {}'.format(
+                wl_interface, band_rate, rate, num_streams, tx_expansion)
+            if short_gi:
+                cmd_string = cmd_string + '--sgi'
+        elif 'he' in mode.lower():
+            cmd_string = 'wl -i {} {} -e {}x{} -l -x {}'.format(
+                wl_interface, band_rate, rate, num_streams, tx_expansion)
+            if short_gi:
+                cmd_string = cmd_string + '-i {}'.format(short_gi)
+
+        response = self.run_ssh_cmd(cmd_string)
+
+        self.ap_settings[interface]['mode'] = mode
+        self.ap_settings[interface]['num_streams'] = num_streams
+        self.ap_settings[interface]['rate'] = rate
+        self.ap_settings[interface]['short_gi'] = short_gi
+
+    def get_rate(self, interface):
+        """Function to get rate used by AP
+
+        Args:
+            interface: interface to get rate on (2G_5G, 6G)
+        Returns:
+            rate string returned by AP.
+        """
+
+        wl_interface = 'wl0' if interface == '6G' else 'wl1'
+
+        if interface == '6G':
+            band_rate = '6g_rate'
+        elif self.ap_settings['2G_5G']['channel'] < 13:
+            band_rate = '2g_rate'
+        else:
+            band_rate = '5g_rate'
+        return self.run_ssh_cmd('wl -i {} {}'.format(wl_interface, band_rate))
+
+    def set_rts_enable(self, interface, enable):
+        """Function to enable or disable RTS/CTS
+
+        Args:
+            interface: interface to be configured (2G_5G, 6G)
+            enable: boolean controlling RTS/CTS behavior
+        """
+        wl_interface = 'wl0' if interface == '6G' else 'wl1'
+        if enable:
+            self.run_ssh_cmd('wl -i {} ampdu_rts 1'.format(wl_interface))
+            self.run_ssh_cmd('wl -i {} rtsthresh 2437'.format(wl_interface))
+        else:
+            self.run_ssh_cmd('wl -i {} ampdu_rts 0'.format(wl_interface))
+            self.run_ssh_cmd('wl -i {} rtsthresh 15000'.format(wl_interface))
+
+    def set_tx_beamformer(self, interface, enable):
+        """Function to enable or disable transmit beamforming
+
+        Args:
+            interface: interface to be configured (2G_5G, 6G)
+            enable: boolean controlling beamformer behavior
+        """
+        wl_interface = 'wl0' if interface == '6G' else 'wl1'
+
+        self.run_ssh_cmd('wl down')
+        self.run_ssh_cmd('wl -i {} txbf {}'.format(wl_interface, int(enable)))
+        self.run_ssh_cmd('wl up')
+
+    def get_sta_rssi(self, interface, sta_macaddr):
+        """Function to get RSSI from connected STA
+
+        Args:
+            interface: interface to be configured (2G_5G, 6G)
+            sta_macaddr: mac address of STA of interest
+        """
+        wl_interface = 'wl0' if interface == '6G' else 'wl1'
+
+        return self.run_ssh_cmd('wl -i {} phy_rssi_ant {}'.format(
+            wl_interface, sta_macaddr))
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/google_wifi.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/google_wifi.py
index a39c516..6c3230e 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/google_wifi.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/google_wifi.py
@@ -119,6 +119,10 @@
         self.access_point = access_point.AccessPoint(init_settings)
         self.configure_ap()
 
+    def teardown(self):
+        self.access_point.stop_all_aps()
+        super().teardown()
+
     def read_ap_settings(self):
         """Function that reads current ap settings."""
         return self.ap_settings.copy()
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7000.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7000.py
index ac118df..9c491a1 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7000.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7000.py
@@ -26,7 +26,11 @@
 
 class NetgearR7000AP(WifiRetailAP):
     """Class that implements Netgear R7000 AP."""
+
     def __init__(self, ap_settings):
+        self.log.warning(
+            'This AP model is no longer maintained and must be updated/verified.'
+        )
         super().__init__(ap_settings)
         self.init_gui_data()
         # Read and update AP settings
@@ -276,6 +280,7 @@
 
 class NetgearR7000NAAP(NetgearR7000AP):
     """Class that implements Netgear R7000 NA AP."""
+
     def init_gui_data(self):
         """Function to initialize data used while interacting with web GUI"""
         super().init_gui_data()
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7500.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7500.py
index 06329b6..f554e0d 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7500.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_r7500.py
@@ -27,7 +27,11 @@
 
 class NetgearR7500AP(WifiRetailAP):
     """Class that implements Netgear R7500 AP."""
+
     def __init__(self, ap_settings):
+        self.log.warning(
+            'This AP model is no longer maintained and must be updated/verified.'
+        )
         super().__init__(ap_settings)
         self.init_gui_data()
         # Read and update AP settings
@@ -329,7 +333,8 @@
 
 class NetgearR7500NAAP(NetgearR7500AP):
     """Class that implements Netgear R7500 NA AP."""
+
     def init_gui_data(self):
         """Function to initialize data used while interacting with web GUI"""
         super().init_gui_data()
-        self.region_map['10'] = 'North America'
\ No newline at end of file
+        self.region_map['10'] = 'North America'
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_rax200.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_rax200.py
index d6c6fad..9363bbe 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_rax200.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_rax200.py
@@ -31,6 +31,7 @@
     Since most of the class' implementation is shared with the R7000, this
     class inherits from NetgearR7000AP and simply redefines config parameters
     """
+
     def __init__(self, ap_settings):
         super().__init__(ap_settings)
         self.init_gui_data()
@@ -271,42 +272,38 @@
             # Visit URL
             browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
 
-            for key, value in self.config_page_fields.items():
-                if 'status' in key:
+            for field_key, field_name in self.config_page_fields.items():
+                if 'status' in field_key:
                     browser.visit_persistent(self.config_page_advanced,
                                              BROWSER_WAIT_MED, 10)
-                    config_item = browser.find_by_name(value)
-                    self.ap_settings[key[0]][key[1]] = int(
-                        config_item.first.checked)
+                    field_value = browser.get_element_value(field_name)
+                    self.ap_settings[field_key[0]][field_key[1]] = int(
+                        field_value)
                     browser.visit_persistent(self.config_page,
                                              BROWSER_WAIT_MED, 10)
                 else:
-                    config_item = browser.find_by_name(value)
-                    if 'enable_ax' in key:
-                        self.ap_settings[key] = int(config_item.first.checked)
-                    elif 'bandwidth' in key:
-                        self.ap_settings[key[0]][key[1]] = self.bw_mode_values[
-                            self.ap_settings['enable_ax']][
-                                config_item.first.value]
-                    elif 'power' in key:
-                        self.ap_settings[key[0]][
-                            key[1]] = self.power_mode_values[
-                                config_item.first.value]
-                    elif 'region' in key:
+                    field_value = browser.get_element_value(field_name)
+                    if 'enable_ax' in field_key:
+                        self.ap_settings[field_key] = int(field_value)
+                    elif 'bandwidth' in field_key:
+                        self.ap_settings[field_key[0]][
+                            field_key[1]] = self.bw_mode_values[
+                                self.ap_settings['enable_ax']][field_value]
+                    elif 'power' in field_key:
+                        self.ap_settings[field_key[0]][
+                            field_key[1]] = self.power_mode_values[field_value]
+                    elif 'region' in field_key:
                         self.ap_settings['region'] = self.region_map[
-                            config_item.first.value]
-                    elif 'security_type' in key:
-                        for item in config_item:
-                            if item.checked:
-                                self.ap_settings[key[0]][key[1]] = item.value
-                    elif 'channel' in key:
-                        config_item = browser.find_by_name(value)
-                        self.ap_settings[key[0]][key[1]] = int(
-                            config_item.first.value)
+                            field_value]
+                    elif 'security_type' in field_key:
+                        self.ap_settings[field_key[0]][
+                            field_key[1]] = field_value
+                    elif 'channel' in field_key:
+                        self.ap_settings[field_key[0]][field_key[1]] = int(
+                            field_value)
                     else:
-                        config_item = browser.find_by_name(value)
-                        self.ap_settings[key[0]][
-                            key[1]] = config_item.first.value
+                        self.ap_settings[field_key[0]][
+                            field_key[1]] = field_value
         return self.ap_settings.copy()
 
     def configure_ap(self, **config_flags):
@@ -323,70 +320,67 @@
                                      BROWSER_WAIT_MED, 10, self.config_page)
 
             # Update region, and power/bandwidth for each network
-            try:
-                config_item = browser.find_by_name(
-                    self.config_page_fields['region']).first
-                config_item.select_by_text(self.ap_settings['region'])
-            except:
+            if browser.is_element_enabled(self.config_page_fields['region']):
+                browser.set_element_value(self.config_page_fields['region'],
+                                          self.ap_settings['region'],
+                                          select_method='text')
+            else:
                 self.log.warning('Cannot change region.')
-            for key, value in self.config_page_fields.items():
-                if 'enable_ax' in key:
-                    config_item = browser.find_by_name(value).first
-                    if self.ap_settings['enable_ax']:
-                        config_item.check()
-                    else:
-                        config_item.uncheck()
-                if 'power' in key:
-                    config_item = browser.find_by_name(value).first
-                    config_item.select_by_text(
-                        self.ap_settings[key[0]][key[1]])
-                elif 'bandwidth' in key:
-                    config_item = browser.find_by_name(value).first
+            for field_key, field_name in self.config_page_fields.items():
+                if 'enable_ax' in field_key:
+                    browser.set_element_value(field_name,
+                                              self.ap_settings['enable_ax'])
+                if 'power' in field_key:
+                    browser.set_element_value(
+                        field_name,
+                        self.ap_settings[field_key[0]][field_key[1]],
+                        select_method='text')
+                elif 'bandwidth' in field_key:
                     try:
-                        config_item.select_by_text(self.bw_mode_text[key[0]][
-                            self.ap_settings[key[0]][key[1]]])
+                        browser.set_element_value(
+                            field_name,
+                            self.bw_mode_text[field_key[0]][self.ap_settings[
+                                field_key[0]][field_key[1]]],
+                            select_method='text')
                     except AttributeError:
                         self.log.warning(
                             'Cannot select bandwidth. Keeping AP default.')
 
             # Update security settings (passwords updated only if applicable)
-            for key, value in self.config_page_fields.items():
-                if 'security_type' in key:
-                    browser.choose(value, self.ap_settings[key[0]][key[1]])
-                    if 'WPA' in self.ap_settings[key[0]][key[1]]:
-                        config_item = browser.find_by_name(
-                            self.config_page_fields[(key[0],
-                                                     'password')]).first
-                        config_item.fill(self.ap_settings[key[0]]['password'])
+            for field_key, field_name in self.config_page_fields.items():
+                if 'security_type' in field_key:
+                    browser.set_element_value(
+                        field_name,
+                        self.ap_settings[field_key[0]][field_key[1]])
+                    if 'WPA' in self.ap_settings[field_key[0]][field_key[1]]:
+                        browser.set_element_value(
+                            self.config_page_fields[(field_key[0],
+                                                     'password')],
+                            self.ap_settings[field_key[0]]['password'])
 
-            for key, value in self.config_page_fields.items():
-                if 'ssid' in key:
-                    config_item = browser.find_by_name(value).first
-                    config_item.fill(self.ap_settings[key[0]][key[1]])
-                elif 'channel' in key:
-                    config_item = browser.find_by_name(value).first
+            for field_key, field_name in self.config_page_fields.items():
+                if 'ssid' in field_key:
+                    browser.set_element_value(
+                        field_name,
+                        self.ap_settings[field_key[0]][field_key[1]])
+                elif 'channel' in field_key:
+                    config_item = browser.find_by_name(field_name).first
                     try:
-                        config_item.select(self.ap_settings[key[0]][key[1]])
-                        time.sleep(BROWSER_WAIT_SHORT)
+                        browser.set_element_value(
+                            field_name,
+                            self.ap_settings[field_key[0]][field_key[1]])
                     except AttributeError:
                         self.log.warning(
                             'Cannot select channel. Keeping AP default.')
                     try:
                         for idx in range(0, 2):
-                            alert = browser.get_alert()
-                            alert.accept()
-                            time.sleep(BROWSER_WAIT_SHORT)
+                            browser.accept_alert_if_present(BROWSER_WAIT_SHORT)
                     except:
                         pass
             time.sleep(BROWSER_WAIT_SHORT)
-            browser.find_by_name('Apply').first.click()
+            browser.click_button('Apply')
+            browser.accept_alert_if_present(BROWSER_WAIT_SHORT)
             time.sleep(BROWSER_WAIT_SHORT)
-            try:
-                alert = browser.get_alert()
-                alert.accept()
-                time.sleep(BROWSER_WAIT_SHORT)
-            except:
-                time.sleep(BROWSER_WAIT_SHORT)
             browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
                                      10)
 
@@ -400,16 +394,14 @@
                                      BROWSER_WAIT_MED, 10)
 
             # Turn radios on or off
-            for key, value in self.config_page_fields.items():
-                if 'status' in key:
-                    config_item = browser.find_by_name(value).first
-                    if self.ap_settings[key[0]][key[1]]:
-                        config_item.check()
-                    else:
-                        config_item.uncheck()
+            for field_key, field_name in self.config_page_fields.items():
+                if 'status' in field_key:
+                    browser.set_element_value(
+                        field_name,
+                        self.ap_settings[field_key[0]][field_key[1]])
 
             time.sleep(BROWSER_WAIT_SHORT)
-            browser.find_by_name('Apply').first.click()
+            browser.click_button('Apply')
             time.sleep(BROWSER_WAIT_EXTRA_LONG)
             browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
                                      10)
diff --git a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_raxe500.py b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_raxe500.py
index 9dc60aa..c885e05 100644
--- a/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_raxe500.py
+++ b/acts_tests/acts_contrib/test_utils/wifi/wifi_retail_ap/netgear_raxe500.py
@@ -33,6 +33,7 @@
     Since most of the class' implementation is shared with the R7000, this
     class inherits from NetgearR7000AP and simply redefines config parameters
     """
+
     def __init__(self, ap_settings):
         super().__init__(ap_settings)
         self.init_gui_data()
@@ -173,8 +174,6 @@
             (('2G', 'bandwidth'), 'opmode'),
             (('5G_1', 'bandwidth'), 'opmode_an'),
             (('6G', 'bandwidth'), 'opmode_an_2'),
-            (('2G', 'power'), 'enable_tpc'),
-            (('5G_1', 'power'), 'enable_tpc_an'),
             (('6G', 'security_type'), 'security_type_an_2'),
             (('5G_1', 'security_type'), 'security_type_an'),
             (('2G', 'security_type'), 'security_type'),
@@ -183,13 +182,6 @@
             (('6G', 'password'), 'passphrase_an_2')
         ])
 
-        self.power_mode_values = {
-            '1': '100%',
-            '2': '75%',
-            '3': '50%',
-            '4': '25%'
-        }
-
     def _set_channel_and_bandwidth(self,
                                    network,
                                    channel=None,
@@ -286,7 +278,9 @@
             browser.visit_persistent(self.firmware_page, BROWSER_WAIT_MED, 10)
             firmware_regex = re.compile(
                 r'Firmware Version[\s\S]+V(?P<version>[0-9._]+)')
-            firmware_version = re.search(firmware_regex, browser.html)
+            #firmware_version = re.search(firmware_regex, browser.html)
+            firmware_version = re.search(firmware_regex,
+                                         browser.driver.page_source)
             if firmware_version:
                 self.ap_settings['firmware_version'] = firmware_version.group(
                     'version')
@@ -300,42 +294,35 @@
             # Visit URL
             browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
 
-            for key, value in self.config_page_fields.items():
-                if 'status' in key:
+            for field_key, field_name in self.config_page_fields.items():
+                if 'status' in field_key:
                     browser.visit_persistent(self.config_page_advanced,
                                              BROWSER_WAIT_MED, 10)
-                    config_item = browser.find_by_name(value)
-                    self.ap_settings[key[0]][key[1]] = int(
-                        config_item.first.checked)
+                    field_value = browser.get_element_value(field_name)
+                    self.ap_settings[field_key[0]][field_key[1]] = int(
+                        field_value)
                     browser.visit_persistent(self.config_page,
                                              BROWSER_WAIT_MED, 10)
                 else:
-                    config_item = browser.find_by_name(value)
-                    if 'enable_ax' in key:
-                        self.ap_settings[key] = int(config_item.first.checked)
-                    elif 'bandwidth' in key:
-                        self.ap_settings[key[0]][key[1]] = self.bw_mode_values[
-                            self.ap_settings['enable_ax']][
-                                config_item.first.value]
-                    elif 'power' in key:
-                        self.ap_settings[key[0]][
-                            key[1]] = self.power_mode_values[
-                                config_item.first.value]
-                    elif 'region' in key:
+                    field_value = browser.get_element_value(field_name)
+                    if 'enable_ax' in field_key:
+                        self.ap_settings[field_key] = int(field_value)
+                    elif 'bandwidth' in field_key:
+                        self.ap_settings[field_key[0]][
+                            field_key[1]] = self.bw_mode_values[
+                                self.ap_settings['enable_ax']][field_value]
+                    elif 'region' in field_key:
                         self.ap_settings['region'] = self.region_map[
-                            config_item.first.value]
-                    elif 'security_type' in key:
-                        for item in config_item:
-                            if item.checked:
-                                self.ap_settings[key[0]][key[1]] = item.value
-                    elif 'channel' in key:
-                        config_item = browser.find_by_name(value)
-                        self.ap_settings[key[0]][key[1]] = int(
-                            config_item.first.value)
+                            field_value]
+                    elif 'security_type' in field_key:
+                        self.ap_settings[field_key[0]][
+                            field_key[1]] = field_value
+                    elif 'channel' in field_key:
+                        self.ap_settings[field_key[0]][field_key[1]] = int(
+                            field_value)
                     else:
-                        config_item = browser.find_by_name(value)
-                        self.ap_settings[key[0]][
-                            key[1]] = config_item.first.value
+                        self.ap_settings[field_key[0]][
+                            field_key[1]] = field_value
         return self.ap_settings.copy()
 
     def configure_ap(self, **config_flags):
@@ -352,68 +339,58 @@
                                      BROWSER_WAIT_MED, 10, self.config_page)
 
             # Update region, and power/bandwidth for each network
-            try:
-                config_item = browser.find_by_name(
-                    self.config_page_fields['region']).first
-                config_item.select_by_text(self.ap_settings['region'])
-            except:
+            if browser.is_element_enabled(self.config_page_fields['region']):
+                browser.set_element_value(self.config_page_fields['region'],
+                                          self.ap_settings['region'],
+                                          select_method='text')
+            else:
                 self.log.warning('Cannot change region.')
-            for key, value in self.config_page_fields.items():
-                if 'enable_ax' in key:
-                    config_item = browser.find_by_name(value).first
-                    if self.ap_settings['enable_ax']:
-                        config_item.check()
-                    else:
-                        config_item.uncheck()
-                if 'power' in key:
-                    config_item = browser.find_by_name(value).first
-                    config_item.select_by_text(
-                        self.ap_settings[key[0]][key[1]])
-                elif 'bandwidth' in key:
-                    config_item = browser.find_by_name(value).first
+            for field_key, field_name in self.config_page_fields.items():
+                if 'enable_ax' in field_key:
+                    browser.set_element_value(field_name,
+                                              self.ap_settings['enable_ax'])
+                elif 'bandwidth' in field_key:
                     try:
-                        config_item.select_by_text(self.bw_mode_text[key[0]][
-                            self.ap_settings[key[0]][key[1]]])
+                        browser.set_element_value(
+                            field_name,
+                            self.bw_mode_text[field_key[0]][self.ap_settings[
+                                field_key[0]][field_key[1]]],
+                            select_method='text')
                     except AttributeError:
                         self.log.warning(
                             'Cannot select bandwidth. Keeping AP default.')
 
             # Update security settings (passwords updated only if applicable)
-            for key, value in self.config_page_fields.items():
-                if 'security_type' in key:
-                    browser.choose(value, self.ap_settings[key[0]][key[1]])
-                    if 'WPA' in self.ap_settings[key[0]][key[1]]:
-                        config_item = browser.find_by_name(
-                            self.config_page_fields[(key[0],
-                                                     'password')]).first
-                        config_item.fill(self.ap_settings[key[0]]['password'])
+            for field_key, field_name in self.config_page_fields.items():
+                if 'security_type' in field_key:
+                    browser.set_element_value(
+                        field_name,
+                        self.ap_settings[field_key[0]][field_key[1]])
+                    if 'WPA' in self.ap_settings[field_key[0]][field_key[1]]:
+                        browser.set_element_value(
+                            self.config_page_fields[(field_key[0],
+                                                     'password')],
+                            self.ap_settings[field_key[0]]['password'])
 
-            for key, value in self.config_page_fields.items():
-                if 'ssid' in key:
-                    config_item = browser.find_by_name(value).first
-                    config_item.fill(self.ap_settings[key[0]][key[1]])
-                elif 'channel' in key:
-                    config_item = browser.find_by_name(value).first
+            for field_key, field_name in self.config_page_fields.items():
+                if 'ssid' in field_key:
+                    browser.set_element_value(
+                        field_name,
+                        self.ap_settings[field_key[0]][field_key[1]])
+                elif 'channel' in field_key:
                     try:
-                        config_item.select(self.ap_settings[key[0]][key[1]])
-                        time.sleep(BROWSER_WAIT_SHORT)
+                        browser.set_element_value(
+                            field_name,
+                            self.ap_settings[field_key[0]][field_key[1]])
                     except AttributeError:
                         self.log.warning(
                             'Cannot select channel. Keeping AP default.')
-                    try:
-                        alert = browser.get_alert()
-                        alert.accept()
-                    except:
-                        pass
+                    browser.accept_alert_if_present(BROWSER_WAIT_SHORT)
+
             time.sleep(BROWSER_WAIT_SHORT)
-            browser.find_by_name('Apply').first.click()
+            browser.click_button('Apply')
+            browser.accept_alert_if_present(BROWSER_WAIT_SHORT)
             time.sleep(BROWSER_WAIT_SHORT)
-            try:
-                alert = browser.get_alert()
-                alert.accept()
-                time.sleep(BROWSER_WAIT_SHORT)
-            except:
-                time.sleep(BROWSER_WAIT_SHORT)
             browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
                                      10)
 
@@ -427,16 +404,14 @@
                                      BROWSER_WAIT_MED, 10)
 
             # Turn radios on or off
-            for key, value in self.config_page_fields.items():
-                if 'status' in key:
-                    config_item = browser.find_by_name(value).first
-                    if self.ap_settings[key[0]][key[1]]:
-                        config_item.check()
-                    else:
-                        config_item.uncheck()
+            for field_key, field_name in self.config_page_fields.items():
+                if 'status' in field_key:
+                    browser.set_element_value(
+                        field_name,
+                        self.ap_settings[field_key[0]][field_key[1]])
 
             time.sleep(BROWSER_WAIT_SHORT)
-            browser.find_by_name('Apply').first.click()
+            browser.click_button('Apply')
             time.sleep(BROWSER_WAIT_EXTRA_LONG)
             browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
                                      10)
diff --git a/acts_tests/tests/OWNERS b/acts_tests/tests/OWNERS
index 189ff2e..eb3d0c0 100644
--- a/acts_tests/tests/OWNERS
+++ b/acts_tests/tests/OWNERS
@@ -18,6 +18,8 @@
 # Pixel GTW
 jasonkmlu@google.com
 hongscott@google.com
+diegowchung@google.com
+kuoyuanchiang@google.com
 markusliu@google.com
 jerrypcchen@google.com
 martschneider@google.com
diff --git a/acts_tests/tests/google/bt/performance/BtA2dpRangeTest.py b/acts_tests/tests/google/bt/performance/BtA2dpRangeTest.py
index c775dd7..3104452 100644
--- a/acts_tests/tests/google/bt/performance/BtA2dpRangeTest.py
+++ b/acts_tests/tests/google/bt/performance/BtA2dpRangeTest.py
@@ -21,20 +21,26 @@
 
 
 class BtA2dpRangeTest(A2dpBaseTest):
+
     def __init__(self, configs):
         super().__init__(configs)
         req_params = ['attenuation_vector', 'codecs']
+        opt_params = ['gain_mismatch', 'dual_chain']
         #'attenuation_vector' is a dict containing: start, stop and step of
         #attenuation changes
         #'codecs' is a list containing all codecs required in the tests
         self.unpack_userparams(req_params)
+        self.unpack_userparams(opt_params, dual_chain=None, gain_mismatch=None)
+
+    def setup_generated_tests(self):
         for codec_config in self.codecs:
-            self.generate_test_case(codec_config)
+            arg_set = [(codec_config, )]
+            self.generate_tests(test_logic=self.BtA2dp_test_logic,
+                                name_func=self.create_test_name,
+                                arg_sets=arg_set)
 
     def setup_class(self):
         super().setup_class()
-        opt_params = ['gain_mismatch', 'dual_chain']
-        self.unpack_userparams(opt_params, dual_chain=None, gain_mismatch=None)
         # Enable BQR on all android devices
         btutils.enable_bqr(self.android_devices)
         if hasattr(self, 'dual_chain') and self.dual_chain == 1:
@@ -49,14 +55,14 @@
             self.atten_c0.set_atten(INIT_ATTEN)
             self.atten_c1.set_atten(INIT_ATTEN)
 
-    def generate_test_case(self, codec_config):
-        def test_case_fn():
-            self.run_a2dp_to_max_range(codec_config)
+    def BtA2dp_test_logic(self, codec_config):
+        self.run_a2dp_to_max_range(codec_config)
 
+    def create_test_name(self, arg_set):
         if hasattr(self, 'dual_chain') and self.dual_chain == 1:
-            test_case_name = 'test_dual_bt_a2dp_range_codec_{}_gainmimatch_{}dB'.format(
-                codec_config['codec_type'], self.gain_mismatch)
+            test_case_name = 'test_dual_bt_a2dp_range_codec_{}_gainmismatch_{}dB'.format(
+                arg_set['codec_type'], self.gain_mismatch)
         else:
             test_case_name = 'test_bt_a2dp_range_codec_{}'.format(
-                codec_config['codec_type'])
-        setattr(self, test_case_name, test_case_fn)
+                arg_set['codec_type'])
+        return test_case_name
diff --git a/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleAdvTest.py b/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleAdvTest.py
index 5fe9503..16fedcd 100644
--- a/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleAdvTest.py
+++ b/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleAdvTest.py
@@ -57,35 +57,39 @@
           test_bt_a2dp_range_codec_SBC_adv_mode_low_latency_adv_tx_power_low
           test_bt_a2dp_range_codec_SBC_adv_mode_low_latency_adv_tx_power_medium
           test_bt_a2dp_range_codec_SBC_adv_mode_low_latency_adv_tx_power_high
-
       """
+
     def __init__(self, configs):
         super().__init__(configs)
         req_params = ['attenuation_vector', 'codecs']
-        #'attenuation_vector' is a dict containing: start, stop and step of
-        #attenuation changes
+        opt_params = ['gain_mismatch', 'dual_chain']
+        #'attenuation_vector' is a dict containing: start, stop and step of attenuation changes
         #'codecs' is a list containing all codecs required in the tests
+        #'gain_mismatch' is an offset value between the BT two chains
+        #'dual_chain' set to 1 enable sweeping attenuation for BT two chains
         self.unpack_userparams(req_params)
+        self.unpack_userparams(opt_params, dual_chian=None, gain_mismatch=None)
+
+    def setup_generated_tests(self):
         for codec_config in self.codecs:
-            # Loop all advertise modes and power levels
             for adv_mode in ble_advertise_settings_modes.items():
-                for adv_power_level in ble_advertise_settings_tx_powers.items(
-                ):
-                    self.generate_test_case(codec_config, adv_mode,
-                                            adv_power_level)
+                for adv_power_level in ble_advertise_settings_tx_powers.items():
+                    arg_set = [(codec_config, adv_mode, adv_power_level)]
+                    self.generate_tests(
+                        test_logic=self.BtA2dp_with_ble_adv_test_logic,
+                        name_func=self.create_test_name,
+                        arg_sets=arg_set)
 
     def setup_class(self):
         super().setup_class()
-        opt_params = ['gain_mismatch', 'dual_chain']
-        self.unpack_userparams(opt_params, dual_chain=None, gain_mismatch=None)
-        return setup_multiple_devices_for_bt_test(self.android_devices)
-        # Enable BQR on all android devices
+        #Enable BQR on all android devices
         btutils.enable_bqr(self.android_devices)
         if hasattr(self, 'dual_chain') and self.dual_chain == 1:
             self.atten_c0 = self.attenuators[0]
             self.atten_c1 = self.attenuators[1]
             self.atten_c0.set_atten(INIT_ATTEN)
             self.atten_c1.set_atten(INIT_ATTEN)
+        return setup_multiple_devices_for_bt_test(self.android_devices)
 
     def teardown_class(self):
         super().teardown_class()
@@ -93,20 +97,22 @@
             self.atten_c0.set_atten(INIT_ATTEN)
             self.atten_c1.set_atten(INIT_ATTEN)
 
-    def generate_test_case(self, codec_config, adv_mode, adv_power_level):
-        def test_case_fn():
-            adv_callback = self.start_ble_adv(adv_mode[1], adv_power_level[1])
-            self.run_a2dp_to_max_range(codec_config)
-            self.dut.droid.bleStopBleAdvertising(adv_callback)
-            self.log.info("Advertisement stopped Successfully")
+    def BtA2dp_with_ble_adv_test_logic(self, codec_config, adv_mode,
+                                       adv_power_level):
+        adv_callback = self.start_ble_adv(adv_mode[1], adv_power_level[1])
+        self.run_a2dp_to_max_range(codec_config)
+        self.dut.droid.bleStopBleAdvertising(adv_callback)
+        self.log.info("Advertisement stopped Successfully")
 
+    def create_test_name(self, codec_config, adv_mode, adv_power_level):
         if hasattr(self, 'dual_chain') and self.dual_chain == 1:
-            test_case_name = 'test_dual_bt_a2dp_range_codec_{}_gainmimatch_{}dB'.format(
-                codec_config['codec_type'], self.gain_mismatch)
+            test_case_name = 'test_dual_bt_a2dp_range_codec_{}_gainmismatch_{}dB_adv_mode_{}_adv_tx_power_{}'.format(
+                codec_config['codec_type'], self.gain_mismatch, adv_mode[0],
+                adv_power_level[0])
         else:
             test_case_name = 'test_bt_a2dp_range_codec_{}_adv_mode_{}_adv_tx_power_{}'.format(
                 codec_config['codec_type'], adv_mode[0], adv_power_level[0])
-        setattr(self, test_case_name, test_case_fn)
+        return test_case_name
 
     def start_ble_adv(self, adv_mode, adv_power_level):
         """Function to start an LE advertisement
@@ -140,3 +146,4 @@
             raise BtTestUtilsError(
                 "Advertiser did not start successfully {}".format(err))
         return advertise_callback
+
diff --git a/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleScanTest.py b/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleScanTest.py
index 6020c4a..4cd2dd7 100644
--- a/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleScanTest.py
+++ b/acts_tests/tests/google/bt/performance/BtA2dpRangeWithBleScanTest.py
@@ -25,32 +25,50 @@
 
 
 class BtA2dpRangeWithBleScanTest(A2dpBaseTest):
-    default_timeout = 10
+    """User can generate test case with below format.
+    test_bt_a2dp_range_codec_"Codec"_with_BLE_scan_"Scan Mode"
+
+    Below are the list of test cases:
+        test_bt_a2dp_range_codec_AAC_with_BLE_scan_balanced
+        test_bt_a2dp_range_codec_AAC_with_BLE_scan_low_latency
+        test_bt_a2dp_range_codec_AAC_with_BLE_scan_low_power
+        test_bt_a2dp_range_codec_AAC_with_BLE_scan_opportunistic
+        test_bt_a2dp_range_codec_SBC_with_BLE_scan_balanced
+        test_bt_a2dp_range_codec_SBC_with_BLE_scan_low_latency
+        test_bt_a2dp_range_codec_SBC_with_BLE_scan_low_power
+        test_bt_a2dp_range_codec_SBC_with_BLE_scan_opportunistic
+    """
 
     def __init__(self, configs):
         super().__init__(configs)
         req_params = ['attenuation_vector', 'codecs']
-        #'attenuation_vector' is a dict containing: start, stop and step of
-        #attenuation changes
+        opt_params = ['gain_mismatch', 'dual_chain']
+        #'attenuation_vector' is a dict containing: start, stop and step of attenuation changes
         #'codecs' is a list containing all codecs required in the tests
+        #'gain_mismatch' is an offset value between the BT two chains
+        #'dual_chain' set to 1 enable sweeping attenuation for BT two chains
         self.unpack_userparams(req_params)
+        self.unpack_userparams(opt_params, dual_chian=None, gain_mismatch=None)
+
+    def setup_generated_tests(self):
         for codec_config in self.codecs:
-            # Loop all BLE Scan modes
             for scan_mode in ble_scan_settings_modes.items():
-                self.generate_test_case(codec_config, scan_mode)
+                arg_set = [(codec_config, scan_mode)]
+                self.generate_tests(
+                    test_logic=self.BtA2dp_with_ble_scan_test_logic,
+                    name_func=self.create_test_name,
+                    arg_sets=arg_set)
 
     def setup_class(self):
         super().setup_class()
-        opt_params = ['gain_mismatch', 'dual_chain']
-        self.unpack_userparams(opt_params, dual_chain=None, gain_mismatch=None)
-        return setup_multiple_devices_for_bt_test(self.android_devices)
-        # Enable BQR on all android devices
+        #Enable BQR on all android devices
         btutils.enable_bqr(self.android_devices)
         if hasattr(self, 'dual_chain') and self.dual_chain == 1:
             self.atten_c0 = self.attenuators[0]
             self.atten_c1 = self.attenuators[1]
             self.atten_c0.set_atten(INIT_ATTEN)
             self.atten_c1.set_atten(INIT_ATTEN)
+        return setup_multiple_devices_for_bt_test(self.android_devices)
 
     def teardown_class(self):
         super().teardown_class()
@@ -58,31 +76,20 @@
             self.atten_c0.set_atten(INIT_ATTEN)
             self.atten_c1.set_atten(INIT_ATTEN)
 
-    def generate_test_case(self, codec_config, scan_mode):
-        """ Below are the list of test case's user can choose to run.
-        Test case list:
-        "test_bt_a2dp_range_codec_AAC_with_BLE_scan_balanced"
-        "test_bt_a2dp_range_codec_AAC_with_BLE_scan_low_latency"
-        "test_bt_a2dp_range_codec_AAC_with_BLE_scan_low_power"
-        "test_bt_a2dp_range_codec_AAC_with_BLE_scan_opportunistic"
-        "test_bt_a2dp_range_codec_SBC_with_BLE_scan_balanced"
-        "test_bt_a2dp_range_codec_SBC_with_BLE_scan_low_latency"
-        "test_bt_a2dp_range_codec_SBC_with_BLE_scan_low_power"
-        "test_bt_a2dp_range_codec_SBC_with_BLE_scan_opportunistic"
-        """
-        def test_case_fn():
-            scan_callback = self.start_ble_scan(scan_mode[1])
-            self.run_a2dp_to_max_range(codec_config)
-            self.dut.droid.bleStopBleScan(scan_callback)
-            self.log.info("BLE Scan stopped succssfully")
+    def BtA2dp_with_ble_scan_test_logic(self, codec_config, scan_mode):
+        scan_callback = self.start_ble_scan(scan_mode[1])
+        self.run_a2dp_to_max_range(codec_config)
+        self.dut.droid.bleStopBleScan(scan_callback)
+        self.log.info("BLE Scan stopped successfully")
 
+    def create_test_name(self, codec_config, scan_mode):
         if hasattr(self, 'dual_chain') and self.dual_chain == 1:
-            test_case_name = 'test_dual_bt_a2dp_range_codec_{}_gainmimatch_{}dB'.format(
-                codec_config['codec_type'], self.gain_mismatch)
+            test_case_name = 'test_dual_bt_a2dp_range_codec_{}_gainmismatch_{}dB_with_BLE_scan_{}'.format(
+                codec_config['codec_type'], self.gain_mismatch, scan_mode[0])
         else:
             test_case_name = 'test_bt_a2dp_range_codec_{}_with_BLE_scan_{}'.format(
                 codec_config['codec_type'], scan_mode[0])
-        setattr(self, test_case_name, test_case_fn)
+        return test_case_name
 
     def start_ble_scan(self, scan_mode):
         """ This function will start Ble Scan with different scan mode.
@@ -99,5 +106,6 @@
             self.dut.droid)
         self.dut.droid.bleStartBleScan(filter_list, scan_settings,
                                        scan_callback)
-        self.log.info("BLE Scanning started succssfully")
+        self.log.info("BLE Scanning started successfully")
         return scan_callback
+
diff --git a/acts_tests/tests/google/bt/performance/BtInterferenceStaticTest.py b/acts_tests/tests/google/bt/performance/BtInterferenceStaticTest.py
index 99218b0..6ec585d 100644
--- a/acts_tests/tests/google/bt/performance/BtInterferenceStaticTest.py
+++ b/acts_tests/tests/google/bt/performance/BtInterferenceStaticTest.py
@@ -29,49 +29,39 @@
 
 
 class BtInterferenceStaticTest(BtInterferenceBaseTest):
+
     def __init__(self, configs):
         super().__init__(configs)
         self.bt_attenuation_range = range(self.attenuation_vector['start'],
                                           self.attenuation_vector['stop'] + 1,
                                           self.attenuation_vector['step'])
-
         self.iperf_duration = self.audio_params['duration'] + TIME_OVERHEAD
-        for level in list(
-                self.static_wifi_interference['interference_level'].keys()):
-            for channels in self.static_wifi_interference['channels']:
-                self.generate_test_case(
-                    self.static_wifi_interference['interference_level'][level],
-                    channels)
-
         test_metrics = [
-            'wifi_chan1_rssi', 'wifi_chan6_rssi', 'wifi_chan11_rssi',
-            'bt_range'
+            'wifi_chan1_rssi', 'wifi_chan6_rssi', 'wifi_chan11_rssi', 'bt_range'
         ]
         for metric in test_metrics:
             setattr(self, '{}_metric'.format(metric),
                     BlackboxMetricLogger.for_test_case(metric_name=metric))
 
-    def generate_test_case(self, interference_level, channels):
-        """Function to generate test cases with different parameters.
+    def setup_generated_tests(self):
+        for level in list(
+                self.static_wifi_interference['interference_level'].values()):
+            for channels in self.static_wifi_interference['channels']:
+                arg_set = [(level, channels)]
+                self.generate_tests(
+                    test_logic=self.bt_range_with_static_wifi_interference,
+                    name_func=self.create_test_name,
+                    arg_sets=arg_set)
 
-        Args:
-           interference_level: wifi interference signal level
-           channels: wifi interference channel or channel combination
-        """
-        def test_case_fn():
-            self.bt_range_with_static_wifi_interference(
-                interference_level, channels)
-
+    def create_test_name(self, level, channels):
         str_channel_test = ''
         for i in channels:
-            str_channel_test = str_channel_test + str(i) + '_'
+            str_channel_test = str_channel_test + str(i) + "_"
         test_case_name = ('test_bt_range_with_static_interference_level_{}_'
-                          'channel_{}'.format(interference_level,
-                                              str_channel_test))
-        setattr(self, test_case_name, test_case_fn)
+                          'channel_{}'.format(level, str_channel_test))
+        return test_case_name
 
-    def bt_range_with_static_wifi_interference(self, interference_level,
-                                               channels):
+    def bt_range_with_static_wifi_interference(self, level, channels):
         """Test function to measure bt range under interference.
 
         Args:
@@ -79,8 +69,7 @@
             channels: wifi interference channels
         """
         #setup wifi interference by setting the correct attenuator
-        inject_static_wifi_interference(self.wifi_int_pairs,
-                                        interference_level, channels)
+        inject_static_wifi_interference(self.wifi_int_pairs, level, channels)
         # Read interference RSSI
         self.get_interference_rssi()
         self.wifi_chan1_rssi_metric.metric_value = self.interference_rssi[0][
@@ -113,8 +102,7 @@
                     self.iperf_duration, obj.iperf_server.port)
                 tag = 'chan_{}'.format(obj.channel)
                 proc_iperf = Process(target=obj.iperf_client.start,
-                                     args=(obj.server_address, iperf_args,
-                                           tag))
+                                     args=(obj.server_address, iperf_args, tag))
                 procs_iperf.append(proc_iperf)
 
             #play a2dp streaming and run thdn analysis
@@ -140,8 +128,8 @@
             self.log.info('THDN results are {} at {} dB attenuation'.format(
                 thdns, atten))
             self.log.info('DUT rssi {} dBm, master tx power level {}, '
-                          'RemoteDevice rssi {} dBm'.format(rssi_primary, pwl_primary,
-                                                     rssi_secondary))
+                          'RemoteDevice rssi {} dBm'.format(
+                              rssi_primary, pwl_primary, rssi_secondary))
             for thdn in thdns:
                 if thdn >= self.audio_params['thdn_threshold']:
                     self.log.info('Under the WiFi interference condition: '
@@ -154,3 +142,4 @@
                     raise TestPass(
                         'Max range for this test is {}, with BT master RSSI at'
                         ' {} dBm'.format(atten, rssi_primary))
+
diff --git a/acts_tests/tests/google/bt/performance/BtRfcommThroughputRangeTest.py b/acts_tests/tests/google/bt/performance/BtRfcommThroughputRangeTest.py
new file mode 100644
index 0000000..342f2f0
--- /dev/null
+++ b/acts_tests/tests/google/bt/performance/BtRfcommThroughputRangeTest.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+#
+# Copyright 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import logging
+import pandas as pd
+import time
+import acts_contrib.test_utils.bt.bt_test_utils as btutils
+from acts_contrib.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest
+from acts_contrib.test_utils.bt.ble_performance_test_utils import plot_graph
+from acts_contrib.test_utils.power.PowerBTBaseTest import ramp_attenuation
+from acts_contrib.test_utils.bt.bt_test_utils import setup_multiple_devices_for_bt_test
+from acts_contrib.test_utils.bt.bt_test_utils import orchestrate_rfcomm_connection
+from acts.signals import TestPass
+from acts import utils
+from acts_contrib.test_utils.bt.bt_test_utils import write_read_verify_data
+
+INIT_ATTEN = 0
+WRITE_ITERATIONS = 500
+
+
+class BtRfcommThroughputRangeTest(BluetoothBaseTest):
+    def __init__(self, configs):
+        super().__init__(configs)
+        req_params = ['attenuation_vector', 'system_path_loss']
+        #'attenuation_vector' is a dict containing: start, stop and step of
+        #attenuation changes
+        self.unpack_userparams(req_params)
+
+    def setup_class(self):
+        super().setup_class()
+        self.dut = self.android_devices[0]
+        self.remote_device = self.android_devices[1]
+        btutils.enable_bqr(self.android_devices)
+        if hasattr(self, 'attenuators'):
+            self.attenuator = self.attenuators[0]
+            self.attenuator.set_atten(INIT_ATTEN)
+        self.attenuation_range = range(self.attenuation_vector['start'],
+                                       self.attenuation_vector['stop'] + 1,
+                                       self.attenuation_vector['step'])
+        self.log_path = os.path.join(logging.log_path, 'results')
+        os.makedirs(self.log_path, exist_ok=True)
+        return setup_multiple_devices_for_bt_test(self.android_devices)
+
+    def teardown_test(self):
+        self.dut.droid.bluetoothSocketConnStop()
+        self.remote_device.droid.bluetoothSocketConnStop()
+        if hasattr(self, 'attenuator'):
+            self.attenuator.set_atten(INIT_ATTEN)
+
+    def test_rfcomm_throughput_range(self):
+        data_points = []
+        message = "x" * 990
+        self.file_output = os.path.join(
+            self.log_path, '{}.csv'.format(self.current_test_name))
+        if not orchestrate_rfcomm_connection(self.dut, self.remote_device):
+            return False
+        self.log.info("RFCOMM Connection established")
+        for atten in self.attenuation_range:
+            ramp_attenuation(self.attenuator, atten)
+            self.log.info('Set attenuation to %d dB', atten)
+            process_data_dict = btutils.get_bt_metric(self.dut)
+            rssi_primary = process_data_dict.get('rssi')
+            pwlv_primary = process_data_dict.get('pwlv')
+            rssi_primary = rssi_primary.get(self.dut.serial)
+            pwlv_primary = pwlv_primary.get(self.dut.serial)
+            self.log.info("DUT RSSI:{} and PwLv:{} with attenuation:{}".format(
+                rssi_primary, pwlv_primary, atten))
+            if type(rssi_primary) != str:
+                data_rate = self.write_read_verify_rfcommdata(
+                    self.dut, self.remote_device, message)
+                data_point = {
+                    'attenuation_db': atten,
+                    'Dut_RSSI': rssi_primary,
+                    'DUT_PwLv': pwlv_primary,
+                    'Pathloss': atten + self.system_path_loss,
+                    'RfcommThroughput': data_rate
+                }
+                data_points.append(data_point)
+                df = pd.DataFrame(data_points)
+                # bokeh data for generating BokehFigure
+                bokeh_data = {
+                    'x_label': 'Pathloss (dBm)',
+                    'primary_y_label': 'RSSI (dBm)',
+                    'log_path': self.log_path,
+                    'current_test_name': self.current_test_name
+                }
+                # plot_data for adding line to existing BokehFigure
+                plot_data = {
+                    'line_one': {
+                        'x_column': 'Pathloss',
+                        'y_column': 'Dut_RSSI',
+                        'legend': 'DUT RSSI (dBm)',
+                        'marker': 'circle_x',
+                        'y_axis': 'default'
+                    },
+                    'line_two': {
+                        'x_column': 'Pathloss',
+                        'y_column': 'RfcommThroughput',
+                        'legend': 'RFCOMM Throughput (bits/sec)',
+                        'marker': 'hex',
+                        'y_axis': 'secondary'
+                    }
+                }
+            else:
+                df.to_csv(self.file_output, index=False)
+                plot_graph(df,
+                           plot_data,
+                           bokeh_data,
+                           secondary_y_label='RFCOMM Throughput (bits/sec)')
+                raise TestPass("Reached RFCOMM Max Range,RFCOMM disconnected.")
+        # Save Data points to csv
+        df.to_csv(self.file_output, index=False)
+        # Plot graph
+        plot_graph(df,
+                   plot_data,
+                   bokeh_data,
+                   secondary_y_label='RFCOMM Throughput (bits/sec)')
+        self.dut.droid.bluetoothRfcommStop()
+        self.remote_device.droid.bluetoothRfcommStop()
+        return True
+
+    def write_read_verify_rfcommdata(self, dut, remote_device, msg):
+        """Verify that the client wrote data to the remote Android device correctly.
+
+        Args:
+            dut: the Android device to perform the write.
+            remote_device: the Android device to read the data written.
+            msg: the message to write.
+        Returns:
+            True if the data written matches the data read, false if not.
+        """
+        start_write_time = time.perf_counter()
+        for n in range(WRITE_ITERATIONS):
+            try:
+                dut.droid.bluetoothSocketConnWrite(msg)
+            except Exception as err:
+                dut.log.error("Failed to write data: {}".format(err))
+                return False
+            try:
+                read_msg = remote_device.droid.bluetoothSocketConnRead()
+            except Exception as err:
+                remote_device.log.error("Failed to read data: {}".format(err))
+                return False
+            if msg != read_msg:
+                self.log.error("Mismatch! Read: {}, Expected: {}".format(
+                    read_msg, msg))
+                return False
+        end_read_time = time.perf_counter()
+        total_num_bytes = 990 * WRITE_ITERATIONS
+        test_time = (end_read_time - start_write_time)
+        if (test_time == 0):
+            dut.log.error("Buffer transmits cannot take zero time")
+            return 0
+        data_rate = (1.000 * total_num_bytes) / test_time
+        self.log.info(
+            "Calculated using total write and read times: total_num_bytes={}, "
+            "test_time={}, data rate={:08.0f} bytes/sec, {:08.0f} bits/sec".
+            format(total_num_bytes, test_time, data_rate, (data_rate * 8)))
+        data_rate = data_rate * 8
+        return data_rate
diff --git a/acts_tests/tests/google/cellular/performance/Cellular5GFR2SensitivityTest.py b/acts_tests/tests/google/cellular/performance/Cellular5GFR2SensitivityTest.py
new file mode 100644
index 0000000..f3e49b8
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/Cellular5GFR2SensitivityTest.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import itertools
+import json
+import numpy
+import os
+from functools import partial
+from acts import asserts
+from acts import context
+from acts import base_test
+from acts import utils
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts.controllers.utils_lib import ssh
+from acts_contrib.test_utils.cellular.keysight_5g_testapp import Keysight5GTestApp
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+from acts_contrib.test_utils.wifi.wifi_performance_test_utils.bokeh_figure import BokehFigure
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+from Cellular5GFR2ThroughputTest import Cellular5GFR2ThroughputTest
+
+
+class Cellular5GFR2SensitivityTest(Cellular5GFR2ThroughputTest):
+    """Class to test cellular throughput
+
+    This class implements cellular throughput tests on a lab/callbox setup.
+    The class setups up the callbox in the desired configurations, configures
+    and connects the phone, and runs traffic/iperf throughput.
+    """
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+
+    def setup_class(self):
+        """Initializes common test hardware and parameters.
+
+        This function initializes hardwares and compiles parameters that are
+        common to all tests in this class.
+        """
+        self.dut = self.android_devices[-1]
+        self.testclass_params = self.user_params['sensitivity_test_params']
+        self.keysight_test_app = Keysight5GTestApp(
+            self.user_params['Keysight5GTestApp'])
+        self.testclass_results = collections.OrderedDict()
+        self.iperf_server = self.iperf_servers[0]
+        self.iperf_client = self.iperf_clients[0]
+        self.remote_server = ssh.connection.SshConnection(
+            ssh.settings.from_config(
+                self.user_params['RemoteServer']['ssh_config']))
+        if self.testclass_params.get('reload_scpi', 1):
+            self.keysight_test_app.import_scpi_file(
+                self.testclass_params['scpi_file'])
+        # Configure test retries
+        self.user_params['retry_tests'] = [self.__class__.__name__]
+
+        # Turn Airplane mode on
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+
+    def process_testcase_results(self):
+        if self.current_test_name not in self.testclass_results:
+            return
+        testcase_results = self.testclass_results[self.current_test_name]
+        cell_power_list = [
+            result['cell_power'] for result in testcase_results['results']
+        ]
+        dl_bler_list = [
+            result['bler_result']['total']['DL']['nack_ratio']
+            for result in testcase_results['results']
+        ]
+        bler_above_threshold = [
+            x > self.testclass_params['bler_threshold'] for x in dl_bler_list
+        ]
+        for idx in range(len(bler_above_threshold)):
+            if all(bler_above_threshold[idx:]):
+                sensitivity_index = max(idx, 1) - 1
+                cell_power_at_sensitivity = cell_power_list[sensitivity_index]
+                break
+        else:
+            sensitivity_index = -1
+            cell_power_at_sensitivity = float('nan')
+        if min(dl_bler_list) < 0.05:
+            testcase_results['sensitivity'] = cell_power_at_sensitivity
+        else:
+            testcase_results['sensitivity'] = float('nan')
+
+        testcase_results['cell_power_list'] = cell_power_list
+        testcase_results['dl_bler_list'] = dl_bler_list
+
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(testcase_results),
+                      results_file,
+                      indent=4)
+
+        result_string = ('DL {}CC MCS {} Sensitivity = {}dBm.'.format(
+            testcase_results['testcase_params']['num_dl_cells'],
+            testcase_results['testcase_params']['dl_mcs'],
+            testcase_results['sensitivity']))
+        if min(dl_bler_list) < 0.05:
+            self.log.info('Test Passed. {}'.format(result_string))
+        else:
+            self.log.info('Result unreliable. {}'.format(result_string))
+
+    def process_testclass_results(self):
+        Cellular5GFR2ThroughputTest.process_testclass_results(self)
+
+        plots = collections.OrderedDict()
+        id_fields = ['band', 'num_dl_cells']
+        for testcase, testcase_data in self.testclass_results.items():
+            testcase_params = testcase_data['testcase_params']
+            plot_id = cputils.extract_test_id(testcase_params, id_fields)
+            plot_id = tuple(plot_id.items())
+            if plot_id not in plots:
+                plots[plot_id] = BokehFigure(title='{} {}CC'.format(
+                    testcase_params['band'], testcase_params['num_dl_cells']),
+                                             x_label='Cell Power (dBm)',
+                                             primary_y_label='BLER (%)')
+            plots[plot_id].add_line(
+                testcase_data['cell_power_list'],
+                testcase_data['dl_bler_list'],
+                'Channel {}, MCS {}'.format(testcase_params['channel'],
+                                            testcase_params['dl_mcs']))
+        figure_list = []
+        for plot_id, plot in plots.items():
+            plot.generate_figure()
+            figure_list.append(plot)
+        output_file_path = os.path.join(self.log_path, 'results.html')
+        BokehFigure.save_figures(figure_list, output_file_path)
+
+    def generate_test_cases(self, bands, channels, mcs_pair_list,
+                            num_dl_cells_list, num_ul_cells_list, **kwargs):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = []
+
+        for band, channel, num_ul_cells, num_dl_cells, mcs_pair in itertools.product(
+                bands, channels, num_ul_cells_list, num_dl_cells_list,
+                mcs_pair_list):
+            if num_ul_cells > num_dl_cells:
+                continue
+            test_name = 'test_nr_sensitivity_{}_{}_DL_{}CC_mcs{}'.format(
+                band, channel, num_dl_cells, mcs_pair[0])
+            test_params = collections.OrderedDict(
+                band=band,
+                channel=channel,
+                dl_mcs=mcs_pair[0],
+                ul_mcs=mcs_pair[1],
+                num_dl_cells=num_dl_cells,
+                num_ul_cells=num_ul_cells,
+                dl_cell_list=list(range(1, num_dl_cells + 1)),
+                ul_cell_list=list(range(1, num_ul_cells + 1)),
+                **kwargs)
+            setattr(self, test_name,
+                    partial(self._test_nr_throughput_bler, test_params))
+            test_cases.append(test_name)
+        return test_cases
+
+
+class Cellular5GFR2_AllBands_SensitivityTest(Cellular5GFR2SensitivityTest):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              ['low', 'mid', 'high'],
+                                              [(16, 4), (27, 4)],
+                                              list(range(1, 9)), [1],
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='DL',
+                                              transform_precoding=0)
+
+
+class Cellular5GFR2_FrequencySweep_SensitivityTest(Cellular5GFR2SensitivityTest
+                                                   ):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        frequency_sweep_params = self.user_params['sensitivity_test_params'][
+            'frequency_sweep']
+        self.tests = self.generate_test_cases(frequency_sweep_params,
+                                              [(16, 4), (27, 4)],
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='DL',
+                                              transform_precoding=0)
+
+    def generate_test_cases(self, dl_frequency_sweep_params, mcs_pair_list,
+                            **kwargs):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = ['test_load_scpi']
+
+        for band, band_config in dl_frequency_sweep_params.items():
+            for num_dl_cells_str, sweep_config in band_config.items():
+                num_dl_cells = int(num_dl_cells_str[0])
+                num_ul_cells = 1
+                freq_vector = numpy.arange(sweep_config[0], sweep_config[1],
+                                           sweep_config[2])
+                for freq in freq_vector:
+                    for mcs_pair in mcs_pair_list:
+                        test_name = 'test_nr_sensitivity_{}_{}_DL_{}CC_mcs{}'.format(
+                            band, freq, num_dl_cells, mcs_pair[0])
+                        test_params = collections.OrderedDict(
+                            band=band,
+                            channel=freq,
+                            dl_mcs=mcs_pair[0],
+                            ul_mcs=mcs_pair[1],
+                            num_dl_cells=num_dl_cells,
+                            num_ul_cells=num_ul_cells,
+                            dl_cell_list=list(range(1, num_dl_cells + 1)),
+                            ul_cell_list=list(range(1, num_ul_cells + 1)),
+                            **kwargs)
+                        setattr(
+                            self, test_name,
+                            partial(self._test_nr_throughput_bler,
+                                    test_params))
+                        test_cases.append(test_name)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/Cellular5GFR2ThroughputTest.py b/acts_tests/tests/google/cellular/performance/Cellular5GFR2ThroughputTest.py
new file mode 100644
index 0000000..9e848a3
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/Cellular5GFR2ThroughputTest.py
@@ -0,0 +1,664 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import csv
+import itertools
+import json
+import numpy
+import os
+import time
+from acts import asserts
+from acts import context
+from acts import base_test
+from acts import utils
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts.controllers.utils_lib import ssh
+from acts.controllers import iperf_server as ipf
+from acts_contrib.test_utils.cellular.keysight_5g_testapp import Keysight5GTestApp
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+
+from functools import partial
+
+LONG_SLEEP = 10
+MEDIUM_SLEEP = 2
+IPERF_TIMEOUT = 10
+SHORT_SLEEP = 1
+SUBFRAME_LENGTH = 0.001
+STOP_COUNTER_LIMIT = 3
+
+
+class Cellular5GFR2ThroughputTest(base_test.BaseTestClass):
+    """Class to test cellular throughput
+
+    This class implements cellular throughput tests on a lab/callbox setup.
+    The class setups up the callbox in the desired configurations, configures
+    and connects the phone, and runs traffic/iperf throughput.
+    """
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+
+    def setup_class(self):
+        """Initializes common test hardware and parameters.
+
+        This function initializes hardwares and compiles parameters that are
+        common to all tests in this class.
+        """
+        self.dut = self.android_devices[-1]
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.keysight_test_app = Keysight5GTestApp(
+            self.user_params['Keysight5GTestApp'])
+        self.testclass_results = collections.OrderedDict()
+        self.iperf_server = self.iperf_servers[0]
+        self.iperf_client = self.iperf_clients[0]
+        self.remote_server = ssh.connection.SshConnection(
+            ssh.settings.from_config(
+                self.user_params['RemoteServer']['ssh_config']))
+        if self.testclass_params.get('reload_scpi', 1):
+            self.keysight_test_app.import_scpi_file(
+                self.testclass_params['scpi_file'])
+        # Configure test retries
+        self.user_params['retry_tests'] = [self.__class__.__name__]
+
+        # Turn Airplane mode on
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+
+    def teardown_class(self):
+        self.log.info('Turning airplane mode on')
+        try:
+            asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                                'Can not turn on airplane mode.')
+        except:
+            self.log.warning('Cannot perform teardown operations on DUT.')
+        try:
+            self.keysight_test_app.set_cell_state('LTE', 1, 0)
+            self.keysight_test_app.destroy()
+        except:
+            self.log.warning('Cannot perform teardown operations on tester.')
+        self.process_testclass_results()
+
+    def setup_test(self):
+        if self.testclass_params['enable_pixel_logs']:
+            cputils.start_pixel_logger(self.dut)
+
+    def on_retry(self):
+        """Function to control test logic on retried tests.
+
+        This function is automatically executed on tests that are being
+        retried. In this case the function resets wifi, toggles it off and on
+        and sets a retry_flag to enable further tweaking the test logic on
+        second attempts.
+        """
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        if self.keysight_test_app.get_cell_state('LTE', 'CELL1'):
+            self.log.info('Turning LTE off.')
+            self.keysight_test_app.set_cell_state('LTE', 'CELL1', 0)
+
+    def teardown_test(self):
+        self.log.info('Turing airplane mode on')
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        log_path = os.path.join(
+            context.get_current_context().get_full_output_path(), 'pixel_logs')
+        os.makedirs(self.log_path, exist_ok=True)
+        if self.testclass_params['enable_pixel_logs']:
+            cputils.stop_pixel_logger(self.dut, log_path)
+        self.process_testcase_results()
+        self.pass_fail_check()
+
+    def process_testcase_results(self):
+        if self.current_test_name not in self.testclass_results:
+            return
+        testcase_data = self.testclass_results[self.current_test_name]
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(testcase_data),
+                      results_file,
+                      indent=4)
+        testcase_result = testcase_data['results'][0]
+        metric_map = {
+            'min_dl_tput':
+            testcase_result['tput_result']['total']['DL']['min_tput'],
+            'max_dl_tput':
+            testcase_result['tput_result']['total']['DL']['max_tput'],
+            'avg_dl_tput':
+            testcase_result['tput_result']['total']['DL']['average_tput'],
+            'theoretical_dl_tput':
+            testcase_result['tput_result']['total']['DL']['theoretical_tput'],
+            'dl_bler':
+            testcase_result['bler_result']['total']['DL']['nack_ratio'] * 100,
+            'min_dl_tput':
+            testcase_result['tput_result']['total']['UL']['min_tput'],
+            'max_dl_tput':
+            testcase_result['tput_result']['total']['UL']['max_tput'],
+            'avg_dl_tput':
+            testcase_result['tput_result']['total']['UL']['average_tput'],
+            'theoretical_dl_tput':
+            testcase_result['tput_result']['total']['UL']['theoretical_tput'],
+            'ul_bler':
+            testcase_result['bler_result']['total']['UL']['nack_ratio'] * 100,
+            'tcp_udp_tput':
+            testcase_result.get('iperf_throughput', float('nan'))
+        }
+        if self.publish_testcase_metrics:
+            for metric_name, metric_value in metric_map.items():
+                self.testcase_metric_logger.add_metric(metric_name,
+                                                       metric_value)
+
+    def pass_fail_check(self):
+        pass
+
+    def process_testclass_results(self):
+        """Saves CSV with all test results to enable comparison."""
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            'results.csv')
+        with open(results_file_path, 'w', newline='') as csvfile:
+            field_names = [
+                'Band', 'Channel', 'DL Carriers', 'UL Carriers', 'DL MCS',
+                'DL MIMO', 'UL MCS', 'UL MIMO', 'Cell Power',
+                'DL Min. Throughput', 'DL Max. Throughput',
+                'DL Avg. Throughput', 'DL Theoretical Throughput',
+                'UL Min. Throughput', 'UL Max. Throughput',
+                'UL Avg. Throughput', 'UL Theoretical Throughput',
+                'DL BLER (%)', 'UL BLER (%)', 'TCP/UDP Throughput'
+            ]
+            writer = csv.DictWriter(csvfile, fieldnames=field_names)
+            writer.writeheader()
+
+            for testcase_name, testcase_results in self.testclass_results.items(
+            ):
+                for result in testcase_results['results']:
+                    writer.writerow({
+                        'Band':
+                        testcase_results['testcase_params']['band'],
+                        'Channel':
+                        testcase_results['testcase_params']['channel'],
+                        'DL Carriers':
+                        testcase_results['testcase_params']['num_dl_cells'],
+                        'UL Carriers':
+                        testcase_results['testcase_params']['num_ul_cells'],
+                        'DL MCS':
+                        testcase_results['testcase_params']['dl_mcs'],
+                        'DL MIMO':
+                        testcase_results['testcase_params']['dl_mimo_config'],
+                        'UL MCS':
+                        testcase_results['testcase_params']['ul_mcs'],
+                        'UL MIMO':
+                        testcase_results['testcase_params']['ul_mimo_config'],
+                        'Cell Power':
+                        result['cell_power'],
+                        'DL Min. Throughput':
+                        result['tput_result']['total']['DL']['min_tput'],
+                        'DL Max. Throughput':
+                        result['tput_result']['total']['DL']['max_tput'],
+                        'DL Avg. Throughput':
+                        result['tput_result']['total']['DL']['average_tput'],
+                        'DL Theoretical Throughput':
+                        result['tput_result']['total']['DL']
+                        ['theoretical_tput'],
+                        'UL Min. Throughput':
+                        result['tput_result']['total']['UL']['min_tput'],
+                        'UL Max. Throughput':
+                        result['tput_result']['total']['UL']['max_tput'],
+                        'UL Avg. Throughput':
+                        result['tput_result']['total']['UL']['average_tput'],
+                        'UL Theoretical Throughput':
+                        result['tput_result']['total']['UL']
+                        ['theoretical_tput'],
+                        'DL BLER (%)':
+                        result['bler_result']['total']['DL']['nack_ratio'] *
+                        100,
+                        'UL BLER (%)':
+                        result['bler_result']['total']['UL']['nack_ratio'] *
+                        100,
+                        'TCP/UDP Throughput':
+                        result.get('iperf_throughput', 0)
+                    })
+
+    def setup_tester(self, testcase_params):
+        if not self.keysight_test_app.get_cell_state('LTE', 'CELL1'):
+            self.log.info('Turning LTE on.')
+            self.keysight_test_app.set_cell_state('LTE', 'CELL1', 1)
+        self.log.info('Turning off airplane mode')
+        asserts.assert_true(utils.force_airplane_mode(self.dut, False),
+                            'Can not turn on airplane mode.')
+        for cell in testcase_params['dl_cell_list']:
+            self.keysight_test_app.set_cell_band('NR5G', cell,
+                                                 testcase_params['band'])
+            self.keysight_test_app.set_cell_mimo_config(
+                'NR5G', cell, 'DL', testcase_params['dl_mimo_config'])
+            self.keysight_test_app.set_cell_dl_power(
+                'NR5G', cell, testcase_params['cell_power_list'][0], 1)
+        for cell in testcase_params['ul_cell_list']:
+            self.keysight_test_app.set_cell_mimo_config(
+                'NR5G', cell, 'UL', testcase_params['ul_mimo_config'])
+        self.keysight_test_app.configure_contiguous_nr_channels(
+            testcase_params['dl_cell_list'][0], testcase_params['band'],
+            testcase_params['channel'])
+        # Consider configuring schedule quick config
+        self.keysight_test_app.set_nr_cell_schedule_scenario(
+            testcase_params['dl_cell_list'][0],
+            testcase_params['schedule_scenario'])
+        self.keysight_test_app.set_nr_ul_dft_precoding(
+            testcase_params['dl_cell_list'][0],
+            testcase_params['transform_precoding'])
+        self.keysight_test_app.set_nr_cell_mcs(
+            testcase_params['dl_cell_list'][0], testcase_params['dl_mcs'],
+            testcase_params['ul_mcs'])
+        self.keysight_test_app.set_dl_carriers(testcase_params['dl_cell_list'])
+        self.keysight_test_app.set_ul_carriers(testcase_params['ul_cell_list'])
+        self.log.info('Waiting for LTE and applying aggregation')
+        if not self.keysight_test_app.wait_for_cell_status(
+                'LTE', 'CELL1', 'CONN', 60):
+            asserts.fail('DUT did not connect to LTE.')
+        self.keysight_test_app.apply_carrier_agg()
+        self.log.info('Waiting for 5G connection')
+        connected = self.keysight_test_app.wait_for_cell_status(
+            'NR5G', testcase_params['dl_cell_list'][-1], ['ACT', 'CONN'], 60)
+        if not connected:
+            asserts.fail('DUT did not connect to NR.')
+        time.sleep(SHORT_SLEEP)
+
+    def run_iperf_traffic(self, testcase_params):
+        self.iperf_server.start(tag=0)
+        dut_ip = self.dut.droid.connectivityGetIPv4Addresses('rmnet0')[0]
+        if 'iperf_server_address' in self.testclass_params:
+            iperf_server_address = self.testclass_params[
+                'iperf_server_address']
+        elif isinstance(self.iperf_server, ipf.IPerfServerOverAdb):
+            iperf_server_address = dut_ip
+        else:
+            iperf_server_address = wputils.get_server_address(
+                self.remote_server, dut_ip, '255.255.255.0')
+        client_output_path = self.iperf_client.start(
+            iperf_server_address, testcase_params['iperf_args'], 0,
+            self.testclass_params['traffic_duration'] + IPERF_TIMEOUT)
+        server_output_path = self.iperf_server.stop()
+        # Parse and log result
+        if testcase_params['use_client_output']:
+            iperf_file = client_output_path
+        else:
+            iperf_file = server_output_path
+        try:
+            iperf_result = ipf.IPerfResult(iperf_file)
+            current_throughput = numpy.mean(iperf_result.instantaneous_rates[
+                self.testclass_params['iperf_ignored_interval']:-1]) * 8 * (
+                    1.024**2)
+        except:
+            self.log.warning(
+                'ValueError: Cannot get iperf result. Setting to 0')
+            current_throughput = 0
+        return current_throughput
+
+    def _test_nr_throughput_bler(self, testcase_params):
+        """Test function to run cellular throughput and BLER measurements.
+
+        The function runs BLER/throughput measurement after configuring the
+        callbox and DUT. The test supports running PHY or TCP/UDP layer traffic
+        in a variety of band/carrier/mcs/etc configurations.
+
+        Args:
+            testcase_params: dict containing test-specific parameters
+        Returns:
+            result: dict containing throughput results and meta data
+        """
+        testcase_params = self.compile_test_params(testcase_params)
+        testcase_results = collections.OrderedDict()
+        testcase_results['testcase_params'] = testcase_params
+        testcase_results['results'] = []
+        # Setup tester and wait for DUT to connect
+        self.setup_tester(testcase_params)
+        # Run test
+        stop_counter = 0
+        for cell_power in testcase_params['cell_power_list']:
+            result = collections.OrderedDict()
+            result['cell_power'] = cell_power
+            # Set DL cell power
+            for cell in testcase_params['dl_cell_list']:
+                self.keysight_test_app.set_cell_dl_power(
+                    'NR5G', cell, result['cell_power'], 1)
+            self.keysight_test_app.select_display_tab(
+                'NR5G', testcase_params['dl_cell_list'][0], 'BTHR', 'OTAGRAPH')
+            time.sleep(SHORT_SLEEP)
+            # Start BLER and throughput measurements
+            self.keysight_test_app.start_bler_measurement(
+                'NR5G', testcase_params['dl_cell_list'],
+                testcase_params['bler_measurement_length'])
+            if self.testclass_params['traffic_type'] != 'PHY':
+                result['iperf_throughput'] = self.run_iperf_traffic(
+                    testcase_params)
+            if self.testclass_params['log_power_metrics']:
+                if testcase_params[
+                        'bler_measurement_length'] >= 5000 and self.testclass_params[
+                            'traffic_type'] == 'PHY':
+                    time.sleep(testcase_params['bler_measurement_length'] /
+                               1000 - 5)
+                    cputils.log_system_power_metrics(self.dut, verbose=0)
+                else:
+                    self.log.warning('Test too short to log metrics')
+
+            result['bler_result'] = self.keysight_test_app.get_bler_result(
+                'NR5G', testcase_params['dl_cell_list'],
+                testcase_params['bler_measurement_length'])
+            result['tput_result'] = self.keysight_test_app.get_throughput(
+                'NR5G', testcase_params['dl_cell_list'])
+
+            # Print Test Summary
+            self.log.info("Cell Power: {}dBm".format(cell_power))
+            self.log.info(
+                "DL PHY Tput (Mbps):\tMin: {:.2f},\tAvg: {:.2f},\tMax: {:.2f},\tTheoretical: {:.2f}"
+                .format(
+                    result['tput_result']['total']['DL']['min_tput'],
+                    result['tput_result']['total']['DL']['average_tput'],
+                    result['tput_result']['total']['DL']['max_tput'],
+                    result['tput_result']['total']['DL']['theoretical_tput']))
+            self.log.info(
+                "UL PHY Tput (Mbps):\tMin: {:.2f},\tAvg: {:.2f},\tMax: {:.2f},\tTheoretical: {:.2f}"
+                .format(
+                    result['tput_result']['total']['UL']['min_tput'],
+                    result['tput_result']['total']['UL']['average_tput'],
+                    result['tput_result']['total']['UL']['max_tput'],
+                    result['tput_result']['total']['UL']['theoretical_tput']))
+            self.log.info("DL BLER: {:.2f}%\tUL BLER: {:.2f}%".format(
+                result['bler_result']['total']['DL']['nack_ratio'] * 100,
+                result['bler_result']['total']['UL']['nack_ratio'] * 100))
+            testcase_results['results'].append(result)
+            if self.testclass_params['traffic_type'] != 'PHY':
+                self.log.info("{} {} Tput: {:.2f} Mbps".format(
+                    self.testclass_params['traffic_type'],
+                    testcase_params['traffic_direction'],
+                    result['iperf_throughput']))
+
+            if result['bler_result']['total']['DL']['nack_ratio'] * 100 > 99:
+                stop_counter = stop_counter + 1
+            else:
+                stop_counter = 0
+            if stop_counter == STOP_COUNTER_LIMIT:
+                break
+        # Turn off NR cells
+        for cell in testcase_params['dl_cell_list'][::-1]:
+            self.keysight_test_app.set_cell_state('NR5G', cell, 0)
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+
+        # Save results
+        self.testclass_results[self.current_test_name] = testcase_results
+
+    def compile_test_params(self, testcase_params):
+        """Function that completes all test params based on the test name.
+
+        Args:
+            testcase_params: dict containing test-specific parameters
+        """
+        testcase_params['bler_measurement_length'] = int(
+            self.testclass_params['traffic_duration'] / SUBFRAME_LENGTH)
+        testcase_params['cell_power_list'] = numpy.arange(
+            self.testclass_params['cell_power_start'],
+            self.testclass_params['cell_power_stop'],
+            self.testclass_params['cell_power_step'])
+        if self.testclass_params['traffic_type'] == 'PHY':
+            return testcase_params
+        if self.testclass_params['traffic_type'] == 'TCP':
+            testcase_params['iperf_socket_size'] = self.testclass_params.get(
+                'tcp_socket_size', None)
+            testcase_params['iperf_processes'] = self.testclass_params.get(
+                'tcp_processes', 1)
+        elif self.testclass_params['traffic_type'] == 'UDP':
+            testcase_params['iperf_socket_size'] = self.testclass_params.get(
+                'udp_socket_size', None)
+            testcase_params['iperf_processes'] = self.testclass_params.get(
+                'udp_processes', 1)
+        if (testcase_params['traffic_direction'] == 'DL'
+                and not isinstance(self.iperf_server, ipf.IPerfServerOverAdb)
+            ) or (testcase_params['traffic_direction'] == 'UL'
+                  and isinstance(self.iperf_server, ipf.IPerfServerOverAdb)):
+            testcase_params['iperf_args'] = wputils.get_iperf_arg_string(
+                duration=self.testclass_params['traffic_duration'],
+                reverse_direction=1,
+                traffic_type=self.testclass_params['traffic_type'],
+                socket_size=testcase_params['iperf_socket_size'],
+                num_processes=testcase_params['iperf_processes'],
+                udp_throughput=self.testclass_params['UDP_rates'].get(
+                    testcase_params['num_dl_cells'],
+                    self.testclass_params['UDP_rates']["default"]),
+                udp_length=1440)
+            testcase_params['use_client_output'] = True
+        elif (testcase_params['traffic_direction'] == 'UL'
+              and not isinstance(self.iperf_server, ipf.IPerfServerOverAdb)
+              ) or (testcase_params['traffic_direction'] == 'DL'
+                    and isinstance(self.iperf_server, ipf.IPerfServerOverAdb)):
+            testcase_params['iperf_args'] = wputils.get_iperf_arg_string(
+                duration=self.testclass_params['traffic_duration'],
+                reverse_direction=0,
+                traffic_type=self.testclass_params['traffic_type'],
+                socket_size=testcase_params['iperf_socket_size'],
+                num_processes=testcase_params['iperf_processes'],
+                udp_throughput=self.testclass_params['UDP_rates'].get(
+                    testcase_params['num_dl_cells'],
+                    self.testclass_params['UDP_rates']["default"]),
+                udp_length=1440)
+            testcase_params['use_client_output'] = False
+        return testcase_params
+
+    def generate_test_cases(self, bands, channels, mcs_pair_list,
+                            num_dl_cells_list, num_ul_cells_list,
+                            dl_mimo_config, ul_mimo_config, **kwargs):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = ['test_load_scpi']
+
+        for band, channel, num_ul_cells, num_dl_cells, mcs_pair in itertools.product(
+                bands, channels, num_ul_cells_list, num_dl_cells_list,
+                mcs_pair_list):
+            if num_ul_cells > num_dl_cells:
+                continue
+            if channel not in cputils.PCC_PRESET_MAPPING[band]:
+                continue
+            test_name = 'test_nr_throughput_bler_{}_{}_DL_{}CC_mcs{}_{}_UL_{}CC_mcs{}_{}'.format(
+                band, channel, num_dl_cells, mcs_pair[0], dl_mimo_config,
+                num_ul_cells, mcs_pair[1], ul_mimo_config)
+            test_params = collections.OrderedDict(
+                band=band,
+                channel=channel,
+                dl_mcs=mcs_pair[0],
+                ul_mcs=mcs_pair[1],
+                num_dl_cells=num_dl_cells,
+                num_ul_cells=num_ul_cells,
+                dl_mimo_config=dl_mimo_config,
+                ul_mimo_config=ul_mimo_config,
+                dl_cell_list=list(range(1, num_dl_cells + 1)),
+                ul_cell_list=list(range(1, num_ul_cells + 1)),
+                **kwargs)
+            setattr(self, test_name,
+                    partial(self._test_nr_throughput_bler, test_params))
+            test_cases.append(test_name)
+        return test_cases
+
+
+class Cellular5GFR2_DL_ThroughputTest(Cellular5GFR2ThroughputTest):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              ['low', 'mid', 'high'],
+                                              [(16, 4), (27, 4)],
+                                              list(range(1, 9)),
+                                              list(range(1, 3)),
+                                              dl_mimo_config='N2X2',
+                                              ul_mimo_config='N1X1',
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='DL',
+                                              transform_precoding=0)
+
+
+class Cellular5GFR2_CP_UL_ThroughputTest(Cellular5GFR2ThroughputTest):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              ['low', 'mid', 'high'],
+                                              [(4, 16), (4, 27)], [1], [1],
+                                              dl_mimo_config='N2X2',
+                                              ul_mimo_config='N1X1',
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='UL',
+                                              transform_precoding=0)
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [1], [1],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=0))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [2], [2],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=0))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [3], [3],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="UL_RMC",
+                                     traffic_direction='UL',
+                                     transform_precoding=0))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [4], [4],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=0))
+
+
+class Cellular5GFR2_DFTS_UL_ThroughputTest(Cellular5GFR2ThroughputTest):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              ['low', 'mid', 'high'],
+                                              [(4, 16), (4, 27)], [1], [1],
+                                              dl_mimo_config='N2X2',
+                                              ul_mimo_config='N1X1',
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='UL',
+                                              transform_precoding=1)
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [1], [1],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=1))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [2], [2],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=1))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [3], [3],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=1))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [4], [4],
+                                     dl_mimo_config='N2X2',
+                                     ul_mimo_config='N2X2',
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=1))
+
+
+class Cellular5GFR2_DL_FrequecySweep_ThroughputTest(Cellular5GFR2ThroughputTest
+                                                    ):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        dl_frequency_sweep_params = self.user_params['throughput_test_params'][
+            'dl_frequency_sweep']
+        self.tests = self.generate_test_cases(dl_frequency_sweep_params,
+                                              [(16, 4), (27, 4)],
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='DL',
+                                              transform_precoding=0,
+                                              dl_mimo_config='N2X2',
+                                              ul_mimo_config='N1X1')
+
+    def generate_test_cases(self, dl_frequency_sweep_params, mcs_pair_list,
+                            **kwargs):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = ['test_load_scpi']
+
+        for band, band_config in dl_frequency_sweep_params.items():
+            for num_dl_cells_str, sweep_config in band_config.items():
+                num_dl_cells = int(num_dl_cells_str[0])
+                num_ul_cells = 1
+                freq_vector = numpy.arange(sweep_config[0], sweep_config[1],
+                                           sweep_config[2])
+                for freq in freq_vector:
+                    for mcs_pair in mcs_pair_list:
+                        test_name = 'test_nr_throughput_bler_{}_{}MHz_DL_{}CC_mcs{}_UL_{}CC_mcs{}'.format(
+                            band, freq, num_dl_cells, mcs_pair[0],
+                            num_ul_cells, mcs_pair[1])
+                        test_params = collections.OrderedDict(
+                            band=band,
+                            channel=freq,
+                            dl_mcs=mcs_pair[0],
+                            ul_mcs=mcs_pair[1],
+                            num_dl_cells=num_dl_cells,
+                            num_ul_cells=num_ul_cells,
+                            dl_cell_list=list(range(1, num_dl_cells + 1)),
+                            ul_cell_list=list(range(1, num_ul_cells + 1)),
+                            **kwargs)
+                        setattr(
+                            self, test_name,
+                            partial(self._test_nr_throughput_bler,
+                                    test_params))
+                        test_cases.append(test_name)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/CellularFr1SensitivityTest.py b/acts_tests/tests/google/cellular/performance/CellularFr1SensitivityTest.py
new file mode 100644
index 0000000..e483e59
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/CellularFr1SensitivityTest.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import csv
+import itertools
+import numpy
+import json
+import os
+from acts import context
+from acts import base_test
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+from acts_contrib.test_utils.wifi.wifi_performance_test_utils.bokeh_figure import BokehFigure
+from CellularLtePlusFr1PeakThroughputTest import CellularFr1SingleCellPeakThroughputTest
+
+from functools import partial
+
+
+class CellularFr1SensitivityTest(CellularFr1SingleCellPeakThroughputTest):
+    """Class to test single cell FR1 NSA sensitivity"""
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.testclass_params = self.user_params['nr_sensitivity_test_params']
+        self.tests = self.generate_test_cases(
+            channel_list=['LOW', 'MID', 'HIGH'],
+            dl_mcs_list=list(numpy.arange(27, -1, -1)),
+            nr_ul_mcs=4,
+            lte_dl_mcs_table='QAM256',
+            lte_dl_mcs=4,
+            lte_ul_mcs_table='QAM256',
+            lte_ul_mcs=4,
+            transform_precoding=0)
+
+    def process_testclass_results(self):
+        # Plot individual test id results raw data and compile metrics
+        plots = collections.OrderedDict()
+        compiled_data = collections.OrderedDict()
+        for testcase_name, testcase_data in self.testclass_results.items():
+            cell_config = testcase_data['testcase_params'][
+                'endc_combo_config']['cell_list'][1]
+            test_id = tuple(('band', cell_config['band']))
+            if test_id not in plots:
+                # Initialize test id data when not present
+                compiled_data[test_id] = {
+                    'mcs': [],
+                    'average_throughput': [],
+                    'theoretical_throughput': [],
+                    'cell_power': [],
+                }
+                plots[test_id] = BokehFigure(
+                    title='Band {} - BLER Curves'.format(cell_config['band']),
+                    x_label='Cell Power (dBm)',
+                    primary_y_label='BLER (Mbps)')
+                test_id_rvr = test_id + tuple('RvR')
+                plots[test_id_rvr] = BokehFigure(
+                    title='Band {} - RvR'.format(cell_config['band']),
+                    x_label='Cell Power (dBm)',
+                    primary_y_label='PHY Rate (Mbps)')
+            # Compile test id data and metrics
+            compiled_data[test_id]['average_throughput'].append(
+                testcase_data['average_throughput_list'])
+            compiled_data[test_id]['cell_power'].append(
+                testcase_data['cell_power_list'])
+            compiled_data[test_id]['mcs'].append(
+                testcase_data['testcase_params']['nr_dl_mcs'])
+            # Add test id to plots
+            plots[test_id].add_line(
+                testcase_data['cell_power_list'],
+                testcase_data['bler_list'],
+                'MCS {}'.format(testcase_data['testcase_params']['nr_dl_mcs']),
+                width=1)
+            plots[test_id_rvr].add_line(
+                testcase_data['cell_power_list'],
+                testcase_data['average_throughput_list'],
+                'MCS {}'.format(testcase_data['testcase_params']['nr_dl_mcs']),
+                width=1,
+                style='dashed')
+
+        # Compute average RvRs and compute metrics over orientations
+        for test_id, test_data in compiled_data.items():
+            test_id_rvr = test_id + tuple('RvR')
+            cell_power_interp = sorted(set(sum(test_data['cell_power'], [])))
+            average_throughput_interp = []
+            for mcs, cell_power, throughput in zip(
+                    test_data['mcs'], test_data['cell_power'],
+                    test_data['average_throughput']):
+                throughput_interp = numpy.interp(cell_power_interp,
+                                                 cell_power[::-1],
+                                                 throughput[::-1])
+                average_throughput_interp.append(throughput_interp)
+            rvr = numpy.max(average_throughput_interp, 0)
+            plots[test_id_rvr].add_line(cell_power_interp, rvr,
+                                        'Rate vs. Range')
+
+        figure_list = []
+        for plot_id, plot in plots.items():
+            plot.generate_figure()
+            figure_list.append(plot)
+        output_file_path = os.path.join(self.log_path, 'results.html')
+        BokehFigure.save_figures(figure_list, output_file_path)
+
+    def process_testcase_results(self):
+        if self.current_test_name not in self.testclass_results:
+            return
+        testcase_data = self.testclass_results[self.current_test_name]
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(testcase_data),
+                      results_file,
+                      indent=4)
+
+        bler_list = []
+        average_throughput_list = []
+        theoretical_throughput_list = []
+        cell_power_list = testcase_data['testcase_params']['cell_power_sweep'][
+            1]
+        for result in testcase_data['results']:
+            bler_list.append(
+                result['nr_bler_result']['total']['DL']['nack_ratio'])
+            average_throughput_list.append(
+                result['nr_tput_result']['total']['DL']['average_tput'])
+            theoretical_throughput_list.append(
+                result['nr_tput_result']['total']['DL']['theoretical_tput'])
+        padding_len = len(cell_power_list) - len(average_throughput_list)
+        average_throughput_list.extend([0] * padding_len)
+        theoretical_throughput_list.extend([0] * padding_len)
+
+        bler_above_threshold = [
+            bler > self.testclass_params['bler_threshold']
+            for bler in bler_list
+        ]
+        for idx in range(len(bler_above_threshold)):
+            if all(bler_above_threshold[idx:]):
+                sensitivity_idx = max(idx, 1) - 1
+                break
+        else:
+            sensitivity_idx = -1
+        sensitivity = cell_power_list[sensitivity_idx]
+        self.log.info('NR Band {} MCS {} Sensitivity = {}dBm'.format(
+            testcase_data['testcase_params']['endc_combo_config']['cell_list']
+            [1]['band'], testcase_data['testcase_params']['nr_dl_mcs'],
+            sensitivity))
+
+        testcase_data['bler_list'] = bler_list
+        testcase_data['average_throughput_list'] = average_throughput_list
+        testcase_data[
+            'theoretical_throughput_list'] = theoretical_throughput_list
+        testcase_data['cell_power_list'] = cell_power_list
+        testcase_data['sensitivity'] = sensitivity
+
+    def get_per_cell_power_sweeps(self, testcase_params):
+        # get reference test
+        current_band = testcase_params['endc_combo_config']['cell_list'][1][
+            'band']
+        reference_test = None
+        reference_sensitivity = None
+        for testcase_name, testcase_data in self.testclass_results.items():
+            if testcase_data['testcase_params']['endc_combo_config'][
+                    'cell_list'][1]['band'] == current_band:
+                reference_test = testcase_name
+                reference_sensitivity = testcase_data['sensitivity']
+        if reference_test and reference_sensitivity and not self.retry_flag:
+            start_atten = reference_sensitivity + self.testclass_params[
+                'adjacent_mcs_gap']
+            self.log.info(
+                "Reference test {} found. Sensitivity {} dBm. Starting at {} dBm"
+                .format(reference_test, reference_sensitivity, start_atten))
+        else:
+            start_atten = self.testclass_params['nr_cell_power_start']
+            self.log.info(
+                "Reference test not found. Starting at {} dBm".format(
+                    start_atten))
+        # get current cell power start
+        nr_cell_sweep = list(
+            numpy.arange(start_atten,
+                         self.testclass_params['nr_cell_power_stop'],
+                         self.testclass_params['nr_cell_power_step']))
+        lte_sweep = [self.testclass_params['lte_cell_power']
+                     ] * len(nr_cell_sweep)
+        cell_power_sweeps = [lte_sweep, nr_cell_sweep]
+        return cell_power_sweeps
+
+    def generate_test_cases(self, channel_list, dl_mcs_list, **kwargs):
+        test_cases = []
+        with open(self.testclass_params['nr_single_cell_configs'],
+                  'r') as csvfile:
+            test_configs = csv.DictReader(csvfile)
+            for test_config, channel, nr_dl_mcs in itertools.product(
+                    test_configs, channel_list, dl_mcs_list):
+                if int(test_config['skip_test']):
+                    continue
+                endc_combo_config = self.generate_endc_combo_config(
+                    test_config)
+                test_name = 'test_fr1_{}_{}_dl_mcs{}'.format(
+                    test_config['nr_band'], channel.lower(), nr_dl_mcs)
+                test_params = collections.OrderedDict(
+                    endc_combo_config=endc_combo_config,
+                    nr_dl_mcs=nr_dl_mcs,
+                    **kwargs)
+                setattr(self, test_name,
+                        partial(self._test_throughput_bler, test_params))
+                test_cases.append(test_name)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/CellularFr2PeakThroughputTest.py b/acts_tests/tests/google/cellular/performance/CellularFr2PeakThroughputTest.py
new file mode 100644
index 0000000..848832e
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/CellularFr2PeakThroughputTest.py
@@ -0,0 +1,602 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import csv
+import itertools
+import json
+import re
+import numpy
+import os
+from acts import context
+from acts import base_test
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+from acts_contrib.test_utils.cellular.performance.CellularThroughputBaseTest import CellularThroughputBaseTest
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+
+from functools import partial
+
+LONG_SLEEP = 10
+MEDIUM_SLEEP = 2
+IPERF_TIMEOUT = 10
+SHORT_SLEEP = 1
+SUBFRAME_LENGTH = 0.001
+STOP_COUNTER_LIMIT = 3
+
+
+class CellularFr2PeakThroughputTest(CellularThroughputBaseTest):
+    """Base class to test cellular FR2 throughput
+
+    This class implements cellular FR2 throughput tests on a callbox setup.
+    The class setups up the callbox in the desired configurations, configures
+    and connects the phone, and runs traffic/iperf throughput.
+    """
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+
+    def process_testcase_results(self):
+        """Publish test case metrics and save results"""
+        if self.current_test_name not in self.testclass_results:
+            return
+        testcase_data = self.testclass_results[self.current_test_name]
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(testcase_data),
+                      results_file,
+                      indent=4)
+        testcase_result = testcase_data['results'][0]
+        metric_map = {
+            'tcp_udp_tput': testcase_result.get('iperf_throughput',
+                                                float('nan'))
+        }
+        if testcase_data['testcase_params']['endc_combo_config'][
+                'nr_cell_count']:
+            metric_map.update({
+                'nr_min_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']['min_tput'],
+                'nr_max_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']['max_tput'],
+                'nr_avg_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']
+                ['average_tput'],
+                'nr_theoretical_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']
+                ['theoretical_tput'],
+                'nr_dl_bler':
+                testcase_result['nr_bler_result']['total']['DL']['nack_ratio']
+                * 100,
+                'nr_min_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']['min_tput'],
+                'nr_max_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']['max_tput'],
+                'nr_avg_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']
+                ['average_tput'],
+                'nr_theoretical_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']
+                ['theoretical_tput'],
+                'nr_ul_bler':
+                testcase_result['nr_bler_result']['total']['UL']['nack_ratio']
+                * 100
+            })
+        if testcase_data['testcase_params']['endc_combo_config'][
+                'lte_cell_count']:
+            metric_map.update({
+                'lte_min_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']['min_tput'],
+                'lte_max_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']['max_tput'],
+                'lte_avg_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']
+                ['average_tput'],
+                'lte_theoretical_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']
+                ['theoretical_tput'],
+                'lte_dl_bler':
+                testcase_result['lte_bler_result']['total']['DL']['nack_ratio']
+                * 100,
+                'lte_min_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']['min_tput'],
+                'lte_max_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']['max_tput'],
+                'lte_avg_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']
+                ['average_tput'],
+                'lte_theoretical_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']
+                ['theoretical_tput'],
+                'lte_ul_bler':
+                testcase_result['lte_bler_result']['total']['UL']['nack_ratio']
+                * 100
+            })
+        if self.publish_testcase_metrics:
+            for metric_name, metric_value in metric_map.items():
+                self.testcase_metric_logger.add_metric(metric_name,
+                                                       metric_value)
+
+    def process_testclass_results(self):
+        """Saves CSV with all test results to enable comparison."""
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            'results.csv')
+        with open(results_file_path, 'w', newline='') as csvfile:
+            field_names = [
+                'Test Name', 'NR DL Min. Throughput', 'NR DL Max. Throughput',
+                'NR DL Avg. Throughput', 'NR DL Theoretical Throughput',
+                'NR UL Min. Throughput', 'NR UL Max. Throughput',
+                'NR UL Avg. Throughput', 'NR UL Theoretical Throughput',
+                'NR DL BLER (%)', 'NR UL BLER (%)', 'LTE DL Min. Throughput',
+                'LTE DL Max. Throughput', 'LTE DL Avg. Throughput',
+                'LTE DL Theoretical Throughput', 'LTE UL Min. Throughput',
+                'LTE UL Max. Throughput', 'LTE UL Avg. Throughput',
+                'LTE UL Theoretical Throughput', 'LTE DL BLER (%)',
+                'LTE UL BLER (%)', 'TCP/UDP Throughput'
+            ]
+            writer = csv.DictWriter(csvfile, fieldnames=field_names)
+            writer.writeheader()
+
+            for testcase_name, testcase_results in self.testclass_results.items(
+            ):
+                for result in testcase_results['results']:
+                    row_dict = {
+                        'Test Name': testcase_name,
+                        'TCP/UDP Throughput':
+                        result.get('iperf_throughput', 0)
+                    }
+                    if testcase_results['testcase_params'][
+                            'endc_combo_config']['nr_cell_count']:
+                        row_dict.update({
+                            'NR DL Min. Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['min_tput'],
+                            'NR DL Max. Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['max_tput'],
+                            'NR DL Avg. Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['average_tput'],
+                            'NR DL Theoretical Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['theoretical_tput'],
+                            'NR UL Min. Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['min_tput'],
+                            'NR UL Max. Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['max_tput'],
+                            'NR UL Avg. Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['average_tput'],
+                            'NR UL Theoretical Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['theoretical_tput'],
+                            'NR DL BLER (%)':
+                            result['nr_bler_result']['total']['DL']
+                            ['nack_ratio'] * 100,
+                            'NR UL BLER (%)':
+                            result['nr_bler_result']['total']['UL']
+                            ['nack_ratio'] * 100
+                        })
+                    if testcase_results['testcase_params'][
+                            'endc_combo_config']['lte_cell_count']:
+                        row_dict.update({
+                            'LTE DL Min. Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['min_tput'],
+                            'LTE DL Max. Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['max_tput'],
+                            'LTE DL Avg. Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['average_tput'],
+                            'LTE DL Theoretical Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['theoretical_tput'],
+                            'LTE UL Min. Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['min_tput'],
+                            'LTE UL Max. Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['max_tput'],
+                            'LTE UL Avg. Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['average_tput'],
+                            'LTE UL Theoretical Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['theoretical_tput'],
+                            'LTE DL BLER (%)':
+                            result['lte_bler_result']['total']['DL']
+                            ['nack_ratio'] * 100,
+                            'LTE UL BLER (%)':
+                            result['lte_bler_result']['total']['UL']
+                            ['nack_ratio'] * 100
+                        })
+                    writer.writerow(row_dict)
+
+    def get_per_cell_power_sweeps(self, testcase_params):
+        """Function to get per cell power sweep lists
+
+        Args:
+            testcase_params: dict containing all test case params
+        Returns:
+            cell_power_sweeps: list of cell power sweeps for each cell under test
+        """
+        cell_power_sweeps = []
+        for cell in testcase_params['endc_combo_config']['cell_list']:
+            if cell['cell_type'] == 'LTE':
+                sweep = [self.testclass_params['lte_cell_power']]
+            else:
+                sweep = [self.testclass_params['nr_cell_power']]
+            cell_power_sweeps.append(sweep)
+        return cell_power_sweeps
+
+    def generate_endc_combo_config(self, test_config):
+        """Function to generate ENDC combo config from CSV test config
+
+        Args:
+            test_config: dict containing ENDC combo config from CSV
+        Returns:
+            endc_combo_config: dictionary with all ENDC combo settings
+        """
+        endc_combo_config = collections.OrderedDict()
+        cell_config_list = []
+
+        lte_cell_count = 1
+        lte_carriers = [1]
+        lte_scc_list = []
+        endc_combo_config['lte_pcc'] = 1
+        lte_cell = {
+            'cell_type':
+            'LTE',
+            'cell_number':
+            1,
+            'pcc':
+            1,
+            'band':
+            test_config['lte_band'],
+            'dl_bandwidth':
+            test_config['lte_bandwidth'],
+            'ul_enabled':
+            1,
+            'duplex_mode':
+            test_config['lte_duplex_mode'],
+            'dl_mimo_config':
+            'D{nss}U{nss}'.format(nss=test_config['lte_dl_mimo_config']),
+            'ul_mimo_config':
+            'D{nss}U{nss}'.format(nss=test_config['lte_ul_mimo_config']),
+            'transmission_mode':
+            'TM1'
+        }
+        cell_config_list.append(lte_cell)
+
+        nr_cell_count = 0
+        nr_dl_carriers = []
+        nr_ul_carriers = []
+        for nr_cell_idx in range(1, test_config['num_dl_cells'] + 1):
+            nr_cell = {
+                'cell_type':
+                'NR5G',
+                'cell_number':
+                nr_cell_idx,
+                'band':
+                test_config['nr_band'],
+                'duplex_mode':
+                test_config['nr_duplex_mode'],
+                'channel':
+                test_config['nr_channel'],
+                'dl_mimo_config':
+                'N{nss}X{nss}'.format(nss=test_config['nr_dl_mimo_config']),
+                'dl_bandwidth_class':
+                'A',
+                'dl_bandwidth':
+                test_config['nr_bandwidth'],
+                'ul_enabled':
+                1 if nr_cell_idx <= test_config['num_ul_cells'] else 0,
+                'ul_bandwidth_class':
+                'A',
+                'ul_mimo_config':
+                'N{nss}X{nss}'.format(nss=test_config['nr_ul_mimo_config']),
+                'subcarrier_spacing':
+                'MU3'
+            }
+            cell_config_list.append(nr_cell)
+            nr_cell_count = nr_cell_count + 1
+            nr_dl_carriers.append(nr_cell_idx)
+            if nr_cell_idx <= test_config['num_ul_cells']:
+                nr_ul_carriers.append(nr_cell_idx)
+
+        endc_combo_config['lte_cell_count'] = lte_cell_count
+        endc_combo_config['nr_cell_count'] = nr_cell_count
+        endc_combo_config['nr_dl_carriers'] = nr_dl_carriers
+        endc_combo_config['nr_ul_carriers'] = nr_ul_carriers
+        endc_combo_config['cell_list'] = cell_config_list
+        endc_combo_config['lte_scc_list'] = lte_scc_list
+        endc_combo_config['lte_carriers'] = lte_carriers
+        return endc_combo_config
+
+    def generate_test_cases(self, bands, channels, nr_mcs_pair_list,
+                            num_dl_cells_list, num_ul_cells_list,
+                            dl_mimo_config, ul_mimo_config, **kwargs):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = []
+        for band, channel, num_ul_cells, num_dl_cells, nr_mcs_pair in itertools.product(
+                bands, channels, num_ul_cells_list, num_dl_cells_list,
+                nr_mcs_pair_list):
+            if num_ul_cells > num_dl_cells:
+                continue
+            if channel not in cputils.PCC_PRESET_MAPPING[band]:
+                continue
+            test_config = {
+                'lte_band': 2,
+                'lte_bandwidth': 'BW20',
+                'lte_duplex_mode': 'FDD',
+                'lte_dl_mimo_config': 1,
+                'lte_ul_mimo_config': 1,
+                'nr_band': band,
+                'nr_bandwidth': 'BW100',
+                'nr_duplex_mode': 'TDD',
+                'nr_channel': channel,
+                'num_dl_cells': num_dl_cells,
+                'num_ul_cells': num_ul_cells,
+                'nr_dl_mimo_config': dl_mimo_config,
+                'nr_ul_mimo_config': ul_mimo_config
+            }
+            endc_combo_config = self.generate_endc_combo_config(test_config)
+            test_name = 'test_fr2_{}_{}_DL_{}CC_mcs{}_{}x{}_UL_{}CC_mcs{}_{}x{}'.format(
+                band, channel, num_dl_cells, nr_mcs_pair[0], dl_mimo_config,
+                dl_mimo_config, num_ul_cells, nr_mcs_pair[1], ul_mimo_config,
+                ul_mimo_config)
+            test_params = collections.OrderedDict(
+                endc_combo_config=endc_combo_config,
+                nr_dl_mcs=nr_mcs_pair[0],
+                nr_ul_mcs=nr_mcs_pair[1],
+                **kwargs)
+            setattr(self, test_name,
+                    partial(self._test_throughput_bler, test_params))
+            test_cases.append(test_name)
+        return test_cases
+
+
+class CellularFr2DlPeakThroughputTest(CellularFr2PeakThroughputTest):
+    """Base class to test cellular FR2 throughput
+
+    This class implements cellular FR2 throughput tests on a callbox setup.
+    The class setups up the callbox in the desired configurations, configures
+    and connects the phone, and runs traffic/iperf throughput.
+    """
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              ['low', 'mid', 'high'],
+                                              [(16, 4), (27, 4)],
+                                              list(range(1, 9)),
+                                              list(range(1, 3)),
+                                              force_contiguous_nr_channel=True,
+                                              dl_mimo_config=2,
+                                              ul_mimo_config=1,
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='DL',
+                                              transform_precoding=0,
+                                              lte_dl_mcs=4,
+                                              lte_dl_mcs_table='QAM256',
+                                              lte_ul_mcs=4,
+                                              lte_ul_mcs_table='QAM64')
+
+
+class CellularFr2CpOfdmUlPeakThroughputTest(CellularFr2PeakThroughputTest):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              ['low', 'mid', 'high'],
+                                              [(4, 16), (4, 27)], [1], [1],
+                                              force_contiguous_nr_channel=True,
+                                              dl_mimo_config=2,
+                                              ul_mimo_config=1,
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='UL',
+                                              transform_precoding=0,
+                                              lte_dl_mcs=4,
+                                              lte_dl_mcs_table='QAM256',
+                                              lte_ul_mcs=4,
+                                              lte_ul_mcs_table='QAM64')
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [1], [1],
+                                     force_contiguous_nr_channel=True,
+                                     dl_mimo_config=2,
+                                     ul_mimo_config=2,
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=0,
+                                     lte_dl_mcs=4,
+                                     lte_dl_mcs_table='QAM256',
+                                     lte_ul_mcs=4,
+                                     lte_ul_mcs_table='QAM64'))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [2], [2],
+                                     force_contiguous_nr_channel=True,
+                                     dl_mimo_config=2,
+                                     ul_mimo_config=2,
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=0,
+                                     lte_dl_mcs=4,
+                                     lte_dl_mcs_table='QAM256',
+                                     lte_ul_mcs=4,
+                                     lte_ul_mcs_table='QAM64'))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [4], [4],
+                                     force_contiguous_nr_channel=True,
+                                     dl_mimo_config=2,
+                                     ul_mimo_config=2,
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=0,
+                                     lte_dl_mcs=4,
+                                     lte_dl_mcs_table='QAM256',
+                                     lte_ul_mcs=4,
+                                     lte_ul_mcs_table='QAM64'))
+
+
+class CellularFr2DftsOfdmUlPeakThroughputTest(CellularFr2PeakThroughputTest):
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              ['low', 'mid', 'high'],
+                                              [(4, 16), (4, 27)], [1], [1],
+                                              force_contiguous_nr_channel=True,
+                                              dl_mimo_config=2,
+                                              ul_mimo_config=1,
+                                              schedule_scenario="FULL_TPUT",
+                                              traffic_direction='UL',
+                                              transform_precoding=1,
+                                              lte_dl_mcs=4,
+                                              lte_dl_mcs_table='QAM256',
+                                              lte_ul_mcs=4,
+                                              lte_ul_mcs_table='QAM64')
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [1], [1],
+                                     force_contiguous_nr_channel=True,
+                                     dl_mimo_config=2,
+                                     ul_mimo_config=2,
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=1,
+                                     lte_dl_mcs=4,
+                                     lte_dl_mcs_table='QAM256',
+                                     lte_ul_mcs=4,
+                                     lte_ul_mcs_table='QAM64'))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [2], [2],
+                                     force_contiguous_nr_channel=True,
+                                     dl_mimo_config=2,
+                                     ul_mimo_config=2,
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=1,
+                                     lte_dl_mcs=4,
+                                     lte_dl_mcs_table='QAM256',
+                                     lte_ul_mcs=4,
+                                     lte_ul_mcs_table='QAM64'))
+        self.tests.extend(
+            self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                     ['low', 'mid', 'high'],
+                                     [(4, 16), (4, 27)], [4], [4],
+                                     force_contiguous_nr_channel=True,
+                                     dl_mimo_config=2,
+                                     ul_mimo_config=2,
+                                     schedule_scenario="FULL_TPUT",
+                                     traffic_direction='UL',
+                                     transform_precoding=1,
+                                     lte_dl_mcs=4,
+                                     lte_dl_mcs_table='QAM256',
+                                     lte_ul_mcs=4,
+                                     lte_ul_mcs_table='QAM64'))
+
+
+class CellularFr2DlFrequencySweepPeakThroughputTest(
+        CellularFr2PeakThroughputTest):
+    """Base class to test cellular FR2 throughput
+
+    This class implements cellular FR2 throughput tests on a callbox setup.
+    The class setups up the callbox in the desired configurations, configures
+    and connects the phone, and runs traffic/iperf throughput.
+    """
+
+    def __init__(self, controllers):
+        super().__init__(controllers)
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.tests = self.generate_test_cases(
+            ['N257', 'N258', 'N260', 'N261'],
+            self.user_params['throughput_test_params']['frequency_sweep'],
+            [(16, 4), (27, 4)],
+            force_contiguous_nr_channel=False,
+            dl_mimo_config=2,
+            ul_mimo_config=1,
+            schedule_scenario="FULL_TPUT",
+            traffic_direction='DL',
+            transform_precoding=0,
+            lte_dl_mcs=4,
+            lte_dl_mcs_table='QAM256',
+            lte_ul_mcs=4,
+            lte_ul_mcs_table='QAM64')
+
+    def generate_test_cases(self, bands, channels, nr_mcs_pair_list,
+                            num_dl_cells_list, num_ul_cells_list,
+                            dl_mimo_config, ul_mimo_config, **kwargs):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = []
+        for band, channel, num_ul_cells, num_dl_cells, nr_mcs_pair in itertools.product(
+                bands, channels, num_ul_cells_list, num_dl_cells_list,
+                nr_mcs_pair_list):
+            if num_ul_cells > num_dl_cells:
+                continue
+            if channel not in cputils.PCC_PRESET_MAPPING[band]:
+                continue
+            test_config = {
+                'lte_band': 2,
+                'lte_bandwidth': 'BW20',
+                'lte_duplex_mode': 'FDD',
+                'lte_dl_mimo_config': 1,
+                'lte_ul_mimo_config': 1,
+                'nr_band': band,
+                'nr_bandwidth': 'BW100',
+                'nr_duplex_mode': 'TDD',
+                'nr_channel': channel,
+                'num_dl_cells': num_dl_cells,
+                'num_ul_cells': num_ul_cells,
+                'nr_dl_mimo_config': dl_mimo_config,
+                'nr_ul_mimo_config': ul_mimo_config
+            }
+            endc_combo_config = self.generate_endc_combo_config(test_config)
+            test_name = 'test_fr2_{}_{}_DL_{}CC_mcs{}_{}x{}_UL_{}CC_mcs{}_{}x{}'.format(
+                band, channel, num_dl_cells, nr_mcs_pair[0], dl_mimo_config,
+                dl_mimo_config, num_ul_cells, nr_mcs_pair[1], ul_mimo_config,
+                ul_mimo_config)
+            test_params = collections.OrderedDict(
+                endc_combo_config=endc_combo_config,
+                nr_dl_mcs=nr_mcs_pair[0],
+                nr_ul_mcs=nr_mcs_pair[1],
+                **kwargs)
+            setattr(self, test_name,
+                    partial(self._test_throughput_bler, test_params))
+            test_cases.append(test_name)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/CellularFr2SensitivityTest.py b/acts_tests/tests/google/cellular/performance/CellularFr2SensitivityTest.py
new file mode 100644
index 0000000..4176a62
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/CellularFr2SensitivityTest.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import csv
+import itertools
+import numpy
+import json
+import os
+from acts import context
+from acts import base_test
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+from acts_contrib.test_utils.wifi.wifi_performance_test_utils.bokeh_figure import BokehFigure
+from CellularFr2PeakThroughputTest import CellularFr2PeakThroughputTest
+
+from functools import partial
+
+
+class CellularFr2SensitivityTest(CellularFr2PeakThroughputTest):
+    """Class to test single cell FR1 NSA sensitivity"""
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.testclass_params = self.user_params['nr_sensitivity_test_params']
+        self.log.info('Hello')
+        self.tests = self.generate_test_cases(
+            band_list=['N257', 'N258', 'N260', 'N261'],
+            channel_list=['low', 'mid', 'high'],
+            dl_mcs_list=list(numpy.arange(27, -1, -1)),
+            num_dl_cells_list=[1, 2, 4, 8],
+            dl_mimo_config=2,
+            nr_ul_mcs=4,
+            lte_dl_mcs_table='QAM256',
+            lte_dl_mcs=4,
+            lte_ul_mcs_table='QAM256',
+            lte_ul_mcs=4,
+            schedule_scenario="FULL_TPUT",
+            force_contiguous_nr_channel=True,
+            transform_precoding=0)
+
+    def process_testclass_results(self):
+        # Plot individual test id results raw data and compile metrics
+        plots = collections.OrderedDict()
+        compiled_data = collections.OrderedDict()
+        for testcase_name, testcase_data in self.testclass_results.items():
+            cell_config = testcase_data['testcase_params'][
+                'endc_combo_config']['cell_list'][1]
+            test_id = tuple(('band', cell_config['band']))
+            if test_id not in plots:
+                # Initialize test id data when not present
+                compiled_data[test_id] = {
+                    'mcs': [],
+                    'average_throughput': [],
+                    'theoretical_throughput': [],
+                    'cell_power': [],
+                }
+                plots[test_id] = BokehFigure(
+                    title='Band {} - BLER Curves'.format(cell_config['band']),
+                    x_label='Cell Power (dBm)',
+                    primary_y_label='BLER (Mbps)')
+                test_id_rvr = test_id + tuple('RvR')
+                plots[test_id_rvr] = BokehFigure(
+                    title='Band {} - RvR'.format(cell_config['band']),
+                    x_label='Cell Power (dBm)',
+                    primary_y_label='PHY Rate (Mbps)')
+            # Compile test id data and metrics
+            compiled_data[test_id]['average_throughput'].append(
+                testcase_data['average_throughput_list'])
+            compiled_data[test_id]['cell_power'].append(
+                testcase_data['cell_power_list'])
+            compiled_data[test_id]['mcs'].append(
+                testcase_data['testcase_params']['nr_dl_mcs'])
+            # Add test id to plots
+            plots[test_id].add_line(
+                testcase_data['cell_power_list'],
+                testcase_data['bler_list'],
+                'MCS {}'.format(testcase_data['testcase_params']['nr_dl_mcs']),
+                width=1)
+            plots[test_id_rvr].add_line(
+                testcase_data['cell_power_list'],
+                testcase_data['average_throughput_list'],
+                'MCS {}'.format(testcase_data['testcase_params']['nr_dl_mcs']),
+                width=1,
+                style='dashed')
+
+        # Compute average RvRs and compute metrics over orientations
+        for test_id, test_data in compiled_data.items():
+            test_id_rvr = test_id + tuple('RvR')
+            cell_power_interp = sorted(set(sum(test_data['cell_power'], [])))
+            average_throughput_interp = []
+            for mcs, cell_power, throughput in zip(
+                    test_data['mcs'], test_data['cell_power'],
+                    test_data['average_throughput']):
+                throughput_interp = numpy.interp(cell_power_interp,
+                                                 cell_power[::-1],
+                                                 throughput[::-1])
+                average_throughput_interp.append(throughput_interp)
+            rvr = numpy.max(average_throughput_interp, 0)
+            plots[test_id_rvr].add_line(cell_power_interp, rvr,
+                                        'Rate vs. Range')
+
+        figure_list = []
+        for plot_id, plot in plots.items():
+            plot.generate_figure()
+            figure_list.append(plot)
+        output_file_path = os.path.join(self.log_path, 'results.html')
+        BokehFigure.save_figures(figure_list, output_file_path)
+
+    def process_testcase_results(self):
+        if self.current_test_name not in self.testclass_results:
+            return
+        testcase_data = self.testclass_results[self.current_test_name]
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(testcase_data),
+                      results_file,
+                      indent=4)
+
+        bler_list = []
+        average_throughput_list = []
+        theoretical_throughput_list = []
+        cell_power_list = testcase_data['testcase_params']['cell_power_sweep'][
+            1]
+        for result in testcase_data['results']:
+            bler_list.append(
+                result['nr_bler_result']['total']['DL']['nack_ratio'])
+            average_throughput_list.append(
+                result['nr_tput_result']['total']['DL']['average_tput'])
+            theoretical_throughput_list.append(
+                result['nr_tput_result']['total']['DL']['theoretical_tput'])
+        padding_len = len(cell_power_list) - len(average_throughput_list)
+        average_throughput_list.extend([0] * padding_len)
+        theoretical_throughput_list.extend([0] * padding_len)
+
+        bler_above_threshold = [
+            bler > self.testclass_params['bler_threshold']
+            for bler in bler_list
+        ]
+        for idx in range(len(bler_above_threshold)):
+            if all(bler_above_threshold[idx:]):
+                sensitivity_idx = max(idx, 1) - 1
+                break
+        else:
+            sensitivity_idx = -1
+        sensitivity = cell_power_list[sensitivity_idx]
+        self.log.info('NR Band {} MCS {} Sensitivity = {}dBm'.format(
+            testcase_data['testcase_params']['endc_combo_config']['cell_list']
+            [1]['band'], testcase_data['testcase_params']['nr_dl_mcs'],
+            sensitivity))
+
+        testcase_data['bler_list'] = bler_list
+        testcase_data['average_throughput_list'] = average_throughput_list
+        testcase_data[
+            'theoretical_throughput_list'] = theoretical_throughput_list
+        testcase_data['cell_power_list'] = cell_power_list
+        testcase_data['sensitivity'] = sensitivity
+
+    def get_per_cell_power_sweeps(self, testcase_params):
+        # get reference test
+        current_band = testcase_params['endc_combo_config']['cell_list'][1][
+            'band']
+        reference_test = None
+        reference_sensitivity = None
+        for testcase_name, testcase_data in self.testclass_results.items():
+            if testcase_data['testcase_params']['endc_combo_config'][
+                    'cell_list'][1]['band'] == current_band:
+                reference_test = testcase_name
+                reference_sensitivity = testcase_data['sensitivity']
+        if reference_test and reference_sensitivity and not self.retry_flag:
+            start_atten = reference_sensitivity + self.testclass_params[
+                'adjacent_mcs_gap']
+            self.log.info(
+                "Reference test {} found. Sensitivity {} dBm. Starting at {} dBm"
+                .format(reference_test, reference_sensitivity, start_atten))
+        else:
+            start_atten = self.testclass_params['nr_cell_power_start']
+            self.log.info(
+                "Reference test not found. Starting at {} dBm".format(
+                    start_atten))
+        # get current cell power start
+        nr_cell_sweep = list(
+            numpy.arange(start_atten,
+                         self.testclass_params['nr_cell_power_stop'],
+                         self.testclass_params['nr_cell_power_step']))
+        lte_sweep = [self.testclass_params['lte_cell_power']
+                     ] * len(nr_cell_sweep)
+        cell_power_sweeps = [lte_sweep]
+        cell_power_sweeps.extend(
+            [nr_cell_sweep] *
+            testcase_params['endc_combo_config']['nr_cell_count'])
+        return cell_power_sweeps
+
+    def generate_test_cases(self, band_list, channel_list, dl_mcs_list,
+                            num_dl_cells_list, dl_mimo_config, **kwargs):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = []
+        for band, channel, num_dl_cells, nr_dl_mcs in itertools.product(
+                band_list, channel_list, num_dl_cells_list, dl_mcs_list):
+            if channel not in cputils.PCC_PRESET_MAPPING[band]:
+                continue
+            test_config = {
+                'lte_band': 2,
+                'lte_bandwidth': 'BW20',
+                'lte_duplex_mode': 'FDD',
+                'lte_dl_mimo_config': 1,
+                'lte_ul_mimo_config': 1,
+                'nr_band': band,
+                'nr_bandwidth': 'BW100',
+                'nr_duplex_mode': 'TDD',
+                'nr_channel': channel,
+                'num_dl_cells': num_dl_cells,
+                'num_ul_cells': 1,
+                'nr_dl_mimo_config': dl_mimo_config,
+                'nr_ul_mimo_config': 1
+            }
+            endc_combo_config = self.generate_endc_combo_config(test_config)
+            test_name = 'test_fr2_{}_{}_{}CC_mcs{}_{}x{}'.format(
+                band, channel.lower(), num_dl_cells, nr_dl_mcs, dl_mimo_config,
+                dl_mimo_config)
+            test_params = collections.OrderedDict(
+                endc_combo_config=endc_combo_config,
+                nr_dl_mcs=nr_dl_mcs,
+                **kwargs)
+            setattr(self, test_name,
+                    partial(self._test_throughput_bler, test_params))
+            test_cases.append(test_name)
+        self.log.info(test_cases)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/CellularLtePlusFr1PeakThroughputTest.py b/acts_tests/tests/google/cellular/performance/CellularLtePlusFr1PeakThroughputTest.py
new file mode 100644
index 0000000..b422a30
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/CellularLtePlusFr1PeakThroughputTest.py
@@ -0,0 +1,566 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import csv
+import itertools
+import json
+import re
+import os
+from acts import context
+from acts import base_test
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+from acts_contrib.test_utils.cellular.performance.CellularThroughputBaseTest import CellularThroughputBaseTest
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+
+from functools import partial
+
+LONG_SLEEP = 10
+MEDIUM_SLEEP = 2
+IPERF_TIMEOUT = 10
+SHORT_SLEEP = 1
+SUBFRAME_LENGTH = 0.001
+STOP_COUNTER_LIMIT = 3
+
+
+class CellularLtePlusFr1PeakThroughputTest(CellularThroughputBaseTest):
+    """Base class to test cellular LTE and FR1 throughput
+
+    This class implements cellular LTE & FR1 throughput tests on a callbox setup.
+    The class setups up the callbox in the desired configurations, configures
+    and connects the phone, and runs traffic/iperf throughput.
+    """
+
+    def process_testcase_results(self):
+        """Publish test case metrics and save results"""
+        if self.current_test_name not in self.testclass_results:
+            return
+        testcase_data = self.testclass_results[self.current_test_name]
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(testcase_data),
+                      results_file,
+                      indent=4)
+        testcase_result = testcase_data['results'][0]
+        metric_map = {
+            'tcp_udp_tput': testcase_result.get('iperf_throughput',
+                                                float('nan'))
+        }
+        if testcase_data['testcase_params']['endc_combo_config'][
+                'nr_cell_count']:
+            metric_map.update({
+                'nr_min_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']['min_tput'],
+                'nr_max_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']['max_tput'],
+                'nr_avg_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']
+                ['average_tput'],
+                'nr_theoretical_dl_tput':
+                testcase_result['nr_tput_result']['total']['DL']
+                ['theoretical_tput'],
+                'nr_dl_bler':
+                testcase_result['nr_bler_result']['total']['DL']['nack_ratio']
+                * 100,
+                'nr_min_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']['min_tput'],
+                'nr_max_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']['max_tput'],
+                'nr_avg_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']
+                ['average_tput'],
+                'nr_theoretical_dl_tput':
+                testcase_result['nr_tput_result']['total']['UL']
+                ['theoretical_tput'],
+                'nr_ul_bler':
+                testcase_result['nr_bler_result']['total']['UL']['nack_ratio']
+                * 100
+            })
+        if testcase_data['testcase_params']['endc_combo_config'][
+                'lte_cell_count']:
+            metric_map.update({
+                'lte_min_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']['min_tput'],
+                'lte_max_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']['max_tput'],
+                'lte_avg_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']
+                ['average_tput'],
+                'lte_theoretical_dl_tput':
+                testcase_result['lte_tput_result']['total']['DL']
+                ['theoretical_tput'],
+                'lte_dl_bler':
+                testcase_result['lte_bler_result']['total']['DL']['nack_ratio']
+                * 100,
+                'lte_min_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']['min_tput'],
+                'lte_max_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']['max_tput'],
+                'lte_avg_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']
+                ['average_tput'],
+                'lte_theoretical_dl_tput':
+                testcase_result['lte_tput_result']['total']['UL']
+                ['theoretical_tput'],
+                'lte_ul_bler':
+                testcase_result['lte_bler_result']['total']['UL']['nack_ratio']
+                * 100
+            })
+        if self.publish_testcase_metrics:
+            for metric_name, metric_value in metric_map.items():
+                self.testcase_metric_logger.add_metric(metric_name,
+                                                       metric_value)
+
+    def process_testclass_results(self):
+        """Saves CSV with all test results to enable comparison."""
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            'results.csv')
+        with open(results_file_path, 'w', newline='') as csvfile:
+            field_names = [
+                'Test Name', 'NR DL Min. Throughput', 'NR DL Max. Throughput',
+                'NR DL Avg. Throughput', 'NR DL Theoretical Throughput',
+                'NR UL Min. Throughput', 'NR UL Max. Throughput',
+                'NR UL Avg. Throughput', 'NR UL Theoretical Throughput',
+                'NR DL BLER (%)', 'NR UL BLER (%)', 'LTE DL Min. Throughput',
+                'LTE DL Max. Throughput', 'LTE DL Avg. Throughput',
+                'LTE DL Theoretical Throughput', 'LTE UL Min. Throughput',
+                'LTE UL Max. Throughput', 'LTE UL Avg. Throughput',
+                'LTE UL Theoretical Throughput', 'LTE DL BLER (%)',
+                'LTE UL BLER (%)', 'TCP/UDP Throughput'
+            ]
+            writer = csv.DictWriter(csvfile, fieldnames=field_names)
+            writer.writeheader()
+
+            for testcase_name, testcase_results in self.testclass_results.items(
+            ):
+                for result in testcase_results['results']:
+                    row_dict = {
+                        'Test Name': testcase_name,
+                        'TCP/UDP Throughput':
+                        result.get('iperf_throughput', 0)
+                    }
+                    if testcase_results['testcase_params'][
+                            'endc_combo_config']['nr_cell_count']:
+                        row_dict.update({
+                            'NR DL Min. Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['min_tput'],
+                            'NR DL Max. Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['max_tput'],
+                            'NR DL Avg. Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['average_tput'],
+                            'NR DL Theoretical Throughput':
+                            result['nr_tput_result']['total']['DL']
+                            ['theoretical_tput'],
+                            'NR UL Min. Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['min_tput'],
+                            'NR UL Max. Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['max_tput'],
+                            'NR UL Avg. Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['average_tput'],
+                            'NR UL Theoretical Throughput':
+                            result['nr_tput_result']['total']['UL']
+                            ['theoretical_tput'],
+                            'NR DL BLER (%)':
+                            result['nr_bler_result']['total']['DL']
+                            ['nack_ratio'] * 100,
+                            'NR UL BLER (%)':
+                            result['nr_bler_result']['total']['UL']
+                            ['nack_ratio'] * 100
+                        })
+                    if testcase_results['testcase_params'][
+                            'endc_combo_config']['lte_cell_count']:
+                        row_dict.update({
+                            'LTE DL Min. Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['min_tput'],
+                            'LTE DL Max. Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['max_tput'],
+                            'LTE DL Avg. Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['average_tput'],
+                            'LTE DL Theoretical Throughput':
+                            result['lte_tput_result']['total']['DL']
+                            ['theoretical_tput'],
+                            'LTE UL Min. Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['min_tput'],
+                            'LTE UL Max. Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['max_tput'],
+                            'LTE UL Avg. Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['average_tput'],
+                            'LTE UL Theoretical Throughput':
+                            result['lte_tput_result']['total']['UL']
+                            ['theoretical_tput'],
+                            'LTE DL BLER (%)':
+                            result['lte_bler_result']['total']['DL']
+                            ['nack_ratio'] * 100,
+                            'LTE UL BLER (%)':
+                            result['lte_bler_result']['total']['UL']
+                            ['nack_ratio'] * 100
+                        })
+                    writer.writerow(row_dict)
+
+    def get_per_cell_power_sweeps(self, testcase_params):
+        """Function to get per cell power sweep lists
+
+        Args:
+            testcase_params: dict containing all test case params
+        Returns:
+            cell_power_sweeps: list of cell power sweeps for each cell under test
+        """
+        cell_power_sweeps = []
+        for cell in testcase_params['endc_combo_config']['cell_list']:
+            if cell['cell_type'] == 'LTE':
+                sweep = [self.testclass_params['lte_cell_power']]
+            else:
+                sweep = [self.testclass_params['nr_cell_power']]
+            cell_power_sweeps.append(sweep)
+        return cell_power_sweeps
+
+
+class CellularLteFr1EndcPeakThroughputTest(CellularLtePlusFr1PeakThroughputTest
+                                           ):
+    """Class to test cellular LTE/FR1 ENDC combo list"""
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.tests = self.generate_test_cases([(27, 4), (4, 27)],
+                                              lte_dl_mcs_table='QAM256',
+                                              lte_ul_mcs_table='QAM256',
+                                              transform_precoding=0)
+
+    def generate_endc_combo_config(self, endc_combo_str):
+        """Function to generate ENDC combo config from combo string
+
+        Args:
+            endc_combo_str: ENDC combo descriptor (e.g. B48A[4];A[1]+N5A[2];A[1])
+        Returns:
+            endc_combo_config: dictionary with all ENDC combo settings
+        """
+        endc_combo_str = endc_combo_str.replace(' ', '')
+        endc_combo_list = endc_combo_str.split('+')
+        endc_combo_list = [combo.split(';') for combo in endc_combo_list]
+        endc_combo_config = collections.OrderedDict()
+        cell_config_list = list()
+        lte_cell_count = 0
+        nr_cell_count = 0
+        lte_scc_list = []
+        nr_dl_carriers = []
+        nr_ul_carriers = []
+        lte_carriers = []
+
+        for cell in endc_combo_list:
+            cell_config = {}
+            dl_config_str = cell[0]
+            dl_config_regex = re.compile(
+                r'(?P<cell_type>[B,N])(?P<band>[0-9]+)(?P<bandwidth_class>[A-Z])\[(?P<mimo_config>[0-9])\]'
+            )
+            dl_config_match = re.match(dl_config_regex, dl_config_str)
+            if dl_config_match.group('cell_type') == 'B':
+                cell_config['cell_type'] = 'LTE'
+                lte_cell_count = lte_cell_count + 1
+                cell_config['cell_number'] = lte_cell_count
+                if cell_config['cell_number'] == 1:
+                    cell_config['pcc'] = 1
+                    endc_combo_config['lte_pcc'] = cell_config['cell_number']
+                else:
+                    cell_config['pcc'] = 0
+                    lte_scc_list.append(cell_config['cell_number'])
+                cell_config['band'] = dl_config_match.group('band')
+                cell_config['duplex_mode'] = 'FDD' if int(
+                    cell_config['band']
+                ) in cputils.DUPLEX_MODE_TO_BAND_MAPPING['LTE'][
+                    'FDD'] else 'TDD'
+                cell_config['dl_mimo_config'] = 'D{nss}U{nss}'.format(
+                    nss=dl_config_match.group('mimo_config'))
+                if int(dl_config_match.group('mimo_config')) == 1:
+                    cell_config['transmission_mode'] = 'TM1'
+                elif int(dl_config_match.group('mimo_config')) == 2:
+                    cell_config['transmission_mode'] = 'TM2'
+                else:
+                    cell_config['transmission_mode'] = 'TM3'
+                lte_carriers.append(cell_config['cell_number'])
+            else:
+                cell_config['cell_type'] = 'NR5G'
+                nr_cell_count = nr_cell_count + 1
+                cell_config['cell_number'] = nr_cell_count
+                nr_dl_carriers.append(cell_config['cell_number'])
+                cell_config['band'] = 'N' + dl_config_match.group('band')
+                cell_config['duplex_mode'] = 'FDD' if cell_config[
+                    'band'] in cputils.DUPLEX_MODE_TO_BAND_MAPPING['NR5G'][
+                        'FDD'] else 'TDD'
+                cell_config['subcarrier_spacing'] = 'MU0' if cell_config[
+                    'duplex_mode'] == 'FDD' else 'MU1'
+                cell_config['dl_mimo_config'] = 'N{nss}X{nss}'.format(
+                    nss=dl_config_match.group('mimo_config'))
+
+            cell_config['dl_bandwidth_class'] = dl_config_match.group(
+                'bandwidth_class')
+            cell_config['dl_bandwidth'] = 'BW20'
+            cell_config['ul_enabled'] = len(cell) > 1
+            if cell_config['ul_enabled']:
+                ul_config_str = cell[1]
+                ul_config_regex = re.compile(
+                    r'(?P<bandwidth_class>[A-Z])\[(?P<mimo_config>[0-9])\]')
+                ul_config_match = re.match(ul_config_regex, ul_config_str)
+                cell_config['ul_bandwidth_class'] = ul_config_match.group(
+                    'bandwidth_class')
+                cell_config['ul_mimo_config'] = 'N{nss}X{nss}'.format(
+                    nss=ul_config_match.group('mimo_config'))
+                if cell_config['cell_type'] == 'NR5G':
+                    nr_ul_carriers.append(cell_config['cell_number'])
+            cell_config_list.append(cell_config)
+        endc_combo_config['lte_cell_count'] = lte_cell_count
+        endc_combo_config['nr_cell_count'] = nr_cell_count
+        endc_combo_config['nr_dl_carriers'] = nr_dl_carriers
+        endc_combo_config['nr_ul_carriers'] = nr_ul_carriers
+        endc_combo_config['cell_list'] = cell_config_list
+        endc_combo_config['lte_scc_list'] = lte_scc_list
+        endc_combo_config['lte_carriers'] = lte_carriers
+        return endc_combo_config
+
+    def generate_test_cases(self, mcs_pair_list, **kwargs):
+        test_cases = []
+
+        with open(self.testclass_params['endc_combo_file'],
+                  'r') as endc_combos:
+            for endc_combo_str in endc_combos:
+                if endc_combo_str[0] == '#':
+                    continue
+                endc_combo_config = self.generate_endc_combo_config(
+                    endc_combo_str)
+                special_chars = '+[];\n'
+                for char in special_chars:
+                    endc_combo_str = endc_combo_str.replace(char, '_')
+                endc_combo_str = endc_combo_str.replace('__', '_')
+                endc_combo_str = endc_combo_str.strip('_')
+                for mcs_pair in mcs_pair_list:
+                    test_name = 'test_lte_fr1_endc_{}_dl_mcs{}_ul_mcs{}'.format(
+                        endc_combo_str, mcs_pair[0], mcs_pair[1])
+                    test_params = collections.OrderedDict(
+                        endc_combo_config=endc_combo_config,
+                        nr_dl_mcs=mcs_pair[0],
+                        nr_ul_mcs=mcs_pair[1],
+                        lte_dl_mcs=mcs_pair[0],
+                        lte_ul_mcs=mcs_pair[1],
+                        **kwargs)
+                    setattr(self, test_name,
+                            partial(self._test_throughput_bler, test_params))
+                    test_cases.append(test_name)
+        return test_cases
+
+
+class CellularSingleCellThroughputTest(CellularLtePlusFr1PeakThroughputTest):
+    """Base Class to test single cell LTE or LTE/FR1"""
+
+    def generate_endc_combo_config(self, test_config):
+        """Function to generate ENDC combo config from CSV test config
+
+        Args:
+            test_config: dict containing ENDC combo config from CSV
+        Returns:
+            endc_combo_config: dictionary with all ENDC combo settings
+        """
+        endc_combo_config = collections.OrderedDict()
+        lte_cell_count = 0
+        nr_cell_count = 0
+        lte_scc_list = []
+        nr_dl_carriers = []
+        nr_ul_carriers = []
+        lte_carriers = []
+
+        cell_config_list = []
+        if test_config['lte_band']:
+            lte_cell = {
+                'cell_type':
+                'LTE',
+                'cell_number':
+                1,
+                'pcc':
+                1,
+                'band':
+                test_config['lte_band'],
+                'dl_bandwidth':
+                test_config['lte_bandwidth'],
+                'ul_enabled':
+                1,
+                'duplex_mode':
+                test_config['lte_duplex_mode'],
+                'dl_mimo_config':
+                'D{nss}U{nss}'.format(nss=test_config['lte_dl_mimo_config']),
+                'ul_mimo_config':
+                'D{nss}U{nss}'.format(nss=test_config['lte_ul_mimo_config'])
+            }
+            if int(test_config['lte_dl_mimo_config']) == 1:
+                lte_cell['transmission_mode'] = 'TM1'
+            elif int(test_config['lte_dl_mimo_config']) == 2:
+                lte_cell['transmission_mode'] = 'TM2'
+            else:
+                lte_cell['transmission_mode'] = 'TM3'
+            cell_config_list.append(lte_cell)
+            endc_combo_config['lte_pcc'] = 1
+            lte_cell_count = 1
+            lte_carriers = [1]
+
+        if test_config['nr_band']:
+            nr_cell = {
+                'cell_type':
+                'NR5G',
+                'cell_number':
+                1,
+                'band':
+                test_config['nr_band'],
+                'duplex_mode':
+                test_config['nr_duplex_mode'],
+                'dl_mimo_config':
+                'N{nss}X{nss}'.format(nss=test_config['nr_dl_mimo_config']),
+                'dl_bandwidth_class':
+                'A',
+                'dl_bandwidth':
+                test_config['nr_bandwidth'],
+                'ul_enabled':
+                1,
+                'ul_bandwidth_class':
+                'A',
+                'ul_mimo_config':
+                'N{nss}X{nss}'.format(nss=test_config['nr_ul_mimo_config']),
+                'subcarrier_spacing':
+                'MU0' if test_config['nr_scs'] == '15' else 'MU1'
+            }
+            cell_config_list.append(nr_cell)
+            nr_cell_count = 1
+            nr_dl_carriers = [1]
+            nr_ul_carriers = [1]
+
+        endc_combo_config['lte_cell_count'] = lte_cell_count
+        endc_combo_config['nr_cell_count'] = nr_cell_count
+        endc_combo_config['nr_dl_carriers'] = nr_dl_carriers
+        endc_combo_config['nr_ul_carriers'] = nr_ul_carriers
+        endc_combo_config['cell_list'] = cell_config_list
+        endc_combo_config['lte_scc_list'] = lte_scc_list
+        endc_combo_config['lte_carriers'] = lte_carriers
+        return endc_combo_config
+
+
+class CellularFr1SingleCellPeakThroughputTest(CellularSingleCellThroughputTest
+                                              ):
+    """Class to test single cell FR1 NSA mode"""
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.tests = self.generate_test_cases(
+            nr_mcs_pair_list=[(27, 4), (4, 27)],
+            nr_channel_list=['LOW', 'MID', 'HIGH'],
+            transform_precoding=0,
+            lte_dl_mcs=4,
+            lte_dl_mcs_table='QAM256',
+            lte_ul_mcs=4,
+            lte_ul_mcs_table='QAM64')
+
+    def generate_test_cases(self, nr_mcs_pair_list, nr_channel_list, **kwargs):
+
+        test_cases = []
+        with open(self.testclass_params['nr_single_cell_configs'],
+                  'r') as csvfile:
+            test_configs = csv.DictReader(csvfile)
+            for test_config, nr_channel, nr_mcs_pair in itertools.product(
+                    test_configs, nr_channel_list, nr_mcs_pair_list):
+                if int(test_config['skip_test']):
+                    continue
+                endc_combo_config = self.generate_endc_combo_config(
+                    test_config)
+                endc_combo_config['cell_list'][1]['channel'] = nr_channel
+                test_name = 'test_fr1_{}_{}_dl_mcs{}_ul_mcs{}'.format(
+                    test_config['nr_band'], nr_channel.lower(), nr_mcs_pair[0],
+                    nr_mcs_pair[1])
+                test_params = collections.OrderedDict(
+                    endc_combo_config=endc_combo_config,
+                    nr_dl_mcs=nr_mcs_pair[0],
+                    nr_ul_mcs=nr_mcs_pair[1],
+                    **kwargs)
+                setattr(self, test_name,
+                        partial(self._test_throughput_bler, test_params))
+                test_cases.append(test_name)
+        return test_cases
+
+
+class CellularLteSingleCellPeakThroughputTest(CellularSingleCellThroughputTest
+                                              ):
+    """Class to test single cell LTE"""
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.testclass_params = self.user_params['throughput_test_params']
+        self.tests = self.generate_test_cases(lte_mcs_pair_list=[
+            (('QAM256', 27), ('QAM256', 4)), (('QAM256', 4), ('QAM256', 27))
+        ],
+                                              transform_precoding=0)
+
+    def generate_test_cases(self, lte_mcs_pair_list, **kwargs):
+        test_cases = []
+        with open(self.testclass_params['lte_single_cell_configs'],
+                  'r') as csvfile:
+            test_configs = csv.DictReader(csvfile)
+            for test_config, lte_mcs_pair in itertools.product(
+                    test_configs, lte_mcs_pair_list):
+                if int(test_config['skip_test']):
+                    continue
+                endc_combo_config = self.generate_endc_combo_config(
+                    test_config)
+                test_name = 'test_lte_B{}_dl_{}_mcs{}_ul_{}_mcs{}'.format(
+                    test_config['lte_band'], lte_mcs_pair[0][0],
+                    lte_mcs_pair[0][1], lte_mcs_pair[1][0], lte_mcs_pair[1][1])
+                test_params = collections.OrderedDict(
+                    endc_combo_config=endc_combo_config,
+                    lte_dl_mcs_table=lte_mcs_pair[0][0],
+                    lte_dl_mcs=lte_mcs_pair[0][1],
+                    lte_ul_mcs_table=lte_mcs_pair[1][0],
+                    lte_ul_mcs=lte_mcs_pair[1][1],
+                    **kwargs)
+                setattr(self, test_name,
+                        partial(self._test_throughput_bler, test_params))
+                test_cases.append(test_name)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/CellularLteSensitivityTest.py b/acts_tests/tests/google/cellular/performance/CellularLteSensitivityTest.py
new file mode 100644
index 0000000..5e27bca
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/CellularLteSensitivityTest.py
@@ -0,0 +1,232 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import csv
+import itertools
+import numpy
+import json
+import re
+import os
+from acts import context
+from acts import base_test
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+from acts_contrib.test_utils.wifi.wifi_performance_test_utils.bokeh_figure import BokehFigure
+from CellularLtePlusFr1PeakThroughputTest import CellularLteSingleCellPeakThroughputTest
+
+from functools import partial
+
+
+class CellularLteSensitivityTest(CellularLteSingleCellPeakThroughputTest):
+    """Class to test single cell LTE sensitivity"""
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.testclass_params = self.user_params['lte_sensitivity_test_params']
+        self.tests = self.generate_test_cases(dl_mcs_list=list(
+            numpy.arange(27, -1, -1)),
+                                              lte_dl_mcs_table='QAM256',
+                                              lte_ul_mcs_table='QAM256',
+                                              lte_ul_mcs=4,
+                                              transform_precoding=0)
+
+    def process_testclass_results(self):
+        # Plot individual test id results raw data and compile metrics
+        plots = collections.OrderedDict()
+        compiled_data = collections.OrderedDict()
+        for testcase_name, testcase_data in self.testclass_results.items():
+            cell_config = testcase_data['testcase_params'][
+                'endc_combo_config']['cell_list'][0]
+            test_id = tuple(('band', cell_config['band']))
+            if test_id not in plots:
+                # Initialize test id data when not present
+                compiled_data[test_id] = {
+                    'mcs': [],
+                    'average_throughput': [],
+                    'theoretical_throughput': [],
+                    'cell_power': [],
+                }
+                plots[test_id] = BokehFigure(
+                    title='Band {} ({}) - BLER Curves'.format(
+                        cell_config['band'],
+                        testcase_data['testcase_params']['lte_dl_mcs_table']),
+                    x_label='Cell Power (dBm)',
+                    primary_y_label='BLER (Mbps)')
+                test_id_rvr = test_id + tuple('RvR')
+                plots[test_id_rvr] = BokehFigure(
+                    title='Band {} ({}) - RvR'.format(
+                        cell_config['band'],
+                        testcase_data['testcase_params']['lte_dl_mcs_table']),
+                    x_label='Cell Power (dBm)',
+                    primary_y_label='PHY Rate (Mbps)')
+            # Compile test id data and metrics
+            compiled_data[test_id]['average_throughput'].append(
+                testcase_data['average_throughput_list'])
+            compiled_data[test_id]['cell_power'].append(
+                testcase_data['cell_power_list'])
+            compiled_data[test_id]['mcs'].append(
+                testcase_data['testcase_params']['lte_dl_mcs'])
+            # Add test id to plots
+            plots[test_id].add_line(
+                testcase_data['cell_power_list'],
+                testcase_data['bler_list'],
+                'MCS {}'.format(
+                    testcase_data['testcase_params']['lte_dl_mcs']),
+                width=1)
+            plots[test_id_rvr].add_line(
+                testcase_data['cell_power_list'],
+                testcase_data['average_throughput_list'],
+                'MCS {}'.format(
+                    testcase_data['testcase_params']['lte_dl_mcs']),
+                width=1,
+                style='dashed')
+
+        # Compute average RvRs and compute metrics over orientations
+        for test_id, test_data in compiled_data.items():
+            test_id_rvr = test_id + tuple('RvR')
+            cell_power_interp = sorted(set(sum(test_data['cell_power'], [])))
+            average_throughput_interp = []
+            for mcs, cell_power, throughput in zip(
+                    test_data['mcs'], test_data['cell_power'],
+                    test_data['average_throughput']):
+                throughput_interp = numpy.interp(cell_power_interp,
+                                                 cell_power[::-1],
+                                                 throughput[::-1])
+                average_throughput_interp.append(throughput_interp)
+            rvr = numpy.max(average_throughput_interp, 0)
+            plots[test_id_rvr].add_line(cell_power_interp, rvr,
+                                        'Rate vs. Range')
+
+        figure_list = []
+        for plot_id, plot in plots.items():
+            plot.generate_figure()
+            figure_list.append(plot)
+        output_file_path = os.path.join(self.log_path, 'results.html')
+        BokehFigure.save_figures(figure_list, output_file_path)
+
+    def process_testcase_results(self):
+        if self.current_test_name not in self.testclass_results:
+            return
+        testcase_data = self.testclass_results[self.current_test_name]
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(testcase_data),
+                      results_file,
+                      indent=4)
+
+        bler_list = []
+        average_throughput_list = []
+        theoretical_throughput_list = []
+        cell_power_list = testcase_data['testcase_params']['cell_power_sweep'][
+            0]
+        for result in testcase_data['results']:
+            bler_list.append(
+                result['lte_bler_result']['total']['DL']['nack_ratio'])
+            average_throughput_list.append(
+                result['lte_tput_result']['total']['DL']['average_tput'])
+            theoretical_throughput_list.append(
+                result['lte_tput_result']['total']['DL']['theoretical_tput'])
+        padding_len = len(cell_power_list) - len(average_throughput_list)
+        average_throughput_list.extend([0] * padding_len)
+        theoretical_throughput_list.extend([0] * padding_len)
+
+        bler_above_threshold = [
+            bler > self.testclass_params['bler_threshold']
+            for bler in bler_list
+        ]
+        for idx in range(len(bler_above_threshold)):
+            if all(bler_above_threshold[idx:]):
+                sensitivity_idx = max(idx, 1) - 1
+                break
+        else:
+            sensitivity_idx = -1
+        sensitivity = cell_power_list[sensitivity_idx]
+        self.log.info('LTE Band {} Table {} MCS {} Sensitivity = {}dBm'.format(
+            testcase_data['testcase_params']['endc_combo_config']['cell_list']
+            [0]['band'], testcase_data['testcase_params']['lte_dl_mcs_table'],
+            testcase_data['testcase_params']['lte_dl_mcs'], sensitivity))
+
+        testcase_data['bler_list'] = bler_list
+        testcase_data['average_throughput_list'] = average_throughput_list
+        testcase_data[
+            'theoretical_throughput_list'] = theoretical_throughput_list
+        testcase_data['cell_power_list'] = cell_power_list
+        testcase_data['sensitivity'] = sensitivity
+
+    def get_per_cell_power_sweeps(self, testcase_params):
+        # get reference test
+        current_band = testcase_params['endc_combo_config']['cell_list'][0][
+            'band']
+        reference_test = None
+        reference_sensitivity = None
+        for testcase_name, testcase_data in self.testclass_results.items():
+            if testcase_data['testcase_params']['endc_combo_config'][
+                    'cell_list'][0]['band'] == current_band:
+                reference_test = testcase_name
+                reference_sensitivity = testcase_data['sensitivity']
+        if reference_test and reference_sensitivity and not self.retry_flag:
+            start_atten = reference_sensitivity + self.testclass_params[
+                'adjacent_mcs_gap']
+            self.log.info(
+                "Reference test {} found. Sensitivity {} dBm. Starting at {} dBm"
+                .format(reference_test, reference_sensitivity, start_atten))
+        else:
+            start_atten = self.testclass_params['lte_cell_power_start']
+            self.log.info(
+                "Reference test not found. Starting at {} dBm".format(
+                    start_atten))
+        # get current cell power start
+        cell_power_sweeps = [
+            list(
+                numpy.arange(start_atten,
+                             self.testclass_params['lte_cell_power_stop'],
+                             self.testclass_params['lte_cell_power_step']))
+        ]
+        return cell_power_sweeps
+
+    def generate_test_cases(self, dl_mcs_list, lte_dl_mcs_table,
+                            lte_ul_mcs_table, lte_ul_mcs, **kwargs):
+        test_cases = []
+        with open(self.testclass_params['lte_single_cell_configs'],
+                  'r') as csvfile:
+            test_configs = csv.DictReader(csvfile)
+            for test_config, lte_dl_mcs in itertools.product(
+                    test_configs, dl_mcs_list):
+                if int(test_config['skip_test']):
+                    continue
+                endc_combo_config = self.generate_endc_combo_config(
+                    test_config)
+                test_name = 'test_lte_B{}_dl_{}_mcs{}'.format(
+                    test_config['lte_band'], lte_dl_mcs_table, lte_dl_mcs)
+                test_params = collections.OrderedDict(
+                    endc_combo_config=endc_combo_config,
+                    lte_dl_mcs_table=lte_dl_mcs_table,
+                    lte_dl_mcs=lte_dl_mcs,
+                    lte_ul_mcs_table=lte_ul_mcs_table,
+                    lte_ul_mcs=lte_ul_mcs,
+                    **kwargs)
+                setattr(self, test_name,
+                        partial(self._test_throughput_bler, test_params))
+                test_cases.append(test_name)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/CellularRxPowerTest.py b/acts_tests/tests/google/cellular/performance/CellularRxPowerTest.py
new file mode 100644
index 0000000..9615c91
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/CellularRxPowerTest.py
@@ -0,0 +1,233 @@
+#!/usr/bin/env python3.4
+#
+#   Copyright 2022 - 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 collections
+import itertools
+import json
+import numpy
+import os
+import time
+from acts import asserts
+from acts import context
+from acts import base_test
+from acts import utils
+from acts.metrics.loggers.blackbox import BlackboxMappedMetricLogger
+from acts_contrib.test_utils.cellular.keysight_5g_testapp import Keysight5GTestApp
+from acts_contrib.test_utils.cellular.performance import cellular_performance_test_utils as cputils
+from acts_contrib.test_utils.cellular.performance.shannon_log_parser import ShannonLogger
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils as wputils
+from acts_contrib.test_utils.wifi.wifi_performance_test_utils.bokeh_figure import BokehFigure
+from functools import partial
+
+
+class CellularRxPowerTest(base_test.BaseTestClass):
+    """Class to test cellular throughput."""
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.tests = self.generate_test_cases(['N257', 'N258', 'N260', 'N261'],
+                                              list(range(1, 9)))
+
+    def setup_class(self):
+        """Initializes common test hardware and parameters.
+
+        This function initializes hardwares and compiles parameters that are
+        common to all tests in this class.
+        """
+        self.dut = self.android_devices[-1]
+        self.testclass_params = self.user_params['rx_power_params']
+        self.keysight_test_app = Keysight5GTestApp(
+            self.user_params['Keysight5GTestApp'])
+        self.sdm_logger = ShannonLogger(self.dut)
+        self.testclass_results = collections.OrderedDict()
+        # Configure test retries
+        self.user_params['retry_tests'] = [self.__class__.__name__]
+
+        # Turn Airplane mode on
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+
+    def teardown_class(self):
+        self.log.info('Turning airplane mode on')
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        self.keysight_test_app.set_cell_state('LTE', 1, 0)
+        self.keysight_test_app.destroy()
+
+    def setup_test(self):
+        cputils.start_pixel_logger(self.dut)
+
+    def on_retry(self):
+        """Function to control test logic on retried tests.
+
+        This function is automatically executed on tests that are being
+        retried. In this case the function resets wifi, toggles it off and on
+        and sets a retry_flag to enable further tweaking the test logic on
+        second attempts.
+        """
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        if self.keysight_test_app.get_cell_state('LTE', 'CELL1'):
+            self.log.info('Turning LTE off.')
+            self.keysight_test_app.set_cell_state('LTE', 'CELL1', 0)
+
+    def teardown_test(self):
+        self.log.info('Turning airplane mode on')
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        log_path = os.path.join(
+            context.get_current_context().get_full_output_path(), 'pixel_logs')
+        os.makedirs(log_path, exist_ok=True)
+        self.log.info(self.current_test_info)
+        self.testclass_results.setdefault(self.current_test_name,
+                                          collections.OrderedDict())
+        self.testclass_results[self.current_test_name].setdefault(
+            'log_path', [])
+        self.testclass_results[self.current_test_name]['log_path'].append(
+            cputils.stop_pixel_logger(self.dut, log_path))
+        self.process_test_results()
+
+    def process_test_results(self):
+        test_result = self.testclass_results[self.current_test_name]
+
+        # Save output as text file
+        results_file_path = os.path.join(
+            self.log_path, '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(test_result),
+                      results_file,
+                      indent=4)
+        # Plot and save
+        if test_result['log_path']:
+            log_data = self.sdm_logger.process_log(test_result['log_path'][-1])
+        else:
+            return
+        figure = BokehFigure(title=self.current_test_name,
+                             x_label='Cell Power Setting (dBm)',
+                             primary_y_label='Time')
+        figure.add_line(log_data.lte.rsrp_time, log_data.lte.rsrp_rx0,
+                        'LTE RSRP (Rx0)')
+        figure.add_line(log_data.lte.rsrp_time, log_data.lte.rsrp_rx1,
+                        'LTE RSRP (Rx1)')
+        figure.add_line(log_data.lte.rsrp2_time, log_data.lte.rsrp2_rx0,
+                        'LTE RSRP2 (Rx0)')
+        figure.add_line(log_data.lte.rsrp2_time, log_data.lte.rsrp2_rx1,
+                        'LTE RSRP2 (Rx0)')
+        figure.add_line(log_data.nr.rsrp_time, log_data.nr.rsrp_rx0,
+                        'NR RSRP (Rx0)')
+        figure.add_line(log_data.nr.rsrp_time, log_data.nr.rsrp_rx1,
+                        'NR RSRP (Rx1)')
+        figure.add_line(log_data.nr.rsrp2_time, log_data.nr.rsrp2_rx0,
+                        'NR RSRP2 (Rx0)')
+        figure.add_line(log_data.nr.rsrp2_time, log_data.nr.rsrp2_rx1,
+                        'NR RSRP2 (Rx0)')
+        figure.add_line(log_data.fr2.rsrp0_time, log_data.fr2.rsrp0,
+                        'NR RSRP (Rx0)')
+        figure.add_line(log_data.fr2.rsrp1_time, log_data.fr2.rsrp1,
+                        'NR RSRP2 (Rx1)')
+        output_file_path = os.path.join(
+            self.log_path, '{}.html'.format(self.current_test_name))
+        figure.generate_figure(output_file_path)
+
+    def _test_nr_rsrp(self, testcase_params):
+        """Test function to run cellular RSRP tests.
+
+        The function runs a sweep of cell powers while collecting pixel logs
+        for later postprocessing and RSRP analysis.
+
+        Args:
+            testcase_params: dict containing test-specific parameters
+        """
+
+        result = collections.OrderedDict()
+        testcase_params['power_range_vector'] = list(
+            numpy.arange(self.testclass_params['cell_power_start'],
+                         self.testclass_params['cell_power_stop'],
+                         self.testclass_params['cell_power_step']))
+
+        if not self.keysight_test_app.get_cell_state('LTE', 'CELL1'):
+            self.log.info('Turning LTE on.')
+            self.keysight_test_app.set_cell_state('LTE', 'CELL1', 1)
+        self.log.info('Turning off airplane mode')
+        asserts.assert_true(utils.force_airplane_mode(self.dut, False),
+                            'Can not turn on airplane mode.')
+
+        for cell in testcase_params['dl_cell_list']:
+            self.keysight_test_app.set_cell_band('NR5G', cell,
+                                                 testcase_params['band'])
+        # Consider configuring schedule quick config
+        self.keysight_test_app.set_nr_cell_schedule_scenario(
+            testcase_params['dl_cell_list'][0], 'BASIC')
+        self.keysight_test_app.set_dl_carriers(testcase_params['dl_cell_list'])
+        self.keysight_test_app.set_ul_carriers(
+            testcase_params['dl_cell_list'][0])
+        self.log.info('Waiting for LTE and applying aggregation')
+        if not self.keysight_test_app.wait_for_cell_status(
+                'LTE', 'CELL1', 'CONN', 60):
+            asserts.fail('DUT did not connect to LTE.')
+        self.keysight_test_app.apply_carrier_agg()
+        self.log.info('Waiting for 5G connection')
+        connected = self.keysight_test_app.wait_for_cell_status(
+            'NR5G', testcase_params['dl_cell_list'][-1], ['ACT', 'CONN'], 60)
+        if not connected:
+            asserts.fail('DUT did not connect to NR.')
+        for cell_power in testcase_params['power_range_vector']:
+            self.log.info('Setting power to {} dBm'.format(cell_power))
+            for cell in testcase_params['dl_cell_list']:
+                self.keysight_test_app.set_cell_dl_power(
+                    'NR5G', cell, cell_power, True)
+            #measure RSRP
+            self.keysight_test_app.start_nr_rsrp_measurement(
+                testcase_params['dl_cell_list'],
+                self.testclass_params['rsrp_measurement_duration'])
+            time.sleep(self.testclass_params['rsrp_measurement_duration'] *
+                       1.5 / 1000)
+            self.keysight_test_app.get_nr_rsrp_measurement_state(
+                testcase_params['dl_cell_list'])
+            self.keysight_test_app.get_nr_rsrp_measurement_results(
+                testcase_params['dl_cell_list'])
+
+        for cell in testcase_params['dl_cell_list'][::-1]:
+            self.keysight_test_app.set_cell_state('NR5G', cell, 0)
+        asserts.assert_true(utils.force_airplane_mode(self.dut, True),
+                            'Can not turn on airplane mode.')
+        # Save results
+        result['testcase_params'] = testcase_params
+        self.testclass_results[self.current_test_name] = result
+        results_file_path = os.path.join(
+            context.get_current_context().get_full_output_path(),
+            '{}.json'.format(self.current_test_name))
+        with open(results_file_path, 'w') as results_file:
+            json.dump(wputils.serialize_dict(result), results_file, indent=4)
+
+    def generate_test_cases(self, bands, num_cells_list):
+        """Function that auto-generates test cases for a test class."""
+        test_cases = []
+
+        for band, num_cells in itertools.product(bands, num_cells_list):
+            test_name = 'test_nr_rsrp_{}_{}CC'.format(band, num_cells)
+            test_params = collections.OrderedDict(band=band,
+                                                  num_cells=num_cells,
+                                                  dl_cell_list=list(
+                                                      range(1, num_cells + 1)))
+            setattr(self, test_name, partial(self._test_nr_rsrp, test_params))
+            test_cases.append(test_name)
+        return test_cases
diff --git a/acts_tests/tests/google/cellular/performance/__init__.py b/acts_tests/tests/google/cellular/performance/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/acts_tests/tests/google/cellular/performance/__init__.py
diff --git a/acts_tests/tests/google/gnss/FlpTtffTest.py b/acts_tests/tests/google/gnss/FlpTtffTest.py
index 0a30fe8..2d1f194 100644
--- a/acts_tests/tests/google/gnss/FlpTtffTest.py
+++ b/acts_tests/tests/google/gnss/FlpTtffTest.py
@@ -17,7 +17,6 @@
 from acts import asserts
 from acts import signals
 from acts.base_test import BaseTestClass
-from acts.test_decorators import test_tracker_info
 from acts.utils import get_current_epoch_time
 from acts_contrib.test_utils.wifi.wifi_test_utils import wifi_toggle_state
 from acts_contrib.test_utils.tel.tel_logging_utils import start_qxdm_logger
@@ -40,6 +39,8 @@
 from acts_contrib.test_utils.gnss.gnss_test_utils import connect_to_wifi_network
 from acts_contrib.test_utils.gnss.gnss_test_utils import gnss_tracking_via_gtw_gpstool
 from acts_contrib.test_utils.gnss.gnss_test_utils import parse_gtw_gpstool_log
+from acts_contrib.test_utils.gnss.gnss_test_utils import log_current_epoch_time
+from acts_contrib.test_utils.gnss.testtracker_util import log_testtracker_uuid
 
 
 class FlpTtffTest(BaseTestClass):
@@ -63,6 +64,8 @@
         _init_device(self.ad)
 
     def setup_test(self):
+        log_current_epoch_time(self.ad, "test_start_time")
+        log_testtracker_uuid(self.ad, self.current_test_name)
         get_baseband_and_gms_version(self.ad)
         if self.collect_logs:
             clear_logd_gnss_qxdm_log(self.ad)
@@ -85,6 +88,7 @@
             set_wifi_and_bt_scanning(self.ad, True)
         if self.ad.droid.wifiCheckState():
             wifi_toggle_state(self.ad, False)
+        log_current_epoch_time(self.ad, "test_end_time")
 
     def on_pass(self, test_name, begin_time):
         if self.collect_logs:
@@ -106,11 +110,11 @@
         for mode in ttff.keys():
             begin_time = get_current_epoch_time()
             process_gnss_by_gtw_gpstool(
-                self.ad, self.standalone_cs_criteria, type="flp")
+                self.ad, self.standalone_cs_criteria, api_type="flp")
             start_ttff_by_gtw_gpstool(
                 self.ad, ttff_mode=mode, iteration=self.ttff_test_cycle)
             ttff_data = process_ttff_by_gtw_gpstool(
-                self.ad, begin_time, location, type="flp")
+                self.ad, begin_time, location, api_type="flp")
             result = check_ttff_data(self.ad, ttff_data, ttff[mode], criteria)
             flp_results.append(result)
         asserts.assert_true(
@@ -124,7 +128,6 @@
 
     """ Test Cases """
 
-    @test_tracker_info(uuid="c11ada6a-d7ad-4dc8-9d4a-0ae3cb9dfa8e")
     def test_flp_one_hour_tracking(self):
         """Verify FLP tracking performance of position error.
 
@@ -137,10 +140,9 @@
         """
         self.start_qxdm_and_tcpdump_log()
         gnss_tracking_via_gtw_gpstool(self.ad, self.standalone_cs_criteria,
-                                      type="flp", testtime=60)
-        parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, type="flp")
+                                      api_type="flp", testtime=60)
+        parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, api_type="flp")
 
-    @test_tracker_info(uuid="8bc4e82d-fdce-4ee8-af8c-5e4a925b5360")
     def test_flp_ttff_strong_signal_wifiscan_on_wifi_connect(self):
         """Verify FLP TTFF Hot Start and Cold Start under strong GNSS signals
         with WiFi scanning on and connected.
@@ -163,7 +165,6 @@
         self.flp_ttff_hs_and_cs(self.flp_ttff_max_threshold,
                                 self.pixel_lab_location)
 
-    @test_tracker_info(uuid="adc1a0c7-3635-420d-9481-0f5816c58334")
     def test_flp_ttff_strong_signal_wifiscan_on_wifi_not_connect(self):
         """Verify FLP TTFF Hot Start and Cold Start under strong GNSS signals
         with WiFi scanning on and not connected.
@@ -183,7 +184,6 @@
         self.flp_ttff_hs_and_cs(self.flp_ttff_max_threshold,
                                 self.pixel_lab_location)
 
-    @test_tracker_info(uuid="3ec3cee2-b881-4c61-9df1-b6b81fcd4527")
     def test_flp_ttff_strong_signal_wifiscan_off(self):
         """Verify FLP TTFF Hot Start and Cold Start with WiFi scanning OFF
            under strong GNSS signals.
@@ -202,7 +202,6 @@
         self.flp_ttff_hs_and_cs(self.flp_ttff_max_threshold,
                                 self.pixel_lab_location)
 
-    @test_tracker_info(uuid="03c0d34f-8312-48d5-8753-93b09151233a")
     def test_flp_ttff_weak_signal_wifiscan_on_wifi_connect(self):
         """Verify FLP TTFF Hot Start and Cold Start under Weak GNSS signals
         with WiFi scanning on and connected
@@ -228,7 +227,6 @@
         self.flp_ttff_hs_and_cs(self.flp_ttff_max_threshold,
                                 self.pixel_lab_location)
 
-    @test_tracker_info(uuid="13daf7b3-5ac5-4107-b3dc-a3a8b5589fed")
     def test_flp_ttff_weak_signal_wifiscan_on_wifi_not_connect(self):
         """Verify FLP TTFF Hot Start and Cold Start under Weak GNSS signals
         with WiFi scanning on and not connected.
@@ -251,7 +249,6 @@
         self.flp_ttff_hs_and_cs(self.flp_ttff_max_threshold,
                                 self.pixel_lab_location)
 
-    @test_tracker_info(uuid="1831f80f-099f-46d2-b484-f332046d5a4d")
     def test_flp_ttff_weak_signal_wifiscan_off(self):
         """Verify FLP TTFF Hot Start and Cold Start with WiFi scanning OFF
            under weak GNSS signals.
diff --git a/acts_tests/tests/google/gnss/GnssBlankingThTest.py b/acts_tests/tests/google/gnss/GnssBlankingThTest.py
index e625171..171a453 100644
--- a/acts_tests/tests/google/gnss/GnssBlankingThTest.py
+++ b/acts_tests/tests/google/gnss/GnssBlankingThTest.py
@@ -28,12 +28,14 @@
         first_wait = self.user_params.get('first_wait', 300)
 
         # Start the test item with gnss_init_power_setting.
-        if self.gnss_init_power_setting(first_wait):
-            self.log.info('Successfully set the GNSS power level to %d' %
-                          self.sa_sensitivity)
+        ret, pwr_lvl = self.gnss_init_power_setting(first_wait)
+        if ret:
+            self.log.info(f'Successfully set the GNSS power level to {pwr_lvl}')
             self.log.info('Start searching for cellular power level threshold')
             # After the GNSS power initialization is done, start the cellular power sweep.
             self.result_cell_pwr = self.cell_power_sweep()
+        else:
+            raise AttributeError('Init power sweep is missing')
 
     def test_gnss_gsm850_sweep(self):
         """
diff --git a/acts_tests/tests/google/gnss/GnssUserBuildBroadcomConfigurationTest.py b/acts_tests/tests/google/gnss/GnssBroadcomConfigurationTest.py
similarity index 88%
rename from acts_tests/tests/google/gnss/GnssUserBuildBroadcomConfigurationTest.py
rename to acts_tests/tests/google/gnss/GnssBroadcomConfigurationTest.py
index 9f3ccfd..64fe9c8 100644
--- a/acts_tests/tests/google/gnss/GnssUserBuildBroadcomConfigurationTest.py
+++ b/acts_tests/tests/google/gnss/GnssBroadcomConfigurationTest.py
@@ -8,6 +8,7 @@
 For more details, please refer to : go/p22_user_build_verification
 """
 import os
+import re
 import shutil
 import tempfile
 import time
@@ -15,9 +16,10 @@
 from acts import asserts
 from acts import signals
 from acts.base_test import BaseTestClass
+from acts_contrib.test_utils.gnss.testtracker_util import log_testtracker_uuid
 from acts.controllers.adb_lib.error import AdbCommandError
 from acts.libs.proc.job import TimeoutError
-from acts.test_decorators import test_tracker_info
+from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
 
 
@@ -147,7 +149,6 @@
         self.lheconsole = "LheConsole"
         self.lheconsole_hub = self.get_lheconsole_value()
         self.esw_crash_dump_pattern = self.get_esw_crash_dump_pattern()
-        self.ad.log.info(f"here is {self.esw_crash_dump_pattern}")
 
     def _adjust_lhe_setting(self, key, enable):
         """Set lhe setting.
@@ -251,14 +252,16 @@
         return False
 
 
-class GnssUserBuildBroadcomConfigurationTest(BaseTestClass):
-    """ GNSS user build configuration Tests on Broadcom device."""
+class GnssBroadcomConfigurationTest(BaseTestClass):
+    """ GNSS configuration Tests on Broadcom device."""
     def setup_class(self):
         super().setup_class()
         self.ad = self.android_devices[0]
+        req_params = ["standalone_cs_criteria"]
+        self.unpack_userparams(req_param_names=req_params)
 
         if not gutils.check_chipset_vendor_by_qualcomm(self.ad):
-            gutils._init_device(self.ad)
+            self.init_device()
             self.gps_config_path = tempfile.mkdtemp()
             self.gps_xml = GpsXml(self.ad)
             self.lhd_conf = LhdConf(self.ad)
@@ -266,19 +269,31 @@
             self.enable_testing_setting()
             self.backup_gps_config()
 
+    def init_device(self):
+        gutils._init_device(self.ad)
+        gutils.enable_supl_mode(self.ad)
+        gutils.enable_vendor_orbit_assistance_data(self.ad)
+        wutils.wifi_toggle_state(self.ad, True)
+        gutils.set_mobile_data(self.ad, state=True)
+
+
     def teardown_class(self):
         if hasattr(self, "gps_config_path") and os.path.isdir(self.gps_config_path):
             shutil.rmtree(self.gps_config_path)
 
     def setup_test(self):
+        gutils.log_current_epoch_time(self.ad, "test_start_time")
+        log_testtracker_uuid(self.ad, self.current_test_name)
         if gutils.check_chipset_vendor_by_qualcomm(self.ad):
             raise signals.TestSkip("Device is Qualcomm, skip the test")
+        gutils.get_baseband_and_gms_version(self.ad)
         gutils.clear_logd_gnss_qxdm_log(self.ad)
 
     def teardown_test(self):
         if not gutils.check_chipset_vendor_by_qualcomm(self.ad):
             self.revert_gps_config()
             self.ad.reboot()
+        gutils.log_current_epoch_time(self.ad, "test_end_time")
 
     def on_fail(self, test_name, begin_time):
         self.ad.take_bug_report(test_name, begin_time)
@@ -317,10 +332,7 @@
     def run_gps_and_capture_log(self):
         """Enable GPS via gps tool for 15s and capture pixel log"""
         gutils.start_pixel_logger(self.ad)
-        gutils.start_gnss_by_gtw_gpstool(self.ad, state=True)
-        time.sleep(15)
-        gutils.start_gnss_by_gtw_gpstool(self.ad, state=False)
-        gutils.stop_pixel_logger(self.ad)
+        gutils.gnss_tracking_via_gtw_gpstool(self.ad, self.standalone_cs_criteria, testtime=1)
 
     def set_gps_logenabled(self, enable):
         """Set LogEnabled in gps.xml / lhd.conf / scd.conf
@@ -337,44 +349,50 @@
             self.scd_conf.disable_diagnostic_log()
             self.lhd_conf.disable_diagnostic_log()
 
-    @test_tracker_info(uuid="1dd68d9c-38b0-4fbc-8635-1228c72872ff")
     def test_gps_logenabled_setting(self):
         """Verify the LogEnabled setting in gps.xml / scd.conf / lhd.conf
         Steps:
             1. default setting is on in user_debug build
-            2. enable gps for 15s
-            3. assert gps log pattern "slog    :" in pixel logger
+            2. run gps tracking for 1 min
+            3. should find slog in pixel logger log files
             4. disable LogEnabled in all the gps conf
-            5. enable gps for 15s
-            6. assert gps log pattern "slog    :" in pixel logger
+            5. run gps tracking for 1 min
+            6. should not find slog in pixel logger log files
         """
         self.run_gps_and_capture_log()
-        result, _ = gutils.parse_brcm_nmea_log(self.ad, "slog    :", [])
+        pattern = re.compile(f".*slog\s+:.*")
+        result, _ = gutils.parse_brcm_nmea_log(self.ad, pattern, [])
         asserts.assert_true(bool(result), "LogEnabled is set to true, but no gps log was found")
 
         self.set_gps_logenabled(enable=False)
         gutils.clear_logd_gnss_qxdm_log(self.ad)
+        # Removes pixel logger path again in case pixel logger still writes log unexpectedly.
+        gutils.remove_pixel_logger_folder(self.ad)
 
         self.run_gps_and_capture_log()
-        result, _ = gutils.parse_brcm_nmea_log(self.ad, "slog    :", [])
-        asserts.assert_false(bool(result), ("LogEnabled is set to False but still found %d slog",
-                                            len(result)))
+        try:
+            result, _ = gutils.parse_brcm_nmea_log(self.ad, pattern, [])
+            asserts.assert_false(
+                bool(result),
+                ("LogEnabled is set to False but still found %d slog" % len(result)))
+        except FileNotFoundError:
+            self.ad.log.info("Test pass because no BRCM log files/folders was found")
 
-    @test_tracker_info(uuid="152a12e0-7957-47e0-9ea7-14725254fd1d")
     def test_gps_supllogenable_setting(self):
         """Verify SuplLogEnable in gps.xml
         Steps:
             1. default setting is on in user_debug build
             2. remove existing supl log
-            3. enable gps for 15s
+            3. run gps tracking for 1 min
             4. supl log should exist
             5. disable SuplLogEnable in gps.xml
             6. remove existing supl log
-            7. enable gps for 15s
+            7. run gps tracking for 1 min
             8. supl log should not exist
         """
         def is_supl_log_exist_after_supl_request():
             self.gps_xml.remove_supl_logs()
+            self.ad.reboot()
             self.run_gps_and_capture_log()
             return self.gps_xml.is_supl_log_file_exist()
 
@@ -382,12 +400,10 @@
         asserts.assert_true(result, "SuplLogEnable is enable, should find supl log file")
 
         self.gps_xml.disable_supl_log()
-        self.ad.reboot()
 
         result = is_supl_log_exist_after_supl_request()
         asserts.assert_false(result, "SuplLogEnable is disable, should not find supl log file")
 
-    @test_tracker_info(uuid="892d0037-8c0c-45b6-bd0f-9e4073d37232")
     def test_lhe_setting(self):
         """Verify lhefailsafe / lheconsole setting in lhd.conf
         Steps:
diff --git a/acts_tests/tests/google/gnss/GnssConcurrencyTest.py b/acts_tests/tests/google/gnss/GnssConcurrencyTest.py
index 9169f4e..3cbb2a7 100644
--- a/acts_tests/tests/google/gnss/GnssConcurrencyTest.py
+++ b/acts_tests/tests/google/gnss/GnssConcurrencyTest.py
@@ -15,15 +15,17 @@
 #   limitations under the License.
 
 import time
-import datetime
 import re
+import statistics
+from datetime import datetime
 from acts import utils
 from acts import signals
 from acts.base_test import BaseTestClass
-from acts.test_decorators import test_tracker_info
+from acts_contrib.test_utils.gnss.testtracker_util import log_testtracker_uuid
 from acts_contrib.test_utils.tel.tel_logging_utils import start_adb_tcpdump
 from acts_contrib.test_utils.tel.tel_logging_utils import stop_adb_tcpdump
 from acts_contrib.test_utils.tel.tel_logging_utils import get_tcpdump_log
+from acts_contrib.test_utils.tel.tel_test_utils import toggle_airplane_mode
 from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
 
 CONCURRENCY_TYPE = {
@@ -32,6 +34,24 @@
     "ap_location": "reportLocation"
 }
 
+GPS_XML_CONFIG = {
+    "CS": [
+        '    IgnorePosition=\"true\"\n', '    IgnoreEph=\"true\"\n',
+        '    IgnoreTime=\"true\"\n', '    AsstIgnoreLto=\"true\"\n',
+        '    IgnoreJniTime=\"true\"\n'
+    ],
+    "WS": [
+        '    IgnorePosition=\"true\"\n', '    AsstIgnoreLto=\"true\"\n',
+        '    IgnoreJniTime=\"true\"\n'
+    ],
+    "HS": []
+}
+
+ONCHIP_CONFIG = [
+    '    EnableOnChipStopNotification=\"1\"\n',
+    '    EnableOnChipStopNotification=\"2\"\n'
+]
+
 
 class GnssConcurrencyTest(BaseTestClass):
     """ GNSS Concurrency TTFF Tests. """
@@ -42,15 +62,16 @@
         req_params = [
             "standalone_cs_criteria", "chre_tolerate_rate", "qdsp6m_path",
             "outlier_criteria", "max_outliers", "pixel_lab_location",
-            "max_interval", "onchip_interval"
+            "max_interval", "onchip_interval", "ttff_test_cycle"
         ]
         self.unpack_userparams(req_param_names=req_params)
         gutils._init_device(self.ad)
         self.ad.adb.shell("setprop persist.vendor.radio.adb_log_on 0")
         self.ad.adb.shell("sync")
-        gutils.reboot(self.ad)
 
     def setup_test(self):
+        gutils.log_current_epoch_time(self.ad, "test_start_time")
+        log_testtracker_uuid(self.ad, self.current_test_name)
         gutils.clear_logd_gnss_qxdm_log(self.ad)
         gutils.start_pixel_logger(self.ad)
         start_adb_tcpdump(self.ad)
@@ -62,6 +83,7 @@
     def teardown_test(self):
         gutils.stop_pixel_logger(self.ad)
         stop_adb_tcpdump(self.ad)
+        gutils.log_current_epoch_time(self.ad, "test_end_time")
 
     def on_fail(self, test_name, begin_time):
         self.ad.take_bug_report(test_name, begin_time)
@@ -78,8 +100,12 @@
         for _ in range(0, 3):
             try:
                 self.ad.log.info("Start to load the nanoapp")
-                res = self.ad.adb.shell("chre_power_test_client load")
-                if "success: 1" in res:
+                cmd = "chre_power_test_client load"
+                if gutils.is_device_wearable(self.ad):
+                    extra_cmd = "tcm /vendor/etc/chre/power_test_tcm.so"
+                    cmd = " ".join([cmd, extra_cmd])
+                res = self.ad.adb.shell(cmd)
+                if "result 1" in res:
                     self.ad.log.info("Nano app loaded successfully")
                     break
             except Exception as e:
@@ -88,65 +114,79 @@
         else:
             raise signals.TestError("Failed to load CHRE nanoapp")
 
-    def enable_chre(self, freq):
+    def enable_chre(self, interval_sec):
         """ Enable or disable gnss concurrency via nanoapp.
 
         Args:
-            freq: an int for frequency, set 0 as disable.
+            interval_sec: an int for frequency, set 0 as disable.
         """
-        freq = freq * 1000
+        if interval_sec == 0:
+            self.ad.log.info(f"Stop CHRE request")
+        else:
+            self.ad.log.info(
+                f"Initiate CHRE with {interval_sec} seconds interval")
+        interval_msec = interval_sec * 1000
         cmd = "chre_power_test_client"
-        option = "enable %d" % freq if freq != 0 else "disable"
+        option = "enable %d" % interval_msec if interval_msec != 0 else "disable"
 
         for type in CONCURRENCY_TYPE.keys():
             if "ap" not in type:
                 self.ad.adb.shell(" ".join([cmd, type, option]))
 
-    def parse_concurrency_result(self, begin_time, type, criteria):
+    def parse_concurrency_result(self,
+                                 begin_time,
+                                 request_type,
+                                 criteria,
+                                 exam_lower=True):
         """ Parse the test result with given time and criteria.
 
         Args:
             begin_time: test begin time.
-            type: str for location request type.
+            request_type: str for location request type.
             criteria: dictionary for test criteria.
+            exam_lower: a boolean to identify the lower bond or not.
         Return: List for the failure and outlier loops and results.
         """
         results = []
         failures = []
         outliers = []
-        search_results = self.ad.search_logcat(CONCURRENCY_TYPE[type],
+        upper_bound = criteria * (
+            1 + self.chre_tolerate_rate) + self.outlier_criteria
+        lower_bound = criteria * (
+            1 - self.chre_tolerate_rate) - self.outlier_criteria
+        search_results = self.ad.search_logcat(CONCURRENCY_TYPE[request_type],
                                                begin_time)
-        start_time = utils.epoch_to_human_time(begin_time)
-        start_time = datetime.datetime.strptime(start_time,
-                                                "%m-%d-%Y %H:%M:%S ")
         if not search_results:
             raise signals.TestFailure(f"No log entry found for keyword:"
-                                      f"{CONCURRENCY_TYPE[type]}")
-        results.append(
-            (search_results[0]["datetime_obj"] - start_time).total_seconds())
-        samples = len(search_results) - 1
-        for i in range(samples):
+                                      f"{CONCURRENCY_TYPE[request_type]}")
+
+        for i in range(len(search_results) - 1):
             target = search_results[i + 1]
             timedelt = target["datetime_obj"] - search_results[i]["datetime_obj"]
             timedelt_sec = timedelt.total_seconds()
             results.append(timedelt_sec)
-            if timedelt_sec > (criteria *
-                               self.chre_tolerate_rate) + self.outlier_criteria:
-                failures.append(target)
-                self.ad.log.error("[Failure][%s]:%.2f sec" %
-                                  (target["time_stamp"], timedelt_sec))
-            elif timedelt_sec > criteria * self.chre_tolerate_rate:
-                outliers.append(target)
-                self.ad.log.info("[Outlier][%s]:%.2f sec" %
-                                 (target["time_stamp"], timedelt_sec))
+            res_tag = ""
+            if timedelt_sec > upper_bound:
+                failures.append(timedelt_sec)
+                res_tag = "Failure"
+            elif timedelt_sec < lower_bound and exam_lower:
+                failures.append(timedelt_sec)
+                res_tag = "Failure"
+            elif timedelt_sec > criteria * (1 + self.chre_tolerate_rate):
+                outliers.append(timedelt_sec)
+                res_tag = "Outlier"
+            if res_tag:
+                self.ad.log.error(
+                    f"[{res_tag}][{target['time_stamp']}]:{timedelt_sec:.2f} sec"
+                )
 
-        res_summary = " ".join([str(res) for res in results])
-        self.ad.log.info("[%s]Overall Result: %s" % (type, res_summary))
-        self.ad.log.info("TestResult %s_samples %d" % (type, samples))
-        self.ad.log.info("TestResult %s_outliers %d" % (type, len(outliers)))
-        self.ad.log.info("TestResult %s_failures %d" % (type, len(failures)))
-        self.ad.log.info("TestResult %s_max_time %.2f" %
-                         (type, max(results[1:])))
+        res_summary = " ".join([str(res) for res in results[1:]])
+        self.ad.log.info(f"[{request_type}]Overall Result: {res_summary}")
+        log_prefix = f"TestResult {request_type}"
+        self.ad.log.info(f"{log_prefix}_samples {len(search_results)}")
+        self.ad.log.info(f"{log_prefix}_outliers {len(outliers)}")
+        self.ad.log.info(f"{log_prefix}_failures {len(failures)}")
+        self.ad.log.info(f"{log_prefix}_max_time {max(results):.2f}")
 
         return outliers, failures, results
 
@@ -157,12 +197,14 @@
             criteria: int for test criteria.
             test_duration: int for test duration.
         """
-        begin_time = utils.get_current_epoch_time()
-        self.ad.log.info("Tests Start at %s" %
-                         utils.epoch_to_human_time(begin_time))
-        gutils.start_gnss_by_gtw_gpstool(
-            self.ad, True, freq=criteria["ap_location"])
         self.enable_chre(criteria["gnss"])
+        TTFF_criteria = criteria["ap_location"] + self.standalone_cs_criteria
+        gutils.process_gnss_by_gtw_gpstool(
+            self.ad, TTFF_criteria, freq=criteria["ap_location"])
+        self.ad.log.info("Tracking 10 sec to prevent flakiness.")
+        time.sleep(10)
+        begin_time = datetime.now()
+        self.ad.log.info(f"Test Start at {begin_time}")
         time.sleep(test_duration)
         self.enable_chre(0)
         gutils.start_gnss_by_gtw_gpstool(self.ad, False)
@@ -175,9 +217,8 @@
             criteria: int for test criteria.
             test_duration: int for test duration.
         """
-        begin_time = utils.get_current_epoch_time()
-        self.ad.log.info("Tests Start at %s" %
-                         utils.epoch_to_human_time(begin_time))
+        begin_time = datetime.now()
+        self.ad.log.info(f"Test Start at {begin_time}")
         self.enable_chre(criteria["gnss"])
         time.sleep(test_duration)
         self.enable_chre(0)
@@ -199,12 +240,12 @@
             self.ad.log.info("Starting process %s result" % request_type)
             outliers[request_type], failures[request_type], results[
                 request_type] = self.parse_concurrency_result(
-                    begin_time, request_type, criteria)
+                    begin_time, request_type, criteria, exam_lower=False)
             if not results[request_type]:
                 failure_log += "[%s] Fail to find location report.\n" % request_type
             if len(failures[request_type]) > 0:
-                failure_log += "[%s] Test exceeds criteria: %.2f\n" % (
-                    request_type, criteria)
+                failure_log += "[%s] Test exceeds criteria(%.2f): %.2f\n" % (
+                    request_type, criteria, max(failures[request_type]))
             if len(outliers[request_type]) > self.max_outliers:
                 failure_log += "[%s] Outliers excceds max amount: %d\n" % (
                     request_type, len(outliers[request_type]))
@@ -219,7 +260,7 @@
             freq: a list identify source1/2 frequency [freq1, freq2]
         """
         request = {"ap_location": self.max_interval}
-        begin_time = utils.get_current_epoch_time()
+        begin_time = datetime.now()
         self.ad.droid.startLocating(freq[0] * 1000, 0)
         time.sleep(10)
         for i in range(5):
@@ -253,73 +294,183 @@
         self.ad.log.info("TestResult max_position_error %.2f" %
                          max(position_errors))
 
+    def get_chre_ttff(self, interval_sec, duration):
+        """ Get the TTFF for the first CHRE report.
+
+        Args:
+            interval_sec: test interval in seconds for CHRE.
+            duration: test duration.
+        """
+        begin_time = datetime.now()
+        self.ad.log.info(f"Test start at {begin_time}")
+        self.enable_chre(interval_sec)
+        time.sleep(duration)
+        self.enable_chre(0)
+        for type, pattern in CONCURRENCY_TYPE.items():
+            if type == "ap_location":
+                continue
+            search_results = self.ad.search_logcat(pattern, begin_time)
+            if not search_results:
+                raise signals.TestFailure(
+                    f"Unable to receive {type} report in {duration} seconds")
+            else:
+                ttff_stamp = search_results[0]["datetime_obj"]
+                self.ad.log.info(search_results[0]["time_stamp"])
+                ttff = (ttff_stamp - begin_time).total_seconds()
+                self.ad.log.info(f"CHRE {type} TTFF = {ttff}")
+
+    def add_ttff_conf(self, conf_type):
+        """ Add mcu ttff config to gps.xml
+
+        Args:
+            conf_type: a string identify the config type
+        """
+        search_line_tag = "<gll\n"
+        append_line_str = GPS_XML_CONFIG[conf_type]
+        gutils.bcm_gps_xml_update_option(self.ad, "add", search_line_tag,
+                                         append_line_str)
+
+    def update_gps_conf(self, search_line, update_line):
+        """ Update gps.xml content
+
+        Args:
+            search_line: target content
+            update_line: update content
+        """
+        gutils.bcm_gps_xml_update_option(
+            self.ad, "update", search_line, update_txt=update_line)
+
+    def delete_gps_conf(self, conf_type):
+        """ Delete gps.xml content
+
+        Args:
+            conf_type: a string identify the config type
+        """
+        search_line_tag = GPS_XML_CONFIG[conf_type]
+        gutils.bcm_gps_xml_update_option(
+            self.ad, "delete", delete_txt=search_line_tag)
+
+    def preset_mcu_test(self, mode):
+        """ Preseting mcu test with config and device state
+
+        mode:
+            mode: a string identify the test type
+        """
+        self.add_ttff_conf(mode)
+        gutils.push_lhd_overlay(self.ad)
+        toggle_airplane_mode(self.ad.log, self.ad, new_state=True)
+        self.update_gps_conf(ONCHIP_CONFIG[1], ONCHIP_CONFIG[0])
+        gutils.clear_aiding_data_by_gtw_gpstool(self.ad)
+        self.ad.reboot(self.ad)
+        self.load_chre_nanoapp()
+
+    def reset_mcu_test(self, mode):
+        """ Resetting mcu test with config and device state
+
+        mode:
+            mode: a string identify the test type
+        """
+        self.delete_gps_conf(mode)
+        self.update_gps_conf(ONCHIP_CONFIG[0], ONCHIP_CONFIG[1])
+
+    def get_mcu_ttff(self):
+        """ Get mcu ttff seconds
+
+        Return:
+            ttff: a float identify ttff seconds
+        """
+        search_res = ""
+        search_pattern = "$PGLOR,0,FIX"
+        ttff_regex = r"FIX,(.*)\*"
+        cmd_base = "chre_power_test_client gnss tcm"
+        cmd_start = " ".join([cmd_base, "enable 1000"])
+        cmd_stop = " ".join([cmd_base, "disable"])
+        begin_time = datetime.now()
+
+        self.ad.log.info("Send CHRE enable to DUT")
+        self.ad.adb.shell(cmd_start)
+        for i in range(6):
+            search_res = self.ad.search_logcat(search_pattern, begin_time)
+            if search_res:
+                break
+            time.sleep(10)
+        else:
+            self.ad.adb.shell(cmd_stop)
+            self.ad.log.error("Unable to get mcu ttff in 60 seconds")
+            return 60
+        self.ad.adb.shell(cmd_stop)
+
+        res = re.search(ttff_regex, search_res[0]["log_message"])
+        ttff = res.group(1)
+        self.ad.log.info(f"TTFF = {ttff}")
+        return float(ttff)
+
+    def run_mcu_ttff_loops(self, mode, loops):
+        """ Run mcu ttff with given mode and loops
+
+        Args:
+            mode: a string identify mode cs/ws/hs.
+            loops: a int to identify the number of loops
+        """
+        ttff_res = []
+        for i in range(10):
+            ttff = self.get_mcu_ttff()
+            self.ad.log.info(f"{mode} TTFF LOOP{i+1} = {ttff}")
+            ttff_res.append(ttff)
+            time.sleep(10)
+        self.ad.log.info(f"TestResult {mode}_MAX_TTFF {max(ttff_res)}")
+        self.ad.log.info(
+            f"TestResult {mode}_AVG_TTFF {statistics.mean(ttff_res)}")
+
     # Concurrency Test Cases
-    @test_tracker_info(uuid="9b0daebf-461e-4005-9773-d5d10aaeaaa4")
-    def test_gnss_concurrency_ct1(self):
+    def test_gnss_concurrency_location_1_chre_1(self):
         test_duration = 15
         criteria = {"ap_location": 1, "gnss": 1, "gnss_meas": 1}
         self.run_gnss_concurrency_test(criteria, test_duration)
 
-    @test_tracker_info(uuid="f423db2f-12a0-4858-b66f-99e7ca6010c3")
-    def test_gnss_concurrency_ct2(self):
+    def test_gnss_concurrency_location_1_chre_8(self):
         test_duration = 30
         criteria = {"ap_location": 1, "gnss": 8, "gnss_meas": 8}
         self.run_gnss_concurrency_test(criteria, test_duration)
 
-    @test_tracker_info(uuid="f72d2df0-f70a-4a11-9f68-2a38f6974454")
-    def test_gnss_concurrency_ct3(self):
+    def test_gnss_concurrency_location_15_chre_8(self):
         test_duration = 60
         criteria = {"ap_location": 15, "gnss": 8, "gnss_meas": 8}
         self.run_gnss_concurrency_test(criteria, test_duration)
 
-    @test_tracker_info(uuid="8e5563fd-afcd-40d3-9392-7fc0d10f49da")
-    def test_gnss_concurrency_aoc1(self):
+    def test_gnss_concurrency_location_61_chre_1(self):
         test_duration = 120
         criteria = {"ap_location": 61, "gnss": 1, "gnss_meas": 1}
         self.run_gnss_concurrency_test(criteria, test_duration)
 
-    @test_tracker_info(uuid="fb258565-6ac8-4bf7-a554-01d63fc4ef54")
-    def test_gnss_concurrency_aoc2(self):
+    def test_gnss_concurrency_location_61_chre_10(self):
         test_duration = 120
         criteria = {"ap_location": 61, "gnss": 10, "gnss_meas": 10}
         self.run_gnss_concurrency_test(criteria, test_duration)
 
     # CHRE Only Test Cases
-    @test_tracker_info(uuid="cb85fa60-9f1a-4957-b5e3-0f2e5db70b47")
-    def test_gnss_chre1(self):
+    def test_gnss_chre_1(self):
         test_duration = 15
         criteria = {"gnss": 1, "gnss_meas": 1}
         self.run_chre_only_test(criteria, test_duration)
 
-    @test_tracker_info(uuid="6ab17866-0d0e-4d9e-b3af-441d9db0e324")
-    def test_gnss_chre2(self):
+    def test_gnss_chre_8(self):
         test_duration = 30
         criteria = {"gnss": 8, "gnss_meas": 8}
         self.run_chre_only_test(criteria, test_duration)
 
     # Interval tests
-    @test_tracker_info(uuid="53b161e5-335e-44a7-ae2e-eae7464a2b37")
     def test_variable_interval_via_chre(self):
         test_duration = 10
-        intervals = [{
-            "gnss": 0.1,
-            "gnss_meas": 0.1
-        }, {
-            "gnss": 0.5,
-            "gnss_meas": 0.5
-        }, {
-            "gnss": 1.5,
-            "gnss_meas": 1.5
-        }]
+        intervals = [0.1, 0.5, 1.5]
         for interval in intervals:
-            self.run_chre_only_test(interval, test_duration)
+            self.get_chre_ttff(interval, test_duration)
 
-    @test_tracker_info(uuid="ee0a46fe-aa5f-4dfd-9cb7-d4924f9e9cea")
     def test_variable_interval_via_framework(self):
         test_duration = 10
         intervals = [0, 0.5, 1.5]
         for interval in intervals:
-            begin_time = utils.get_current_epoch_time()
+            begin_time = datetime.now()
             self.ad.droid.startLocating(interval * 1000, 0)
             time.sleep(test_duration)
             self.ad.droid.stopLocating()
@@ -327,14 +478,30 @@
             self.parse_concurrency_result(begin_time, "ap_location", criteria)
 
     # Engine switching test
-    @test_tracker_info(uuid="8b42bcb2-cb8c-4ef9-bd98-4fb74a521224")
     def test_gps_engine_switching_host_to_onchip(self):
         self.is_brcm_test()
         freq = [1, self.onchip_interval]
         self.run_engine_switching_test(freq)
 
-    @test_tracker_info(uuid="636041dc-2bd6-4854-aa5d-61c87943d99c")
     def test_gps_engine_switching_onchip_to_host(self):
         self.is_brcm_test()
         freq = [self.onchip_interval, 1]
         self.run_engine_switching_test(freq)
+
+    def test_mcu_cs_ttff(self):
+        mode = "CS"
+        self.preset_mcu_test(mode)
+        self.run_mcu_ttff_loops(mode, self.ttff_test_cycle)
+        self.reset_mcu_test(mode)
+
+    def test_mcu_ws_ttff(self):
+        mode = "WS"
+        self.preset_mcu_test(mode)
+        self.run_mcu_ttff_loops(mode, self.ttff_test_cycle)
+        self.reset_mcu_test(mode)
+
+    def test_mcu_hs_ttff(self):
+        mode = "HS"
+        self.preset_mcu_test(mode)
+        self.run_mcu_ttff_loops(mode, self.ttff_test_cycle)
+        self.reset_mcu_test(mode)
diff --git a/acts_tests/tests/google/gnss/GnssFunctionTest.py b/acts_tests/tests/google/gnss/GnssFunctionTest.py
index d45a997..b22cd4f 100644
--- a/acts_tests/tests/google/gnss/GnssFunctionTest.py
+++ b/acts_tests/tests/google/gnss/GnssFunctionTest.py
@@ -13,18 +13,14 @@
 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
-import time
+
 import os
 import re
 import fnmatch
-from multiprocessing import Process
 
 from acts import asserts
 from acts import signals
 from acts.base_test import BaseTestClass
-from acts.test_decorators import test_tracker_info
-from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
-from acts_contrib.test_utils.tel import tel_test_utils as tutils
 from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
 from acts.utils import get_current_epoch_time
 from acts.utils import unzip_maintain_permissions
@@ -32,9 +28,8 @@
 from acts_contrib.test_utils.tel.tel_bootloader_utils import flash_radio
 from acts_contrib.test_utils.tel.tel_test_utils import verify_internet_connection
 from acts_contrib.test_utils.tel.tel_test_utils import check_call_state_connected_by_adb
-from acts_contrib.test_utils.tel.tel_voice_utils import initiate_call
+from acts_contrib.test_utils.tel.tel_test_utils import toggle_airplane_mode
 from acts_contrib.test_utils.tel.tel_voice_utils import hangup_call
-from acts_contrib.test_utils.tel.tel_data_utils import http_file_download_by_sl4a
 from acts_contrib.test_utils.gnss.gnss_test_utils import get_baseband_and_gms_version
 from acts_contrib.test_utils.gnss.gnss_test_utils import set_attenuator_gnss_signal
 from acts_contrib.test_utils.gnss.gnss_test_utils import _init_device
@@ -49,28 +44,18 @@
 from acts_contrib.test_utils.gnss.gnss_test_utils import launch_google_map
 from acts_contrib.test_utils.gnss.gnss_test_utils import check_location_api
 from acts_contrib.test_utils.gnss.gnss_test_utils import set_battery_saver_mode
-from acts_contrib.test_utils.gnss.gnss_test_utils import kill_xtra_daemon
 from acts_contrib.test_utils.gnss.gnss_test_utils import start_gnss_by_gtw_gpstool
 from acts_contrib.test_utils.gnss.gnss_test_utils import process_gnss_by_gtw_gpstool
-from acts_contrib.test_utils.gnss.gnss_test_utils import start_ttff_by_gtw_gpstool
-from acts_contrib.test_utils.gnss.gnss_test_utils import process_ttff_by_gtw_gpstool
-from acts_contrib.test_utils.gnss.gnss_test_utils import check_ttff_data
-from acts_contrib.test_utils.gnss.gnss_test_utils import start_youtube_video
-from acts_contrib.test_utils.gnss.gnss_test_utils import fastboot_factory_reset
-from acts_contrib.test_utils.gnss.gnss_test_utils import gnss_trigger_modem_ssr_by_mds
-from acts_contrib.test_utils.gnss.gnss_test_utils import disable_supl_mode
 from acts_contrib.test_utils.gnss.gnss_test_utils import connect_to_wifi_network
-from acts_contrib.test_utils.gnss.gnss_test_utils import check_xtra_download
 from acts_contrib.test_utils.gnss.gnss_test_utils import gnss_tracking_via_gtw_gpstool
 from acts_contrib.test_utils.gnss.gnss_test_utils import parse_gtw_gpstool_log
-from acts_contrib.test_utils.gnss.gnss_test_utils import enable_supl_mode
 from acts_contrib.test_utils.gnss.gnss_test_utils import start_toggle_gnss_by_gtw_gpstool
 from acts_contrib.test_utils.gnss.gnss_test_utils import grant_location_permission
 from acts_contrib.test_utils.gnss.gnss_test_utils import is_mobile_data_on
 from acts_contrib.test_utils.gnss.gnss_test_utils import is_wearable_btwifi
-from acts_contrib.test_utils.gnss.gnss_test_utils import delete_lto_file
 from acts_contrib.test_utils.gnss.gnss_test_utils import is_device_wearable
-from acts_contrib.test_utils.tel.tel_logging_utils import start_adb_tcpdump
+from acts_contrib.test_utils.gnss.gnss_test_utils import log_current_epoch_time
+from acts_contrib.test_utils.gnss.testtracker_util import log_testtracker_uuid
 from acts_contrib.test_utils.tel.tel_logging_utils import stop_adb_tcpdump
 from acts_contrib.test_utils.tel.tel_logging_utils import get_tcpdump_log
 
@@ -80,25 +65,20 @@
     def setup_class(self):
         super().setup_class()
         self.ad = self.android_devices[0]
-        req_params = ["pixel_lab_network", "standalone_cs_criteria",
+        req_params = ["pixel_lab_network",
                       "standalone_ws_criteria", "standalone_hs_criteria",
-                      "supl_cs_criteria", "supl_ws_criteria",
-                      "supl_hs_criteria", "xtra_cs_criteria",
-                      "xtra_ws_criteria", "xtra_hs_criteria",
-                      "weak_signal_supl_cs_criteria",
-                      "weak_signal_supl_ws_criteria",
-                      "weak_signal_supl_hs_criteria",
-                      "weak_signal_xtra_cs_criteria",
-                      "weak_signal_xtra_ws_criteria",
-                      "weak_signal_xtra_hs_criteria",
+                      "supl_cs_criteria",
+                      "supl_hs_criteria",
+                      "standalone_cs_criteria",
                       "wearable_reboot_hs_criteria",
                       "default_gnss_signal_attenuation",
                       "weak_gnss_signal_attenuation",
-                      "no_gnss_signal_attenuation", "gnss_init_error_list",
+                      "gnss_init_error_list",
                       "gnss_init_error_allowlist", "pixel_lab_location",
-                      "qdsp6m_path", "supl_capabilities", "ttff_test_cycle",
+                      "qdsp6m_path", "ttff_test_cycle",
                       "collect_logs", "dpo_threshold",
-                      "brcm_error_log_allowlist"]
+                      "brcm_error_log_allowlist", "onchip_interval", "adr_ratio_threshold",
+                      "set_attenuator", "weak_signal_criteria", "weak_signal_cs_criteria"]
         self.unpack_userparams(req_param_names=req_params)
         # create hashmap for SSID
         self.ssid_map = {}
@@ -109,23 +89,35 @@
                           "ws": "Warm Start",
                           "hs": "Hot Start",
                           "csa": "CSWith Assist"}
-        if self.collect_logs and \
-            gutils.check_chipset_vendor_by_qualcomm(self.ad):
+        if self.collect_logs and gutils.check_chipset_vendor_by_qualcomm(self.ad):
             self.flash_new_radio_or_mbn()
             self.push_gnss_cfg()
-        _init_device(self.ad)
+        self.init_device()
+
+    def init_device(self):
+        gutils._init_device(self.ad)
+        gutils.enable_supl_mode(self.ad)
+        gutils.enable_vendor_orbit_assistance_data(self.ad)
+        gutils.disable_ramdump(self.ad)
 
     def setup_test(self):
+        log_current_epoch_time(self.ad, "test_start_time")
+        log_testtracker_uuid(self.ad, self.current_test_name)
         get_baseband_and_gms_version(self.ad)
         if self.collect_logs:
             clear_logd_gnss_qxdm_log(self.ad)
+        if self.set_attenuator:
             set_attenuator_gnss_signal(self.ad, self.attenuators,
                                        self.default_gnss_signal_attenuation)
         # TODO (b/202101058:chenstanley): Need to double check how to disable wifi successfully in wearable projects.
         if is_wearable_btwifi(self.ad):
             wifi_toggle_state(self.ad, True)
             connect_to_wifi_network(
-            self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+                self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+        else:
+            wifi_toggle_state(self.ad, False)
+            set_mobile_data(self.ad, True)
+
         if not verify_internet_connection(self.ad.log, self.ad, retries=3,
                                           expected_state=True):
             raise signals.TestFailure("Fail to connect to LTE network.")
@@ -134,6 +126,7 @@
         if self.collect_logs:
             gutils.stop_pixel_logger(self.ad)
             stop_adb_tcpdump(self.ad)
+        if self.set_attenuator:
             set_attenuator_gnss_signal(self.ad, self.attenuators,
                                        self.default_gnss_signal_attenuation)
         # TODO(chenstanley): sim structure issue
@@ -142,17 +135,11 @@
                 hangup_call(self.ad.log, self.ad)
         if self.ad.droid.connectivityCheckAirplaneMode():
             self.ad.log.info("Force airplane mode off")
-            self.ad.droid.connectivityToggleAirplaneMode(False)
-        if not is_wearable_btwifi and self.ad.droid.wifiCheckState():
-            wifi_toggle_state(self.ad, False)
-        if not is_mobile_data_on(self.ad):
-            set_mobile_data(self.ad, True)
+            toggle_airplane_mode(self.ad.log, self.ad, new_state=False)
         if int(self.ad.adb.shell(
             "settings get global wifi_scan_always_enabled")) != 1:
             set_wifi_and_bt_scanning(self.ad, True)
-        if not verify_internet_connection(self.ad.log, self.ad, retries=3,
-                                          expected_state=True):
-            raise signals.TestFailure("Fail to connect to LTE network.")
+        log_current_epoch_time(self.ad, "test_end_time")
 
     def on_fail(self, test_name, begin_time):
         if self.collect_logs:
@@ -248,41 +235,6 @@
                 self.ad.log.error("cat mcfg.version with error %s", e)
                 return False
 
-    def run_ttff_via_gtw_gpstool(self, mode, criteria):
-        """Run GNSS TTFF test with selected mode and parse the results.
-
-        Args:
-            mode: "cs", "ws" or "hs"
-            criteria: Criteria for the TTFF.
-        """
-        begin_time = get_current_epoch_time()
-        process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-        start_ttff_by_gtw_gpstool(self.ad, mode, self.ttff_test_cycle)
-        ttff_data = process_ttff_by_gtw_gpstool(
-            self.ad, begin_time, self.pixel_lab_location)
-        result = check_ttff_data(
-            self.ad, ttff_data, self.ttff_mode.get(mode), criteria)
-        asserts.assert_true(
-            result, "TTFF %s fails to reach designated criteria of %d "
-                    "seconds." % (self.ttff_mode.get(mode), criteria))
-
-    def start_qxdm_and_tcpdump_log(self):
-        """Start QXDM and adb tcpdump if collect_logs is True."""
-        if self.collect_logs:
-            gutils.start_pixel_logger(self.ad)
-            start_adb_tcpdump(self.ad)
-
-    def supl_ttff_with_sim(self, mode, criteria):
-        """Verify SUPL TTFF functionality.
-
-        Args:
-            mode: "cs", "ws" or "hs"
-            criteria: Criteria for the test.
-        """
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.run_ttff_via_gtw_gpstool(mode, criteria)
-
     def standalone_ttff_airplane_mode_on(self, mode, criteria):
         """Verify Standalone GNSS TTFF functionality while airplane mode is on.
 
@@ -290,92 +242,79 @@
             mode: "cs", "ws" or "hs"
             criteria: Criteria for the test.
         """
-        self.start_qxdm_and_tcpdump_log()
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
         self.ad.log.info("Turn airplane mode on")
-        self.ad.droid.connectivityToggleAirplaneMode(True)
-        self.run_ttff_via_gtw_gpstool(mode, criteria)
-
-    def supl_ttff_weak_gnss_signal(self, mode, criteria):
-        """Verify SUPL TTFF functionality under weak GNSS signal.
-
-        Args:
-            mode: "cs", "ws" or "hs"
-            criteria: Criteria for the test.
-        """
-        set_attenuator_gnss_signal(self.ad, self.attenuators,
-                                   self.weak_gnss_signal_attenuation)
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.run_ttff_via_gtw_gpstool(mode, criteria)
-
-    def xtra_ttff_mobile_data(self, mode, criteria):
-        """Verify XTRA\LTO TTFF functionality with mobile data.
-
-        Args:
-            mode: "cs", "ws" or "hs"
-            criteria: Criteria for the test.
-        """
-        disable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.run_ttff_via_gtw_gpstool(mode, criteria)
-
-    def xtra_ttff_weak_gnss_signal(self, mode, criteria):
-        """Verify XTRA\LTO TTFF functionality under weak GNSS signal.
-
-        Args:
-            mode: "cs", "ws" or "hs"
-            criteria: Criteria for the test.
-        """
-        set_attenuator_gnss_signal(self.ad, self.attenuators,
-                                   self.weak_gnss_signal_attenuation)
-        disable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.run_ttff_via_gtw_gpstool(mode, criteria)
-
-    def xtra_ttff_wifi(self, mode, criteria):
-        """Verify XTRA\LTO TTFF functionality with WiFi.
-
-        Args:
-            mode: "cs", "ws" or "hs"
-            criteria: Criteria for the test.
-        """
-        disable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.ad.log.info("Turn airplane mode on")
-        self.ad.droid.connectivityToggleAirplaneMode(True)
-        wifi_toggle_state(self.ad, True)
-        connect_to_wifi_network(
-            self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
-        self.run_ttff_via_gtw_gpstool(mode, criteria)
-
-    def ttff_with_assist(self, mode, criteria):
-        """Verify CS/WS TTFF functionality with Assist data.
-
-        Args:
-            mode: "csa" or "ws"
-            criteria: Criteria for the test.
-        """
-        disable_supl_mode(self.ad)
-        begin_time = get_current_epoch_time()
-        process_gnss_by_gtw_gpstool(
-            self.ad, self.standalone_cs_criteria)
-        check_xtra_download(self.ad, begin_time)
-        self.ad.log.info("Turn airplane mode on")
-        self.ad.droid.connectivityToggleAirplaneMode(True)
-        start_gnss_by_gtw_gpstool(self.ad, True)
-        start_ttff_by_gtw_gpstool(
-            self.ad, mode, iteration=self.ttff_test_cycle)
-        ttff_data = process_ttff_by_gtw_gpstool(
-            self.ad, begin_time, self.pixel_lab_location)
-        result = check_ttff_data(
-            self.ad, ttff_data, mode, criteria)
-        asserts.assert_true(
-            result, "TTFF %s fails to reach designated criteria of %d "
-                    "seconds." % (self.ttff_mode.get(mode), criteria))
+        toggle_airplane_mode(self.ad.log, self.ad, new_state=True)
+        gutils.run_ttff_via_gtw_gpstool(
+            self.ad, mode, criteria, self.ttff_test_cycle, self.pixel_lab_location)
 
     """ Test Cases """
 
-    @test_tracker_info(uuid="ab859f2a-2c95-4d15-bb7f-bd0e3278340f")
+    def test_cs_first_fixed_system_server_restart(self):
+        """Verify cs first fixed after system server restart.
+
+        Steps:
+            1. Get location fixed within supl_cs_criteria.
+            2. Restarts android runtime.
+            3. Get location fixed within supl_cs_criteria.
+
+        Expected Results:
+            Location fixed within supl_cs_criteria.
+        """
+        overall_test_result = []
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
+        for test_loop in range(1, 6):
+            gutils.process_gnss_by_gtw_gpstool(self.ad, self.supl_cs_criteria)
+            gutils.start_gnss_by_gtw_gpstool(self.ad, False)
+            self.ad.restart_runtime()
+            self.ad.unlock_screen(password=None)
+            test_result = gutils.process_gnss_by_gtw_gpstool(self.ad, self.supl_cs_criteria)
+            gutils.start_gnss_by_gtw_gpstool(self.ad, False)
+            self.ad.log.info("Iteration %d => %s" % (test_loop, test_result))
+            overall_test_result.append(test_result)
+
+        asserts.assert_true(all(overall_test_result),
+                            "SUPL fail after system server restart.")
+
+    def test_cs_ttff_after_gps_service_restart(self):
+        """Verify cs ttff after modem silent reboot / GPS daemons restart.
+
+        Steps:
+            1. Trigger modem crash by adb/Restart GPS daemons by killing PID.
+            2. Wait 1 minute for modem to recover.
+            3. TTFF Cold Start for 3 iteration.
+            4. Repeat Step 1. to Step 3. for 5 times.
+
+        Expected Results:
+            All SUPL TTFF Cold Start results should be within supl_cs_criteria.
+        """
+        supl_ssr_test_result_all = []
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
+        for times in range(1, 6):
+            begin_time = get_current_epoch_time()
+            if gutils.check_chipset_vendor_by_qualcomm(self.ad):
+                test_info = "Modem SSR"
+                gutils.gnss_trigger_modem_ssr_by_mds(self.ad)
+            else:
+                test_info = "restarting GPS daemons"
+                gutils.restart_gps_daemons(self.ad)
+            if not verify_internet_connection(self.ad.log, self.ad, retries=3,
+                                                expected_state=True):
+                raise signals.TestFailure("Fail to connect to LTE network.")
+            gutils.process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
+            gutils.start_ttff_by_gtw_gpstool(self.ad, ttff_mode="cs", iteration=3)
+            ttff_data = gutils.process_ttff_by_gtw_gpstool(self.ad, begin_time,
+                                                    self.pixel_lab_location)
+            supl_ssr_test_result = gutils.check_ttff_data(
+                self.ad, ttff_data, ttff_mode="Cold Start",
+                criteria=self.supl_cs_criteria)
+            self.ad.log.info("SUPL after %s test %d times -> %s" % (
+                test_info, times, supl_ssr_test_result))
+            supl_ssr_test_result_all.append(supl_ssr_test_result)
+
+        asserts.assert_true(all(supl_ssr_test_result_all),
+                            "TTFF fails to reach designated criteria")
+
     def test_gnss_one_hour_tracking(self):
         """Verify GNSS tracking performance of signal strength and position
         error.
@@ -387,14 +326,17 @@
         Expected Results:
             DUT could finish 60 minutes test and output track data.
         """
-        self.start_qxdm_and_tcpdump_log()
+        test_time = 60
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
         gnss_tracking_via_gtw_gpstool(self.ad, self.standalone_cs_criteria,
-                                      type="gnss", testtime=60)
-        parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, type="gnss")
+                                      api_type="gnss", testtime=test_time)
+        location_data = parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, api_type="gnss")
+        gutils.validate_location_fix_rate(self.ad, location_data, run_time=test_time,
+                                          fix_rate_criteria=0.99)
+        gutils.verify_gps_time_should_be_close_to_device_time(self.ad, location_data)
 
-    @test_tracker_info(uuid="623628ab-fdab-449d-9025-ebf4e9a404c2")
-    def test_dpo_function(self):
-        """Verify DPO Functionality.
+    def test_duty_cycle_function(self):
+        """Verify duty cycle Functionality.
 
         Steps:
             1. Launch GTW_GPSTool.
@@ -403,14 +345,14 @@
             4. Calculate the count diff of "HardwareClockDiscontinuityCount"
 
         Expected Results:
-            DPO should be engaged in 5 minutes GNSS tracking.
+            Duty cycle should be engaged in 5 minutes GNSS tracking.
         """
         tracking_minutes = 5
-        self.start_qxdm_and_tcpdump_log()
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
         dpo_begin_time = get_current_epoch_time()
         gnss_tracking_via_gtw_gpstool(self.ad,
                                       self.standalone_cs_criteria,
-                                      type="gnss",
+                                      api_type="gnss",
                                       testtime=tracking_minutes,
                                       meas_flag=True)
         if gutils.check_chipset_vendor_by_qualcomm(self.ad):
@@ -422,7 +364,6 @@
                                                self.dpo_threshold,
                                                self.brcm_error_log_allowlist)
 
-    @test_tracker_info(uuid="499d2091-640a-4735-9c58-de67370e4421")
     def test_gnss_init_error(self):
         """Check if there is any GNSS initialization error after reboot.
 
@@ -452,30 +393,6 @@
         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):
-        """Verify SUPL capabilities.
-
-        Steps:
-            1. Root DUT.
-            2. Check SUPL capabilities.
-
-        Expected Results:
-            CAPABILITIES=0x37 which supports MSA + MSB.
-            CAPABILITIES=0x17 = ON_DEMAND_TIME | MSA | MSB | SCHEDULING
-        """
-        if not gutils.check_chipset_vendor_by_qualcomm(self.ad):
-            raise signals.TestSkip("Not Qualcomm chipset. Skip the test.")
-        capabilities_state = str(
-            self.ad.adb.shell(
-                "cat vendor/etc/gps.conf | grep CAPABILITIES")).split("=")[-1]
-        self.ad.log.info("SUPL capabilities - %s" % capabilities_state)
-        asserts.assert_true(capabilities_state in self.supl_capabilities,
-                            "Wrong default SUPL capabilities is set. Found %s, "
-                            "expected any of %r" % (capabilities_state,
-                                                    self.supl_capabilities))
-
-    @test_tracker_info(uuid="dcae6979-ddb4-4cad-9d14-fbdd9439cf42")
     def test_sap_valid_modes(self):
         """Verify SAP Valid Modes.
 
@@ -494,7 +411,6 @@
         asserts.assert_true("SAP=PREMIUM" in sap_state,
                             "Wrong SAP Valid Modes is set")
 
-    @test_tracker_info(uuid="14daaaba-35b4-42d9-8d2c-2a803dd746a6")
     def test_network_location_provider_cell(self):
         """Verify LocationManagerService API reports cell Network Location.
 
@@ -508,7 +424,7 @@
             Test devices could report cell Network Location.
         """
         test_result_all = []
-        self.start_qxdm_and_tcpdump_log()
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
         set_wifi_and_bt_scanning(self.ad, False)
         for i in range(1, 6):
             test_result = check_network_location(
@@ -519,7 +435,6 @@
         asserts.assert_true(all(test_result_all),
                             "Fail to get networkLocationType=cell")
 
-    @test_tracker_info(uuid="a45bdc7d-29fa-4a1d-ba34-6340b90e308d")
     def test_network_location_provider_wifi(self):
         """Verify LocationManagerService API reports wifi Network Location.
 
@@ -533,7 +448,7 @@
             Test devices could report wifi Network Location.
         """
         test_result_all = []
-        self.start_qxdm_and_tcpdump_log()
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
         set_wifi_and_bt_scanning(self.ad, True)
         for i in range(1, 6):
             test_result = check_network_location(
@@ -543,62 +458,6 @@
         asserts.assert_true(all(test_result_all),
                             "Fail to get networkLocationType=wifi")
 
-    @test_tracker_info(uuid="0919d375-baf2-4fe7-b66b-3f72d386f791")
-    def test_gmap_location_report_gps_network(self):
-        """Verify GnssLocationProvider API reports location to Google Map
-           when GPS and Location Accuracy are on.
-
-        Steps:
-            1. GPS and NLP are on.
-            2. Launch Google Map.
-            3. Verify whether test devices could report location.
-            4. Repeat Step 2. to Step 3. for 5 times.
-
-        Expected Results:
-            Test devices could report location to Google Map.
-        """
-        test_result_all = []
-        self.start_qxdm_and_tcpdump_log()
-        for i in range(1, 6):
-            grant_location_permission(self.ad, True)
-            launch_google_map(self.ad)
-            test_result = check_location_api(self.ad, retries=3)
-            self.ad.send_keycode("HOME")
-            test_result_all.append(test_result)
-            self.ad.log.info("Iteration %d => %s" % (i, test_result))
-        asserts.assert_true(all(test_result_all), "Fail to get location update")
-
-    @test_tracker_info(uuid="513361d2-7d72-41b0-a944-fb259c606b81")
-    def test_gmap_location_report_gps(self):
-        """Verify GnssLocationProvider API reports location to Google Map
-           when GPS is on and Location Accuracy is off.
-
-        Steps:
-            1. GPS is on.
-            2. Location Accuracy is off.
-            3. Launch Google Map.
-            4. Verify whether test devices could report location.
-            5. Repeat Step 3. to Step 4. for 5 times.
-
-        Expected Results:
-            Test devices could report location to Google Map.
-        """
-        test_result_all = []
-        self.start_qxdm_and_tcpdump_log()
-        self.ad.adb.shell("settings put secure location_mode 1")
-        out = int(self.ad.adb.shell("settings get secure location_mode"))
-        self.ad.log.info("Modify current Location Mode to %d" % out)
-        for i in range(1, 6):
-            grant_location_permission(self.ad, True)
-            launch_google_map(self.ad)
-            test_result = check_location_api(self.ad, retries=3)
-            self.ad.send_keycode("HOME")
-            test_result_all.append(test_result)
-            self.ad.log.info("Iteration %d => %s" % (i, test_result))
-        check_location_service(self.ad)
-        asserts.assert_true(all(test_result_all), "Fail to get location update")
-
-    @test_tracker_info(uuid="91a65121-b87d-450d-bd0f-387ade450ab7")
     def test_gmap_location_report_battery_saver(self):
         """Verify GnssLocationProvider API reports location to Google Map
            when Battery Saver is enabled.
@@ -615,7 +474,7 @@
             Test devices could report location to Google Map.
         """
         test_result_all = []
-        self.start_qxdm_and_tcpdump_log()
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
         set_battery_saver_mode(self.ad, True)
         for i in range(1, 6):
             grant_location_permission(self.ad, True)
@@ -627,174 +486,6 @@
         set_battery_saver_mode(self.ad, False)
         asserts.assert_true(all(test_result_all), "Fail to get location update")
 
-    @test_tracker_info(uuid="a59c72af-5d56-4d88-9746-ae2749cac671")
-    def test_supl_ttff_cs(self):
-        """Verify SUPL functionality of TTFF Cold Start.
-
-        Steps:
-            1. Kill XTRA/LTO daemon to support SUPL only case.
-            2. SUPL TTFF Cold Start for 10 iteration.
-
-        Expected Results:
-            All SUPL TTFF Cold Start results should be less than
-            supl_cs_criteria.
-        """
-        self.supl_ttff_with_sim("cs", self.supl_cs_criteria)
-
-    @test_tracker_info(uuid="9a91c8ad-1978-414a-a9ac-8ebc782f77ff")
-    def test_supl_ttff_ws(self):
-        """Verify SUPL functionality of TTFF Warm Start.
-
-        Steps:
-            1. Kill XTRA/LTO daemon to support SUPL only case.
-            2. SUPL TTFF Warm Start for 10 iteration.
-
-        Expected Results:
-            All SUPL TTFF Warm Start results should be less than
-            supl_ws_criteria.
-        """
-        self.supl_ttff_with_sim("ws", self.supl_ws_criteria)
-
-    @test_tracker_info(uuid="bbd5aad4-3309-4579-a3b2-a06bfb674dfa")
-    def test_supl_ttff_hs(self):
-        """Verify SUPL functionality of TTFF Hot Start.
-
-        Steps:
-            1. Kill XTRA/LTO daemon to support SUPL only case.
-            2. SUPL TTFF Hot Start for 10 iteration.
-
-        Expected Results:
-            All SUPL TTFF Hot Start results should be less than
-            supl_hs_criteria.
-        """
-        self.supl_ttff_with_sim("hs", self.supl_hs_criteria)
-
-    @test_tracker_info(uuid="60c0aeec-0c8f-4a96-bc6c-05cba1260e73")
-    def test_supl_ongoing_call(self):
-        """Verify SUPL functionality during phone call.
-
-        Steps:
-            1. Kill XTRA/LTO daemon to support SUPL only case.
-            2. Initiate call on DUT.
-            3. SUPL TTFF Cold Start for 10 iteration.
-            4. DUT hang up call.
-
-        Expected Results:
-            All SUPL TTFF Cold Start results should be less than
-            supl_cs_criteria.
-        """
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.ad.droid.setVoiceCallVolume(25)
-        initiate_call(self.ad.log, self.ad, "99117")
-        time.sleep(5)
-        if not check_call_state_connected_by_adb(self.ad):
-            raise signals.TestFailure("Call is not connected.")
-        self.run_ttff_via_gtw_gpstool("cs", self.supl_cs_criteria)
-
-    @test_tracker_info(uuid="df605509-328f-43e8-b6d8-00635bf701ef")
-    def test_supl_downloading_files(self):
-        """Verify SUPL functionality when downloading files.
-
-        Steps:
-            1. Kill XTRA/LTO daemon to support SUPL only case.
-            2. DUT start downloading files by sl4a.
-            3. SUPL TTFF Cold Start for 10 iteration.
-            4. DUT cancel downloading files.
-
-        Expected Results:
-            All SUPL TTFF Cold Start results should be within supl_cs_criteria.
-        """
-        begin_time = get_current_epoch_time()
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        download = Process(target=http_file_download_by_sl4a,
-                           args=(self.ad, "https://speed.hetzner.de/10GB.bin",
-                                 None, None, True, 3600))
-        download.start()
-        time.sleep(10)
-        process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-        start_ttff_by_gtw_gpstool(
-            self.ad, ttff_mode="cs", iteration=self.ttff_test_cycle)
-        ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
-                                                self.pixel_lab_location)
-        download.terminate()
-        time.sleep(3)
-        result = check_ttff_data(self.ad, ttff_data, ttff_mode="Cold Start",
-                                 criteria=self.supl_cs_criteria)
-        asserts.assert_true(result, "TTFF fails to reach designated criteria")
-
-    @test_tracker_info(uuid="66b9f9d4-1397-4da7-9e55-8b89b1732017")
-    def test_supl_watching_youtube(self):
-        """Verify SUPL functionality when watching video on youtube.
-
-        Steps:
-            1. Kill XTRA/LTO daemon to support SUPL only case.
-            2. DUT start watching video on youtube.
-            3. SUPL TTFF Cold Start for 10 iteration at the background.
-            4. DUT stop watching video on youtube.
-
-        Expected Results:
-            All SUPL TTFF Cold Start results should be within supl_cs_criteria.
-        """
-        begin_time = get_current_epoch_time()
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.ad.droid.setMediaVolume(25)
-        process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-        start_ttff_by_gtw_gpstool(
-            self.ad, ttff_mode="cs", iteration=self.ttff_test_cycle)
-        start_youtube_video(self.ad,
-                            url="https://www.youtube.com/watch?v=AbdVsi1VjQY",
-                            retries=3)
-        ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
-                                                self.pixel_lab_location)
-        result = check_ttff_data(self.ad, ttff_data, ttff_mode="Cold Start",
-                                 criteria=self.supl_cs_criteria)
-        asserts.assert_true(result, "TTFF fails to reach designated criteria")
-
-    @test_tracker_info(uuid="a748af8b-e1eb-4ec6-bde3-74bcefa1c680")
-    def test_supl_modem_ssr(self):
-        """Verify SUPL functionality after modem silent reboot /
-        GPS daemons restart.
-
-        Steps:
-            1. Trigger modem crash by adb/Restart GPS daemons by killing PID.
-            2. Wait 1 minute for modem to recover.
-            3. SUPL TTFF Cold Start for 3 iteration.
-            4. Repeat Step 1. to Step 3. for 5 times.
-
-        Expected Results:
-            All SUPL TTFF Cold Start results should be within supl_cs_criteria.
-        """
-        supl_ssr_test_result_all = []
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        for times in range(1, 6):
-            begin_time = get_current_epoch_time()
-            if gutils.check_chipset_vendor_by_qualcomm(self.ad):
-                test_info = "Modem SSR"
-                gnss_trigger_modem_ssr_by_mds(self.ad)
-            else:
-                test_info = "restarting GPS daemons"
-                gutils.restart_gps_daemons(self.ad)
-            if not verify_internet_connection(self.ad.log, self.ad, retries=3,
-                                              expected_state=True):
-                raise signals.TestFailure("Fail to connect to LTE network.")
-            process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-            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)
-            self.ad.log.info("SUPL after %s test %d times -> %s" % (
-                test_info, times, supl_ssr_test_result))
-            supl_ssr_test_result_all.append(supl_ssr_test_result)
-        asserts.assert_true(all(supl_ssr_test_result_all),
-                            "TTFF fails to reach designated criteria")
-
-    @test_tracker_info(uuid="01602e65-8ded-4459-8df1-7df70a1bfe8a")
     def test_gnss_ttff_cs_airplane_mode_on(self):
         """Verify Standalone GNSS functionality of TTFF Cold Start while
         airplane mode is on.
@@ -809,7 +500,6 @@
         """
         self.standalone_ttff_airplane_mode_on("cs", self.standalone_cs_criteria)
 
-    @test_tracker_info(uuid="30b9e7c2-0048-4ccd-b3ae-f385eb5f4e46")
     def test_gnss_ttff_ws_airplane_mode_on(self):
         """Verify Standalone GNSS functionality of TTFF Warm Start while
         airplane mode is on.
@@ -824,7 +514,6 @@
         """
         self.standalone_ttff_airplane_mode_on("ws", self.standalone_ws_criteria)
 
-    @test_tracker_info(uuid="8f3c323a-c625-4339-ab7a-6a41d34cba8f")
     def test_gnss_ttff_hs_airplane_mode_on(self):
         """Verify Standalone GNSS functionality of TTFF Hot Start while
         airplane mode is on.
@@ -839,372 +528,54 @@
         """
         self.standalone_ttff_airplane_mode_on("hs", self.standalone_hs_criteria)
 
-    @test_tracker_info(uuid="23731b0d-cb80-4c79-a877-cfe7c2faa447")
-    def test_gnss_mobile_data_off(self):
-        """Verify Standalone GNSS functionality while mobile radio is off.
-
-        Steps:
-            1. Disable mobile data.
-            2. TTFF Cold Start for 10 iteration.
-            3. Enable mobile data.
-
-        Expected Results:
-            All Standalone TTFF Cold Start results should be within
-            standalone_cs_criteria.
-        """
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        set_mobile_data(self.ad, False)
-        self.run_ttff_via_gtw_gpstool("cs", self.standalone_cs_criteria)
-
-    @test_tracker_info(uuid="085b86a9-0212-4c0f-8ca1-2e467a0a2e6e")
-    def test_supl_after_regain_gnss_signal(self):
-        """Verify SUPL functionality after regain GNSS signal.
-
-        Steps:
-            1. Get location fixed.
-            2  Let device do GNSS tracking for 1 minute.
-            3. Set attenuation value to block GNSS signal.
-            4. Let DUT stay in no GNSS signal for 5 minutes.
-            5. Set attenuation value to regain GNSS signal.
-            6. Try to get location reported again.
-            7. Repeat Step 1. to Step 6. for 5 times.
-
-        Expected Results:
-            After setting attenuation value to 10 (GPS signal regain),
-            DUT could get location fixed again.
-        """
-        supl_no_gnss_signal_all = []
-        enable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        for times in range(1, 6):
-            process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-            self.ad.log.info("Let device do GNSS tracking for 1 minute.")
-            time.sleep(60)
-            set_attenuator_gnss_signal(self.ad, self.attenuators,
-                                       self.no_gnss_signal_attenuation)
-            self.ad.log.info("Let device stay in no GNSS signal for 5 minutes.")
-            time.sleep(300)
-            set_attenuator_gnss_signal(self.ad, self.attenuators,
-                                       self.default_gnss_signal_attenuation)
-            supl_no_gnss_signal = check_location_api(self.ad, retries=3)
-            start_gnss_by_gtw_gpstool(self.ad, False)
-            self.ad.log.info("SUPL without GNSS signal test %d times -> %s"
-                             % (times, supl_no_gnss_signal))
-            supl_no_gnss_signal_all.append(supl_no_gnss_signal)
-        asserts.assert_true(all(supl_no_gnss_signal_all),
-                            "Fail to get location update")
-
-    @test_tracker_info(uuid="3ff2f2fa-42d8-47fa-91de-060816cca9df")
-    def test_supl_ttff_cs_weak_gnss_signal(self):
-        """Verify SUPL functionality of TTFF Cold Start under weak GNSS signal.
+    def test_cs_ttff_in_weak_gnss_signal(self):
+        """Verify TTFF cold start under weak GNSS signal.
 
         Steps:
             1. Set attenuation value to weak GNSS signal.
-            2. Kill XTRA/LTO daemon to support SUPL only case.
-            3. SUPL TTFF Cold Start for 10 iteration.
-
-        Expected Results:
-            All SUPL TTFF Cold Start results should be less than
-            weak_signal_supl_cs_criteria.
-        """
-        self.supl_ttff_weak_gnss_signal("cs", self.weak_signal_supl_cs_criteria)
-
-    @test_tracker_info(uuid="d72364d4-dad8-4d46-8190-87183def9822")
-    def test_supl_ttff_ws_weak_gnss_signal(self):
-        """Verify SUPL functionality of TTFF Warm Start under weak GNSS signal.
-
-        Steps:
-            1. Set attenuation value to weak GNSS signal.
-            2. Kill XTRA/LTO daemon to support SUPL only case.
-            3. SUPL TTFF Warm Start for 10 iteration.
-
-        Expected Results:
-            All SUPL TTFF Warm Start results should be less than
-            weak_signal_supl_ws_criteria.
-        """
-        self.supl_ttff_weak_gnss_signal("ws", self.weak_signal_supl_ws_criteria)
-
-    @test_tracker_info(uuid="aeb95733-9829-470d-bfc7-e3b059bf881f")
-    def test_supl_ttff_hs_weak_gnss_signal(self):
-        """Verify SUPL functionality of TTFF Hot Start under weak GNSS signal.
-
-        Steps:
-            1. Set attenuation value to weak GNSS signal.
-            2. Kill XTRA/LTO daemon to support SUPL only case.
-            3. SUPL TTFF Hot Start for 10 iteration.
-
-        Expected Results:
-            All SUPL TTFF Hot Start results should be less than
-            weak_signal_supl_hs_criteria.
-        """
-        self.supl_ttff_weak_gnss_signal("hs", self.weak_signal_supl_hs_criteria)
-
-    @test_tracker_info(uuid="4ad4a371-949a-42e1-b1f4-628c79fa8ddc")
-    def test_supl_factory_reset(self):
-        """Verify SUPL functionality after factory reset.
-
-        Steps:
-            1. Factory reset device.
-            2. Kill XTRA/LTO daemon to support SUPL only case.
-            3. SUPL TTFF Cold Start for 10 iteration.
-            4. Repeat Step 1. to Step 3. for 3 times.
-
-        Expected Results:
-            All SUPL TTFF Cold Start results should be within supl_cs_criteria.
-        """
-        for times in range(1, 4):
-            fastboot_factory_reset(self.ad, True)
-            self.ad.unlock_screen(password=None)
-            _init_device(self.ad)
-            begin_time = get_current_epoch_time()
-            kill_xtra_daemon(self.ad)
-            self.start_qxdm_and_tcpdump_log()
-            process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-            start_ttff_by_gtw_gpstool(
-                self.ad, ttff_mode="cs", iteration=self.ttff_test_cycle)
-            ttff_data = process_ttff_by_gtw_gpstool(self.ad, begin_time,
-                                                    self.pixel_lab_location)
-            if not check_ttff_data(self.ad, ttff_data, ttff_mode="Cold Start",
-                                   criteria=self.supl_cs_criteria):
-                raise signals.TestFailure("SUPL after Factory Reset test %d "
-                                          "times -> FAIL" % times)
-            self.ad.log.info("SUPL after Factory Reset test %d times -> "
-                             "PASS" % times)
-
-    @test_tracker_info(uuid="ea3096cf-4f72-4e91-bfb3-0bcbfe865ab4")
-    def test_xtra_ttff_cs_mobile_data(self):
-        """Verify XTRA/LTO functionality of TTFF Cold Start with mobile data.
-
-        Steps:
-            1. Disable SUPL mode.
             2. TTFF Cold Start for 10 iteration.
 
         Expected Results:
-            XTRA/LTO TTFF Cold Start results should be within xtra_cs_criteria.
+            TTFF CS results should be within weak_signal_cs_criteria.
         """
-        self.xtra_ttff_mobile_data("cs", self.xtra_cs_criteria)
+        gutils.set_attenuator_gnss_signal(self.ad, self.attenuators,
+                                          self.weak_gnss_signal_attenuation)
+        gutils.run_ttff(self.ad, mode="cs", criteria=self.weak_signal_cs_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
 
-    @test_tracker_info(uuid="c9b22894-deb3-4dc2-af14-4dcbb8ebad66")
-    def test_xtra_ttff_ws_mobile_data(self):
-        """Verify XTRA/LTO functionality of TTFF Warm Start with mobile data.
+    def test_ws_ttff_in_weak_gnss_signal(self):
+        """Verify TTFF warm start under weak GNSS signal.
 
         Steps:
-            1. Disable SUPL mode.
-            2. TTFF Warm Start for 10 iteration.
-
-        Expected Results:
-            XTRA/LTO TTFF Warm Start results should be within xtra_ws_criteria.
-        """
-        self.xtra_ttff_mobile_data("ws", self.xtra_ws_criteria)
-
-    @test_tracker_info(uuid="273741e2-0815-4817-96df-9c13401119dd")
-    def test_xtra_ttff_hs_mobile_data(self):
-        """Verify XTRA/LTO functionality of TTFF Hot Start with mobile data.
-
-        Steps:
-            1. Disable SUPL mode.
-            2. TTFF Hot Start for 10 iteration.
-
-        Expected Results:
-            XTRA/LTO TTFF Hot Start results should be within xtra_hs_criteria.
-        """
-        self.xtra_ttff_mobile_data("hs", self.xtra_hs_criteria)
-
-    @test_tracker_info(uuid="c91ba740-220e-41de-81e5-43af31f63907")
-    def test_xtra_ttff_cs_weak_gnss_signal(self):
-        """Verify XTRA/LTO functionality of TTFF Cold Start under weak GNSS
-        signal.
-
-        Steps:
-            1. Disable SUPL mode.
-            2. Set attenuation value to weak GNSS signal.
-            3. TTFF Cold Start for 10 iteration.
-
-        Expected Results:
-            XTRA/LTO TTFF Cold Start results should be within
-            weak_signal_xtra_cs_criteria.
-        """
-        self.xtra_ttff_weak_gnss_signal("cs", self.weak_signal_xtra_cs_criteria)
-
-    @test_tracker_info(uuid="2a285be7-3571-49fb-8825-01efa2e65f10")
-    def test_xtra_ttff_ws_weak_gnss_signal(self):
-        """Verify XTRA/LTO functionality of TTFF Warm Start under weak GNSS
-        signal.
-
-        Steps:
-            1. Disable SUPL mode.
             2. Set attenuation value to weak GNSS signal.
             3. TTFF Warm Start for 10 iteration.
 
         Expected Results:
-            XTRA/LTO TTFF Warm Start results should be within
-            weak_signal_xtra_ws_criteria.
+            TTFF WS result should be within weak_signal_criteria.
         """
-        self.xtra_ttff_weak_gnss_signal("ws", self.weak_signal_xtra_ws_criteria)
+        gutils.set_attenuator_gnss_signal(self.ad, self.attenuators,
+                                          self.weak_gnss_signal_attenuation)
+        gutils.run_ttff(self.ad, mode="ws", criteria=self.weak_signal_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
 
-    @test_tracker_info(uuid="249bf484-8b04-4cd9-a372-aa718e5f4ec6")
-    def test_xtra_ttff_hs_weak_gnss_signal(self):
-        """Verify XTRA/LTO functionality of TTFF Hot Start under weak GNSS
-        signal.
+    def test_hs_ttff_in_weak_gnss_signal(self):
+        """Verify TTFF hot start under weak GNSS signal.
 
         Steps:
-            1. Disable SUPL mode.
             2. Set attenuation value to weak GNSS signal.
             3. TTFF Hot Start for 10 iteration.
 
         Expected Results:
-            XTRA/LTO TTFF Hot Start results should be within
-            weak_signal_xtra_hs_criteria.
+            TTFF HS result should be within weak_signal_criteria.
         """
-        self.xtra_ttff_weak_gnss_signal("hs", self.weak_signal_xtra_hs_criteria)
+        gutils.set_attenuator_gnss_signal(self.ad, self.attenuators,
+                                          self.weak_gnss_signal_attenuation)
+        gutils.run_ttff(self.ad, mode="hs", criteria=self.weak_signal_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
 
-    @test_tracker_info(uuid="beeb3454-bcb2-451e-83fb-26289e89b515")
-    def test_xtra_ttff_cs_wifi(self):
-        """Verify XTRA/LTO functionality of TTFF Cold Start with WiFi.
-
-        Steps:
-            1. Disable SUPL mode and turn airplane mode on.
-            2. Connect to WiFi.
-            3. TTFF Cold Start for 10 iteration.
-
-        Expected Results:
-            XTRA/LTO TTFF Cold Start results should be within
-            xtra_cs_criteria.
-        """
-        self.xtra_ttff_wifi("cs", self.xtra_cs_criteria)
-
-    @test_tracker_info(uuid="f6e79b31-99d5-49ca-974f-4543957ea449")
-    def test_xtra_ttff_ws_wifi(self):
-        """Verify XTRA/LTO functionality of TTFF Warm Start with WiFi.
-
-        Steps:
-            1. Disable SUPL mode and turn airplane mode on.
-            2. Connect to WiFi.
-            3. TTFF Warm Start for 10 iteration.
-
-        Expected Results:
-            XTRA/LTO TTFF Warm Start results should be within xtra_ws_criteria.
-        """
-        self.xtra_ttff_wifi("ws", self.xtra_ws_criteria)
-
-    @test_tracker_info(uuid="8981363c-f64f-4c37-9674-46733c40473b")
-    def test_xtra_ttff_hs_wifi(self):
-        """Verify XTRA/LTO functionality of TTFF Hot Start with WiFi.
-
-        Steps:
-            1. Disable SUPL mode and turn airplane mode on.
-            2. Connect to WiFi.
-            3. TTFF Hot Start for 10 iteration.
-
-        Expected Results:
-            XTRA/LTO TTFF Hot Start results should be within xtra_hs_criteria.
-        """
-        self.xtra_ttff_wifi("hs", self.xtra_hs_criteria)
-
-    @test_tracker_info(uuid="1745b8a4-5925-4aa0-809a-1b17e848dc9c")
-    def test_xtra_modem_ssr(self):
-        """Verify XTRA/LTO functionality after modem silent reboot /
-        GPS daemons restart.
-
-        Steps:
-            1. Trigger modem crash by adb/Restart GPS daemons by killing PID.
-            2. Wait 1 minute for modem to recover.
-            3. XTRA/LTO TTFF Cold Start for 3 iteration.
-            4. Repeat Step1. to Step 3. for 5 times.
-
-        Expected Results:
-            All XTRA/LTO TTFF Cold Start results should be within
-            xtra_cs_criteria.
-        """
-        xtra_ssr_test_result_all = []
-        disable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        for times in range(1, 6):
-            begin_time = get_current_epoch_time()
-            if gutils.check_chipset_vendor_by_qualcomm(self.ad):
-                test_info = "XTRA after Modem SSR"
-                gnss_trigger_modem_ssr_by_mds(self.ad)
-            else:
-                test_info = "LTO after restarting GPS daemons"
-                gutils.restart_gps_daemons(self.ad)
-            if not verify_internet_connection(self.ad.log, self.ad, retries=3,
-                                              expected_state=True):
-                raise signals.TestFailure("Fail to connect to LTE network.")
-            process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-            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)
-            self.ad.log.info("%s test %d times -> %s" % (
-                test_info, times, xtra_ssr_test_result))
-            xtra_ssr_test_result_all.append(xtra_ssr_test_result)
-        asserts.assert_true(all(xtra_ssr_test_result_all),
-                            "TTFF fails to reach designated criteria")
-
-    @test_tracker_info(uuid="4d6e81e1-3abb-4e03-b732-7b6b497a2258")
-    def test_xtra_download_mobile_data(self):
-        """Verify XTRA/LTO data could be downloaded via mobile data.
-
-        Steps:
-            1. Delete all GNSS aiding data.
-            2. Get location fixed.
-            3. Verify whether XTRA/LTO is downloaded and injected.
-            4. Repeat Step 1. to Step 3. for 5 times.
-
-        Expected Results:
-            XTRA/LTO data is properly downloaded and injected via mobile data.
-        """
-        mobile_xtra_result_all = []
-        disable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        for i in range(1, 6):
-            begin_time = get_current_epoch_time()
-            process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
-            time.sleep(5)
-            start_gnss_by_gtw_gpstool(self.ad, False)
-            mobile_xtra_result = check_xtra_download(self.ad, begin_time)
-            self.ad.log.info("Iteration %d => %s" % (i, mobile_xtra_result))
-            mobile_xtra_result_all.append(mobile_xtra_result)
-        asserts.assert_true(all(mobile_xtra_result_all),
-                            "Fail to Download and Inject XTRA/LTO File.")
-
-    @test_tracker_info(uuid="625ac665-1446-4406-a722-e6a19645222c")
-    def test_xtra_download_wifi(self):
-        """Verify XTRA/LTO data could be downloaded via WiFi.
-
-        Steps:
-            1. Connect to WiFi.
-            2. Delete all GNSS aiding data.
-            3. Get location fixed.
-            4. Verify whether XTRA/LTO is downloaded and injected.
-            5. Repeat Step 2. to Step 4. for 5 times.
-
-        Expected Results:
-            XTRA data is properly downloaded and injected via WiFi.
-        """
-        wifi_xtra_result_all = []
-        disable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        self.ad.log.info("Turn airplane mode on")
-        self.ad.droid.connectivityToggleAirplaneMode(True)
-        wifi_toggle_state(self.ad, True)
-        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)
-            time.sleep(5)
-            start_gnss_by_gtw_gpstool(self.ad, False)
-            wifi_xtra_result = check_xtra_download(self.ad, begin_time)
-            wifi_xtra_result_all.append(wifi_xtra_result)
-            self.ad.log.info("Iteration %d => %s" % (i, wifi_xtra_result))
-        asserts.assert_true(all(wifi_xtra_result_all),
-                            "Fail to Download and Inject XTRA/LTO 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.
@@ -1219,70 +590,10 @@
         Expected Results:
             No single Timeout is seen in 10 iterations.
         """
-        enable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
         start_toggle_gnss_by_gtw_gpstool(
             self.ad, iteration=self.ttff_test_cycle)
 
-    @test_tracker_info(uuid="9f565b32-9938-42c0-a29d-f4d28b5f4d75")
-    def test_supl_system_server_restart(self):
-        """Verify SUPL functionality after system server restart.
-
-        Steps:
-            1. Kill XTRA/LTO daemon to support SUPL only case.
-            2. Get location fixed within supl_cs_criteria.
-            3. Restarts android runtime.
-            4. Get location fixed within supl_cs_criteria.
-
-        Expected Results:
-            Location fixed within supl_cs_criteria.
-        """
-        overall_test_result = []
-        kill_xtra_daemon(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        for test_loop in range(1, 6):
-            process_gnss_by_gtw_gpstool(self.ad, self.supl_cs_criteria)
-            start_gnss_by_gtw_gpstool(self.ad, False)
-            self.ad.restart_runtime()
-            self.ad.unlock_screen(password=None)
-            test_result = process_gnss_by_gtw_gpstool(self.ad,
-                                                      self.supl_cs_criteria)
-            start_gnss_by_gtw_gpstool(self.ad, False)
-            self.ad.log.info("Iteration %d => %s" % (test_loop, test_result))
-            overall_test_result.append(test_result)
-        asserts.assert_true(all(overall_test_result),
-                            "SUPL fail after system server restart.")
-
-    @test_tracker_info(uuid="a9a64900-9016-46d0-ad7e-cab30e8152cd")
-    def test_xtra_system_server_restart(self):
-        """Verify XTRA/LTO functionality after system server restart.
-
-        Steps:
-            1. Disable SUPL mode.
-            2. Get location fixed within xtra_cs_criteria.
-            3. Restarts android runtime.
-            4. Get location fixed within xtra_cs_criteria.
-
-        Expected Results:
-            Location fixed within xtra_cs_criteria.
-        """
-        overall_test_result = []
-        disable_supl_mode(self.ad)
-        self.start_qxdm_and_tcpdump_log()
-        for test_loop in range(1, 6):
-            process_gnss_by_gtw_gpstool(self.ad, self.xtra_cs_criteria)
-            start_gnss_by_gtw_gpstool(self.ad, False)
-            self.ad.restart_runtime()
-            self.ad.unlock_screen(password=None)
-            test_result = process_gnss_by_gtw_gpstool(self.ad,
-                                                      self.xtra_cs_criteria)
-            start_gnss_by_gtw_gpstool(self.ad, False)
-            self.ad.log.info("Iteration %d => %s" % (test_loop, test_result))
-            overall_test_result.append(test_result)
-        asserts.assert_true(all(overall_test_result),
-                            "XTRA/LTO fail after system server restart.")
-
-    @test_tracker_info(uuid="ab5ef9f7-0b28-48ed-a693-7f1d902ca3e1")
     def test_gnss_init_after_reboot(self):
         """Verify SUPL and XTRA/LTO functionality after reboot.
 
@@ -1296,12 +607,14 @@
             Location fixed within supl_hs_criteria.
         """
         overall_test_result = []
-        enable_supl_mode(self.ad)
+        # As b/252971345 requests, we need the log before reboot for debugging.
+        gutils.start_pixel_logger(self.ad)
         process_gnss_by_gtw_gpstool(self.ad, self.supl_cs_criteria)
         start_gnss_by_gtw_gpstool(self.ad, False)
+        gutils.stop_pixel_logger(self.ad)
         for test_loop in range(1, 11):
             reboot(self.ad)
-            self.start_qxdm_and_tcpdump_log()
+            gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
             if is_device_wearable(self.ad):
                 test_result = process_gnss_by_gtw_gpstool(
                     self.ad, self.wearable_reboot_hs_criteria, clear_data=False)
@@ -1318,63 +631,184 @@
         asserts.assert_true(all(overall_test_result),
                             "GNSS init fail after reboot.")
 
-    @test_tracker_info(uuid="2c62183a-4354-4efc-92f2-84580cbd3398")
-    def test_lto_download_after_reboot(self):
-        """Verify LTO data could be downloaded and injected after device reboot.
+    def test_host_gnssstatus_validation(self):
+        """Verify GnssStatus integrity during host tracking for 1 minute.
 
         Steps:
-            1. Reboot device.
-            2. Verify whether LTO is auto downloaded and injected without trigger GPS.
-            3. Repeat Step 1 to Step 2 for 5 times.
+            1. Launch GTW_GPSTool.
+            2. GNSS tracking for 1 minute with 1 second frequency.
+            3. Validate all the GnssStatus raw data.(SV, SVID, Elev, Azim)
 
         Expected Results:
-            LTO data is properly downloaded and injected at the first time tether to phone.
+            GnssStatus obj should return no failures
         """
-        reboot_lto_test_results_all = []
-        disable_supl_mode(self.ad)
-        for times in range(1, 6):
-            delete_lto_file(self.ad)
-            reboot(self.ad)
-            self.start_qxdm_and_tcpdump_log()
-            # Wait 20 seconds for boot busy and lto auto-download time
-            time.sleep(20)
-            begin_time = get_current_epoch_time()
-            reboot_lto_test_result = gutils.check_xtra_download(self.ad, begin_time)
-            self.ad.log.info("Iteration %d => %s" % (times, reboot_lto_test_result))
-            reboot_lto_test_results_all.append(reboot_lto_test_result)
-            gutils.stop_pixel_logger(self.ad)
-            tutils.stop_adb_tcpdump(self.ad)
-        asserts.assert_true(all(reboot_lto_test_results_all),
-                                "Fail to Download and Inject LTO File.")
+        gnss_tracking_via_gtw_gpstool(self.ad, self.standalone_cs_criteria,
+                                      api_type="gnss", testtime=1)
+        parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, api_type="gnss",
+                              validate_gnssstatus=True)
 
-    @test_tracker_info(uuid="a7048a4f-8a40-40a4-bb6c-7fc90e8227bd")
-    def test_ws_with_assist(self):
-        """Verify Warm Start functionality with existed LTO data.
+    def test_onchip_gnssstatus_validation(self):
+        """Verify GnssStatus integrity during onchip tracking for 1 minute.
 
         Steps:
-            1. Disable SUPL mode.
-            2. Make LTO is downloaded.
-            3. Turn on AirPlane mode to make sure there's no network connection.
-            4. TTFF Warm Start with Assist for 10 iteration.
+            1. Launch GTW_GPSTool.
+            2. GNSS tracking for 1 minute with 6 second frequency.
+            3. Validate all the GnssStatus raw data.(SV, SVID, Elev, Azim)
 
         Expected Results:
-            All TTFF Warm Start with Assist results should be within
-            xtra_ws_criteria.
+            GnssStatus obj should return no failures
         """
-        self.ttff_with_assist("ws", self.xtra_ws_criteria)
+        if gutils.check_chipset_vendor_by_qualcomm(self.ad):
+            raise signals.TestSkip("Not BRCM chipset. Skip the test.")
+        gnss_tracking_via_gtw_gpstool(self.ad, self.standalone_cs_criteria,
+                                      api_type="gnss", testtime=1, freq=self.onchip_interval)
+        parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, api_type="gnss",
+                              validate_gnssstatus=True)
 
-    @test_tracker_info(uuid="c5fb9519-63b0-42bd-bd79-fce7593604ea")
-    def test_cs_with_assist(self):
-        """Verify Cold Start functionality with existed LTO data.
-
-        Steps:
-            1. Disable SUPL mode.
-            2. Make sure LTO is downloaded.
-            3. Turn on AirPlane mode to make sure there's no network connection.
-            4. TTFF Cold Start with Assist for 10 iteration.
-
-        Expected Results:
-            All TTFF Cold Start with Assist results should be within
-            standalone_cs_criteria.
+    def test_location_update_after_resuming_from_deep_suspend(self):
+        """Verify the GPS location reported after resume from suspend mode
+        1. Enable GPS location report for 1 min to make sure the GPS is working
+        2. Force DUT into deep suspend mode for a while(3 times with 15s interval)
+        3. Enable GPS location report for 5 mins
+        4. Check the report frequency
+        5. Check the location fix rate
         """
-        self.ttff_with_assist("csa", self.standalone_cs_criteria)
+
+        gps_enable_minutes = 1
+        gnss_tracking_via_gtw_gpstool(self.ad, criteria=self.supl_cs_criteria, api_type="gnss",
+                                      testtime=gps_enable_minutes)
+        result = parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, api_type="gnss")
+        self.ad.log.debug("Location report details before suspend")
+        self.ad.log.debug(result)
+        gutils.validate_location_fix_rate(self.ad, result, run_time=gps_enable_minutes,
+                                          fix_rate_criteria=0.95)
+
+        gutils.deep_suspend_device(self.ad)
+
+        gps_enable_minutes = 5
+        gnss_tracking_via_gtw_gpstool(self.ad, criteria=self.supl_cs_criteria, api_type="gnss",
+                                      testtime=gps_enable_minutes)
+        result = parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, api_type="gnss")
+        self.ad.log.debug("Location report details after suspend")
+        self.ad.log.debug(result)
+
+        location_report_time = list(result.keys())
+        gutils.check_location_report_interval(self.ad, location_report_time,
+                                              gps_enable_minutes * 60, tolerance=0.01)
+        gutils.validate_location_fix_rate(self.ad, result, run_time=gps_enable_minutes,
+                                          fix_rate_criteria=0.99)
+
+    def test_location_mode_in_battery_saver_with_screen_off(self):
+        """Ensure location request with foreground permission can work
+        in battery saver mode (screen off)
+
+        1. unplug power
+        2. enter battery saver mode
+        3. start tracking for 2 mins with screen off
+        4. repest step 3 for 3 times
+        """
+        try:
+            gutils.set_battery_saver_mode(self.ad, state=True)
+            test_time = 2
+            for i in range(1, 4):
+                self.ad.log.info("Tracking attempt %s" % str(i))
+                gnss_tracking_via_gtw_gpstool(
+                    self.ad, criteria=self.supl_cs_criteria, api_type="gnss", testtime=test_time,
+                    is_screen_off=True)
+                result = parse_gtw_gpstool_log(self.ad, self.pixel_lab_location, api_type="gnss")
+                gutils.validate_location_fix_rate(self.ad, result, run_time=test_time,
+                                                  fix_rate_criteria=0.99)
+        finally:
+            gutils.set_battery_saver_mode(self.ad, state=False)
+
+    def test_measure_adr_rate_after_10_mins_tracking(self):
+        """Verify ADR rate
+
+        1. Enable "Force full gnss measurement"
+        2. Start tracking with GnssMeasurement enabled for 10 mins
+        3. Check ADR usable rate / valid rate
+        4. Disable "Force full gnss measurement"
+        """
+        adr_threshold = self.adr_ratio_threshold.get(self.ad.model)
+        if not adr_threshold:
+            self.ad.log.warn((f"Can't get '{self.ad.model}' threshold from config "
+                              f"{self.adr_ratio_threshold}, use default threshold 0.5"))
+            adr_threshold = 0.5
+        with gutils.full_gnss_measurement(self.ad):
+            gnss_tracking_via_gtw_gpstool(self.ad, criteria=self.supl_cs_criteria, api_type="gnss",
+                                          testtime=10, meas_flag=True)
+            gutils.validate_adr_rate(self.ad, pass_criteria=float(adr_threshold))
+
+
+    def test_hal_crashing_should_resume_tracking(self):
+        """Make sure location request can be resumed after HAL restart.
+
+        1. Start GPS tool and get First Fixed
+        2. Wait for 1 min for tracking
+        3. Restart HAL service
+        4. Wait for 1 min for tracking
+        5. Check fix rate
+        """
+
+        first_fixed_time = process_gnss_by_gtw_gpstool(self.ad, criteria=self.supl_cs_criteria)
+        begin_time = int(first_fixed_time.timestamp() * 1000)
+
+        self.ad.log.info("Start 2 mins tracking")
+
+        gutils.wait_n_mins_for_gnss_tracking(self.ad, begin_time, testtime=1,
+                                             ignore_hal_crash=False)
+        gutils.restart_hal_service(self.ad)
+        # The test case is designed to run the tracking for 2 mins, so we assign testime to 2 to
+        # indicate the total run time is 2 mins (starting from begin_time).
+        gutils.wait_n_mins_for_gnss_tracking(self.ad, begin_time, testtime=2, ignore_hal_crash=True)
+
+        start_gnss_by_gtw_gpstool(self.ad, state=False)
+
+        result = parse_gtw_gpstool_log(self.ad, self.pixel_lab_location)
+        gutils.validate_location_fix_rate(self.ad, result, run_time=2,
+                                          fix_rate_criteria=0.95)
+
+
+    def test_power_save_mode_should_apply_latest_measurement_setting(self):
+        """Ensure power save mode will apply the GNSS measurement setting.
+
+        1. Turn off full GNSS measurement.
+        2. Run tracking for 2 mins
+        3. Check the power save mode status
+        4. Turn on full GNSS measurement and re-register measurement callback
+        6. Run tracking for 30s
+        7. Check the power save mode status
+        8. Turn off full GNSS measurement and re-register measurement callback
+        9. Run tracking for 2 mins
+        10. Check the power save mode status
+        """
+        def wait_for_power_state_changes(wait_time):
+            gutils.re_register_measurement_callback(self.ad)
+            tracking_begin_time = get_current_epoch_time()
+            gutils.wait_n_mins_for_gnss_tracking(self.ad, tracking_begin_time, testtime=wait_time)
+            return tracking_begin_time
+
+        if self.ad.model.lower() == "sunfish":
+            raise signals.TestSkip(
+                "According to b/241049795, it's HW issue and won't be fixed.")
+
+        gutils.start_pixel_logger(self.ad)
+        with gutils.run_gnss_tracking(self.ad, criteria=self.supl_cs_criteria, meas_flag=True):
+            start_time = wait_for_power_state_changes(wait_time=2)
+            gutils.check_power_save_mode_status(
+                self.ad, full_power=False, begin_time=start_time,
+                brcm_error_allowlist=self.brcm_error_log_allowlist)
+
+            with gutils.full_gnss_measurement(self.ad):
+                start_time = wait_for_power_state_changes(wait_time=0.5)
+                gutils.check_power_save_mode_status(
+                    self.ad, full_power=True, begin_time=start_time,
+                    brcm_error_allowlist=self.brcm_error_log_allowlist)
+
+            start_time = wait_for_power_state_changes(wait_time=2)
+            gutils.check_power_save_mode_status(
+                self.ad, full_power=False, begin_time=start_time,
+                brcm_error_allowlist=self.brcm_error_log_allowlist)
+
+        gutils.stop_pixel_logger(self.ad)
+
diff --git a/acts_tests/tests/google/gnss/GnssHsSenTest.py b/acts_tests/tests/google/gnss/GnssHsSenTest.py
index 5269ae0..bd4e398 100644
--- a/acts_tests/tests/google/gnss/GnssHsSenTest.py
+++ b/acts_tests/tests/google/gnss/GnssHsSenTest.py
@@ -13,11 +13,12 @@
 #   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.
+"""Lab GNSS Hot Start Sensitivity Test"""
 
 import os
 from acts_contrib.test_utils.gnss.GnssBlankingBase import GnssBlankingBase
 from acts_contrib.test_utils.gnss.dut_log_test_utils import get_gpstool_logs
-from acts_contrib.test_utils.gnss.gnss_test_utils import excute_eecoexer_function
+from acts_contrib.test_utils.gnss.gnss_test_utils import execute_eecoexer_function
 
 
 class GnssHsSenTest(GnssBlankingBase):
@@ -25,11 +26,19 @@
 
     def __init__(self, controllers):
         super().__init__(controllers)
-        self.gnss_simulator_power_level = -130
-        self.sa_sensitivity = -150
-        self.gnss_pwr_lvl_offset = 5
+        self.cell_tx_ant = None
+        self.cell_pwr = None
+        self.eecoex_func = ''
+        self.coex_stop_cmd = ''
+        self.coex_params = {}
 
-    def gnss_hot_start_sensitivity_search_base(self, cellular_enable=False):
+    def setup_class(self):
+        super().setup_class()
+        self.coex_params = self.user_params.get('coex_params', {})
+        self.cell_tx_ant = self.coex_params.get('cell_tx_ant', 'PRIMARY')
+        self.cell_pwr = self.coex_params.get('cell_pwr', 'Infinity')
+
+    def gnss_hot_start_sensitivity_search_base(self, coex_enable=False):
         """
         Perform GNSS hot start sensitivity search.
 
@@ -41,49 +50,32 @@
         # Get parameters from user_params.
         first_wait = self.user_params.get('first_wait', 300)
         wait_between_pwr = self.user_params.get('wait_between_pwr', 60)
-        gnss_pwr_sweep = self.user_params.get('gnss_pwr_sweep')
-        gnss_init_pwr = gnss_pwr_sweep.get('init')
-        self.gnss_simulator_power_level = gnss_init_pwr[0]
-        self.sa_sensitivity = gnss_init_pwr[1]
-        self.gnss_pwr_lvl_offset = gnss_init_pwr[2]
-        gnss_pwr_fine_sweep = gnss_pwr_sweep.get('fine_sweep')
         ttft_iteration = self.user_params.get('ttff_iteration', 25)
 
         # Start the test item with gnss_init_power_setting.
-        if self.gnss_init_power_setting(first_wait):
-            self.log.info('Successfully set the GNSS power level to %d' %
-                          self.sa_sensitivity)
+        ret, pwr_lvl = self.gnss_init_power_setting(first_wait)
+        if ret:
+            self.log.info(f'Successfully set the GNSS power level to {pwr_lvl}')
             # Create gnss log folders for init and cellular sweep
             gnss_init_log_dir = os.path.join(self.gnss_log_path, 'GNSS_init')
 
             # Pull all exist GPStool logs into GNSS_init folder
             get_gpstool_logs(self.dut, gnss_init_log_dir, False)
-
-            if cellular_enable:
-                self.log.info('Start cellular coexistence test.')
-                # Set cellular Tx power level.
-                eecoex_cmd = self.eecoex_func.format('Infinity')
-                eecoex_cmd_file_str = eecoex_cmd.replace(',', '_')
-                excute_eecoexer_function(self.dut, eecoex_cmd)
-            else:
-                self.log.info('Start stand alone test.')
-                eecoex_cmd_file_str = 'Stand_alone'
-
-            for i, gnss_pwr in enumerate(gnss_pwr_fine_sweep):
-                self.log.info('Start fine GNSS power level sweep part %d' %
-                              (i + 1))
-                sweep_start = gnss_pwr[0]
-                sweep_stop = gnss_pwr[1]
-                sweep_offset = gnss_pwr[2]
-                self.log.info(
-                    'The GNSS simulator (start, stop, offset): (%.1f, %.1f, %.1f)'
-                    % (sweep_start, sweep_stop, sweep_offset))
-                result, sensitivity = self.hot_start_gnss_power_sweep(
-                    sweep_start, sweep_stop, sweep_offset, wait_between_pwr,
-                    ttft_iteration, True, eecoex_cmd_file_str)
-                if not result:
-                    break
-            self.log.info('The sensitivity level is: %.1f' % sensitivity)
+        if coex_enable:
+            self.log.info('Start coexistence test.')
+            eecoex_cmd_file_str = self.eecoex_func.replace(',', '_')
+            execute_eecoexer_function(self.dut, self.eecoex_func)
+        else:
+            self.log.info('Start stand alone test.')
+            eecoex_cmd_file_str = 'Stand_alone'
+        for i, gnss_pwr_swp in enumerate(self.gnss_pwr_sweep_fine_sweep_ls):
+            self.log.info(f'Start fine GNSS power level sweep part {i + 1}')
+            result, sensitivity = self.hot_start_gnss_power_sweep(
+                gnss_pwr_swp, wait_between_pwr, ttft_iteration, True,
+                eecoex_cmd_file_str)
+            if not result:
+                break
+        self.log.info(f'The sensitivity level is: {sensitivity}')
 
     def test_hot_start_sensitivity_search(self):
         """
@@ -95,86 +87,127 @@
         """
         GNSS hot start GSM850 Ch190 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,2,850,190,1,1,{}'
-        self.log.info('Running GSM850 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,2,850,190,1,{self.cell_tx_ant},{self.cell_pwr}'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running GSM850 with {self.cell_tx_ant} antenna \
+                and GNSS coexistence sensitivity search.'
+
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_gsm900(self):
         """
         GNSS hot start GSM900 Ch20 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,2,900,20,1,1,{}'
-        self.log.info('Running GSM900 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,2,900,20,1,{self.cell_tx_ant},{self.cell_pwr}'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running GSM900 with {self.cell_tx_ant} \
+                antenna and GNSS coexistence sensitivity search.'
+
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_gsm1800(self):
         """
         GNSS hot start GSM1800 Ch699 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,2,1800,699,1,1,{}'
-        self.log.info(
-            'Running GSM1800 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,2,1800,699,1,{self.cell_tx_ant},{self.cell_pwr}'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running GSM1800 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_gsm1900(self):
         """
         GNSS hot start GSM1900 Ch661 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,2,1900,661,1,1,{}'
-        self.log.info(
-            'Running GSM1900 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,2,1900,661,1,{self.cell_tx_ant},{self.cell_pwr}'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running GSM1900 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_lte_b38(self):
         """
         GNSS hot start LTE B38 Ch38000 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,5,38,38000,true,PRIMARY,{},10MHz,0,12'
-        self.log.info(
-            'Running LTE B38 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,5,38,38000,true,{self.cell_tx_ant},{self.cell_pwr},10MHz,0,12'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running LTE B38 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_lte_b39(self):
         """
         GNSS hot start LTE B39 Ch38450 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,5,39,38450,true,PRIMARY,{},10MHz,0,12'
-        self.log.info(
-            'Running LTE B38 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,5,39,38450,true,{self.cell_tx_ant},{self.cell_pwr},10MHz,0,12'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running LTE B38 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_lte_b40(self):
         """
         GNSS hot start LTE B40 Ch39150 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,5,40,39150,true,PRIMARY,{},10MHz,0,12'
-        self.log.info(
-            'Running LTE B38 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,5,40,39150,true,{self.cell_tx_ant},{self.cell_pwr},10MHz,0,12'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running LTE B38 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_lte_b41(self):
         """
         GNSS hot start LTE B41 Ch40620 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,5,41,40620,true,PRIMARY,{},10MHz,0,12'
-        self.log.info(
-            'Running LTE B41 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,5,41,40620,true,{self.cell_tx_ant},{self.cell_pwr},10MHz,0,12'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running LTE B41 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_lte_b42(self):
         """
         GNSS hot start LTE B42 Ch42590 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,5,42,42590,true,PRIMARY,{},10MHz,0,12'
-        self.log.info(
-            'Running LTE B42 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,5,42,42590,true,{self.cell_tx_ant},{self.cell_pwr},10MHz,0,12'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running LTE B42 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
 
     def test_hot_start_sensitivity_search_lte_b48(self):
         """
         GNSS hot start LTE B48 Ch55990 coexistence sensitivity search.
         """
-        self.eecoex_func = 'CELLR,5,48,55990,true,PRIMARY,{},10MHz,0,12'
-        self.log.info(
-            'Running LTE B48 and GNSS coexistence sensitivity search.')
+        self.eecoex_func = f'CELLR,5,48,55990,true,{self.cell_tx_ant},{self.cell_pwr},10MHz,0,12'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = f'Running LTE B48 {self.cell_tx_ant} antenna and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
         self.gnss_hot_start_sensitivity_search_base(True)
+
+    def test_hot_start_sensitivity_search_fr2_n2605(self):
+        """
+        GNSS hot start 5G NR B260 CH2234165 coexistence sensitivity search.
+        """
+        self.eecoex_func = f'CELLR,30,260,2234165,183,{self.cell_pwr}'
+        self.coex_stop_cmd = 'CELLR,19'
+        msg = 'Running 5G NR B260 CH2234165 and GNSS coexistence sensitivity search.'
+        self.log.info(msg)
+        self.gnss_hot_start_sensitivity_search_base(True)
+
+    def test_hot_start_sensitivity_custom_case(self):
+        """
+        GNSS hot start custom case coexistence sensitivity search.
+        """
+        cust_cmd = self.coex_params.get('custom_cmd', '')
+        cust_stop_cmd = self.coex_params.get('custom_stop_cmd', '')
+        if cust_cmd and cust_stop_cmd:
+            self.eecoex_func = cust_cmd
+            self.coex_stop_cmd = cust_stop_cmd
+            msg = f'Running custom {self.eecoex_func} and GNSS coexistence sensitivity search.'
+            self.log.info(msg)
+            self.gnss_hot_start_sensitivity_search_base(True)
+        else:
+            self.log.warning('No custom coex command is provided')
diff --git a/acts_tests/tests/google/gnss/GnssSuplTest.py b/acts_tests/tests/google/gnss/GnssSuplTest.py
new file mode 100644
index 0000000..853a228
--- /dev/null
+++ b/acts_tests/tests/google/gnss/GnssSuplTest.py
@@ -0,0 +1,294 @@
+from multiprocessing import Process
+import time
+
+from acts import asserts
+from acts import signals
+from acts.base_test import BaseTestClass
+from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
+from acts_contrib.test_utils.gnss import supl
+from acts_contrib.test_utils.gnss import gnss_defines
+from acts_contrib.test_utils.gnss.testtracker_util import log_testtracker_uuid
+from acts_contrib.test_utils.tel.tel_data_utils import http_file_download_by_sl4a
+from acts_contrib.test_utils.tel.tel_logging_utils import get_tcpdump_log
+from acts_contrib.test_utils.tel.tel_logging_utils import stop_adb_tcpdump
+from acts_contrib.test_utils.tel.tel_logging_utils import get_tcpdump_log
+from acts_contrib.test_utils.tel.tel_test_utils import check_call_state_connected_by_adb
+from acts_contrib.test_utils.tel.tel_test_utils import verify_internet_connection
+from acts_contrib.test_utils.tel.tel_test_utils import toggle_airplane_mode
+from acts_contrib.test_utils.tel.tel_voice_utils import initiate_call
+from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
+from acts.utils import get_current_epoch_time
+
+
+class GnssSuplTest(BaseTestClass):
+    def setup_class(self):
+        super().setup_class()
+        self.ad = self.android_devices[0]
+        req_params = [
+            "pixel_lab_network", "standalone_cs_criteria", "supl_cs_criteria", "supl_ws_criteria",
+            "supl_hs_criteria", "default_gnss_signal_attenuation", "pixel_lab_location",
+            "qdsp6m_path", "collect_logs", "ttff_test_cycle",
+            "supl_capabilities", "no_gnss_signal_attenuation", "set_attenuator"
+        ]
+        self.unpack_userparams(req_param_names=req_params)
+        # create hashmap for SSID
+        self.ssid_map = {}
+        for network in self.pixel_lab_network:
+            SSID = network["SSID"]
+            self.ssid_map[SSID] = network
+        self.init_device()
+
+    def only_brcm_device_runs_wifi_case(self):
+        """SUPL over wifi is only supported by BRCM devices, for QUAL device, skip the test.
+        """
+        if gutils.check_chipset_vendor_by_qualcomm(self.ad):
+            raise signals.TestSkip("Qualcomm device doesn't support SUPL over wifi")
+
+    def wearable_btwifi_should_skip_mobile_data_case(self):
+        if gutils.is_wearable_btwifi(self.ad):
+            raise signals.TestSkip("Skip mobile data case for BtWiFi sku")
+
+    def init_device(self):
+        """Init GNSS test devices for SUPL suite."""
+        gutils._init_device(self.ad)
+        gutils.disable_vendor_orbit_assistance_data(self.ad)
+        gutils.enable_supl_mode(self.ad)
+        self.enable_supl_over_wifi()
+        gutils.reboot(self.ad)
+
+    def enable_supl_over_wifi(self):
+        if not gutils.check_chipset_vendor_by_qualcomm(self.ad):
+            supl.set_supl_over_wifi_state(self.ad, turn_on=True)
+
+    def setup_test(self):
+        gutils.log_current_epoch_time(self.ad, "test_start_time")
+        log_testtracker_uuid(self.ad, self.current_test_name)
+        gutils.clear_logd_gnss_qxdm_log(self.ad)
+        gutils.get_baseband_and_gms_version(self.ad)
+        toggle_airplane_mode(self.ad.log, self.ad, new_state=False)
+        if gutils.is_wearable_btwifi(self.ad):
+            wutils.wifi_toggle_state(self.ad, True)
+            gutils.connect_to_wifi_network(self.ad,
+                                           self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+        else:
+            wutils.wifi_toggle_state(self.ad, False)
+            gutils.set_mobile_data(self.ad, state=True)
+        if not verify_internet_connection(self.ad.log, self.ad, retries=3,
+                                          expected_state=True):
+            raise signals.TestFailure("Fail to connect to LTE network.")
+        # Once the device is rebooted, the xtra service will be alive again
+        # In order not to affect the supl case, disable it in setup_test.
+        if gutils.check_chipset_vendor_by_qualcomm(self.ad):
+            gutils.disable_qualcomm_orbit_assistance_data(self.ad)
+
+    def teardown_test(self):
+        if self.collect_logs:
+            gutils.stop_pixel_logger(self.ad)
+            stop_adb_tcpdump(self.ad)
+        if self.set_attenuator:
+            gutils.set_attenuator_gnss_signal(self.ad, self.attenuators,
+                                              self.default_gnss_signal_attenuation)
+        gutils.log_current_epoch_time(self.ad, "test_end_time")
+
+    def on_fail(self, test_name, begin_time):
+        if self.collect_logs:
+            self.ad.take_bug_report(test_name, begin_time)
+            gutils.get_gnss_qxdm_log(self.ad, self.qdsp6m_path)
+            self.get_brcm_gps_xml_to_sponge()
+            get_tcpdump_log(self.ad, test_name, begin_time)
+
+    def get_brcm_gps_xml_to_sponge(self):
+        # request from b/250506003 - to check the SUPL setting
+        if not gutils.check_chipset_vendor_by_qualcomm(self.ad):
+            self.ad.pull_files(gnss_defines.BCM_GPS_XML_PATH, self.ad.device_log_path)
+
+    def run_ttff(self, mode, criteria):
+        """Triggers TTFF.
+
+        Args:
+            mode: "cs", "ws" or "hs"
+            criteria: Criteria for the test.
+        """
+        return gutils.run_ttff(self.ad, mode, criteria, self.ttff_test_cycle,
+                               self.pixel_lab_location, self.collect_logs)
+
+    def supl_ttff_weak_gnss_signal(self, mode, criteria):
+        """Verify SUPL TTFF functionality under weak GNSS signal.
+
+        Args:
+            mode: "cs", "ws" or "hs"
+            criteria: Criteria for the test.
+        """
+        gutils.set_attenuator_gnss_signal(self.ad, self.attenuators,
+                                          self.weak_gnss_signal_attenuation)
+        self.run_ttff(mode, criteria)
+
+    def connect_to_wifi_with_mobile_data_off(self):
+        gutils.set_mobile_data(self.ad, False)
+        wutils.wifi_toggle_state(self.ad, True)
+        gutils.connect_to_wifi_network(self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+
+    def connect_to_wifi_with_airplane_mode_on(self):
+        toggle_airplane_mode(self.ad.log, self.ad, new_state=True)
+        wutils.wifi_toggle_state(self.ad, True)
+        gutils.connect_to_wifi_network(self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+
+    def check_position_mode(self, begin_time: int, mode: str):
+        logcat_results = self.ad.search_logcat(
+            matching_string="setting position_mode to", begin_time=begin_time)
+        return all([result["log_message"].split(" ")[-1] == mode for result in logcat_results])
+
+    def test_supl_capabilities(self):
+        """Verify SUPL capabilities.
+
+        Steps:
+            1. Root DUT.
+            2. Check SUPL capabilities.
+
+        Expected Results:
+            CAPABILITIES=0x37 which supports MSA + MSB.
+            CAPABILITIES=0x17 = ON_DEMAND_TIME | MSA | MSB | SCHEDULING
+        """
+        if not gutils.check_chipset_vendor_by_qualcomm(self.ad):
+            raise signals.TestSkip("Not Qualcomm chipset. Skip the test.")
+        capabilities_state = str(
+            self.ad.adb.shell(
+                "cat vendor/etc/gps.conf | grep CAPABILITIES")).split("=")[-1]
+        self.ad.log.info("SUPL capabilities - %s" % capabilities_state)
+
+        asserts.assert_true(capabilities_state in self.supl_capabilities,
+                            "Wrong default SUPL capabilities is set. Found %s, "
+                            "expected any of %r" % (capabilities_state,
+                                                    self.supl_capabilities))
+
+
+    def test_supl_ttff_cs(self):
+        """Verify SUPL functionality of TTFF Cold Start.
+
+        Steps:
+            1. Kill XTRA/LTO daemon to support SUPL only case.
+            2. SUPL TTFF Cold Start for 10 iteration.
+
+        Expected Results:
+            All SUPL TTFF Cold Start results should be less than
+            supl_cs_criteria.
+        """
+        self.run_ttff("cs", self.supl_cs_criteria)
+
+    def test_supl_ttff_ws(self):
+        """Verify SUPL functionality of TTFF Warm Start.
+
+        Steps:
+            1. Kill XTRA/LTO daemon to support SUPL only case.
+            2. SUPL TTFF Warm Start for 10 iteration.
+
+        Expected Results:
+            All SUPL TTFF Warm Start results should be less than
+            supl_ws_criteria.
+        """
+        self.run_ttff("ws", self.supl_ws_criteria)
+
+    def test_supl_ttff_hs(self):
+        """Verify SUPL functionality of TTFF Hot Start.
+
+        Steps:
+            1. Kill XTRA/LTO daemon to support SUPL only case.
+            2. SUPL TTFF Hot Start for 10 iteration.
+
+        Expected Results:
+            All SUPL TTFF Hot Start results should be less than
+            supl_hs_criteria.
+        """
+        self.run_ttff("hs", self.supl_hs_criteria)
+
+    def test_cs_ttff_supl_over_wifi_with_airplane_mode_on(self):
+        """ Test supl can works through wifi with airplane mode on
+
+        Test steps are executed in the following sequence.
+        - Turn on airplane mode
+        - Connect to wifi
+        - Run SUPL CS TTFF
+        """
+        self.only_brcm_device_runs_wifi_case()
+
+        self.connect_to_wifi_with_airplane_mode_on()
+
+        self.run_ttff(mode="cs", criteria=self.supl_cs_criteria)
+
+    def test_ws_ttff_supl_over_wifi_with_airplane_mode_on(self):
+        """ Test supl can works through wifi with airplane mode on
+
+        Test steps are executed in the following sequence.
+        - Turn on airplane mode
+        - Connect to wifi
+        - Run SUPL WS TTFF
+        """
+        self.only_brcm_device_runs_wifi_case()
+
+        self.connect_to_wifi_with_airplane_mode_on()
+
+        self.run_ttff("ws", self.supl_ws_criteria)
+
+    def test_hs_ttff_supl_over_wifi_with_airplane_mode_on(self):
+        """ Test supl can works through wifi with airplane mode on
+
+        Test steps are executed in the following sequence.
+        - Turn on airplane mode
+        - Connect to wifi
+        - Run SUPL WS TTFF
+        """
+        self.only_brcm_device_runs_wifi_case()
+
+        self.connect_to_wifi_with_airplane_mode_on()
+
+        self.run_ttff("hs", self.supl_ws_criteria)
+
+    def test_ttff_gla_on(self):
+        """ Test the turn on "Google Location Accuracy" in settings work or not.
+
+        Test steps are executed in the following sequence.
+        - Turn off airplane mode
+        - Connect to Cellular
+        - Turn off LTO/RTO
+        - Turn on SUPL
+        - Turn on GLA
+        - Run CS TTFF
+
+        Expected Results:
+        - The position mode must be "MS_BASED"
+        - The TTFF time should be less than 10 seconds
+        """
+        begin_time = get_current_epoch_time()
+        gutils.gla_mode(self.ad, True)
+
+        self.run_ttff("cs", self.supl_cs_criteria)
+        asserts.assert_true(self.check_position_mode(begin_time, "MS_BASED"),
+                                msg=f"Fail to enter the MS_BASED mode")
+
+    def test_ttff_gla_off(self):
+        """ Test the turn off "Google Location Accuracy" in settings work or not.
+
+        Test steps are executed in the following sequence.
+        - Turn off airplane mode
+        - Connect to Cellular
+        - Turn off LTO/RTO
+        - Turn on SUPL
+        - Turn off GLA
+        - Run CS TTFF
+
+        Expected Results:
+        - The position mode must be "standalone"
+        - The TTFF time must be between slower than supl_ws and faster than standalone_cs.
+        """
+        begin_time = get_current_epoch_time()
+        gutils.gla_mode(self.ad, False)
+
+        ttff_data = self.run_ttff("cs", self.standalone_cs_criteria)
+
+        asserts.assert_true(any(float(ttff_data[key].ttff_sec) > self.supl_ws_criteria
+                                for key in ttff_data.keys()),
+                            msg=f"One or more TTFF Cold Start are faster than \
+                            test criteria {self.supl_ws_criteria} seconds")
+
+        asserts.assert_true(self.check_position_mode(begin_time, "standalone"),
+                                msg=f"Fail to enter the standalone mode")
diff --git a/acts_tests/tests/google/gnss/GnssVendorFeaturesTest.py b/acts_tests/tests/google/gnss/GnssVendorFeaturesTest.py
new file mode 100644
index 0000000..e94626f
--- /dev/null
+++ b/acts_tests/tests/google/gnss/GnssVendorFeaturesTest.py
@@ -0,0 +1,290 @@
+import time
+
+from acts import asserts
+from acts import signals
+from acts.base_test import BaseTestClass
+from acts.utils import get_current_epoch_time
+from acts_contrib.test_utils.gnss import gnss_constant
+from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
+from acts_contrib.test_utils.gnss.testtracker_util import log_testtracker_uuid
+from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
+from acts_contrib.test_utils.tel import tel_logging_utils
+from acts_contrib.test_utils.tel.tel_test_utils import verify_internet_connection
+from acts_contrib.test_utils.tel.tel_test_utils import toggle_airplane_mode
+
+
+class GnssVendorFeaturesTest(BaseTestClass):
+    """Validate vendor specific features."""
+    def setup_class(self):
+        super().setup_class()
+        self.ad = self.android_devices[0]
+        req_params = ["pixel_lab_network", "default_gnss_signal_attenuation", "pixel_lab_location",
+                      "qdsp6m_path", "collect_logs", "ttff_test_cycle", "standalone_cs_criteria",
+                      "xtra_cs_criteria",  "xtra_ws_criteria", "xtra_hs_criteria",
+                      "set_attenuator"]
+        self.unpack_userparams(req_param_names=req_params)
+        # create hashmap for SSID
+        self.ssid_map = {}
+        for network in self.pixel_lab_network:
+            SSID = network["SSID"]
+            self.ssid_map[SSID] = network
+        self.init_device()
+
+    def init_device(self):
+        """Init GNSS test devices for vendor features suite."""
+        gutils._init_device(self.ad)
+        gutils.disable_supl_mode(self.ad)
+        gutils.enable_vendor_orbit_assistance_data(self.ad)
+
+    def setup_test(self):
+        gutils.log_current_epoch_time(self.ad, "test_start_time")
+        log_testtracker_uuid(self.ad, self.current_test_name)
+        gutils.clear_logd_gnss_qxdm_log(self.ad)
+        gutils.get_baseband_and_gms_version(self.ad)
+        toggle_airplane_mode(self.ad.log, self.ad, new_state=False)
+        if gutils.is_wearable_btwifi(self.ad):
+            wutils.wifi_toggle_state(self.ad, True)
+            gutils.connect_to_wifi_network(self.ad,
+                                           self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+        else:
+            wutils.wifi_toggle_state(self.ad, False)
+            gutils.set_mobile_data(self.ad, state=True)
+        if not verify_internet_connection(self.ad.log, self.ad, retries=3,
+                                          expected_state=True):
+            raise signals.TestFailure("Fail to connect to internet.")
+
+    def teardown_test(self):
+        if self.collect_logs:
+            gutils.stop_pixel_logger(self.ad)
+            tel_logging_utils.stop_adb_tcpdump(self.ad)
+        if self.set_attenuator:
+            gutils.set_attenuator_gnss_signal(self.ad, self.attenuators,
+                                              self.default_gnss_signal_attenuation)
+        gutils.log_current_epoch_time(self.ad, "test_end_time")
+
+    def on_fail(self, test_name, begin_time):
+        if self.collect_logs:
+            self.ad.take_bug_report(test_name, begin_time)
+            gutils.get_gnss_qxdm_log(self.ad, self.qdsp6m_path)
+            tel_logging_utils.get_tcpdump_log(self.ad, test_name, begin_time)
+
+    def connect_to_wifi_with_airplane_mode_on(self):
+        self.ad.log.info("Turn airplane mode on")
+        toggle_airplane_mode(self.ad.log, self.ad, new_state=True)
+        wutils.wifi_toggle_state(self.ad, True)
+        gutils.connect_to_wifi_network(self.ad, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
+
+    def ttff_with_assist(self, mode, criteria):
+        """Verify CS/WS TTFF functionality with Assist data.
+
+        Args:
+            mode: "csa" or "ws"
+            criteria: Criteria for the test.
+        """
+        begin_time = get_current_epoch_time()
+        gutils.process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
+        gutils.check_xtra_download(self.ad, begin_time)
+        self.ad.log.info("Turn airplane mode on")
+        toggle_airplane_mode(self.ad.log, self.ad, new_state=True)
+        gutils.start_gnss_by_gtw_gpstool(self.ad, True)
+        gutils.start_ttff_by_gtw_gpstool(self.ad, mode, iteration=self.ttff_test_cycle)
+        ttff_data = gutils.process_ttff_by_gtw_gpstool(self.ad, begin_time, self.pixel_lab_location)
+        result = gutils.check_ttff_data(self.ad, ttff_data, mode, criteria)
+        asserts.assert_true(
+            result, "TTFF %s fails to reach designated criteria of %d "
+                    "seconds." % (gnss_constant.TTFF_MODE.get(mode), criteria))
+
+    def test_xtra_ttff_cs_mobile_data(self):
+        """Verify XTRA/LTO functionality of TTFF Cold Start with mobile data.
+
+        Steps:
+            1. TTFF Cold Start for 10 iteration.
+
+        Expected Results:
+            XTRA/LTO TTFF Cold Start results should be within xtra_cs_criteria.
+        """
+        gutils.run_ttff(self.ad, mode="cs", criteria=self.xtra_cs_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
+
+    def test_xtra_ttff_ws_mobile_data(self):
+        """Verify XTRA/LTO functionality of TTFF Warm Start with mobile data.
+
+        Steps:
+            1. TTFF Warm Start for 10 iteration.
+
+        Expected Results:
+            XTRA/LTO TTFF Warm Start results should be within xtra_ws_criteria.
+        """
+        gutils.run_ttff(self.ad, mode="ws", criteria=self.xtra_ws_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
+
+    def test_xtra_ttff_hs_mobile_data(self):
+        """Verify XTRA/LTO functionality of TTFF Hot Start with mobile data.
+
+        Steps:
+            1. TTFF Hot Start for 10 iteration.
+
+        Expected Results:
+            XTRA/LTO TTFF Hot Start results should be within xtra_hs_criteria.
+        """
+        gutils.run_ttff(self.ad, mode="hs", criteria=self.xtra_hs_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
+
+    def test_xtra_ttff_cs_wifi(self):
+        """Verify XTRA/LTO functionality of TTFF Cold Start with WiFi.
+
+        Steps:
+            1. Turn airplane mode on.
+            2. Connect to WiFi.
+            3. TTFF Cold Start for 10 iteration.
+
+        Expected Results:
+            XTRA/LTO TTFF Cold Start results should be within
+            xtra_cs_criteria.
+        """
+        self.connect_to_wifi_with_airplane_mode_on()
+        gutils.run_ttff(self.ad, mode="cs", criteria=self.xtra_cs_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
+
+    def test_xtra_ttff_ws_wifi(self):
+        """Verify XTRA/LTO functionality of TTFF Warm Start with WiFi.
+
+        Steps:
+            1. Turn airplane mode on.
+            2. Connect to WiFi.
+            3. TTFF Warm Start for 10 iteration.
+
+        Expected Results:
+            XTRA/LTO TTFF Warm Start results should be within xtra_ws_criteria.
+        """
+        self.connect_to_wifi_with_airplane_mode_on()
+        gutils.run_ttff(self.ad, mode="ws", criteria=self.xtra_ws_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
+
+    def test_xtra_ttff_hs_wifi(self):
+        """Verify XTRA/LTO functionality of TTFF Hot Start with WiFi.
+
+        Steps:
+            1. Turn airplane mode on.
+            2. Connect to WiFi.
+            3. TTFF Hot Start for 10 iteration.
+
+        Expected Results:
+            XTRA/LTO TTFF Hot Start results should be within xtra_hs_criteria.
+        """
+        self.connect_to_wifi_with_airplane_mode_on()
+        gutils.run_ttff(self.ad, mode="hs", criteria=self.xtra_hs_criteria,
+                        test_cycle=self.ttff_test_cycle, base_lat_long=self.pixel_lab_location,
+                        collect_logs=self.collect_logs)
+
+    def test_xtra_download_mobile_data(self):
+        """Verify XTRA/LTO data could be downloaded via mobile data.
+
+        Steps:
+            1. Delete all GNSS aiding data.
+            2. Get location fixed.
+            3. Verify whether XTRA/LTO is downloaded and injected.
+            4. Repeat Step 1. to Step 3. for 5 times.
+
+        Expected Results:
+            XTRA/LTO data is properly downloaded and injected via mobile data.
+        """
+        mobile_xtra_result_all = []
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
+        for i in range(1, 6):
+            begin_time = get_current_epoch_time()
+            gutils.process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
+            time.sleep(5)
+            gutils.start_gnss_by_gtw_gpstool(self.ad, False)
+            mobile_xtra_result = gutils.check_xtra_download(self.ad, begin_time)
+            self.ad.log.info("Iteration %d => %s" % (i, mobile_xtra_result))
+            mobile_xtra_result_all.append(mobile_xtra_result)
+        asserts.assert_true(all(mobile_xtra_result_all),
+                            "Fail to Download and Inject XTRA/LTO File.")
+
+    def test_xtra_download_wifi(self):
+        """Verify XTRA/LTO data could be downloaded via WiFi.
+
+        Steps:
+            1. Connect to WiFi.
+            2. Delete all GNSS aiding data.
+            3. Get location fixed.
+            4. Verify whether XTRA/LTO is downloaded and injected.
+            5. Repeat Step 2. to Step 4. for 5 times.
+
+        Expected Results:
+            XTRA data is properly downloaded and injected via WiFi.
+        """
+        wifi_xtra_result_all = []
+        gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
+        self.connect_to_wifi_with_airplane_mode_on()
+        for i in range(1, 6):
+            begin_time = get_current_epoch_time()
+            gutils.process_gnss_by_gtw_gpstool(self.ad, self.standalone_cs_criteria)
+            time.sleep(5)
+            gutils.start_gnss_by_gtw_gpstool(self.ad, False)
+            wifi_xtra_result = gutils.check_xtra_download(self.ad, begin_time)
+            wifi_xtra_result_all.append(wifi_xtra_result)
+            self.ad.log.info("Iteration %d => %s" % (i, wifi_xtra_result))
+        asserts.assert_true(all(wifi_xtra_result_all),
+                            "Fail to Download and Inject XTRA/LTO File.")
+
+    def test_lto_download_after_reboot(self):
+        """Verify LTO data could be downloaded and injected after device reboot.
+
+        Steps:
+            1. Reboot device.
+            2. Verify whether LTO is auto downloaded and injected without trigger GPS.
+            3. Repeat Step 1 to Step 2 for 5 times.
+
+        Expected Results:
+            LTO data is properly downloaded and injected at the first time tether to phone.
+        """
+        reboot_lto_test_results_all = []
+        for times in range(1, 6):
+            gutils.delete_lto_file(self.ad)
+            gutils.reboot(self.ad)
+            gutils.start_qxdm_and_tcpdump_log(self.ad, self.collect_logs)
+            # Wait 20 seconds for boot busy and lto auto-download time
+            time.sleep(20)
+            begin_time = get_current_epoch_time()
+            reboot_lto_test_result = gutils.check_xtra_download(self.ad, begin_time)
+            self.ad.log.info("Iteration %d => %s" % (times, reboot_lto_test_result))
+            reboot_lto_test_results_all.append(reboot_lto_test_result)
+            gutils.stop_pixel_logger(self.ad)
+            tel_logging_utils.stop_adb_tcpdump(self.ad)
+        asserts.assert_true(all(reboot_lto_test_results_all),
+                                "Fail to Download and Inject LTO File.")
+
+    def test_ws_with_assist(self):
+        """Verify Warm Start functionality with existed LTO data.
+
+        Steps:
+            2. Make LTO is downloaded.
+            3. Turn on AirPlane mode to make sure there's no network connection.
+            4. TTFF Warm Start with Assist for 10 iteration.
+
+        Expected Results:
+            All TTFF Warm Start with Assist results should be within
+            xtra_ws_criteria.
+        """
+        self.ttff_with_assist("ws", self.xtra_ws_criteria)
+
+    def test_cs_with_assist(self):
+        """Verify Cold Start functionality with existed LTO data.
+
+        Steps:
+            2. Make sure LTO is downloaded.
+            3. Turn on AirPlane mode to make sure there's no network connection.
+            4. TTFF Cold Start with Assist for 10 iteration.
+
+        Expected Results:
+            All TTFF Cold Start with Assist results should be within
+            standalone_cs_criteria.
+        """
+        self.ttff_with_assist("csa", self.standalone_cs_criteria)
diff --git a/acts_tests/tests/google/gnss/GnssWearableTetherFunctionTest.py b/acts_tests/tests/google/gnss/GnssWearableTetherFunctionTest.py
index 0604c65..a90a01f 100644
--- a/acts_tests/tests/google/gnss/GnssWearableTetherFunctionTest.py
+++ b/acts_tests/tests/google/gnss/GnssWearableTetherFunctionTest.py
@@ -14,15 +14,16 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 import time
+import statistics
 
 from acts import asserts
 from acts import signals
 from acts.base_test import BaseTestClass
-from acts.test_decorators import test_tracker_info
+from acts_contrib.test_utils.gnss.testtracker_util import log_testtracker_uuid
 from acts_contrib.test_utils.gnss import gnss_test_utils as gutils
-from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.tel import tel_logging_utils as tutils
 from acts_contrib.test_utils.tel.tel_test_utils import verify_internet_connection
+from acts_contrib.test_utils.tel.tel_test_utils import toggle_airplane_mode
 from acts.utils import get_current_epoch_time
 from acts_contrib.test_utils.gnss.gnss_test_utils import delete_lto_file, pair_to_wearable
 from acts_contrib.test_utils.gnss.gnss_test_utils import process_gnss_by_gtw_gpstool
@@ -33,57 +34,56 @@
 
 class GnssWearableTetherFunctionTest(BaseTestClass):
     """ GNSS Wearable Tether Function Tests"""
-
     def setup_class(self):
         super().setup_class()
         self.watch = self.android_devices[0]
         self.phone = self.android_devices[1]
         self.phone.uia = Device(self.phone.serial)
-        req_params = [
-            "pixel_lab_network", "standalone_cs_criteria",
-            "flp_ttff_max_threshold", "pixel_lab_location", "flp_ttff_cycle",
-            "default_gnss_signal_attenuation", "flp_waiting_time",
-            "tracking_test_time", "fast_start_criteria"
-        ]
+        req_params = ["pixel_lab_network", "standalone_cs_criteria",
+                      "flp_ttff_max_threshold", "pixel_lab_location",
+                      "flp_ttff_cycle", "default_gnss_signal_attenuation",
+                      "flp_waiting_time", "tracking_test_time",
+                      "far_start_criteria", "ttff_test_cycle"]
         self.unpack_userparams(req_param_names=req_params)
         # create hashmap for SSID
         self.ssid_map = {}
         for network in self.pixel_lab_network:
             SSID = network["SSID"]
             self.ssid_map[SSID] = network
-        self.ttff_mode = {
-            "cs": "Cold Start",
-            "ws": "Warm Start",
-            "hs": "Hot Start"
-        }
+        self.ttff_mode = {"cs": "Cold Start",
+                          "ws": "Warm Start",
+                          "hs": "Hot Start"}
         gutils._init_device(self.watch)
         pair_to_wearable(self.watch, self.phone)
 
+    def teardown_class(self):
+        super().teardown_class()
+        gutils.reboot(self.phone)
+
     def setup_test(self):
+        gutils.log_current_epoch_time(self.watch, "test_start_time")
+        log_testtracker_uuid(self.watch, self.current_test_name)
         gutils.get_baseband_and_gms_version(self.watch)
         gutils.clear_logd_gnss_qxdm_log(self.watch)
         gutils.clear_logd_gnss_qxdm_log(self.phone)
         gutils.set_attenuator_gnss_signal(self.watch, self.attenuators,
                                           self.default_gnss_signal_attenuation)
-        if not gutils.is_mobile_data_on(self.watch):
-            gutils.set_mobile_data(self.watch, True)
-        # TODO (b/202101058:chenstanley): Need to double check how to disable wifi successfully in wearable projects.
-        if gutils.is_wearable_btwifi(self.watch):
-            wutils.wifi_toggle_state(self.watch, True)
-            gutils.connect_to_wifi_network(
-                self.watch, self.ssid_map[self.pixel_lab_network[0]["SSID"]])
-        if not verify_internet_connection(
-                self.watch.log, self.watch, retries=3, expected_state=True):
-            raise signals.TestFailure(
-                "Fail to connect to LTE or WiFi network.")
-        if not gutils.is_bluetooth_connected(self.watch, self.phone):
-            gutils.pair_to_wearable(self.phone, self.watch)
+        if not verify_internet_connection(self.watch.log, self.watch, retries=5,
+                                          expected_state=True):
+            time.sleep(60)
+            if not verify_internet_connection(self.watch.log, self.watch, retries=3,
+                                              expected_state=True):
+                raise signals.TestFailure("Fail to connect to LTE network.")
 
     def teardown_test(self):
         gutils.stop_pixel_logger(self.watch)
         tutils.stop_adb_tcpdump(self.watch)
         gutils.set_attenuator_gnss_signal(self.watch, self.attenuators,
-                                          self.default_gnss_signal_attenuation)
+                                       self.default_gnss_signal_attenuation)
+        if self.watch.droid.connectivityCheckAirplaneMode():
+            self.watch.log.info("Force airplane mode off")
+            toggle_airplane_mode(self.watch.log, self.watch, new_state=False)
+        gutils.log_current_epoch_time(self.watch, "test_end_time")
 
     def on_fail(self, test_name, begin_time):
         self.watch.take_bug_report(test_name, begin_time)
@@ -97,64 +97,44 @@
 
     def flp_ttff(self, mode, criteria, location):
         self.start_qxdm_and_tcpdump_log()
-        start_gnss_by_gtw_gpstool(self.phone, True, type="FLP")
+        start_gnss_by_gtw_gpstool(self.phone, True, api_type="FLP")
         time.sleep(self.flp_waiting_time)
         self.watch.unlock_screen(password=None)
         begin_time = get_current_epoch_time()
-        process_gnss_by_gtw_gpstool(self.watch,
-                                    self.standalone_cs_criteria,
-                                    type="flp")
-        gutils.start_ttff_by_gtw_gpstool(self.watch,
-                                         mode,
-                                         iteration=self.flp_ttff_cycle)
-        results = gutils.process_ttff_by_gtw_gpstool(self.watch,
-                                                     begin_time,
-                                                     location,
-                                                     type="flp")
+        process_gnss_by_gtw_gpstool(
+            self.watch, self.standalone_cs_criteria, api_type="flp")
+        gutils.start_ttff_by_gtw_gpstool(
+            self.watch, mode, iteration=self.flp_ttff_cycle)
+        results = gutils.process_ttff_by_gtw_gpstool(
+            self.watch, begin_time, location, api_type="flp")
         gutils.check_ttff_data(self.watch, results, mode, criteria)
         self.check_location_from_phone()
-        start_gnss_by_gtw_gpstool(self.phone, False, type="FLP")
+        start_gnss_by_gtw_gpstool(self.phone, False, api_type="FLP")
 
     def check_location_from_phone(self):
         watch_file = check_tracking_file(self.watch)
         phone_file = check_tracking_file(self.phone)
-        return gutils.compare_watch_phone_location(self, watch_file,
-                                                   phone_file)
+        return gutils.compare_watch_phone_location(self, watch_file, phone_file)
+
+    def run_ttff_via_gtw_gpstool(self, mode, criteria):
+        """Run GNSS TTFF test with selected mode and parse the results.
+
+        Args:
+            mode: "cs", "ws" or "hs"
+            criteria: Criteria for the TTFF.
+        """
+        begin_time = get_current_epoch_time()
+        gutils.process_gnss_by_gtw_gpstool(self.watch, self.standalone_cs_criteria, clear_data=False)
+        gutils.start_ttff_by_gtw_gpstool(self.watch, mode, self.ttff_test_cycle)
+        ttff_data = gutils.process_ttff_by_gtw_gpstool(
+            self.watch, begin_time, self.pixel_lab_location)
+        result = gutils.check_ttff_data(
+            self.watch, ttff_data, self.ttff_mode.get(mode), criteria)
+        asserts.assert_true(
+            result, "TTFF %s fails to reach designated criteria of %d "
+                    "seconds." % (self.ttff_mode.get(mode), criteria))
 
     """ Test Cases """
-
-    @test_tracker_info(uuid="2c62183a-4354-4efc-92f2-84580cbd3398")
-    def test_lto_download_after_reboot(self):
-        """Verify LTO data could be downloaded and injected after device reboot.
-
-        Steps:
-            1. Reboot device.
-            2. Verify whether LTO is auto downloaded and injected without trigger GPS.
-            3. Repeat Step 1 to Step 2 for 5 times.
-
-        Expected Results:
-            LTO data is properly downloaded and injected at the first time tether to phone.
-        """
-        reboot_lto_test_results_all = []
-        gutils.disable_supl_mode(self.watch)
-        for times in range(1, 6):
-            delete_lto_file(self.watch)
-            gutils.reboot(self.watch)
-            self.start_qxdm_and_tcpdump_log()
-            # Wait 20 seconds for boot busy and lto auto-download time
-            time.sleep(20)
-            begin_time = get_current_epoch_time()
-            reboot_lto_test_result = gutils.check_xtra_download(
-                self.watch, begin_time)
-            self.watch.log.info("Iteration %d => %s" %
-                                (times, reboot_lto_test_result))
-            reboot_lto_test_results_all.append(reboot_lto_test_result)
-            gutils.stop_pixel_logger(self.watch)
-            tutils.stop_adb_tcpdump(self.watch)
-        asserts.assert_true(all(reboot_lto_test_results_all),
-                            "Fail to Download and Inject LTO File.")
-
-    @test_tracker_info(uuid="7ed596df-df71-42ca-bdb3-69a3cad81963")
     def test_flp_ttff_cs(self):
         """Verify FLP TTFF Cold Start while tether with phone.
 
@@ -168,10 +148,8 @@
             flp_ttff_max_threshold.
             2. Watch uses phone's FLP location.
         """
-        self.flp_ttff("cs", self.flp_ttff_max_threshold,
-                      self.pixel_lab_location)
+        self.flp_ttff("cs", self.flp_ttff_max_threshold, self.pixel_lab_location)
 
-    @test_tracker_info(uuid="de19617c-1f03-4077-99af-542b300ab4ed")
     def test_flp_ttff_ws(self):
         """Verify FLP TTFF Warm Start while tether with phone.
 
@@ -185,10 +163,8 @@
             flp_ttff_max_threshold.
             2. Watch uses phone's FLP location.
         """
-        self.flp_ttff("ws", self.flp_ttff_max_threshold,
-                      self.pixel_lab_location)
+        self.flp_ttff("ws", self.flp_ttff_max_threshold, self.pixel_lab_location)
 
-    @test_tracker_info(uuid="c58c90ae-9f4a-4619-a9f8-f2f98c930008")
     def test_flp_ttff_hs(self):
         """Verify FLP TTFF Hot Start while tether with phone.
 
@@ -202,10 +178,8 @@
             flp_ttff_max_threshold.
             2. Watch uses phone's FLP location.
         """
-        self.flp_ttff("hs", self.flp_ttff_max_threshold,
-                      self.pixel_lab_location)
+        self.flp_ttff("hs", self.flp_ttff_max_threshold, self.pixel_lab_location)
 
-    @test_tracker_info(uuid="ca955ad3-e2eb-4fde-af2b-3e19abe47792")
     def test_tracking_during_bt_disconnect_resume(self):
         """Verify tracking is correct during Bluetooth disconnect and resume.
 
@@ -222,18 +196,17 @@
             2. Tracking results should be within pixel_lab_location criteria.
         """
         self.start_qxdm_and_tcpdump_log()
-        for i in range(1, 6):
+        for i in range(1, 4):
             if not self.watch.droid.bluetoothCheckState():
                 self.watch.droid.bluetoothToggleState(True)
                 self.watch.log.info("Turn Bluetooth on")
-                self.watch.log.info("Wait 1 min for Bluetooth auto re-connect")
-                time.sleep(60)
-            if not gutils.is_bluetooth_connect(self.watch, self.phone):
-                raise signals.TestFailure(
-                    "Fail to connect to device via Bluetooth.")
-            start_gnss_by_gtw_gpstool(self.phone, True, type="FLP")
+                self.watch.log.info("Wait 40s for Bluetooth auto re-connect")
+                time.sleep(40)
+            if not gutils.is_bluetooth_connected(self.watch, self.phone):
+                raise signals.TestFailure("Fail to connect to device via Bluetooth.")
+            start_gnss_by_gtw_gpstool(self.phone, True, api_type="FLP")
             time.sleep(self.flp_waiting_time)
-            start_gnss_by_gtw_gpstool(self.watch, True, type="FLP")
+            start_gnss_by_gtw_gpstool(self.watch, True, api_type="FLP")
             time.sleep(self.flp_waiting_time)
             self.watch.log.info("Wait 1 min for tracking")
             time.sleep(self.tracking_test_time)
@@ -245,14 +218,11 @@
             time.sleep(self.tracking_test_time)
             if self.check_location_from_phone():
                 raise signals.TestError("Watch should not use phone location")
-            gutils.parse_gtw_gpstool_log(self.watch,
-                                         self.pixel_lab_location,
-                                         type="FLP")
-            start_gnss_by_gtw_gpstool(self.phone, False, type="FLP")
+            gutils.parse_gtw_gpstool_log(self.watch, self.pixel_lab_location, api_type="FLP")
+            start_gnss_by_gtw_gpstool(self.phone, False, api_type="FLP")
 
-    @test_tracker_info(uuid="654a8f1b-f9c6-433e-a21f-59224cce822e")
-    def test_fast_start_first_fix_and_ttff(self):
-        """Verify first fix and TTFF of Fast Start (Warm Start v4) within the criteria
+    def test_oobe_first_fix(self):
+        """Verify first fix after OOBE pairing within the criteria
 
         Steps:
             1. Pair watch to phone during OOBE.
@@ -261,33 +231,78 @@
             4. Enable AirPlane mode to untether to phone.
             5. Open GPSTool to get first fix in LTO and UTC time injected.
             6. Repeat Step1 ~ Step5 for 5 times.
-            7. After Step6, Warm Start TTFF for 10 iterations.
 
         Expected Results:
-            1. First fix should be within fast_start_threshold.
-            2. TTFF should be within fast_start_threshold.
+            1. First fix should be within far_start_threshold.
         """
-        for i in range(1, 6):
-            self.watch.log.info("First fix of Fast Start - attempts %s" % i)
+        oobe_results_all = []
+        for i in range(1,4):
+            self.watch.log.info("First fix after OOBE pairing - attempts %s" % i)
             pair_to_wearable(self.watch, self.phone)
-            gutils.enable_framework_log(self.watch)
             self.start_qxdm_and_tcpdump_log()
             begin_time = get_current_epoch_time()
             gutils.check_xtra_download(self.watch, begin_time)
             gutils.check_inject_time(self.watch)
             self.watch.log.info("Turn airplane mode on")
-            self.watch.droid.connectivityToggleAirplaneMode(True)
-            self.watch.unlock_screen(password=None)
-            gutils.process_gnss_by_gtw_gpstool(self.watch,
-                                               self.fast_start_criteria,
-                                               clear_data=False)
-        gutils.start_ttff_by_gtw_gpstool(self.watch,
-                                         ttff_mode="ws",
-                                         iteration=self.ttff_test_cycle)
-        ttff_data = gutils.process_ttff_by_gtw_gpstool(self.watch, begin_time,
-                                                       self.pixel_lab_location)
-        result = gutils.check_ttff_data(self.watch,
-                                        ttff_data,
-                                        self.ttff_mode.get("ws"),
-                                        criteria=self.fast_start_criteria)
-        asserts.assert_true(result, "TTFF fails to reach designated criteria")
+            # self.watch.droid.connectivityToggleAirplaneMode(True)
+            toggle_airplane_mode(self.watch.log, self.watch, new_state=True)
+            oobe_results = gutils.process_gnss(
+                self.watch, self.far_start_criteria, clear_data=False)
+            oobe_results_all.append(oobe_results)
+        self.watch.log.info(f"TestResult Max_OOBE_First_Fix {max(oobe_results_all)}")
+        self.watch.log.info(f"TestResult Avg_OOBE_First_Fix {statistics.mean(oobe_results_all)}")
+        # self.watch.droid.connectivityToggleAirplaneMode(False)
+        toggle_airplane_mode(self.watch.log, self.watch, new_state=False)
+        self.watch.log.info("Turn airplane mode off")
+
+    def test_oobe_first_fix_with_network_connection(self):
+        """Verify first fix after OOBE pairing within the criteria
+
+        Steps:
+            1. Pair watch to phone during OOBE.
+            2. Ensure LTO file download in watch.
+            3. Ensure UTC time inject in watch.
+            4. Turn off Bluetooth to untether to phone.
+            5. Open GPSTool to get first fix in LTO and UTC time injected.
+            6. Repeat Step1 ~ Step5 for 5 times.
+
+        Expected Results:
+            1. First fix should be within far_start_threshold.
+        """
+        oobe_results_all = []
+        for i in range(1,4):
+            self.watch.log.info("First fix after OOBE pairing - attempts %s" % i)
+            pair_to_wearable(self.watch, self.phone)
+            self.start_qxdm_and_tcpdump_log()
+            begin_time = get_current_epoch_time()
+            gutils.check_xtra_download(self.watch, begin_time)
+            gutils.check_inject_time(self.watch)
+            self.watch.log.info("Turn off Bluetooth to disconnect to phone")
+            self.watch.droid.bluetoothToggleState(False)
+            oobe_results = gutils.process_gnss(
+                self.watch, self.far_start_criteria, clear_data=False)
+            oobe_results_all.append(oobe_results)
+        self.watch.log.info(f"TestResult Max_OOBE_First_Fix {max(oobe_results_all)}")
+        self.watch.log.info(f"TestResult Avg_OOBE_First_Fix {statistics.mean(oobe_results_all)}")
+
+    def test_far_start_ttff(self):
+        """Verify Far Start (Warm Start v4) TTFF within the criteria
+
+        Steps:
+            1. Pair watch to phone during OOBE.
+            2. Ensure LTO file download in watch.
+            3. Ensure UTC time inject in watch.
+            4. Enable AirPlane mode to untether to phone.
+            5. TTFF Warm Start for 10 iteration.
+
+        Expected Results:
+            1. TTFF should be within far_start_threshold.
+        """
+        pair_to_wearable(self.watch, self.phone)
+        self.start_qxdm_and_tcpdump_log()
+        begin_time = get_current_epoch_time()
+        gutils.check_xtra_download(self.watch, begin_time)
+        gutils.check_inject_time(self.watch)
+        self.watch.log.info("Turn airplane mode on")
+        toggle_airplane_mode(self.watch.log, self.watch, new_state=True)
+        self.run_ttff_via_gtw_gpstool("ws", self.far_start_criteria)
diff --git a/acts_tests/tests/google/gnss/LabGnssPowerSweepTest.py b/acts_tests/tests/google/gnss/LabGnssPowerSweepTest.py
new file mode 100644
index 0000000..f641cc4
--- /dev/null
+++ b/acts_tests/tests/google/gnss/LabGnssPowerSweepTest.py
@@ -0,0 +1,324 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2020 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import os
+from acts_contrib.test_utils.gnss.GnssBlankingBase import GnssBlankingBase
+from collections import namedtuple
+from acts_contrib.test_utils.gnss.LabTtffTestBase import LabTtffTestBase
+from acts_contrib.test_utils.gnss.gnss_test_utils import detect_crash_during_tracking, gnss_tracking_via_gtw_gpstool, \
+                                    start_gnss_by_gtw_gpstool, process_ttff_by_gtw_gpstool, calculate_position_error
+from acts.context import get_current_context
+from acts.utils import get_current_epoch_time
+from time import sleep
+import csv
+import matplotlib.pyplot as plt
+from mpl_toolkits.mplot3d.axes3d import Axes3D
+import statistics
+
+
+class LabGnssPowerSweepTest(GnssBlankingBase):
+
+    def gnss_plot_2D_result(self, position_error):
+        """Plot 2D position error result
+        """
+        x_axis = []
+        y_axis = []
+        z_axis = []
+        for key in position_error:
+            tmp = key.split('_')
+            l1_pwr = float(tmp[1])
+            l5_pwr = float(tmp[3])
+            position_error_value = position_error[key]
+            x_axis.append(l1_pwr)
+            y_axis.append(l5_pwr)
+            z_axis.append(position_error_value)
+
+        fig = plt.figure(figsize=(12, 7))
+        ax = plt.axes(projection='3d')
+        ax.scatter(x_axis, y_axis, z_axis)
+        plt.title("Z axis Position Error", fontsize=12)
+        plt.xlabel("L1 PWR (dBm)", fontsize=12)
+        plt.ylabel("L5 PWR (dBm)", fontsize=12)
+        plt.show()
+        path_name = os.path.join(self.gnss_log_path, 'result.png')
+        plt.savefig(path_name)
+
+    def gnss_wait_for_ephemeris_download(self):
+        """Launch GTW GPSTool and Clear all GNSS aiding data
+           Start GNSS tracking on GTW_GPSTool.
+           Wait for "first_wait" at simulator power = "power_level" to download Ephemeris
+        """
+        first_wait = self.user_params.get('first_wait', 300)
+        LabTtffTestBase.start_set_gnss_power(self)
+        self.start_gnss_and_wait(first_wait)
+
+    def gnss_check_fix(self, json_tag):
+        """Launch GTW GPSTool and check position fix or not
+          Returns:
+            True : Can fix within 120 sec
+            False
+        """
+        # Check Latitude for fix
+        self.dut.log.info("Restart GTW GPSTool in gnss_check_fix")
+        start_gnss_by_gtw_gpstool(self.dut, state=True)
+        begin_time = get_current_epoch_time()
+        if not self.dut.is_adb_logcat_on:
+            self.dut.start_adb_logcat()
+        while True:
+            if get_current_epoch_time() - begin_time >= 120000:
+                self.dut.log.info("Location fix timeout in gnss_check_fix")
+                start_gnss_by_gtw_gpstool(self.dut, state=False)
+                json_tag = json_tag + '_gnss_check_fix_timeout'
+                self.dut.cat_adb_log(tag=json_tag,
+                                     begin_time=begin_time,
+                                     end_time=None,
+                                     dest_path=self.gnss_log_path)
+                return False
+            sleep(1)
+            logcat_results = self.dut.search_logcat("Latitude", begin_time)
+            if logcat_results:
+                self.dut.log.info("Location fix successfully in gnss_check_fix")
+                json_tag = json_tag + '_gnss_check_fix_success'
+                self.dut.cat_adb_log(tag=json_tag,
+                                     begin_time=begin_time,
+                                     end_time=None,
+                                     dest_path=self.gnss_log_path)
+                return True
+
+    def gnss_check_l5_engaging(self, json_tag):
+        """check L5 engaging
+          Returns:
+            True : L5 engaged
+            False
+        """
+        # Check L5 engaging rate
+        begin_time = get_current_epoch_time()
+        if not self.dut.is_adb_logcat_on:
+            self.dut.start_adb_logcat()
+        while True:
+            if get_current_epoch_time() - begin_time >= 120000:
+                self.dut.log.info(
+                    "L5 engaging timeout in gnss_check_l5_engaging")
+                start_gnss_by_gtw_gpstool(self.dut, state=False)
+                json_tag = json_tag + '_gnss_check_l5_engaging_timeout'
+                self.dut.cat_adb_log(tag=json_tag,
+                                     begin_time=begin_time,
+                                     end_time=None,
+                                     dest_path=self.gnss_log_path)
+                return False
+            sleep(1)
+            logcat_results = self.dut.search_logcat("L5 engaging rate:",
+                                                    begin_time)
+            if logcat_results:
+                start_idx = logcat_results[-1]['log_message'].find(
+                    "L5 engaging rate:")
+                tmp = logcat_results[-1]['log_message'][(start_idx + 18):]
+                l5_engaging_rate = float(tmp.strip('%'))
+
+                if l5_engaging_rate != 0:
+                    self.dut.log.info("L5 engaged")
+                    json_tag = json_tag + '_gnss_check_l5_engaging_success'
+                    self.dut.cat_adb_log(tag=json_tag,
+                                         begin_time=begin_time,
+                                         end_time=None,
+                                         dest_path=self.gnss_log_path)
+                    return True
+
+    def gnss_check_position_error(self, json_tag):
+        """check position error
+          Returns:
+            position error average value
+        """
+        average_position_error_count = 60
+        position_error_all = []
+        hacc_all = []
+        default_position_error_mean = 6666
+        default_position_error_std = 6666
+        default_hacc_mean = 6666
+        default_hacc_std = 6666
+        idx = 0
+        begin_time = get_current_epoch_time()
+        if not self.dut.is_adb_logcat_on:
+            self.dut.start_adb_logcat()
+        while True:
+            if get_current_epoch_time() - begin_time >= 120000:
+                self.dut.log.info(
+                    "Position error calculation timeout in gnss_check_position_error"
+                )
+                start_gnss_by_gtw_gpstool(self.dut, state=False)
+                json_tag = json_tag + '_gnss_check_position_error_timeout'
+                self.dut.cat_adb_log(tag=json_tag,
+                                     begin_time=begin_time,
+                                     end_time=None,
+                                     dest_path=self.gnss_log_path)
+                return default_position_error_mean, default_position_error_std, default_hacc_mean, default_hacc_std
+            sleep(1)
+            gnss_results = self.dut.search_logcat("GPSService: Check item",
+                                                  begin_time)
+            if gnss_results:
+                self.dut.log.info(gnss_results[-1]["log_message"])
+                gnss_location_log = \
+                    gnss_results[-1]["log_message"].split()
+                ttff_lat = float(gnss_location_log[8].split("=")[-1].strip(","))
+                ttff_lon = float(gnss_location_log[9].split("=")[-1].strip(","))
+                loc_time = int(gnss_location_log[10].split("=")[-1].strip(","))
+                ttff_haccu = float(
+                    gnss_location_log[11].split("=")[-1].strip(","))
+                hacc_all.append(ttff_haccu)
+                position_error = calculate_position_error(
+                    ttff_lat, ttff_lon, self.simulator_location)
+                position_error_all.append(abs(position_error))
+                idx = idx + 1
+                if idx >= average_position_error_count:
+                    position_error_mean = statistics.mean(position_error_all)
+                    position_error_std = statistics.stdev(position_error_all)
+                    hacc_mean = statistics.mean(hacc_all)
+                    hacc_std = statistics.stdev(hacc_all)
+                    json_tag = json_tag + '_gnss_check_position_error_success'
+                    self.dut.cat_adb_log(tag=json_tag,
+                                         begin_time=begin_time,
+                                         end_time=None,
+                                         dest_path=self.gnss_log_path)
+                    return position_error_mean, position_error_std, hacc_mean, hacc_std
+
+    def gnss_tracking_L5_position_error_capture(self, json_tag):
+        """Capture position error after L5 engaged
+        Args:
+        Returns:
+            Position error with L5
+        """
+        self.dut.log.info('Start gnss_tracking_L5_position_error_capture')
+        fixed = self.gnss_check_fix(json_tag)
+        if fixed:
+            l5_engaged = self.gnss_check_l5_engaging(json_tag)
+            if l5_engaged:
+                position_error_mean, position_error_std, hacc_mean, hacc_std = self.gnss_check_position_error(
+                    json_tag)
+                start_gnss_by_gtw_gpstool(self.dut, state=False)
+            else:
+                position_error_mean = 8888
+                position_error_std = 8888
+                hacc_mean = 8888
+                hacc_std = 8888
+        else:
+            position_error_mean = 9999
+            position_error_std = 9999
+            hacc_mean = 9999
+            hacc_std = 9999
+            self.position_fix_timeout_cnt = self.position_fix_timeout_cnt + 1
+
+            if self.position_fix_timeout_cnt > (self.l1_sweep_cnt / 2):
+                self.l1_sensitivity_point = self.current_l1_pwr
+
+        return position_error_mean, position_error_std, hacc_mean, hacc_std
+
+    def gnss_power_tracking_loop(self):
+        """Launch GTW GPSTool and Clear all GNSS aiding data
+           Start GNSS tracking on GTW_GPSTool.
+
+        Args:
+
+        Returns:
+            True: First fix TTFF are within criteria.
+            False: First fix TTFF exceed criteria.
+        """
+        test_period = 60
+        type = 'gnss'
+        start_time = get_current_epoch_time()
+        start_gnss_by_gtw_gpstool(self.dut, state=True, type=type)
+        while get_current_epoch_time() - start_time < test_period * 1000:
+            detect_crash_during_tracking(self.dut, start_time, type)
+        stop_time = get_current_epoch_time()
+
+        return start_time, stop_time
+
+    def parse_tracking_log_cat(self, log_dir):
+        self.log.warning(f'Parsing log cat {log_dir} results into dataframe!')
+
+    def check_l5_points(self, gnss_pwr_swp):
+        cnt = 0
+        for kk in range(len(gnss_pwr_swp[1])):
+            if gnss_pwr_swp[1][kk][0] == gnss_pwr_swp[1][kk + 1][0]:
+                cnt = cnt + 1
+            else:
+                return cnt
+
+    def test_tracking_power_sweep(self):
+        # Create log file path
+        full_output_path = get_current_context().get_full_output_path()
+        self.gnss_log_path = os.path.join(full_output_path, '')
+        os.makedirs(self.gnss_log_path, exist_ok=True)
+        self.log.debug(f'Create log path: {self.gnss_log_path}')
+        csv_path = self.gnss_log_path + 'L1_L5_2D_search_result.csv'
+        csvfile = open(csv_path, 'w')
+        writer = csv.writer(csvfile)
+        writer.writerow([
+            "csv_result_tag", "position_error_mean", "position_error_std",
+            "hacc_mean", "hacc_std"
+        ])
+        # for L1 position fix early termination
+        self.l1_sensitivity_point = -999
+        self.enable_early_terminate = 1
+        self.position_fix_timeout_cnt = 0
+        self.current_l1_pwr = 0
+        self.l1_sweep_cnt = 0
+
+        self.gnss_wait_for_ephemeris_download()
+        l1_cable_loss = self.gnss_sim_params.get('L1_cable_loss')
+        l5_cable_loss = self.gnss_sim_params.get('L5_cable_loss')
+
+        for i, gnss_pwr_swp in enumerate(self.gnss_pwr_sweep_fine_sweep_ls):
+            self.log.info(f'Start fine GNSS power level sweep part {i + 1}')
+            self.l1_sweep_cnt = self.check_l5_points(gnss_pwr_swp)
+            for gnss_pwr_params in gnss_pwr_swp[1]:
+                json_tag = f'test_'
+                csv_result_tag = ''
+                for ii, pwr in enumerate(
+                        gnss_pwr_params):  # Setup L1 and L5 power
+                    sat_sys = gnss_pwr_swp[0][ii].get('sat').upper()
+                    band = gnss_pwr_swp[0][ii].get('band').upper()
+                    if band == "L1":
+                        pwr_biased = pwr + l1_cable_loss
+                        if pwr != self.current_l1_pwr:
+                            self.position_fix_timeout_cnt = 0
+                            self.current_l1_pwr = pwr
+                    elif band == "L5":
+                        pwr_biased = pwr + l5_cable_loss
+                    else:
+                        pwr_biased = pwr
+                    # Set GNSS Simulator power level
+                    self.gnss_simulator.ping_inst()
+                    self.gnss_simulator.set_scenario_power(
+                        power_level=pwr_biased,
+                        sat_system=sat_sys,
+                        freq_band=band)
+                    self.log.info(f'Set {sat_sys} {band} with power {pwr}')
+                    json_tag = json_tag + f'{sat_sys}_{band}_{pwr}'
+                    csv_result_tag = csv_result_tag + f'{band}_{pwr}_'
+
+                if self.current_l1_pwr < self.l1_sensitivity_point and self.enable_early_terminate == 1:
+                    position_error_mean = -1
+                    position_error_std = -1
+                    hacc_mean = -1
+                    hacc_std = -1
+                else:
+                    position_error_mean, position_error_std, hacc_mean, hacc_std = self.gnss_tracking_L5_position_error_capture(
+                        json_tag)
+                writer = csv.writer(csvfile)
+                writer.writerow([
+                    csv_result_tag, position_error_mean, position_error_std,
+                    hacc_mean, hacc_std
+                ])
+        csvfile.close()
diff --git a/acts_tests/tests/google/gnss/LabTtffGeneralCoexTest.py b/acts_tests/tests/google/gnss/LabTtffGeneralCoexTest.py
index 24da4d3..664c5b6 100644
--- a/acts_tests/tests/google/gnss/LabTtffGeneralCoexTest.py
+++ b/acts_tests/tests/google/gnss/LabTtffGeneralCoexTest.py
@@ -16,7 +16,8 @@
 
 from acts_contrib.test_utils.gnss import LabTtffTestBase as lttb
 from acts_contrib.test_utils.gnss.gnss_test_utils import launch_eecoexer
-from acts_contrib.test_utils.gnss.gnss_test_utils import excute_eecoexer_function
+from acts_contrib.test_utils.gnss.gnss_test_utils import execute_eecoexer_function
+
 
 
 class LabTtffGeneralCoexTest(lttb.LabTtffTestBase):
@@ -26,6 +27,8 @@
         super().setup_class()
         req_params = ['coex_testcase_ls']
         self.unpack_userparams(req_param_names=req_params)
+        self.test_cmd = ''
+        self.stop_cmd = ''
 
     def setup_test(self):
         super().setup_test()
@@ -34,16 +37,9 @@
         self.dut.adb.shell(
             'setprop persist.com.google.eecoexer.cellular.temperature_limit 60')
 
-    def exe_eecoexer_loop_cmd(self, cmd_list=list()):
-        """
-        Function for execute EECoexer command list
-            Args:
-                cmd_list: a list of EECoexer function command.
-                Type, list.
-        """
-        for cmd in cmd_list:
-            self.log.info('Execute EEcoexer Command: {}'.format(cmd))
-            excute_eecoexer_function(self.dut, cmd)
+    def teardown_test(self):
+        super().teardown_test()
+        self.exe_eecoexer_loop_cmd(self.stop_cmd)
 
     def gnss_ttff_ffpe_coex_base(self, mode):
         """
@@ -54,29 +50,35 @@
                 cs(cold start), ws(warm start), hs(hot start)
         """
         # Loop all test case in coex_testcase_ls
-        for test_item in self.coex_testcase_ls:
+        for i, test_item in enumerate(self.coex_testcase_ls):
+
+            if i > 0:
+                self.setup_test()
 
             # get test_log_path from coex_testcase_ls['test_name']
             test_log_path = test_item['test_name']
 
             # get test_cmd from coex_testcase_ls['test_cmd']
-            test_cmd = test_item['test_cmd']
+            self.test_cmd = test_item['test_cmd']
 
             # get stop_cmd from coex_testcase_ls['stop_cmd']
-            stop_cmd = test_item['stop_cmd']
+            self.stop_cmd = test_item['stop_cmd']
 
             # Start aggressor Tx by EEcoexer
-            self.exe_eecoexer_loop_cmd(test_cmd)
+            # self.exe_eecoexer_loop_cmd(test_cmd)
 
             # Start GNSS TTFF FFPE testing
-            self.gnss_ttff_ffpe(mode, test_log_path)
+            self.gnss_ttff_ffpe(mode, test_log_path, self.test_cmd, self.stop_cmd)
 
             # Stop aggressor Tx by EEcoexer
-            self.exe_eecoexer_loop_cmd(stop_cmd)
+            # self.exe_eecoexer_loop_cmd(stop_cmd)
 
             # Clear GTW GPSTool log. Need to clean the log every round of the test.
             self.clear_gps_log()
 
+            if i < len(self.coex_testcase_ls) - 1:
+                self.teardown_test()
+
     def test_gnss_cold_ttff_ffpe_coex(self):
         """
         Cold start TTFF and FFPE GNSS general coex testing
diff --git a/acts_tests/tests/google/gnss/LocationPlatinumTest.py b/acts_tests/tests/google/gnss/LocationPlatinumTest.py
index 6f4c253..a50acb4 100644
--- a/acts_tests/tests/google/gnss/LocationPlatinumTest.py
+++ b/acts_tests/tests/google/gnss/LocationPlatinumTest.py
@@ -67,7 +67,7 @@
         gutils.check_location_service(self.ad)
         if not self.ad.droid.wifiCheckState():
             wutils.wifi_toggle_state(self.ad, True)
-            gutils.connect_to_wifi_network(self.ad, self.wifi_network)
+        gutils.connect_to_wifi_network(self.ad, self.wifi_network)
 
     def get_and_verify_ttff(self, mode):
         """Retrieve ttff with designate mode.
diff --git a/acts_tests/tests/google/power/bt/PowerBLEadvertiseTest.py b/acts_tests/tests/google/power/bt/PowerBLEadvertiseTest.py
index 4f42ae0..27fca6b 100644
--- a/acts_tests/tests/google/power/bt/PowerBLEadvertiseTest.py
+++ b/acts_tests/tests/google/power/bt/PowerBLEadvertiseTest.py
@@ -25,6 +25,7 @@
 
 
 class PowerBLEadvertiseTest(PBtBT.PowerBTBaseTest):
+
     def __init__(self, configs):
         super().__init__(configs)
         req_params = ['adv_modes', 'adv_power_levels']
@@ -32,7 +33,9 @@
         # Loop all advertise modes and power levels
         for adv_mode in self.adv_modes:
             for adv_power_level in self.adv_power_levels:
-                self.generate_test_case(adv_mode, adv_power_level)
+                # As a temporary fix, set high power tests directly
+                if adv_power_level != 3:
+                    self.generate_test_case(adv_mode, adv_power_level)
 
     def setup_class(self):
 
@@ -58,3 +61,12 @@
                                    self.adv_duration)
         time.sleep(EXTRA_ADV_TIME)
         self.measure_power_and_validate()
+
+    def test_BLE_ADVERTISE_MODE_LOW_POWER_TX_POWER_HIGH(self):
+        self.measure_ble_advertise_power(0, 3)
+
+    def test_BLE_ADVERTISE_MODE_BALANCED_TX_POWER_HIGH(self):
+        self.measure_ble_advertise_power(1, 3)
+
+    def test_BLE_ADVERTISE_MODE_LOW_LATENCY_TX_POWER_HIGH(self):
+        self.measure_ble_advertise_power(2, 3)
diff --git a/acts_tests/tests/google/power/bt/PowerBLEscanTest.py b/acts_tests/tests/google/power/bt/PowerBLEscanTest.py
index 0aa5ad9..f76f796 100644
--- a/acts_tests/tests/google/power/bt/PowerBLEscanTest.py
+++ b/acts_tests/tests/google/power/bt/PowerBLEscanTest.py
@@ -30,9 +30,6 @@
         req_params = ['scan_modes']
         self.unpack_userparams(req_params)
 
-        for scan_mode in self.scan_modes:
-            self.generate_test_case_no_devices_around(scan_mode)
-
     def setup_class(self):
 
         super().setup_class()
@@ -54,3 +51,12 @@
         btputils.start_apk_ble_scan(self.dut, scan_mode, self.scan_duration)
         time.sleep(EXTRA_SCAN_TIME)
         self.measure_power_and_validate()
+
+    def test_BLE_SCAN_MODE_LOW_POWER_no_advertisers(self):
+        self.measure_ble_scan_power(0)
+
+    def test_BLE_SCAN_MODE_BALANCED_no_advertisers(self):
+        self.measure_ble_scan_power(1)
+
+    def test_BLE_SCAN_MODE_LOW_LATENCY_no_advertisers(self):
+        self.measure_ble_scan_power(2)
diff --git a/acts_tests/tests/google/power/bt/PowerBTa2dpTest.py b/acts_tests/tests/google/power/bt/PowerBTa2dpTest.py
index 419c418..7c882bd 100644
--- a/acts_tests/tests/google/power/bt/PowerBTa2dpTest.py
+++ b/acts_tests/tests/google/power/bt/PowerBTa2dpTest.py
@@ -39,7 +39,9 @@
         # Loop all codecs and tx power levels
         for codec_config in self.codecs:
             for tpl in self.tx_power_levels:
-                self.generate_test_case(codec_config, tpl)
+                # As a temporary fix, directly set the test with tpl 10
+                if tpl != 10:
+                    self.generate_test_case(codec_config, tpl)
 
     def setup_test(self):
         super().setup_test()
@@ -107,3 +109,9 @@
             codec_config['codec_type'], tpl))
         self.dut.droid.goToSleepNow()
         self.measure_power_and_validate()
+
+    def test_BTa2dp_AAC_codec_at_EPA_BF(self):
+        self.measure_a2dp_power(self.codecs[0], 10)
+
+    def test_BTa2dp_SBC_codec_at_EPA_BF(self):
+        self.measure_a2dp_power(self.codecs[1], 10)
\ No newline at end of file
diff --git a/acts_tests/tests/google/power/bt/PowerBTcalibrationTest.py b/acts_tests/tests/google/power/bt/PowerBTcalibrationTest.py
index c0bac23..db92276 100644
--- a/acts_tests/tests/google/power/bt/PowerBTcalibrationTest.py
+++ b/acts_tests/tests/google/power/bt/PowerBTcalibrationTest.py
@@ -73,3 +73,9 @@
         with open(self.log_file, 'w', newline='') as f:
             writer = csv.writer(f)
             writer.writerows(self.cal_matrix)
+
+    def teardown_test(self):
+        """
+        Clean up in teardown_test : Disable BQR
+        """
+        btutils.disable_bqr(self.dut)
diff --git a/acts_tests/tests/google/power/tel/PowerTelAirplaneMode_Test.py b/acts_tests/tests/google/power/tel/PowerTelAirplaneMode_Test.py
new file mode 100644
index 0000000..44fab02
--- /dev/null
+++ b/acts_tests/tests/google/power/tel/PowerTelAirplaneMode_Test.py
@@ -0,0 +1,36 @@
+#   Copyright 2022 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the 'License');
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an 'AS IS' BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import time
+
+import acts_contrib.test_utils.power.cellular.cellular_power_preset_base_test as PB
+
+
+class PowerTelAirplaneModeTest(PB.PowerCellularPresetLabBaseTest):
+
+    def power_tel_airplane_mode_test(self):
+        """Measure power while airplane mode is on. """
+        # Start airplane mode
+        self.cellular_dut.toggle_airplane_mode(True)
+
+        # Allow airplane mode to propagate
+        time.sleep(3)
+
+        # Measure power
+        self.collect_power_data()
+        # Check if power measurement is within the required values
+        self.pass_fail_check(self.avg_current)
+
+class PowerTelAirplaneMode_Test(PowerTelAirplaneModeTest):
+    def test_airplane_mode(self):
+        self.power_tel_airplane_mode_test()
\ No newline at end of file
diff --git a/acts_tests/tests/google/power/tel/PowerTelIdle_Preset_Test.py b/acts_tests/tests/google/power/tel/PowerTelIdle_Preset_Test.py
new file mode 100644
index 0000000..8efbc76
--- /dev/null
+++ b/acts_tests/tests/google/power/tel/PowerTelIdle_Preset_Test.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2022 - 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 acts_contrib.test_utils.power.cellular.cellular_power_preset_base_test as PB
+
+
+class PowerTelIdle_Preset_Test(PB.PowerCellularPresetLabBaseTest):
+    def power_tel_idle_test(self):
+        """ Measures power when the device is on RRC idle state."""
+        idle_wait_time = self.simulation.rrc_sc_timer + 30
+        # Wait for RRC status change to trigger
+        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(self.avg_current)
+
+    def test_preset_LTE_idle(self):
+        self.power_tel_idle_test()
+
+    def test_preset_sa_idle_fr1(self):
+        self.power_tel_idle_test()
diff --git a/acts_tests/tests/google/power/tel/PowerTelIms_Preset_Test.py b/acts_tests/tests/google/power/tel/PowerTelIms_Preset_Test.py
new file mode 100644
index 0000000..a18653d
--- /dev/null
+++ b/acts_tests/tests/google/power/tel/PowerTelIms_Preset_Test.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+#
+#   Copyright 20022 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the 'License');
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an 'AS IS' BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+import time
+
+from acts_contrib.test_utils.power.cellular.ims_api_connector_utils import ImsApiConnector
+import acts_contrib.test_utils.power.cellular.cellular_power_base_test as PWCEL
+from acts_contrib.test_utils.tel.tel_test_utils import set_phone_silent_mode
+from acts_contrib.test_utils.tel.tel_voice_utils import hangup_call
+import acts_contrib.test_utils.power.cellular.cellular_power_preset_base_test as PB
+
+
+class PowerTelImsPresetTest(PB.PowerCellularPresetLabBaseTest):
+    ADB_CMD_ENABLE_IMS = ('am broadcast '
+        '-a com.google.android.carrier.action.LOCAL_OVERRIDE '
+        '-n com.google.android.carrier/.ConfigOverridingReceiver '
+        '--ez carrier_volte_available_bool true '
+        '--ez carrier_wfc_ims_available_bool true '
+        '--ez carrier_vt_available_bool true '
+        '--ez carrier_supports_ss_over_ut_bool true '
+        '--ez vonr_setting_visibility_bool true '
+        '--ez vonr_enabled_bool true')
+
+    ADB_CMD_DISABLE_IMS = ('am broadcast '
+        '-a com.google.android.carrier.action.LOCAL_OVERRIDE '
+        '-n com.google.android.carrier/.ConfigOverridingReceiver '
+        '--ez carrier_volte_available_bool false '
+        '--ez carrier_wfc_ims_available_bool false '
+        '--ez carrier_vt_available_bool false '
+        '--ez carrier_supports_ss_over_ut_bool false '
+        '--ez vonr_setting_visibility_bool false '
+        '--ez vonr_enabled_bool false')
+
+    # set NV command
+    # !NRCAPA.Gen.VoiceOverNr, 0, 01
+    ADB_SET_GOOG_NV = 'echo at+googsetnv="{nv_name}",{index},"{value}" > /dev/umts_router'
+
+    # Key IMS simulator default value
+    IMS_CLIENT_DEFAULT_IP = '127.0.0.1'
+    IMS_CLIENT_DEFAULT_PORT = 8250
+    IMS_CLIENT_DEFAULT_API_TOKEN = 'myclient'
+    IMS_API_CONNECTOR_DEFAULT_PORT = 5050
+
+    # IMS available app
+    IMS_CLIENT = 'client'
+    IMS_SERVER = 'server'
+
+    def setup_class(self):
+        """ Executed only once when initializing the class. """
+        super().setup_class()
+
+        # disable mobile data
+        self.log.info('Disable mobile data.')
+        self.dut.adb.shell('svc data disable')
+
+        # Enable IMS on UE
+        self.log.info('Enable VoLTE using adb command.')
+        self.dut.adb.shell(self.ADB_CMD_ENABLE_IMS)
+
+        # reboot device for settings to update
+        self.log.info('Reboot for VoLTE settings to update.')
+        self.dut.reboot()
+
+        # Set voice call volume to minimum
+        set_phone_silent_mode(self.log, self.dut)
+
+        # initialize ims simulator connector wrapper
+        self.unpack_userparams(api_connector_port=self.IMS_API_CONNECTOR_DEFAULT_PORT,
+                               api_token=self.IMS_CLIENT_DEFAULT_API_TOKEN,
+                               ims_client_ip=self.IMS_CLIENT_DEFAULT_IP,
+                               ims_client_port=self.IMS_CLIENT_DEFAULT_PORT)
+        self.ims_client = ImsApiConnector(
+            self.uxm_ip,
+            self.api_connector_port,
+            self.IMS_CLIENT,
+            self.api_token,
+            self.ims_client_ip,
+            self.ims_client_port,
+            self.log
+        )
+
+    def setup_test(self):
+        # Enable NR if it is VoNR test case
+        self.log.info(f'test name: {self.test_name}')
+        if 'NR' in self.test_name:
+            self.log.info('Enable VoNR for UE.')
+            self.enable_ims_nr()
+        super().setup_test()
+
+    def power_ims_call_test(self):
+        """ Measures power during a VoLTE call.
+
+        Measurement step in this test. Starts the voice call and
+        initiates power measurement. Pass or fail is decided with a
+        threshold value. """
+        # create dedicated bearer
+        self.log.info('create dedicated bearer.')
+        if 'LTE' in self.test_name:
+            self.cellular_simulator.create_dedicated_bearer()
+
+        time.sleep(5)
+
+        # Initiate the voice call
+        self.log.info('Callbox initiates call to UE.')
+        self.ims_client.initiate_call('001010123456789')
+
+        time.sleep(5)
+
+        # pick up call
+        self.log.info('UE pick up call.')
+        self.dut.adb.shell('input keyevent KEYCODE_CALL')
+
+        # Mute the call
+        self.dut.droid.telecomCallMute()
+
+        # Turn of screen
+        self.dut.droid.goToSleepNow()
+
+        # Measure power
+        self.collect_power_data()
+
+        # End the call
+        hangup_call(self.log, self.dut)
+
+        # Check if power measurement is within the required values
+        self.pass_fail_check()
+
+    def teardown_test(self):
+        super().teardown_test()
+        #self.cellular_simulator.deregister_ue_ims()
+        self.ims_client.remove_ims_app_link()
+
+    def teardown_class(self):
+        super().teardown_class()
+        self.log.info('Disable IMS.')
+        self.dut.adb.shell(self.ADB_CMD_DISABLE_IMS)
+
+
+class PowerTelIms_Preset_Test(PowerTelImsPresetTest):
+    def test_preset_LTE_voice(self):
+        self.power_ims_call_test()
+
+    def test_preset_NR_voice(self):
+        self.power_ims_call_test()
diff --git a/acts_tests/tests/google/power/tel/PowerTelPdcchFr2_Preset_Test.py b/acts_tests/tests/google/power/tel/PowerTelPdcchFr2_Preset_Test.py
new file mode 100644
index 0000000..3f023f3
--- /dev/null
+++ b/acts_tests/tests/google/power/tel/PowerTelPdcchFr2_Preset_Test.py
@@ -0,0 +1,36 @@
+#   Copyright 2022 - 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 acts_contrib.test_utils.power.cellular.cellular_power_preset_base_test as PB
+
+
+class PowerTelPdcchFr2_Preset_Test(PB.PowerCellularPresetLabBaseTest):
+    def power_pdcch_test(self):
+        """ Measures power during PDCCH only.
+
+        There's nothing to do here other than starting the power measurement
+        and deciding for pass or fail, as the base class will handle attaching.
+        Requirements for this test are that mac padding is off and that the
+        inactivity timer is not enabled. """
+
+        # Measure power
+        self.collect_power_data()
+
+        # Check if power measurement is within the required values
+        self.pass_fail_check(self.avg_current)
+
+    def test_preset_nsa_cdrx_fr2(self):
+        self.power_pdcch_test()
+
+    def test_preset_nsa_pdcch_fr2(self):
+        self.power_pdcch_test()
diff --git a/acts_tests/tests/google/power/tel/PowerTelPdcch_Preset_Test.py b/acts_tests/tests/google/power/tel/PowerTelPdcch_Preset_Test.py
index 1ee069a..2704638 100644
--- a/acts_tests/tests/google/power/tel/PowerTelPdcch_Preset_Test.py
+++ b/acts_tests/tests/google/power/tel/PowerTelPdcch_Preset_Test.py
@@ -11,26 +11,38 @@
 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
+import acts_contrib.test_utils.power.cellular.cellular_power_preset_base_test as PB
 
-from acts import context
-import acts_contrib.test_utils.power.cellular.cellular_pdcch_power_test as cppt
+class PowerTelPdcch_Preset_Test(PB.PowerCellularPresetLabBaseTest):
+    def power_pdcch_test(self):
+        """ Measures power during PDCCH only.
 
+        There's nothing to do here other than starting the power measurement
+        and deciding for pass or fail, as the base class will handle attaching.
+        Requirements for this test are that mac padding is off and that the
+        inactivity timer is not enabled. """
 
-class PowerTelPdcch_Preset_Test(cppt.PowerTelPDCCHTest):
-    def test_preset_sa_pdcch(self):
+        # Measure power
+        self.collect_power_data()
+
+        # Check if power measurement is within the required values
+        self.pass_fail_check(self.avg_current)
+
+    def test_preset_sa_pdcch_fr1(self):
         self.power_pdcch_test()
 
-    def test_preset_nsa_pdcch(self):
+    def test_preset_nsa_pdcch_fr1(self):
         self.power_pdcch_test()
 
     def test_preset_LTE_pdcch(self):
         self.power_pdcch_test()
 
-    def test_preset_sa_cdrx(self):
+    def test_preset_sa_cdrx_fr1(self):
         self.power_pdcch_test()
 
-    def test_preset_nsa_cdrx(self):
+    def test_preset_nsa_cdrx_fr1(self):
         self.power_pdcch_test()
 
     def test_preset_LTE_cdrx(self):
         self.power_pdcch_test()
+
diff --git a/acts_tests/tests/google/power/tel/PowerTelTraffic_Preset_Test.py b/acts_tests/tests/google/power/tel/PowerTelTraffic_Preset_Test.py
index ee30986..d05c801 100644
--- a/acts_tests/tests/google/power/tel/PowerTelTraffic_Preset_Test.py
+++ b/acts_tests/tests/google/power/tel/PowerTelTraffic_Preset_Test.py
@@ -11,22 +11,24 @@
 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
+import os
+import time
 
-import paramiko
-import acts_contrib.test_utils.power.cellular.cellular_power_base_test as PWCEL
+import acts_contrib.test_utils.power.cellular.cellular_power_preset_base_test as PB
 
 
-class PowerTelTrafficPresetTest(PWCEL.PowerCellularLabBaseTest):
+class PowerTelTrafficPresetTest(PB.PowerCellularPresetLabBaseTest):
+    # command to enable mobile data
+    ADB_CMD_ENABLE_MOBILE_DATA = 'svc data enable'
+
     # command to start iperf server on UE
     START_IPERF_SV_UE_CMD = 'nohup > /dev/null 2>&1 sh -c "iperf3 -s -i1 -p5201 > /dev/null  &"'
 
     # command to start iperf server on UE
     # (require: 1.path to iperf exe 2.hostname/hostIP)
-    START_IPERF_CLIENT_UE_CMD = (
-        'nohup > /dev/null 2>&1 sh -c '
-        '"iperf3 -c {iperf_host_ip} -i1 -p5202 -w8m -t2000 > /dev/null &"')
+    START_IPERF_CLIENT_UE_CMD = 'nohup > /dev/null 2>&1 sh -c "iperf3 -c {iperf_host_ip} -i1 -p5202 -w8m -t2000 > /dev/null &"'
 
-    #command to start iperf server on host()
+    # command to start iperf server on host()
     START_IPERF_SV_HOST_CMD = '{exe_path}\\iperf3 -s -p5202'
 
     # command to start iperf client on host
@@ -34,58 +36,56 @@
     START_IPERF_CLIENT_HOST_CMD = (
         '{exe_path}\\iperf3 -c {ue_ip} -w16M -t1000 -p5201')
 
+    START_IPERF_CLIENT_HOST_CMD_FR2 = (
+        '{exe_path}\\iperf3 -c {ue_ip} -w16M -t1000 -p5201 -P32')
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.ssh_iperf_client = None
         self.ssh_iperf_server = None
+        self.iperf_out_err = {}
 
     def setup_class(self):
         super().setup_class()
 
         # Unpack test parameters used in this class
-        self.unpack_userparams(ssh_host_ip=None,
-                               host_username=None,
-                               client_ssh_private_key_file=None,
-                               iperf_exe_path=None,
+        self.unpack_userparams(iperf_exe_path=None,
                                ue_ip=None,
                                iperf_host_ip=None)
 
         # Verify required config
-        for param in ('ssh_host_ip', 'host_username', 'client_ssh_private_key_file',
-                  'iperf_exe_path', 'ue_ip', 'iperf_host_ip'):
+        for param in ('iperf_exe_path', 'ue_ip', 'iperf_host_ip'):
             if getattr(self, param) is None:
                 raise RuntimeError(
                     f'Parameter "{param}" is required to run this type of test')
 
     def setup_test(self):
         # Call parent method first to setup simulation
-        if not super().setup_test():
-            return False
+        super().setup_test()
 
         # setup ssh client
-        self.ssh_iperf_client = self._create_ssh_client()
-        self.ssh_iperf_server = self._create_ssh_client()
+        self.ssh_iperf_client = self.cellular_simulator.create_ssh_client()
+        self.ssh_iperf_server = self.cellular_simulator.create_ssh_client()
+
+        self.turn_on_mobile_data()
 
     def power_tel_traffic_test(self):
         """Measure power while data is transferring."""
         # Start data traffic
-        self.start_downlink_process()
         self.start_uplink_process()
+        time.sleep(5)
+        self.start_downlink_process()
 
         # Measure power
         self.collect_power_data()
 
-    def _create_ssh_client(self):
-        """Create a ssh client to host."""
-        ssh = paramiko.SSHClient()
-        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-        mykey = paramiko.Ed25519Key.from_private_key_file(
-            self.client_ssh_private_key_file)
-        ssh.connect(hostname=self.ssh_host_ip,
-                    username=self.host_username,
-                    pkey=mykey)
-        self.log.info('SSH client to %s is connected' % self.ssh_host_ip)
-        return ssh
+        # Write iperf log
+        self.ssh_iperf_server.close()
+        uplink_log_name = self.test_name + '_uplink.txt'
+        self._write_iperf_log(uplink_log_name, self.ssh_iperf_server)
+        self.ssh_iperf_client.close()
+        downlink_log_name = self.test_name + '_downlink.txt'
+        self._write_iperf_log(downlink_log_name, self.ssh_iperf_client)
 
     def _exec_ssh_cmd(self, ssh_client, cmd):
         """Execute command on given ssh client.
@@ -95,24 +95,34 @@
             cmd: command to execute via ssh.
         """
         self.log.info('Sending cmd to ssh host: ' + cmd)
-        stdin, _, _ = ssh_client.exec_command(cmd, get_pty=True)
+        stdin, stdout, stderr = ssh_client.exec_command(cmd, get_pty=True)
         stdin.close()
-        # TODO: stdout.readline cause program to hang
-        # implement a workaround to getting response
-        # from executed command
+        self.iperf_out_err[ssh_client] = (stdout, stderr)
 
     def start_downlink_process(self):
         """UE transfer data to host."""
         self.log.info('Start downlink process')
         # start UE iperf server
         self.cellular_dut.ad.adb.shell(self.START_IPERF_SV_UE_CMD)
+        self.log.info('cmd sent to UE: ' + self.START_IPERF_SV_UE_CMD)
         self.log.info('UE iperf server started')
+        time.sleep(5)
         # start host iperf client
-        cmd = self.START_IPERF_CLIENT_HOST_CMD.format(
-            exe_path=self.iperf_exe_path,
-            ue_ip=self.ue_ip)
+        cmd = None
+        if 'fr2' in self.test_name:
+            cmd = self.START_IPERF_CLIENT_HOST_CMD_FR2.format(
+                exe_path=self.iperf_exe_path,
+                ue_ip=self.ue_ip)
+        else:
+            cmd = self.START_IPERF_CLIENT_HOST_CMD.format(
+                exe_path=self.iperf_exe_path,
+                ue_ip=self.ue_ip)
+
+        if not cmd:
+            raise RuntimeError('Cannot format command to start iperf client.')
         self._exec_ssh_cmd(self.ssh_iperf_client, cmd)
         self.log.info('Host iperf client started')
+        time.sleep(5)
 
     def start_uplink_process(self):
         """Host transfer data to UE."""
@@ -121,13 +131,48 @@
         cmd = self.START_IPERF_SV_HOST_CMD.format(exe_path=self.iperf_exe_path)
         self._exec_ssh_cmd(self.ssh_iperf_server, cmd)
         self.log.info('Host iperf server started')
+        time.sleep(5)
         # start UE iperf
-        self.cellular_dut.ad.adb.shell(
-            self.START_IPERF_CLIENT_UE_CMD.format(iperf_host_ip=self.iperf_host_ip))
+        adb_cmd = self.START_IPERF_CLIENT_UE_CMD.format(
+            iperf_host_ip=self.iperf_host_ip)
+        self.cellular_dut.ad.adb.shell(adb_cmd)
+        self.log.info('cmd sent to UE: ' + adb_cmd)
         self.log.info('UE iperf client started')
+        time.sleep(5)
+
+    def _write_iperf_log(self, file_name, ssh):
+        """ Writing ssh stdout and stdin to log file.
+
+        Args:
+            file_name: log file name to write log to.
+            ssh: paramiko client object.
+        """
+        iperf_log_dir = os.path.join(self.root_output_path, 'iperf')
+        os.makedirs(iperf_log_dir, exist_ok=True)
+        iperf_log_file_path = os.path.join(iperf_log_dir, file_name)
+        with open(iperf_log_file_path, 'w') as f:
+            out, err = self.iperf_out_err[ssh]
+            out_content = ''.join(out.readlines())
+            err_content = ''.join(err.readlines())
+            f.write(out_content)
+            f.write('\nErrors:\n')
+            f.write(err_content)
+
+    def turn_on_mobile_data(self):
+        self.dut.adb.shell(self.ADB_CMD_ENABLE_MOBILE_DATA)
 
 
 class PowerTelTraffic_Preset_Test(PowerTelTrafficPresetTest):
-
     def test_preset_LTE_traffic(self):
         self.power_tel_traffic_test()
+
+    def test_preset_nsa_traffic_fr1(self):
+        self.power_tel_traffic_test()
+
+    def test_preset_sa_traffic_fr1(self):
+        self.power_tel_traffic_test()
+
+
+class PowerTelTrafficFr2_Preset_Test(PowerTelTrafficPresetTest):
+    def test_preset_nsa_traffic_fr2(self):
+        self.power_tel_traffic_test()
diff --git a/acts_tests/tests/google/power/wifi/PowerWiFiHotspotTest.py b/acts_tests/tests/google/power/wifi/PowerWiFiHotspotTest.py
index 7e7c3b8..e0013b1 100644
--- a/acts_tests/tests/google/power/wifi/PowerWiFiHotspotTest.py
+++ b/acts_tests/tests/google/power/wifi/PowerWiFiHotspotTest.py
@@ -23,6 +23,7 @@
 from acts_contrib.test_utils.wifi import wifi_power_test_utils as wputils
 from acts_contrib.test_utils.power.IperfHelper import IperfHelper
 
+WAIT_TIME_BEFORE_CONNECT = 10
 
 class PowerWiFiHotspotTest(PWBT.PowerWiFiBaseTest):
     """ WiFi Tethering HotSpot Power test.
@@ -60,6 +61,7 @@
         Configures the Hotspot SSID
         """
         super().setup_class()
+        self.set_attenuation([0, 0, 0, 0])
 
         # If an SSID and password are indicated in the configuration parameters,
         # use those. If not, use default parameters and warn the user.
@@ -101,6 +103,15 @@
         super().setup_test()
         wutils.reset_wifi(self.android_devices[1])
 
+    def teardown_test(self):
+        # Tearing down tethering on dut
+        if self.dut.droid.isSdkAtLeastS():
+            hotspot_cmd = "cmd wifi stop-softap " + str(self.network[wutils.WifiEnums.SSID_KEY]) + \
+                          " wpa2 " + str(self.network[wutils.WifiEnums.PWD_KEY]) + " -b " + \
+                          str(list(self.test_configs.band)[0])
+            self.dut.adb.shell(hotspot_cmd)
+            wutils.reset_wifi(self.android_devices[1])
+
     def setup_hotspot(self, connect_client=False):
         """Configure Hotspot and connects client device.
 
@@ -134,12 +145,21 @@
                 self.main_network[self.test_configs.wifi_sharing])
 
         # Setup tethering on dut
-        wutils.start_wifi_tethering(
-            self.dut, self.network[wutils.WifiEnums.SSID_KEY],
-            self.network[wutils.WifiEnums.PWD_KEY], wifi_band_id)
+        if self.dut.droid.isSdkAtLeastS():
+            # Setup tethering on dut with adb command.
+            hotspot_cmd = "cmd wifi start-softap " + str(self.network[wutils.WifiEnums.SSID_KEY]) + \
+                          " wpa2 " + str(self.network[wutils.WifiEnums.PWD_KEY]) + " -b " + \
+                          str(list(self.test_configs.band)[0])
+            self.log.info(str(hotspot_cmd))
+            self.dut.adb.shell(hotspot_cmd)
+        else:
+            wutils.start_wifi_tethering(
+                self.dut, self.network[wutils.WifiEnums.SSID_KEY],
+                self.network[wutils.WifiEnums.PWD_KEY], wifi_band_id)
 
         # Connect client device to Hotspot
         if connect_client:
+            time.sleep(WAIT_TIME_BEFORE_CONNECT)
             wutils.wifi_connect(
                 self.android_devices[1],
                 self.network,
@@ -168,7 +188,14 @@
             self.client_iperf_helper.process_iperf_results(
                 self.dut, self.log, self.iperf_servers, self.test_name)
 
-        self.pass_fail_check(self.avg_current)
+        if hasattr(self, 'bitses'):
+            """
+            If measurement is taken through BITS, metric value is avg_power,
+            else metric value is avg_current.
+            """
+            self.pass_fail_check(self.power_result.metric_value)
+        else:
+            self.pass_fail_check(self.avg_current)
 
     def power_idle_tethering_test(self):
         """ Start power test when Hotspot is idle
diff --git a/acts_tests/tests/google/power/wifi/PowerWiFiscanTest.py b/acts_tests/tests/google/power/wifi/PowerWiFiscanTest.py
index 72f733f..da1c67d 100644
--- a/acts_tests/tests/google/power/wifi/PowerWiFiscanTest.py
+++ b/acts_tests/tests/google/power/wifi/PowerWiFiscanTest.py
@@ -18,10 +18,20 @@
 from acts.test_decorators import test_tracker_info
 from acts_contrib.test_utils.power import PowerWiFiBaseTest as PWBT
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
+from acts_contrib.test_utils.wifi.wifi_power_test_utils import CHRE_WIFI_SCAN_TYPE
+from acts_contrib.test_utils.wifi.wifi_power_test_utils import CHRE_WIFI_RADIO_CHAIN
+from acts_contrib.test_utils.wifi.wifi_power_test_utils import CHRE_WIFI_CHANNEL_SET
 
 UNLOCK_SCREEN = 'input keyevent 82'
 LOCATION_ON = 'settings put secure location_mode 3'
-
+ENABLE_WIFI_SCANNING = 'cmd wifi enable-scanning enabled'
+DISABLE_WIFI_SCANNING = 'cmd wifi enable-scanning disabled'
+CHRE_POWER_TEST_CLIENT = 'chre_power_test_client'
+UNLOAD_ALL_CHRE_PRODUCTION_APP = CHRE_POWER_TEST_CLIENT + ' unloadall'
+LOAD_CHRE_TEST_NANOAPP = CHRE_POWER_TEST_CLIENT + ' load'
+DISABLE_CHRE_WIFI_SCAN = CHRE_POWER_TEST_CLIENT + ' wifi disable'
+CHRE_SCAN_INTERVAL = 5
+NS = 1000000000
 
 class PowerWiFiscanTest(PWBT.PowerWiFiBaseTest):
     def setup_class(self):
@@ -71,6 +81,25 @@
                 atten_setting = self.test_configs.wifi_band + '_roaming'
                 self.set_attenuation(self.atten_level[atten_setting])
 
+    def chre_scan_setup(self):
+        self.dut.adb.shell("cmd wifi force-country-code enabled US")
+        time.sleep(1)
+        country_code = self.dut.adb.shell("cmd wifi get-country-code")
+        if "US" in country_code:
+            self.log.info("Country-Code is set to US")
+        else:
+            self.log.warning("Country Code is : " + str(country_code))
+        self.dut.adb.shell(DISABLE_WIFI_SCANNING)
+        self.dut.adb.shell(UNLOAD_ALL_CHRE_PRODUCTION_APP)
+        self.dut.adb.shell(LOAD_CHRE_TEST_NANOAPP)
+        chre_wifi_scan_trigger_cmd = CHRE_POWER_TEST_CLIENT + ' wifi enable ' + \
+                                     str(CHRE_SCAN_INTERVAL*NS) + \
+                                     " " + CHRE_WIFI_SCAN_TYPE[self.test_configs.wifi_scan_type] + " " + \
+                                     CHRE_WIFI_RADIO_CHAIN[self.test_configs.wifi_radio_chain] + " " + \
+                                     CHRE_WIFI_CHANNEL_SET[self.test_configs.wifi_channel_set]
+        self.dut.log.info(chre_wifi_scan_trigger_cmd)
+        self.dut.adb.shell(chre_wifi_scan_trigger_cmd)
+
     def wifi_scan_test_func(self):
 
         attrs = [
@@ -94,6 +123,22 @@
         self.scan_setup()
         self.measure_power_and_validate()
 
+    def chre_wifi_scan_test_func(self):
+        attrs = [
+            'screen_status', 'wifi_scan_type', 'wifi_radio_chain', 'wifi_channel_set'
+        ]
+        indices = [2, 6, 7, 8]
+        self.decode_test_configs(attrs, indices)
+        wutils.wifi_toggle_state(self.dut, True)
+        if self.test_configs.screen_status == 'OFF':
+            self.dut.droid.goToSleepNow()
+            self.dut.log.info('Screen is OFF')
+        time.sleep(5)
+        self.chre_scan_setup()
+        self.measure_power_and_validate()
+        self.dut.adb.shell(DISABLE_CHRE_WIFI_SCAN)
+        self.dut.adb.shell(ENABLE_WIFI_SCANNING)
+
     # Test cases
     # Power.apk triggered singleshot scans
     @test_tracker_info(uuid='e5539b01-e208-43c6-bebf-6f1e73d8d8cb')
@@ -157,3 +202,52 @@
 
         """
         self.wifi_scan_test_func()
+
+    def test_screen_OFF_CHRE_wifi_scan_activePassiveDfs_highAccuracy_all(self):
+        """
+        Trigger CHRE based scan for the following parameters :
+        wifi_scan_type : activePassiveDfs
+        wifi_radio_chain : highAccuracy
+        wifi_channel_set : all
+        """
+        self.chre_wifi_scan_test_func()
+
+
+    def test_screen_OFF_CHRE_wifi_scan_activePassiveDfs_lowLatency_all(self):
+        """
+        Trigger CHRE based scan for the following parameters :
+        wifi_scan_type : activePassiveDfs
+        wifi_radio_chain : lowLatency
+        wifi_channel_set : all
+        """
+        self.chre_wifi_scan_test_func()
+
+
+    def test_screen_OFF_CHRE_wifi_scan_noPreference_lowPower_all(self):
+        """
+        Trigger CHRE based scan for the following parameters :
+        wifi_scan_type : noPreference
+        wifi_radio_chain : lowPower
+        wifi_channel_set : all
+        """
+        self.chre_wifi_scan_test_func()
+
+
+    def test_screen_OFF_CHRE_wifi_scan_passive_lowLatency_all(self):
+        """
+        Trigger CHRE based scan for the following parameters :
+        wifi_scan_type : passive
+        wifi_radio_chain : lowLatency
+        wifi_channel_set : all
+        """
+        self.chre_wifi_scan_test_func()
+
+
+    def test_screen_OFF_CHRE_wifi_scan_passive_highAccuracy_all(self):
+        """
+        Trigger CHRE based scan for the following parameters :
+        wifi_scan_type : passive
+        wifi_radio_chain : highAccuracy
+        wifi_channel_set : all
+        """
+        self.chre_wifi_scan_test_func()
\ No newline at end of file
diff --git a/acts_tests/tests/google/wifi/WifiBridgedApTest.py b/acts_tests/tests/google/wifi/WifiBridgedApTest.py
index 01cb744..d98a7cb 100644
--- a/acts_tests/tests/google/wifi/WifiBridgedApTest.py
+++ b/acts_tests/tests/google/wifi/WifiBridgedApTest.py
@@ -57,7 +57,10 @@
             self.client1 = self.android_devices[1]
             self.client2 = self.android_devices[2]
         else:
-            raise signals.TestFailure("WifiBridgedApTest requires 3 DUTs")
+            raise signals.TestAbortClass("WifiBridgedApTest requires 3 DUTs")
+
+        if not self.dut.droid.wifiIsBridgedApConcurrencySupported():
+            raise signals.TestAbortClass("Legacy phone is not supported")
 
         req_params = ["dbs_supported_models"]
         opt_param = []
@@ -1329,4 +1332,4 @@
             self.dut, [WifiEnums.WIFI_CONFIG_SOFTAP_BAND_2G,
                        WifiEnums.WIFI_CONFIG_SOFTAP_BAND_5G], False)
         # Restore config
-        wutils.save_wifi_soft_ap_config(self.dut, original_softap_config)
+        wutils.save_wifi_soft_ap_config(self.dut, original_softap_config)
\ No newline at end of file
diff --git a/acts_tests/tests/google/wifi/WifiPingTest.py b/acts_tests/tests/google/wifi/WifiPingTest.py
index 096a935..ca1ccd7 100644
--- a/acts_tests/tests/google/wifi/WifiPingTest.py
+++ b/acts_tests/tests/google/wifi/WifiPingTest.py
@@ -504,26 +504,56 @@
         """
         # Configure AP
         self.setup_ap(testcase_params)
-        # Set attenuator to 0 dB
+        # Set attenuator to starting attenuation
+        band = wputils.CHANNEL_TO_BAND_MAP[testcase_params['channel']]
         for attenuator in self.attenuators:
-            attenuator.set_atten(testcase_params['atten_range'][0],
-                                 strict=False,
-                                 retry=True)
+            attenuator.set_atten(
+                self.testclass_params['range_atten_start'].get(band, 0),
+                strict=False,
+                retry=True)
         # Reset, configure, and connect DUT
         self.setup_dut(testcase_params)
 
     def get_range_start_atten(self, testcase_params):
         """Gets the starting attenuation for this ping test.
 
-        This function is used to get the starting attenuation for ping range
-        tests. This implementation returns the default starting attenuation,
-        however, defining this function enables a more involved configuration
-        for over-the-air test classes.
+        The function gets the starting attenuation by checking whether a test
+        at the same configuration has executed. If so it sets the starting
+        point a configurable number of dBs below the reference test.
 
         Args:
-            testcase_params: dict containing all test params
+            testcase_params: dict containing all test parameters
+        Returns:
+            start_atten: starting attenuation for current test
         """
-        return self.testclass_params['range_atten_start']
+        band = wputils.CHANNEL_TO_BAND_MAP[testcase_params['channel']]
+        # If the test is being retried, start from the beginning
+        if self.retry_flag:
+            self.log.info('Retry flag set. Setting attenuation to minimum.')
+            return self.testclass_params['range_atten_start'].get(band, 0)
+        # Get the current and reference test config. The reference test is the
+        # one performed at the current MCS+1
+        ref_test_params = wputils.extract_sub_dict(
+            testcase_params, testcase_params['reference_params'])
+        # Check if reference test has been run and set attenuation accordingly
+        previous_params = [
+            wputils.extract_sub_dict(result['testcase_params'],
+                                     testcase_params['reference_params'])
+            for result in self.testclass_results
+        ]
+        try:
+            ref_index = previous_params[::-1].index(ref_test_params)
+            ref_index = len(previous_params) - 1 - ref_index
+            start_atten = self.testclass_results[ref_index][
+                'atten_at_range'] - (
+                    self.testclass_params['adjacent_range_test_gap'])
+        except ValueError:
+            start_atten = self.testclass_params['range_atten_start'].get(
+                band, 0)
+            self.log.info(
+                'Reference test not found. Starting from {} dB'.format(
+                    start_atten))
+        return start_atten
 
     def compile_test_params(self, testcase_params):
         # Check if test should be skipped.
@@ -534,6 +564,7 @@
         band = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
         testcase_params['test_network'] = self.main_network[band]
+        testcase_params['band'] = band
         if testcase_params['chain_mask'] in ['0', '1']:
             testcase_params['attenuated_chain'] = 'DUT-Chain-{}'.format(
                 1 if testcase_params['chain_mask'] == '0' else 0)
@@ -595,7 +626,7 @@
         self.pass_fail_check(ping_result)
 
     def generate_test_cases(self, ap_power, channels, modes, chain_mask,
-                            test_types):
+                            test_types, **kwargs):
         """Function that auto-generates test cases for a test class."""
         test_cases = []
         allowed_configs = {
@@ -620,7 +651,8 @@
                                                       channel=channel,
                                                       mode=mode,
                                                       bandwidth=bandwidth,
-                                                      chain_mask=chain)
+                                                      chain_mask=chain,
+                                                      **kwargs)
             setattr(self, testcase_name,
                     partial(self._test_ping, testcase_params))
             test_cases.append(testcase_name)
@@ -628,46 +660,50 @@
 
 
 class WifiPing_TwoChain_Test(WifiPingTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
-        self.tests = self.generate_test_cases(ap_power='standard',
-                                              channels=[
-                                                  1, 6, 11, 36, 40, 44, 48,
-                                                  149, 153, 157, 161, '6g37',
-                                                  '6g117', '6g213'
-                                              ],
-                                              modes=['bw20', 'bw40', 'bw80'],
-                                              test_types=[
-                                                  'test_ping_range',
-                                                  'test_fast_ping_rtt',
-                                                  'test_slow_ping_rtt'
-                                              ],
-                                              chain_mask=['2x2'])
+        self.tests = self.generate_test_cases(
+            ap_power='standard',
+            channels=[
+                1, 6, 11, 36, 40, 44, 48, 149, 153, 157, 161, '6g37', '6g117',
+                '6g213'
+            ],
+            modes=['bw20', 'bw80', 'bw160'],
+            test_types=[
+                'test_ping_range', 'test_fast_ping_rtt', 'test_slow_ping_rtt'
+            ],
+            chain_mask=['2x2'],
+            reference_params=['band', 'chain_mask'])
 
 
 class WifiPing_PerChainRange_Test(WifiPingTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
-        self.tests = self.generate_test_cases(ap_power='standard',
-                                              chain_mask=['0', '1', '2x2'],
-                                              channels=[
-                                                  1, 6, 11, 36, 40, 44, 48,
-                                                  149, 153, 157, 161, '6g37',
-                                                  '6g117', '6g213'
-                                              ],
-                                              modes=['bw20', 'bw40', 'bw80'],
-                                              test_types=['test_ping_range'])
+        self.tests = self.generate_test_cases(
+            ap_power='standard',
+            chain_mask=['0', '1', '2x2'],
+            channels=[
+                1, 6, 11, 36, 40, 44, 48, 149, 153, 157, 161, '6g37', '6g117',
+                '6g213'
+            ],
+            modes=['bw20', 'bw80', 'bw160'],
+            test_types=['test_ping_range'],
+            reference_params=['band', 'chain_mask'])
 
 
 class WifiPing_LowPowerAP_Test(WifiPingTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
             ap_power='low_power',
             chain_mask=['0', '1', '2x2'],
             channels=[1, 6, 11, 36, 40, 44, 48, 149, 153, 157, 161],
-            modes=['bw20', 'bw40', 'bw80'],
-            test_types=['test_ping_range'])
+            modes=['bw20', 'bw80'],
+            test_types=['test_ping_range'],
+            reference_params=['band', 'chain_mask'])
 
 
 # Over-the air version of ping tests
@@ -678,6 +714,7 @@
     setting turntable orientation and other chamber parameters to study
     performance in varying channel conditions
     """
+
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
@@ -788,45 +825,8 @@
         # Continue setting up ping test
         WifiPingTest.setup_ping_test(self, testcase_params)
 
-    def get_range_start_atten(self, testcase_params):
-        """Gets the starting attenuation for this ping test.
-
-        The function gets the starting attenuation by checking whether a test
-        at the same configuration has executed. If so it sets the starting
-        point a configurable number of dBs below the reference test.
-
-        Returns:
-            start_atten: starting attenuation for current test
-        """
-        # If the test is being retried, start from the beginning
-        if self.retry_flag:
-            self.log.info('Retry flag set. Setting attenuation to minimum.')
-            return self.testclass_params['range_atten_start']
-        # Get the current and reference test config. The reference test is the
-        # one performed at the current MCS+1
-        ref_test_params = wputils.extract_sub_dict(
-            testcase_params, ['channel', 'mode', 'chain_mask'])
-        # Check if reference test has been run and set attenuation accordingly
-        previous_params = [
-            wputils.extract_sub_dict(result['testcase_params'],
-                                     ['channel', 'mode', 'chain_mask'])
-            for result in self.testclass_results
-        ]
-        try:
-            ref_index = previous_params[::-1].index(ref_test_params)
-            ref_index = len(previous_params) - 1 - ref_index
-            start_atten = self.testclass_results[ref_index][
-                'atten_at_range'] - (
-                    self.testclass_params['adjacent_range_test_gap'])
-        except ValueError:
-            self.log.info(
-                'Reference test not found. Starting from {} dB'.format(
-                    self.testclass_params['range_atten_start']))
-            start_atten = self.testclass_params['range_atten_start']
-        return start_atten
-
     def generate_test_cases(self, ap_power, channels, modes, chain_masks,
-                            chamber_mode, positions):
+                            chamber_mode, positions, **kwargs):
         test_cases = []
         allowed_configs = {
             20: [
@@ -853,7 +853,8 @@
                 chain_mask=chain_mask,
                 chamber_mode=chamber_mode,
                 total_positions=len(positions),
-                position=position)
+                position=position,
+                **kwargs)
             setattr(self, testcase_name,
                     partial(self._test_ping, testcase_params))
             test_cases.append(testcase_name)
@@ -861,6 +862,7 @@
 
 
 class WifiOtaPing_TenDegree_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
         self.tests = self.generate_test_cases(
@@ -869,49 +871,57 @@
             modes=['bw20'],
             chain_masks=['2x2'],
             chamber_mode='orientation',
-            positions=list(range(0, 360, 10)))
+            positions=list(range(0, 360, 10)),
+            reference_params=['channel', 'mode', 'chain_mask'])
 
 
 class WifiOtaPing_45Degree_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
-        self.tests = self.generate_test_cases(ap_power='standard',
-                                              channels=[
-                                                  1, 6, 11, 36, 40, 44, 48,
-                                                  149, 153, 157, 161, '6g37',
-                                                  '6g117', '6g213'
-                                              ],
-                                              modes=['bw20'],
-                                              chain_masks=['2x2'],
-                                              chamber_mode='orientation',
-                                              positions=list(range(0, 360,
-                                                                   45)))
+        self.tests = self.generate_test_cases(
+            ap_power='standard',
+            channels=[
+                1, 6, 11, 36, 40, 44, 48, 149, 153, 157, 161, '6g37', '6g117',
+                '6g213'
+            ],
+            modes=['bw20'],
+            chain_masks=['2x2'],
+            chamber_mode='orientation',
+            positions=list(range(0, 360, 45)),
+            reference_params=['channel', 'mode', 'chain_mask'])
 
 
 class WifiOtaPing_SteppedStirrers_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
-        self.tests = self.generate_test_cases(ap_power='standard',
-                                              channels=[6, 36, 149],
-                                              modes=['bw20'],
-                                              chain_masks=['2x2'],
-                                              chamber_mode='stepped stirrers',
-                                              positions=list(range(100)))
+        self.tests = self.generate_test_cases(
+            ap_power='standard',
+            channels=[6, 36, 149],
+            modes=['bw20'],
+            chain_masks=['2x2'],
+            chamber_mode='stepped stirrers',
+            positions=list(range(100)),
+            reference_params=['channel', 'mode', 'chain_mask'])
 
 
 class WifiOtaPing_LowPowerAP_TenDegree_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
-        self.tests = self.generate_test_cases(ap_power='low_power',
-                                              channels=[6, 36, 149],
-                                              modes=['bw20'],
-                                              chain_masks=['2x2'],
-                                              chamber_mode='orientation',
-                                              positions=list(range(0, 360,
-                                                                   10)))
+        self.tests = self.generate_test_cases(
+            ap_power='low_power',
+            channels=[6, 36, 149],
+            modes=['bw20'],
+            chain_masks=['2x2'],
+            chamber_mode='orientation',
+            positions=list(range(0, 360, 10)),
+            reference_params=['channel', 'mode', 'chain_mask'])
 
 
 class WifiOtaPing_LowPowerAP_45Degree_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
         self.tests = self.generate_test_cases(
@@ -920,33 +930,40 @@
             modes=['bw20'],
             chain_masks=['2x2'],
             chamber_mode='orientation',
-            positions=list(range(0, 360, 45)))
+            positions=list(range(0, 360, 45)),
+            reference_params=['channel', 'mode', 'chain_mask'])
 
 
 class WifiOtaPing_LowPowerAP_SteppedStirrers_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
-        self.tests = self.generate_test_cases(ap_power='low_power',
-                                              channels=[6, 36, 149],
-                                              modes=['bw20'],
-                                              chain_masks=['2x2'],
-                                              chamber_mode='stepped stirrers',
-                                              positions=list(range(100)))
+        self.tests = self.generate_test_cases(
+            ap_power='low_power',
+            channels=[6, 36, 149],
+            modes=['bw20'],
+            chain_masks=['2x2'],
+            chamber_mode='stepped stirrers',
+            positions=list(range(100)),
+            reference_params=['channel', 'mode', 'chain_mask'])
 
 
 class WifiOtaPing_LowPowerAP_PerChain_TenDegree_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
-        self.tests = self.generate_test_cases(ap_power='low_power',
-                                              channels=[6, 36, 149],
-                                              modes=['bw20'],
-                                              chain_masks=[0, 1, '2x2'],
-                                              chamber_mode='orientation',
-                                              positions=list(range(0, 360,
-                                                                   10)))
+        self.tests = self.generate_test_cases(
+            ap_power='low_power',
+            channels=[6, 36, 149],
+            modes=['bw20'],
+            chain_masks=[0, 1, '2x2'],
+            chamber_mode='orientation',
+            positions=list(range(0, 360, 10)),
+            reference_params=['channel', 'mode', 'chain_mask'])
 
 
 class WifiOtaPing_PerChain_TenDegree_Test(WifiOtaPingTest):
+
     def __init__(self, controllers):
         WifiOtaPingTest.__init__(self, controllers)
         self.tests = self.generate_test_cases(
@@ -955,4 +972,5 @@
             modes=['bw20'],
             chain_masks=[0, 1, '2x2'],
             chamber_mode='orientation',
-            positions=list(range(0, 360, 10)))
+            positions=list(range(0, 360, 10)),
+            reference_params=['channel', 'mode', 'chain_mask'])
diff --git a/acts_tests/tests/google/wifi/WifiRssiTest.py b/acts_tests/tests/google/wifi/WifiRssiTest.py
index 06eed43..452591f 100644
--- a/acts_tests/tests/google/wifi/WifiRssiTest.py
+++ b/acts_tests/tests/google/wifi/WifiRssiTest.py
@@ -53,6 +53,7 @@
     configurable attenuation waveforms.For an example config file to run this
     test class see example_connectivity_performance_ap_sta.json.
     """
+
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
@@ -494,12 +495,12 @@
             thread_future = wputils.get_ping_stats_nb(
                 self.remote_server, self.dut_ip,
                 testcase_params['traffic_timeout'], 0.5, 64)
+        llstats_obj.update_stats()
         for atten in testcase_params['rssi_atten_range']:
             # Set Attenuation
             self.log.info('Setting attenuation to {} dB'.format(atten))
             for attenuator in self.attenuators:
                 attenuator.set_atten(atten)
-            llstats_obj.update_stats()
             current_rssi = collections.OrderedDict()
             current_rssi = wputils.get_connected_rssi(
                 self.dut, testcase_params['connected_measurements'],
@@ -633,9 +634,13 @@
             testclass_params['rssi_vs_atten_connected_measurements'],
             scan_measurements=self.
             testclass_params['rssi_vs_atten_scan_measurements'],
-            first_measurement_delay=MED_SLEEP,
-            rssi_under_test=self.testclass_params['rssi_vs_atten_metrics'],
+            first_measurement_delay=SHORT_SLEEP,
             absolute_accuracy=1)
+        rssi_under_test = self.testclass_params['rssi_vs_atten_metrics']
+        if self.testclass_params[
+                'rssi_vs_atten_scan_measurements'] == 0 and 'scan_rssi' in rssi_under_test:
+            rssi_under_test.remove('scan_rssi')
+        testcase_params['rssi_under_test'] = rssi_under_test
 
         testcase_params['band'] = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
@@ -677,7 +682,7 @@
                 self.testclass_params['rssi_stability_duration'] /
                 self.testclass_params['polling_frequency']),
             scan_measurements=0,
-            first_measurement_delay=MED_SLEEP,
+            first_measurement_delay=SHORT_SLEEP,
             rssi_atten_range=self.testclass_params['rssi_stability_atten'])
         testcase_params['band'] = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
@@ -846,6 +851,7 @@
 
 
 class WifiRssi_2GHz_ActiveTraffic_Test(WifiRssiTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -854,6 +860,7 @@
 
 
 class WifiRssi_5GHz_ActiveTraffic_Test(WifiRssiTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -863,6 +870,7 @@
 
 
 class WifiRssi_AllChannels_ActiveTraffic_Test(WifiRssiTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -873,6 +881,7 @@
 
 
 class WifiRssi_SampleChannels_NoTraffic_Test(WifiRssiTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -881,6 +890,7 @@
 
 
 class WifiRssiTrackingTest(WifiRssiTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(['test_rssi_tracking'],
@@ -897,6 +907,7 @@
     It allows setting orientation and other chamber parameters to study
     performance in varying channel conditions
     """
+
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
@@ -1027,7 +1038,7 @@
         testcase_params.update(connected_measurements=int(
             rssi_test_duration / self.testclass_params['polling_frequency']),
                                scan_measurements=0,
-                               first_measurement_delay=MED_SLEEP,
+                               first_measurement_delay=SHORT_SLEEP,
                                rssi_atten_range=rssi_ota_test_attenuation)
         testcase_params['band'] = self.access_point.band_lookup_by_channel(
             testcase_params['channel'])
@@ -1100,6 +1111,7 @@
 
 
 class WifiOtaRssi_Accuracy_Test(WifiOtaRssiTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(['test_rssi_vs_atten'],
@@ -1110,6 +1122,7 @@
 
 
 class WifiOtaRssi_StirrerVariation_Test(WifiOtaRssiTest):
+
     def __init__(self, controllers):
         WifiRssiTest.__init__(self, controllers)
         self.tests = self.generate_test_cases(['test_rssi_variation'],
@@ -1119,6 +1132,7 @@
 
 
 class WifiOtaRssi_TenDegree_Test(WifiOtaRssiTest):
+
     def __init__(self, controllers):
         WifiRssiTest.__init__(self, controllers)
         self.tests = self.generate_test_cases(['test_rssi_over_orientation'],
diff --git a/acts_tests/tests/google/wifi/WifiRvrTest.py b/acts_tests/tests/google/wifi/WifiRvrTest.py
index ffa52d5..4a64552 100644
--- a/acts_tests/tests/google/wifi/WifiRvrTest.py
+++ b/acts_tests/tests/google/wifi/WifiRvrTest.py
@@ -137,14 +137,23 @@
                     primary_y_label='Throughput (Mbps)')
             plots[plot_id].add_line(result['total_attenuation'],
                                     result['throughput_receive'],
-                                    result['test_name'],
+                                    result['test_name'].strip('test_rvr_'),
                                     hover_text=result['hover_text'],
                                     marker='circle')
             plots[plot_id].add_line(result['total_attenuation'],
-                                    result['avg_phy_rate'],
-                                    result['test_name'] + ' (PHY)',
+                                    result['rx_phy_rate'],
+                                    result['test_name'].strip('test_rvr_') +
+                                    ' (Rx PHY)',
                                     hover_text=result['hover_text'],
-                                    marker='circle')
+                                    style='dashed',
+                                    marker='inverted_triangle')
+            plots[plot_id].add_line(result['total_attenuation'],
+                                    result['tx_phy_rate'],
+                                    result['test_name'].strip('test_rvr_') +
+                                    ' (Tx PHY)',
+                                    hover_text=result['hover_text'],
+                                    style='dashed',
+                                    marker='triangle')
 
         figure_list = []
         for plot_id, plot in plots.items():
@@ -311,39 +320,36 @@
                 ) for rssi in rvr_result['rssi']
             ]
         }
-        if 'DL' in self.current_test_name:
-            rvr_result['avg_phy_rate'] = [
-                curr_llstats['summary'].get('mean_rx_phy_rate', 0)
-                for curr_llstats in rvr_result['llstats']
-            ]
-        else:
-            rvr_result['avg_phy_rate'] = [
-                curr_llstats['summary'].get('mean_tx_phy_rate', 0)
-                for curr_llstats in rvr_result['llstats']
-            ]
+
         figure.add_line(rvr_result['total_attenuation'],
                         rvr_result['throughput_receive'],
                         'Measured Throughput',
                         hover_text=rvr_result['hover_text'],
-                        color='red',
+                        color='black',
                         marker='circle')
-        rvr_result['avg_phy_rate'].extend(
-            [0] * (len(rvr_result['total_attenuation']) -
-                   len(rvr_result['avg_phy_rate'])))
-        figure.add_line(rvr_result['total_attenuation'],
-                        rvr_result['avg_phy_rate'],
-                        'Average PHY Rate',
-                        hover_text=rvr_result['hover_text'],
-                        color='red',
-                        style='dashed',
-                        marker='square')
+        figure.add_line(
+            rvr_result['total_attenuation'][0:len(rvr_result['rx_phy_rate'])],
+            rvr_result['rx_phy_rate'],
+            'Rx PHY Rate',
+            hover_text=rvr_result['hover_text'],
+            color='blue',
+            style='dashed',
+            marker='inverted_triangle')
+        figure.add_line(
+            rvr_result['total_attenuation'][0:len(rvr_result['rx_phy_rate'])],
+            rvr_result['tx_phy_rate'],
+            'Tx PHY Rate',
+            hover_text=rvr_result['hover_text'],
+            color='red',
+            style='dashed',
+            marker='triangle')
 
         output_file_path = os.path.join(
             self.log_path, '{}.html'.format(self.current_test_name))
         figure.generate_figure(output_file_path)
 
     def compute_test_metrics(self, rvr_result):
-        #Set test metrics
+        # Set test metrics
         rvr_result['metrics'] = {}
         rvr_result['metrics']['peak_tput'] = max(
             rvr_result['throughput_receive'])
@@ -362,7 +368,7 @@
         for idx in range(len(tput_below_limit)):
             if all(tput_below_limit[idx:]):
                 if idx == 0:
-                    #Throughput was never above limit
+                    # Throughput was never above limit
                     rvr_result['metrics']['high_tput_range'] = -1
                 else:
                     rvr_result['metrics']['high_tput_range'] = rvr_result[
@@ -411,6 +417,8 @@
             self.testclass_params.get('monitor_llstats', 1))
         zero_counter = 0
         throughput = []
+        rx_phy_rate = []
+        tx_phy_rate = []
         llstats = []
         rssi = []
         for atten in testcase_params['atten_range']:
@@ -479,6 +487,10 @@
             llstats_obj.update_stats()
             curr_llstats = llstats_obj.llstats_incremental.copy()
             llstats.append(curr_llstats)
+            rx_phy_rate.append(curr_llstats['summary'].get(
+                'mean_rx_phy_rate', 0))
+            tx_phy_rate.append(curr_llstats['summary'].get(
+                'mean_tx_phy_rate', 0))
             self.log.info(
                 ('Throughput at {0:.2f} dB is {1:.2f} Mbps. '
                  'RSSI = {2:.2f} [{3:.2f}, {4:.2f}].').format(
@@ -492,9 +504,11 @@
             if zero_counter == self.MAX_CONSECUTIVE_ZEROS:
                 self.log.info(
                     'Throughput stable at 0 Mbps. Stopping test now.')
-                throughput.extend(
-                    [0] *
-                    (len(testcase_params['atten_range']) - len(throughput)))
+                zero_padding = len(
+                    testcase_params['atten_range']) - len(throughput)
+                throughput.extend([0] * zero_padding)
+                rx_phy_rate.extend([0] * zero_padding)
+                tx_phy_rate.extend([0] * zero_padding)
                 break
         for attenuator in self.attenuators:
             attenuator.set_atten(0, strict=False, retry=True)
@@ -512,6 +526,8 @@
         ]
         rvr_result['rssi'] = rssi
         rvr_result['throughput_receive'] = throughput
+        rvr_result['rx_phy_rate'] = rx_phy_rate
+        rvr_result['tx_phy_rate'] = tx_phy_rate
         rvr_result['llstats'] = llstats
         return rvr_result
 
@@ -556,8 +572,9 @@
             self.sta_dut.droid.wakeLockAcquireDim()
         else:
             self.sta_dut.go_to_sleep()
-        if wputils.validate_network(self.sta_dut,
-                                    testcase_params['test_network']['SSID']):
+        if (wputils.validate_network(self.sta_dut,
+                                     testcase_params['test_network']['SSID'])
+                and not self.testclass_params.get('force_reconnect', 0)):
             self.log.info('Already connected to desired network')
         else:
             wutils.wifi_toggle_state(self.sta_dut, False)
@@ -733,6 +750,7 @@
 
 
 class WifiRvr_TCP_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -746,6 +764,7 @@
 
 
 class WifiRvr_VHT_TCP_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -756,6 +775,7 @@
 
 
 class WifiRvr_HE_TCP_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -769,6 +789,7 @@
 
 
 class WifiRvr_SampleUDP_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -779,6 +800,7 @@
 
 
 class WifiRvr_VHT_SampleUDP_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -789,6 +811,7 @@
 
 
 class WifiRvr_HE_SampleUDP_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -799,6 +822,7 @@
 
 
 class WifiRvr_SampleDFS_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -809,6 +833,7 @@
 
 
 class WifiRvr_SingleChain_TCP_Test(WifiRvrTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -867,6 +892,7 @@
     setting turntable orientation and other chamber parameters to study
     performance in varying channel conditions
     """
+
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
@@ -902,7 +928,12 @@
                 ]).items())
             if test_id not in plots:
                 # Initialize test id data when not present
-                compiled_data[test_id] = {'throughput': [], 'metrics': {}}
+                compiled_data[test_id] = {
+                    'throughput': [],
+                    'rx_phy_rate': [],
+                    'tx_phy_rate': [],
+                    'metrics': {}
+                }
                 compiled_data[test_id]['metrics'] = {
                     key: []
                     for key in result['metrics'].keys()
@@ -927,6 +958,8 @@
             # Compile test id data and metrics
             compiled_data[test_id]['throughput'].append(
                 result['throughput_receive'])
+            compiled_data[test_id]['rx_phy_rate'].append(result['rx_phy_rate'])
+            compiled_data[test_id]['tx_phy_rate'].append(result['tx_phy_rate'])
             compiled_data[test_id]['total_attenuation'] = result[
                 'total_attenuation']
             for metric_key, metric_value in result['metrics'].items():
@@ -935,18 +968,27 @@
             # Add test id to plots
             plots[test_id].add_line(result['total_attenuation'],
                                     result['throughput_receive'],
-                                    result['test_name'],
+                                    result['test_name'].strip('test_rvr_'),
                                     hover_text=result['hover_text'],
                                     width=1,
                                     style='dashed',
                                     marker='circle')
-            plots[test_id_phy].add_line(result['total_attenuation'],
-                                        result['avg_phy_rate'],
-                                        result['test_name'] + ' PHY',
-                                        hover_text=result['hover_text'],
-                                        width=1,
-                                        style='dashed',
-                                        marker='circle')
+            plots[test_id_phy].add_line(
+                result['total_attenuation'],
+                result['rx_phy_rate'],
+                result['test_name'].strip('test_rvr_') + ' Rx PHY Rate',
+                hover_text=result['hover_text'],
+                width=1,
+                style='dashed',
+                marker='inverted_triangle')
+            plots[test_id_phy].add_line(
+                result['total_attenuation'],
+                result['tx_phy_rate'],
+                result['test_name'].strip('test_rvr_') + ' Tx PHY Rate',
+                hover_text=result['hover_text'],
+                width=1,
+                style='dashed',
+                marker='triangle')
 
         # Compute average RvRs and compute metrics over orientations
         for test_id, test_data in compiled_data.items():
@@ -966,6 +1008,10 @@
                     metric_key, metric_value)
             test_data['avg_rvr'] = numpy.mean(test_data['throughput'], 0)
             test_data['median_rvr'] = numpy.median(test_data['throughput'], 0)
+            test_data['avg_rx_phy_rate'] = numpy.mean(test_data['rx_phy_rate'],
+                                                      0)
+            test_data['avg_tx_phy_rate'] = numpy.mean(test_data['tx_phy_rate'],
+                                                      0)
             plots[test_id].add_line(test_data['total_attenuation'],
                                     test_data['avg_rvr'],
                                     legend='Average Throughput',
@@ -974,6 +1020,15 @@
                                     test_data['median_rvr'],
                                     legend='Median Throughput',
                                     marker='square')
+            test_id_phy = test_id + tuple('PHY')
+            plots[test_id_phy].add_line(test_data['total_attenuation'],
+                                        test_data['avg_rx_phy_rate'],
+                                        legend='Average Rx Rate',
+                                        marker='inverted_triangle')
+            plots[test_id_phy].add_line(test_data['total_attenuation'],
+                                        test_data['avg_tx_phy_rate'],
+                                        legend='Average Tx Rate',
+                                        marker='triangle')
 
         figure_list = []
         for plot_id, plot in plots.items():
@@ -1019,6 +1074,7 @@
 
 
 class WifiOtaRvr_StandardOrientation_Test(WifiOtaRvrTest):
+
     def __init__(self, controllers):
         WifiOtaRvrTest.__init__(self, controllers)
         self.tests = self.generate_test_cases(
@@ -1028,6 +1084,7 @@
 
 
 class WifiOtaRvr_SampleChannel_Test(WifiOtaRvrTest):
+
     def __init__(self, controllers):
         WifiOtaRvrTest.__init__(self, controllers)
         self.tests = self.generate_test_cases([6], ['bw20'],
@@ -1042,6 +1099,7 @@
 
 
 class WifiOtaRvr_SingleOrientation_Test(WifiOtaRvrTest):
+
     def __init__(self, controllers):
         WifiOtaRvrTest.__init__(self, controllers)
         self.tests = self.generate_test_cases(
@@ -1050,6 +1108,7 @@
 
 
 class WifiOtaRvr_SingleChain_Test(WifiOtaRvrTest):
+
     def __init__(self, controllers):
         WifiOtaRvrTest.__init__(self, controllers)
         self.tests = self.generate_test_cases([6], ['bw20'],
diff --git a/acts_tests/tests/google/wifi/WifiRvrTwTest.py b/acts_tests/tests/google/wifi/WifiRvrTwTest.py
index e732b83..6e2babe 100644
--- a/acts_tests/tests/google/wifi/WifiRvrTwTest.py
+++ b/acts_tests/tests/google/wifi/WifiRvrTwTest.py
@@ -25,6 +25,8 @@
 from acts.test_decorators import test_tracker_info
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
 from acts.controllers import iperf_server as ipf
+from acts.controllers import attenuator
+from acts.controllers.sl4a_lib import rpc_client
 
 import json
 import logging
@@ -35,446 +37,688 @@
 
 import serial
 import sys
+import urllib.request
 
+from acts_contrib.test_utils.wifi import wifi_performance_test_utils_RSSI as wperfutils
 
 WifiEnums = wutils.WifiEnums
 
 
-class WifiRvrTWTest(WifiBaseTest):
-    """ Tests for wifi RVR performance
+class WifiRvrTwTest(WifiBaseTest):
+  """ Tests for wifi RVR performance.
 
         Test Bed Requirement:
           * One Android device
           * Wi-Fi networks visible to the device
+  """
+  TEST_TIMEOUT = 10
+  IPERF_SETUP_TIME = 5
+  TURN_TABLE_SETUP_TIME = 5
+
+  def __init__(self, controllers):
+    WifiBaseTest.__init__(self, controllers)
+
+  def setup_class(self):
+    self.dut = self.android_devices[0]
+
+    req_params = ["rvr_networks", "rvr_test_params", "attenuators"]
+    opt_params = ["angle_params", "usb_port"]
+    self.unpack_userparams(
+        req_param_names=req_params, opt_param_names=opt_params)
+    asserts.assert_true(
+        len(self.rvr_networks) > 0, "Need at least one network.")
+
+    if "rvr_test_params" in self.user_params:
+      self.iperf_server = self.iperf_servers[0]
+      self.maxdb = self.rvr_test_params["rvr_atten_maxdb"]
+      self.mindb = self.rvr_test_params["rvr_atten_mindb"]
+      self.stepdb = self.rvr_test_params["rvr_atten_step"]
+      self.country_code = self.rvr_test_params["country_code"]
+    if "angle_params" in self.user_params:
+      self.angle_list = self.angle_params
+    if "usb_port" in self.user_params:
+      self.turntable_port = self.read_comport(self.usb_port["turntable"])
+
+    # Init DUT
+    wutils.wifi_test_device_init(self.dut, self.country_code)
+    self.dut.droid.bluetoothToggleState(False)
+    utils.set_location_service(self.dut, False)
+    wutils.wifi_toggle_state(self.dut, True)
+    utils.subprocess.check_output(
+        "adb root", shell=True, timeout=self.TEST_TIMEOUT)
+    utils.subprocess.check_output(
+        "adb shell settings put system screen_off_timeout 18000000",
+        shell=True,
+        timeout=self.TEST_TIMEOUT)
+    utils.subprocess.check_output(
+        "adb shell svc power stayon true",
+        shell=True,
+        timeout=self.TEST_TIMEOUT)
+
+    # create folder for rvr test result
+    self.log_path = os.path.join(logging.log_path, "rvr_results")
+    utils.create_dir(self.log_path)
+
+    Header = ("Test_date", "Project", "Device_SN", "ROM", "HW_Stage",
+              "test_SSID", "Frequency", "Turn_table_orientation",
+              "Attenuate_dB", "Signal_poll_avg_rssi", "Chain_0_rssi",
+              "Chain_1_rssi", "Link_speed", "TX_throughput_Mbps",
+              "RX_throughput_Mbps", "HE_Capable", "Country_code", "Channel",
+              "WiFi_chip", "Type", "Host_name", "AP_model",
+              "Incremental_build_id", "Build_type", "TCP_UDP_Protocol",
+              "Security_type", "Test_tool", "Airplane_mode_status", "BT_status",
+              "Bug_ID", "Comment")
+    self.csv_write(Header)
+
+  def setup_test(self):
+    self.dut.droid.wakeLockAcquireBright()
+    self.dut.droid.wakeUpNow()
+    rom_info = self.get_rominfo()
+    self.testdate = time.strftime("%Y-%m-%d", time.localtime())
+    self.rom = rom_info[0]
+    self.build_id = rom_info[1]
+    self.build_type = rom_info[2]
+    self.project = rom_info[3]
+    self.ret_country_code = self.get_country_code()
+    self.ret_hw_stage = self.get_hw_stage()
+    self.ret_platform = wperfutils.detect_wifi_platform(self.dut)
+
+  def teardown_test(self):
+    self.dut.droid.wakeLockRelease()
+    self.dut.droid.goToSleepNow()
+    wutils.set_attns(self.attenuators, "default")
+
+  def teardown_class(self):
+    if "rvr_test_params" in self.user_params:
+      self.iperf_server.stop()
+
+  def on_fail(self, test_name, begin_time):
+    self.dut.take_bug_report(test_name, begin_time)
+    self.dut.cat_adb_log(test_name, begin_time)
+
+  """Helper Functions"""
+
+  def csv_write(self, data):
+    """Output .CSV file for test result.
+
+    Args:
+        data: Dict containing attenuation, throughput and other meta data.
     """
-    TEST_TIMEOUT = 10
+    with open(
+        "{}/Result.csv".format(self.log_path), "a", newline="") as csv_file:
+      csv_writer = csv.writer(csv_file, delimiter=",")
+      csv_writer.writerow(data)
+      csv_file.close()
 
-    def setup_class(self):
-        super().setup_class()
+  def set_atten(self, db):
+    """Setup attenuator dB for current test.
 
-        self.dut = self.android_devices[0]
-        wutils.wifi_test_device_init(self.dut)
+    Args:
+       db: Attenuator setup dB.
+    """
+    if db < 0:
+      db = 0
+    elif db > 95:
+      db = 95
+    self.log.info("[Attenuation] %s", "Set dB = " + str(db) + "dB")
+    for atten in self.attenuators:
+      atten.set_atten(db)
+      self.log.info("[Attenuation] %s",
+                    "Current dB = " + str(atten.get_atten()) + "dB")
+      retry = 0
+      while atten.get_atten() != db and retry < 11:
+        retry = retry + 1
+        self.log.info(
+            "[Attenuation] %s", "Fail to set Attenuator to " + str(db) + ", " +
+            str(retry) + " times try to reset")
+        self.set_atten(db)
+      if retry == 11:
+        self.log.info("Attenuation] %s",
+                      "Retry Attenuator fail for 10 cycles, end test!")
+        sys.exit()
 
-        req_params = [ "iot_networks","rvr_test_params"]
-        opt_params = [ "angle_params","usb_port"]
-        self.unpack_userparams(req_param_names=req_params,
-                               opt_param_names=opt_params)
+  def read_comport(self, com):
+    """Read com port for current test.
 
-        asserts.assert_true(
-            len(self.iot_networks) > 0,
-            "Need at least one iot network with psk.")
+    Args:
+        com: Serial port.
 
-        wutils.wifi_toggle_state(self.dut, True)
-        if "rvr_test_params" in self.user_params:
-            self.iperf_server = self.iperf_servers[0]
-            self.MaxdB= self.rvr_test_params ["rvr_atten_MaxDB"]
-            self.MindB= self.rvr_test_params ["rvr_atten_MinDB"]
-            self.stepdB= self.rvr_test_params ["rvr_atten_step"]
+    Returns:
+        port: Serial port with baud rate.
+    """
+    port = serial.Serial(com, 9600, timeout=1)
+    time.sleep(1)
+    return port
 
-        if "angle_params" in self.user_params:
-            self.angle = self.angle_params
+  def get_angle(self, port):
+    """Get turn table angle for current test.
 
-        if "usb_port" in self.user_params:
-            self.T1=self.readport(self.usb_port["turntable"])
-            self.ATT1=self.readport(self.usb_port["atten1"])
-            self.ATT2=self.readport(self.usb_port["atten2"])
-            self.ATT3=self.readport(self.usb_port["atten3"])
+    Args:
+        port: Turn table com port.
 
-        # create hashmap for testcase name and SSIDs
-        self.iot_test_prefix = "test_iot_connection_to_"
-        self.ssid_map = {}
-        for network in self.iot_networks:
-            SSID = network['SSID'].replace('-','_')
-            self.ssid_map[SSID] = network
+    Returns:
+        angle: Angle from turn table.
+    """
+    angle = ""
+    port.write("DG?;".encode())
+    time.sleep(0.1)
+    degree_data = port.readline().decode("utf-8")
+    for data in range(len(degree_data)):
+      if (degree_data[data].isdigit()) is True:
+        angle = angle + degree_data[data]
+    if angle == "":
+      return -1
+    return int(angle)
 
-        # create folder for rvr test result
-        self.log_path = os.path.join(logging.log_path, "rvr_results")
-        os.makedirs(self.log_path, exist_ok=True)
+  def set_angle(self, port, angle):
+    """Setup turn table angle for current test.
 
-        Header=("test_SSID","Turn table (angle)","Attenuator(dBm)",
-                "TX throughput (Mbps)","RX throughput (Mbps)",
-                "RSSI","Link speed","Frequency")
-        self.csv_write(Header)
+    Args:
+        port: Turn table com port
+        angle: Turn table setup angle
+    """
+    if angle > 359:
+      angle = 359
+    elif angle < 0:
+      angle = 0
+    self.log.info("Set angle to " + str(angle))
+    input_angle = str("DG") + str(angle) + str(";")
+    port.write(input_angle.encode())
+    time.sleep(self.TURN_TABLE_SETUP_TIME)
 
-    def setup_test(self):
-        self.dut.droid.wakeLockAcquireBright()
-        self.dut.droid.wakeUpNow()
+  def check_angle(self, port, angle):
+    """Check turn table angle for current test.
 
-    def teardown_test(self):
-        self.dut.droid.wakeLockRelease()
-        self.dut.droid.goToSleepNow()
+    Args:
+        port: Turn table com port
+        angle: Turn table setup angle
+    """
+    retrytime = self.TEST_TIMEOUT
+    retry = 0
+    while self.get_angle(port) != angle and retry < retrytime:
+      retry = retry + 1
+      self.log.info("Turntable] %s",
+                    "Current angle = " + str(self.get_angle(port)))
+      self.log.info(
+          "Turntable] %s", "Fail set angle to " + str(angle) + ", " +
+          str(retry) + " times try to reset")
+      self.set_angle(port, angle)
+      time.sleep(self.TURN_TABLE_SETUP_TIME)
+    if retry == retrytime:
+      self.log.info(
+          "Turntable] %s",
+          "Retry turntable fail for " + str(retry) + " cycles, end test!")
+      sys.exit()
 
-    def teardown_class(self):
-        if "rvr_test_params" in self.user_params:
-            self.iperf_server.stop()
+  def get_wifiinfo(self):
+    """Get WiFi RSSI/ link speed/ frequency for current test.
 
-    def on_fail(self, test_name, begin_time):
-        self.dut.take_bug_report(test_name, begin_time)
-        self.dut.cat_adb_log(test_name, begin_time)
+    Returns:
+        [rssi,link_speed,frequency]: DUT WiFi RSSI,Link speed and Frequency.
+    """
+    def is_number(string):
+      for i in string:
+        if i.isdigit() is False:
+          if (i == "-" or i == "."):
+            continue
+          return str(-1)
+      return string
 
-    """Helper Functions"""
+    try:
+      cmd = "adb shell iw wlan0 link"
+      wifiinfo = utils.subprocess.check_output(
+          cmd, shell=True, timeout=self.TEST_TIMEOUT)
 
-    def csv_write(self,data):
-        """Output .CSV file for test result.
+      # Check RSSI Enhance
+      rssi = self.get_rssi_func()
 
-        Args:
-            data: Dict containing attenuation, throughput and other meta data.
-        """
-        with open("{}/Result.csv".format(self.log_path), "a", newline="") as csv_file:
-            csv_writer = csv.writer(csv_file,delimiter=',')
-            csv_writer.writerow(data)
-            csv_file.close()
+      # Check link speed
+      link_speed = wifiinfo.decode(
+          "utf-8")[wifiinfo.decode("utf-8").find("bitrate:") +
+                   8:wifiinfo.decode("utf-8").find("Bit/s") - 2]
+      link_speed = link_speed.strip(" ")
+      link_speed = is_number(link_speed)
+      # Check frequency
+      frequency = wifiinfo.decode(
+          "utf-8")[wifiinfo.decode("utf-8").find("freq:") +
+                   6:wifiinfo.decode("utf-8").find("freq:") + 10]
+      frequency = frequency.strip(" ")
+      frequency = is_number(frequency)
+    except:
+      return -1, -1, -1
+    return [rssi, link_speed, frequency]
 
-    def readport(self,com):
-        """Read com port for current test.
+  def get_rssi_func(self):
+    """Get RSSI from brcm/qcom wifi chip.
 
-        Args:
-            com: Attenuator or turn table com port
-        """
-        port=serial.Serial(com,9600,timeout=1)
-        time.sleep(1)
-        return port
+    Returns:
+         current_rssi: DUT WiFi RSSI.
+    """
+    if self.ret_platform == "brcm":
+      rssi_future = wperfutils.get_connected_rssi_brcm(self.dut)
+      signal_poll_avg_rssi_tmp = rssi_future.pop("signal_poll_avg_rssi").pop(
+          "mean")
+      chain_0_rssi_tmp = rssi_future.pop("chain_0_rssi").pop("mean")
+      chain_1_rssi_tmp = rssi_future.pop("chain_1_rssi").pop("mean")
+      current_rssi = {
+          "signal_poll_avg_rssi": signal_poll_avg_rssi_tmp,
+          "chain_0_rssi": chain_0_rssi_tmp,
+          "chain_1_rssi": chain_1_rssi_tmp
+      }
+    elif self.ret_platform == "qcom":
+      rssi_future = wperfutils.get_connected_rssi_qcom(
+          self.dut, interface="wlan0")
+      signal_poll_avg_rssi_tmp = rssi_future.pop("signal_poll_avg_rssi").pop(
+          "mean")
+      chain_0_rssi_tmp = rssi_future.pop("chain_0_rssi").pop("mean")
+      chain_1_rssi_tmp = rssi_future.pop("chain_1_rssi").pop("mean")
+      if math.isnan(signal_poll_avg_rssi_tmp):
+        signal_poll_avg_rssi_tmp = -1
+      if math.isnan(chain_0_rssi_tmp):
+        chain_0_rssi_tmp = -1
+      if math.isnan(chain_1_rssi_tmp):
+        chain_1_rssi_tmp = -1
 
-    def getdB(self,port):
-        """Get attenuator dB for current test.
+      if signal_poll_avg_rssi_tmp == -1 & chain_0_rssi_tmp == -1 & chain_1_rssi_tmp == -1:
+        current_rssi = -1
+      else:
+        current_rssi = {
+            "signal_poll_avg_rssi": signal_poll_avg_rssi_tmp,
+            "chain_0_rssi": chain_0_rssi_tmp,
+            "chain_1_rssi": chain_1_rssi_tmp
+        }
+    else:
+      current_rssi = {
+          "signal_poll_avg_rssi": float("nan"),
+          "chain_0_rssi": float("nan"),
+          "chain_1_rssi": float("nan")
+      }
+    return current_rssi
 
-        Args:
-            port: Attenuator com port
-        """
-        port.write('V?;'.encode())
-        dB=port.readline().decode()
-        dB=dB.strip(';')
-        dB=dB[dB.find('V')+1:]
-        return int(dB)
+  def get_rominfo(self):
+    """Get DUT ROM build info.
 
-    def setdB(self,port,dB):
-        """Setup attenuator dB for current test.
+    Returns:
+         rom, build_id, build_type, project: DUT Build info,Build ID,
+         Build type, and Project name
+    """
+    rom = "NA"
+    build_id = "NA"
+    build_type = "NA"
+    project = "NA"
+    rominfo = self.dut.adb.shell("getprop ro.build.display.id").split()
 
-        Args:
-            port: Attenuator com port
-            dB: Attenuator setup dB
-        """
-        if dB<0:
-            dB=0
-        elif dB>101:
-            dB=101
-        self.log.info("Set dB to "+str(dB))
-        InputdB=str('V')+str(dB)+str(';')
-        port.write(InputdB.encode())
-        time.sleep(0.1)
+    if rominfo:
+      rom = rominfo[2]
+      build_id = rominfo[3]
+      project, build_type = rominfo[0].split("-")
 
-    def set_Three_Att_dB(self,port1,port2,port3,dB):
-        """Setup 3 attenuator dB for current test.
+    return rom, build_id, build_type, project
 
-        Args:
-            port1: Attenuator1 com port
-            port1: Attenuator2 com port
-            port1: Attenuator com port
-            dB: Attenuator setup dB
-        """
-        self.setdB(port1,dB)
-        self.setdB(port2,dB)
-        self.setdB(port3,dB)
-        self.checkdB(port1,dB)
-        self.checkdB(port2,dB)
-        self.checkdB(port3,dB)
+  def get_hw_stage(self):
+    """Get DUT HW stage.
 
-    def checkdB(self,port,dB):
-        """Check attenuator dB for current test.
+    Returns:
+         hw_stage: DUT HW stage e.g. EVT/DVT/PVT..etc.
+    """
+    cmd = "adb shell getprop ro.boot.hardware.revision"
+    hw_stage_temp = utils.subprocess.check_output(
+        cmd, shell=True, timeout=self.TEST_TIMEOUT)
+    hw_stage = hw_stage_temp.decode("utf-8").split("\n")[0]
+    return hw_stage
 
-        Args:
-            port: Attenuator com port
-            dB: Attenuator setup dB
-        """
-        retry=0
-        while self.getdB(port)!=dB and retry<10:
-            retry=retry+1
-            self.log.info("Current dB = "+str(self.getdB(port)))
-            self.log.info("Fail to set Attenuator to "+str(dB)+", "
-                          +str(retry)+" times try to reset")
-            self.setdB(port,dB)
-        if retry ==10:
-            self.log.info("Retry Attenuator fail for 9 cycles, end test!")
-            sys.exit()
-        return 0
+  def get_country_code(self):
+    """Get DUT country code.
 
-    def getDG(self,port):
-        """Get turn table angle for current test.
+    Returns:
+         country_code: DUT country code e.g. US/JP/GE..etc.
+    """
+    cmd = "adb shell cmd wifi get-country-code"
+    country_code_temp = utils.subprocess.check_output(
+        cmd, shell=True, timeout=self.TEST_TIMEOUT)
+    country_code = country_code_temp.decode("utf-8").split(" ")[4].split(
+        "\n")[0]
+    return country_code
 
-        Args:
-            port: Turn table com port
-        """
-        DG = ""
-        port.write('DG?;'.encode())
-        time.sleep(0.1)
-        data = port.readline().decode('utf-8')
-        for i in range(len(data)):
-            if (data[i].isdigit()) == True:
-                DG = DG + data[i]
-        if DG == "":
-            return -1
-        return int(DG)
+  def get_channel(self):
+    """Get DUT WiFi channel.
 
-    def setDG(self,port,DG):
-        """Setup turn table angle for current test.
+    Returns:
+         country_code: DUT channel e.g. 6/36/37..etc.
+    """
+    if self.ret_platform == "brcm":
+      cmd = 'adb shell wl assoc | grep "Primary channel:"'
+      channel_temp = utils.subprocess.check_output(
+          cmd, shell=True, timeout=self.TEST_TIMEOUT)
+      channel = channel_temp.decode("utf-8").split(": ")[1].split("\n")[0]
+    elif self.ret_platform == "qcom":
+      cmd = "adb shell iw wlan0 info | grep channel"
+      channel_temp = utils.subprocess.check_output(
+          cmd, shell=True, timeout=self.TEST_TIMEOUT)
+      channel = channel_temp.decode("utf-8").split(" ")[1].split("\n")[0]
+    return channel
 
-        Args:
-            port: Turn table com port
-            DG: Turn table setup angle
-        """
-        if DG>359:
-            DG=359
-        elif DG<0:
-            DG=0
-        self.log.info("Set angle to "+str(DG))
-        InputDG=str('DG')+str(DG)+str(';')
-        port.write(InputDG.encode())
+  def get_he_capable(self):
+    """Get DUT WiFi high efficiency capable status .
 
-    def checkDG(self,port,DG):
-        """Check turn table angle for current test.
+    Returns:
+         he_capable: DUT high efficiency capable status.
+    """
+    if self.ret_platform == "brcm":
+      cmd = 'adb shell wl assoc | grep "Chanspec:"'
+      he_temp = utils.subprocess.check_output(
+          cmd, shell=True, timeout=self.TEST_TIMEOUT)
+      he_capable = he_temp.decode("utf-8").split(": ")[1].split("\n")[0].split(
+          "MHz")[0].split(" ")[3]
+    elif self.ret_platform == "qcom":
+      cmd = "adb shell iw wlan0 info | grep channel"
+      he_temp = utils.subprocess.check_output(
+          cmd, shell=True, timeout=self.TEST_TIMEOUT)
+      he_capable = he_temp.decode("utf-8").split("width: ")[1].split(" ")[0]
+    return he_capable
 
-        Args:
-            port: Turn table com port
-            DG: Turn table setup angle
-        """
-        retrytime = self.TEST_TIMEOUT
-        retry = 0
-        while self.getDG(port)!=DG and retry<retrytime:
-            retry=retry+1
-            self.log.info('Current angle = '+str(self.getDG(port)))
-            self.log.info('Fail set angle to '+str(DG)+', '+str(retry)+' times try to reset')
-            self.setDG(port,DG)
-            time.sleep(10)
-        if retry == retrytime:
-            self.log.info('Retry turntable fail for '+str(retry)+' cycles, end test!')
-            sys.exit()
-        return 0
+  def post_process_results(self, rvr_result):
+    """Saves JSON formatted results.
 
-    def getwifiinfo(self):
-        """Get WiFi RSSI/ link speed/ frequency for current test.
+    Args:
+        rvr_result: Dict containing attenuation, throughput and other meta data
+    Returns:
+        wifiinfo[0]: To check WiFi connection by RSSI value
+    """
+    # Save output as text file
+    wifiinfo = self.get_wifiinfo()
+    if wifiinfo[0] != -1:
+      rvr_result["signal_poll_avg_rssi"] = wifiinfo[0]["signal_poll_avg_rssi"]
+      rvr_result["chain_0_rssi"] = wifiinfo[0]["chain_0_rssi"]
+      rvr_result["chain_1_rssi"] = wifiinfo[0]["chain_1_rssi"]
+    else:
+      rvr_result["signal_poll_avg_rssi"] = wifiinfo[0]
+      rvr_result["chain_0_rssi"] = wifiinfo[0]
+      rvr_result["chain_1_rssi"] = wifiinfo[0]
+    if rvr_result["signal_poll_avg_rssi"] == -1:
+      rvr_result["channel"] = "NA"
+    else:
+      rvr_result["channel"] = self.ret_channel
+    rvr_result["country_code"] = self.ret_country_code
+    rvr_result["hw_stage"] = self.ret_hw_stage
+    rvr_result["wifi_chip"] = self.ret_platform
+    rvr_result["test_ssid"] = self.ssid
+    rvr_result["test_angle"] = self.angle_list[self.angle]
+    rvr_result["test_dB"] = self.db
+    rvr_result["test_link_speed"] = wifiinfo[1]
+    rvr_result["test_frequency"] = wifiinfo[2]
 
-        Returns:
-            [RSSI,LS,FR]: WiFi RSSI/ link speed/ frequency
-        """
-        def is_number(string):
-            for i in string:
-                if i.isdigit() == False:
-                    if (i=="-" or i=="."):
-                        continue
-                    return str(-1)
-            return string
+    data = (
+        self.testdate,
+        self.project,
+        self.dut.serial,
+        self.rom,
+        rvr_result["hw_stage"],
+        rvr_result["test_ssid"],
+        rvr_result["test_frequency"],
+        rvr_result["test_angle"],
+        rvr_result["test_dB"],
+        rvr_result["signal_poll_avg_rssi"],
+        rvr_result["chain_0_rssi"],
+        rvr_result["chain_1_rssi"],
+        rvr_result["test_link_speed"],
+        rvr_result["throughput_TX"][0],
+        rvr_result["throughput_RX"][0],
+        "HE" + self.he_capable,
+        rvr_result["country_code"],
+        rvr_result["channel"],
+        rvr_result["wifi_chip"],
+        "OTA_RvR",
+        "OTA_Testbed2",
+        "RAXE500",
+        self.build_id,
+        self.build_type,
+        "TCP",
+        "WPA3",
+        "iperf3",
+        "OFF",
+        "OFF",
+    )
+    self.csv_write(data)
 
-        try:
-            cmd = "adb shell iw wlan0 link"
-            wifiinfo = utils.subprocess.check_output(cmd,shell=True,
-                                                     timeout=self.TEST_TIMEOUT)
-            # Check RSSI
-            RSSI = wifiinfo.decode("utf-8")[wifiinfo.decode("utf-8").find("signal:") +
-                                            7:wifiinfo.decode("utf-8").find("dBm") - 1]
-            RSSI = RSSI.strip(' ')
-            RSSI = is_number(RSSI)
-            # Check link speed
-            LS = wifiinfo.decode("utf-8")[wifiinfo.decode("utf-8").find("bitrate:") +
-                                          8:wifiinfo.decode("utf-8").find("Bit/s") - 2]
-            LS = LS.strip(' ')
-            LS = is_number(LS)
-            # Check frequency
-            FR = wifiinfo.decode("utf-8")[wifiinfo.decode("utf-8").find("freq:") +
-                                          6:wifiinfo.decode("utf-8").find("freq:") + 10]
-            FR = FR.strip(' ')
-            FR = is_number(FR)
-        except:
-            return -1, -1, -1
-        return [RSSI,LS,FR]
+    results_file_path = "{}/{}_angle{}_{}dB.json".format(
+        self.log_path, self.ssid, self.angle_list[self.angle], self.db)
+    with open(results_file_path, "w") as results_file:
+      json.dump(rvr_result, results_file, indent=4)
+    return wifiinfo[0]
 
-    def post_process_results(self, rvr_result):
-        """Saves JSON formatted results.
+  def connect_to_wifi_network(self, network):
+    """Connection logic for wifi networks.
 
-        Args:
-            rvr_result: Dict containing attenuation, throughput and other meta
-            data
-        """
-        # Save output as text file
-        data=(rvr_result["test_name"],rvr_result["test_angle"],rvr_result["test_dB"],
-              rvr_result["throughput_TX"][0],rvr_result["throughput_RX"][0],
-              rvr_result["test_RSSI"],rvr_result["test_LS"],rvr_result["test_FR"])
-        self.csv_write(data)
+    Args:
+        params: Dictionary with network info.
+    """
+    ssid = network[WifiEnums.SSID_KEY]
+    self.dut.ed.clear_all_events()
+    wutils.start_wifi_connection_scan(self.dut)
+    scan_results = self.dut.droid.wifiGetScanResults()
+    wutils.assert_network_in_list({WifiEnums.SSID_KEY: ssid}, scan_results)
+    wutils.wifi_connect(self.dut, network, num_of_tries=3)
 
-        results_file_path = "{}/{}_angle{}_{}dB.json".format(self.log_path,
-                                                        self.ssid,
-                                                        self.angle[self.ag],self.DB)
-        with open(results_file_path, 'w') as results_file:
-            json.dump(rvr_result, results_file, indent=4)
+  def run_iperf_init(self, network):
+    self.iperf_server.start(tag="init")
+    self.log.info("[Iperf] %s", "Starting iperf traffic init.")
+    time.sleep(self.IPERF_SETUP_TIME)
+    try:
+      port_arg = "-p {} -J -R -t10".format(self.iperf_server.port)
+      self.dut.run_iperf_client(
+          self.rvr_test_params["iperf_server_address"],
+          port_arg,
+          timeout=self.rvr_test_params["iperf_duration"] + self.TEST_TIMEOUT)
+      self.iperf_server.stop()
+      self.log.info("[Iperf] %s", "iperf traffic init Pass")
+    except:
+      self.log.warning("ValueError: iperf init ERROR.")
 
-    def connect_to_wifi_network(self, network):
-        """Connection logic for psk wifi networks.
+  def run_iperf_client(self, network):
+    """Run iperf TX throughput after connection.
 
-        Args:
-            params: Dictionary with network info.
-        """
-        SSID = network[WifiEnums.SSID_KEY]
-        self.dut.ed.clear_all_events()
-        wutils.start_wifi_connection_scan(self.dut)
-        scan_results = self.dut.droid.wifiGetScanResults()
-        wutils.assert_network_in_list({WifiEnums.SSID_KEY: SSID}, scan_results)
-        wutils.wifi_connect(self.dut, network, num_of_tries=3)
+    Args:
+        network: Dictionary with network info.
 
-    def run_iperf_client(self, network):
-        """Run iperf TX throughput after connection.
+    Returns:
+        rvr_result: Dict containing TX rvr_results.
+    """
+    rvr_result = []
+    try:
+      self.iperf_server.start(tag="TX_server_{}_angle{}_{}dB".format(
+          self.ssid, self.angle_list[self.angle], self.db))
+      ssid = network[WifiEnums.SSID_KEY]
+      self.log.info("[Iperf] %s",
+                    "Starting iperf traffic TX through {}".format(ssid))
+      time.sleep(self.IPERF_SETUP_TIME)
+      port_arg = "-p {} -J {}".format(self.iperf_server.port,
+                                      self.rvr_test_params["iperf_port_arg"])
+      success, data = self.dut.run_iperf_client(
+          self.rvr_test_params["iperf_server_address"],
+          port_arg,
+          timeout=self.rvr_test_params["iperf_duration"] + self.TEST_TIMEOUT)
+      # Parse and log result
+      client_output_path = os.path.join(
+          self.iperf_server.log_path,
+          "IperfDUT,{},TX_client_{}_angle{}_{}dB".format(
+              self.iperf_server.port, self.ssid, self.angle_list[self.angle],
+              self.db))
+      with open(client_output_path, "w") as out_file:
+        out_file.write("\n".join(data))
+      self.iperf_server.stop()
 
-        Args:
-            params: Dictionary with network info.
+      iperf_file = self.iperf_server.log_files[-1]
+      iperf_result = ipf.IPerfResult(iperf_file)
+      curr_throughput = (math.fsum(iperf_result.instantaneous_rates[
+          self.rvr_test_params["iperf_ignored_interval"]:-1]) /
+                         len(iperf_result.instantaneous_rates[
+                             self.rvr_test_params["iperf_ignored_interval"]:-1])
+                        ) * 8 * (1.024**2)
+      rvr_result.append(curr_throughput)
+      self.log.info(
+          "[Iperf] %s", "TX Throughput at {0:.2f} dB is {1:.2f} Mbps".format(
+              self.db, curr_throughput))
+      self.log.debug(pprint.pformat(data))
+      asserts.assert_true(success, "Error occurred in iPerf traffic.")
+      return rvr_result
+    except:
+      rvr_result = ["NA"]
+      self.log.warning("ValueError: TX iperf ERROR.")
+      self.iperf_server.stop()
+      return rvr_result
 
-        Returns:
-            rvr_result: Dict containing rvr_results
-        """
-        rvr_result = []
-        self.iperf_server.start(tag="TX_server_{}_angle{}_{}dB".format(
-            self.ssid,self.angle[self.ag],self.DB))
-        wait_time = 5
-        SSID = network[WifiEnums.SSID_KEY]
-        self.log.info("Starting iperf traffic TX through {}".format(SSID))
-        time.sleep(wait_time)
-        port_arg = "-p {} -J {}".format(self.iperf_server.port,
-                                        self.rvr_test_params["iperf_port_arg"])
-        success, data = self.dut.run_iperf_client(
-            self.rvr_test_params["iperf_server_address"],
-            port_arg,
-            timeout=self.rvr_test_params["iperf_duration"] + self.TEST_TIMEOUT)
-        # Parse and log result
-        client_output_path = os.path.join(
-            self.iperf_server.log_path, "IperfDUT,{},TX_client_{}_angle{}_{}dB".format(
-                self.iperf_server.port,self.ssid,self.angle[self.ag],self.DB))
-        with open(client_output_path, 'w') as out_file:
-            out_file.write("\n".join(data))
-        self.iperf_server.stop()
+  def run_iperf_server(self, network):
+    """Run iperf RX throughput after connection.
 
-        iperf_file = self.iperf_server.log_files[-1]
-        try:
-            iperf_result = ipf.IPerfResult(iperf_file)
-            curr_throughput = (math.fsum(iperf_result.instantaneous_rates[
-                self.rvr_test_params["iperf_ignored_interval"]:-1]) / len(
-                    iperf_result.instantaneous_rates[self.rvr_test_params[
-                        "iperf_ignored_interval"]:-1])) * 8 * (1.024**2)
-        except:
-            self.log.warning(
-                "ValueError: Cannot get iperf result. Setting to 0")
-            curr_throughput = 0
-        rvr_result.append(curr_throughput)
-        self.log.info("TX Throughput at {0:.2f} dB is {1:.2f} Mbps".format(
-            self.DB, curr_throughput))
+    Args:
+        network: Dictionary with network info.
 
-        self.log.debug(pprint.pformat(data))
-        asserts.assert_true(success, "Error occurred in iPerf traffic.")
-        return rvr_result
+    Returns:
+        rvr_result: Dict containing RX rvr_results.
+    """
 
-    def run_iperf_server(self, network):
-        """Run iperf RX throughput after connection.
+    rvr_result = []
+    try:
+      self.iperf_server.start(tag="RX_client_{}_angle{}_{}dB".format(
+          self.ssid, self.angle_list[self.angle], self.db))
+      ssid = network[WifiEnums.SSID_KEY]
+      self.log.info("[Iperf] %s",
+                    "Starting iperf traffic RX through {}".format(ssid))
+      time.sleep(self.IPERF_SETUP_TIME)
+      port_arg = "-p {} -J -R {}".format(self.iperf_server.port,
+                                         self.rvr_test_params["iperf_port_arg"])
+      success, data = self.dut.run_iperf_client(
+          self.rvr_test_params["iperf_server_address"],
+          port_arg,
+          timeout=self.rvr_test_params["iperf_duration"] + self.TEST_TIMEOUT)
+      # Parse and log result
+      client_output_path = os.path.join(
+          self.iperf_server.log_path,
+          "IperfDUT,{},RX_server_{}_angle{}_{}dB".format(
+              self.iperf_server.port, self.ssid, self.angle_list[self.angle],
+              self.db))
+      with open(client_output_path, "w") as out_file:
+        out_file.write("\n".join(data))
+      self.iperf_server.stop()
 
-        Args:
-            params: Dictionary with network info.
+      iperf_file = client_output_path
+      iperf_result = ipf.IPerfResult(iperf_file)
+      curr_throughput = (math.fsum(iperf_result.instantaneous_rates[
+          self.rvr_test_params["iperf_ignored_interval"]:-1]) /
+                         len(iperf_result.instantaneous_rates[
+                             self.rvr_test_params["iperf_ignored_interval"]:-1])
+                        ) * 8 * (1.024**2)
+      rvr_result.append(curr_throughput)
+      self.log.info(
+          "[Iperf] %s", "RX Throughput at {0:.2f} dB is {1:.2f} Mbps".format(
+              self.db, curr_throughput))
 
-        Returns:
-            rvr_result: Dict containing rvr_results
-        """
-        rvr_result = []
-        self.iperf_server.start(tag="RX_client_{}_angle{}_{}dB".format(
-            self.ssid,self.angle[self.ag],self.DB))
-        wait_time = 5
-        SSID = network[WifiEnums.SSID_KEY]
-        self.log.info("Starting iperf traffic RX through {}".format(SSID))
-        time.sleep(wait_time)
-        port_arg = "-p {} -J -R {}".format(self.iperf_server.port,
-                                           self.rvr_test_params["iperf_port_arg"])
-        success, data = self.dut.run_iperf_client(
-            self.rvr_test_params["iperf_server_address"],
-            port_arg,
-            timeout=self.rvr_test_params["iperf_duration"] + self.TEST_TIMEOUT)
-        # Parse and log result
-        client_output_path = os.path.join(
-        self.iperf_server.log_path, "IperfDUT,{},RX_server_{}_angle{}_{}dB".format(
-            self.iperf_server.port,self.ssid,self.angle[self.ag],self.DB))
-        with open(client_output_path, 'w') as out_file:
-            out_file.write("\n".join(data))
-        self.iperf_server.stop()
+      self.log.debug(pprint.pformat(data))
+      asserts.assert_true(success, "Error occurred in iPerf traffic.")
+      return rvr_result
+    except:
+      rvr_result = ["NA"]
+      self.log.warning("ValueError: RX iperf ERROR.")
+      self.iperf_server.stop()
+      return rvr_result
 
-        iperf_file = client_output_path
-        try:
-            iperf_result = ipf.IPerfResult(iperf_file)
-            curr_throughput = (math.fsum(iperf_result.instantaneous_rates[
-                self.rvr_test_params["iperf_ignored_interval"]:-1]) / len(
-                    iperf_result.instantaneous_rates[self.rvr_test_params[
-                        "iperf_ignored_interval"]:-1])) * 8 * (1.024**2)
-        except:
-            self.log.warning(
-                "ValueError: Cannot get iperf result. Setting to 0")
-            curr_throughput = 0
-        rvr_result.append(curr_throughput)
-        self.log.info("RX Throughput at {0:.2f} dB is {1:.2f} Mbps".format(
-            self.DB, curr_throughput))
+  def iperf_test_func(self, network):
+    """Main function to test iperf TX/RX.
 
-        self.log.debug(pprint.pformat(data))
-        asserts.assert_true(success, "Error occurred in iPerf traffic.")
-        return rvr_result
+    Args:
+        network: Dictionary with network info.
+    """
+    # Initialize
+    rvr_result = {}
+    # Run RvR and log result
+    rvr_result["throughput_RX"] = self.run_iperf_server(network)
+    retry_time = 2
+    for retry in range(retry_time):
+      if rvr_result["throughput_RX"] == ["NA"]:
+        if not self.iperf_retry():
+          time.sleep(self.IPERF_SETUP_TIME)
+          rvr_result["throughput_RX"] = self.run_iperf_server(network)
+        else:
+          break
+      else:
+        break
+    rvr_result["throughput_TX"] = self.run_iperf_client(network)
+    retry_time = 2
+    for retry in range(retry_time):
+      if rvr_result["throughput_TX"] == ["NA"]:
+        if not self.iperf_retry():
+          time.sleep(self.IPERF_SETUP_TIME)
+          rvr_result["throughput_TX"] = self.run_iperf_client(network)
+        else:
+          break
+      else:
+        break
+    self.post_process_results(rvr_result)
+    self.rssi = wifiinfo[0]
+    return self.rssi
 
-    def iperf_test_func(self,network):
-        """Main function to test iperf TX/RX.
+  def iperf_retry(self):
+    """Check iperf TX/RX status and retry."""
+    try:
+      cmd = "adb -s {} shell pidof iperf3| xargs adb shell kill -9".format(
+          self.dut.serial)
+      utils.subprocess.call(cmd, shell=True, timeout=self.TEST_TIMEOUT)
+      self.log.warning("ValueError: Killed DUT iperf process, keep test")
+    except:
+      self.log.info("[Iperf] %s", "No iperf DUT process found, keep test")
 
-        Args:
-            params: Dictionary with network info
-        """
-        if "rvr_test_params" in self.user_params:
-            # Initialize
-            rvr_result = {}
-            # Run RvR and log result
-            wifiinfo = self.getwifiinfo()
-            rvr_result["throughput_TX"] = self.run_iperf_client(network)
-            rvr_result["throughput_RX"] = self.run_iperf_server(network)
-            rvr_result["test_name"] = self.ssid
-            rvr_result["test_angle"] = self.angle[self.ag]
-            rvr_result["test_dB"] = self.DB
-            rvr_result["test_RSSI"] = wifiinfo[0]
-            rvr_result["test_LS"] = wifiinfo[1]
-            rvr_result["test_FR"] = wifiinfo[2]
-            self.post_process_results(rvr_result)
+    wifiinfo = self.get_wifiinfo()
+    print("--[iperf_retry]--", wifiinfo[0])
+    self.log.info("[WiFiinfo] %s", "Current RSSI = " + str(wifiinfo[0]) + "dBm")
+    if wifiinfo[0] == -1:
+      self.log.warning("ValueError: Cannot get RSSI, stop throughput test")
+      return True
+    else:
+      return False
 
-    def rvr_test(self,network):
-        """Test function to run RvR.
+  def rvr_test(self, network):
+    """Test function to run RvR.
 
-        The function runs an RvR test in the current device/AP configuration.
-        Function is called from another wrapper function that sets up the
-        testbed for the RvR test
+    The function runs an RvR test in the current device/AP configuration.
+    Function is called from another wrapper function that sets up the
+    testbed for the RvR test
 
-        Args:
-            params: Dictionary with network info
-        """
-        wait_time = 5
-        utils.subprocess.check_output('adb root', shell=True, timeout=20)
-        self.ssid = network[WifiEnums.SSID_KEY]
-        self.log.info("Start rvr test")
-        for i in range(len(self.angle)):
-          self.setDG(self.T1,self.angle[i])
-          time.sleep(wait_time)
-          self.checkDG(self.T1,self.angle[i])
-          self.set_Three_Att_dB(self.ATT1,self.ATT2,self.ATT3,0)
-          time.sleep(wait_time)
-          self.connect_to_wifi_network(network)
-          self.set_Three_Att_dB(self.ATT1,self.ATT2,self.ATT3,self.MindB)
-          for j in range(self.MindB,self.MaxdB+self.stepdB,self.stepdB):
-            self.DB=j
-            self.ag=i
-            self.set_Three_Att_dB(self.ATT1,self.ATT2,self.ATT3,self.DB)
-            self.iperf_test_func(network)
-          wutils.reset_wifi(self.dut)
+    Args:
+        params: Dictionary with network info
+    """
+    self.ssid = network[WifiEnums.SSID_KEY]
+    self.log.info("Start rvr test")
 
-    """Tests"""
+    for angle in range(len(self.angle_list)):
+      self.angle = angle
+      self.set_angle(self.turntable_port, self.angle_list[angle])
+      self.check_angle(self.turntable_port, self.angle_list[angle])
+      self.set_atten(0)
+      self.connect_to_wifi_network(network)
+      self.ret_channel = self.get_channel()
+      self.he_capable = self.get_he_capable()
+      self.run_iperf_init(network)
+      for db in range(self.mindb, self.maxdb + self.stepdb, self.stepdb):
+        self.db = db
+        self.set_atten(self.db)
+        self.iperf_test_func(network)
+        if self.rssi == -1:
+          self.log.warning("ValueError: Cannot get RSSI. Run next angle")
+          break
+        else:
+          continue
+      wutils.reset_wifi(self.dut)
 
-    @test_tracker_info(uuid="93816af8-4c63-45f8-b296-cb49fae0b158")
-    def test_iot_connection_to_RVR_2G(self):
-        ssid_key = self.current_test_name.replace(self.iot_test_prefix, "")
-        self.rvr_test(self.ssid_map[ssid_key])
+  """Tests"""
+  def test_rvr_2g(self):
+    network = self.rvr_networks[0]
+    self.rvr_test(network)
 
-    @test_tracker_info(uuid="e1a67e13-946f-4d91-aa73-3f945438a1ac")
-    def test_iot_connection_to_RVR_5G(self):
-        ssid_key = self.current_test_name.replace(self.iot_test_prefix, "")
-        self.rvr_test(self.ssid_map[ssid_key])
\ No newline at end of file
+  def test_rvr_5g(self):
+    network = self.rvr_networks[1]
+    self.rvr_test(network)
+
+  def test_rvr_6g(self):
+    network = self.rvr_networks[2]
+    self.rvr_test(network)
diff --git a/acts_tests/tests/google/wifi/WifiSensitivityTest.py b/acts_tests/tests/google/wifi/WifiSensitivityTest.py
index 954bc90..535572d 100644
--- a/acts_tests/tests/google/wifi/WifiSensitivityTest.py
+++ b/acts_tests/tests/google/wifi/WifiSensitivityTest.py
@@ -375,18 +375,21 @@
             self.testbed_params['ap_tx_power_offset'][str(
                 testcase_params['channel'])] - ping_result['range'])
 
-    def setup_sensitivity_test(self, testcase_params):
-        # Setup test
-        if testcase_params['traffic_type'].lower() == 'ping':
-            self.setup_ping_test(testcase_params)
-            self.run_sensitivity_test = self.run_ping_test
-            self.process_sensitivity_test_results = (
-                self.process_ping_test_results)
-        else:
-            self.setup_rvr_test(testcase_params)
-            self.run_sensitivity_test = self.run_rvr_test
-            self.process_sensitivity_test_results = (
-                self.process_rvr_test_results)
+    def setup_ping_test(self, testcase_params):
+        """Function that gets devices ready for the test.
+
+        Args:
+            testcase_params: dict containing test-specific parameters
+        """
+        # Configure AP
+        self.setup_ap(testcase_params)
+        # Set attenuator to starting attenuation
+        for attenuator in self.attenuators:
+            attenuator.set_atten(testcase_params['atten_start'],
+                                 strict=False,
+                                 retry=True)
+        # Reset, configure, and connect DUT
+        self.setup_dut(testcase_params)
 
     def setup_ap(self, testcase_params):
         """Sets up the AP and attenuator to compensate for AP chain imbalance.
@@ -586,9 +589,14 @@
         ]
 
         # Prepare devices and run test
-        self.setup_sensitivity_test(testcase_params)
-        result = self.run_sensitivity_test(testcase_params)
-        self.process_sensitivity_test_results(testcase_params, result)
+        if testcase_params['traffic_type'].lower() == 'ping':
+            self.setup_ping_test(testcase_params)
+            result = self.run_ping_test(testcase_params)
+            self.process_ping_test_results(testcase_params, result)
+        else:
+            self.setup_rvr_test(testcase_params)
+            result = self.run_rvr_test(testcase_params)
+            self.process_rvr_test_results(testcase_params, result)
 
         # Post-process results
         self.testclass_results.append(result)
@@ -644,6 +652,7 @@
 
 
 class WifiSensitivity_AllChannels_Test(WifiSensitivityTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -652,6 +661,7 @@
 
 
 class WifiSensitivity_SampleChannels_Test(WifiSensitivityTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases([6, 36, 149],
@@ -660,6 +670,7 @@
 
 
 class WifiSensitivity_2GHz_Test(WifiSensitivityTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases([1, 2, 6, 10, 11], ['VHT20'],
@@ -667,6 +678,7 @@
 
 
 class WifiSensitivity_5GHz_Test(WifiSensitivityTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases(
@@ -675,6 +687,7 @@
 
 
 class WifiSensitivity_UNII1_Test(WifiSensitivityTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases([36, 40, 44, 48],
@@ -683,6 +696,7 @@
 
 
 class WifiSensitivity_UNII3_Test(WifiSensitivityTest):
+
     def __init__(self, controllers):
         super().__init__(controllers)
         self.tests = self.generate_test_cases([149, 153, 157, 161],
@@ -698,6 +712,7 @@
     It allows setting orientation and other chamber parameters to study
     performance in varying channel conditions
     """
+
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         self.testcase_metric_logger = (
@@ -911,6 +926,7 @@
 
 
 class WifiOtaSensitivity_TenDegree_Test(WifiOtaSensitivityTest):
+
     def __init__(self, controllers):
         WifiOtaSensitivityTest.__init__(self, controllers)
         requested_channels = [6, 36, 149]
@@ -929,6 +945,7 @@
 
 
 class WifiOtaSensitivity_PerChain_TenDegree_Test(WifiOtaSensitivityTest):
+
     def __init__(self, controllers):
         WifiOtaSensitivityTest.__init__(self, controllers)
         requested_channels = [6, 36, 149]
@@ -947,6 +964,7 @@
 
 
 class WifiOtaSensitivity_ThirtyDegree_Test(WifiOtaSensitivityTest):
+
     def __init__(self, controllers):
         WifiOtaSensitivityTest.__init__(self, controllers)
         requested_channels = [6, 36, 149]
@@ -971,6 +989,7 @@
 
 
 class WifiOtaSensitivity_45Degree_Test(WifiOtaSensitivityTest):
+
     def __init__(self, controllers):
         WifiOtaSensitivityTest.__init__(self, controllers)
         requested_rates = [
diff --git a/acts_tests/tests/google/wifi/WifiThroughputStabilityTest.py b/acts_tests/tests/google/wifi/WifiThroughputStabilityTest.py
index a8d9628..ddd25f7 100644
--- a/acts_tests/tests/google/wifi/WifiThroughputStabilityTest.py
+++ b/acts_tests/tests/google/wifi/WifiThroughputStabilityTest.py
@@ -50,6 +50,7 @@
     example config file to run this test class see
     example_connectivity_performance_ap_sta.json.
     """
+
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         # Define metrics to be uploaded to BlackBox
@@ -125,6 +126,7 @@
             except:
                 self.log.warning('Could not start sniffer. Disabling sniffs.')
                 self.testbed_params['sniffer_enable'] = 0
+        self.sniffer_subsampling = 1
         self.log_path = os.path.join(logging.log_path, 'test_results')
         os.makedirs(self.log_path, exist_ok=True)
         self.log.info('Access Point Configuration: {}'.format(
@@ -159,44 +161,18 @@
             test_result_dict: dict containing attenuation, throughput and other
             meta data
         """
-        avg_throughput = test_result['iperf_summary']['avg_throughput']
-        min_throughput = test_result['iperf_summary']['min_throughput']
-        std_dev_percent = (
-            test_result['iperf_summary']['std_deviation'] /
-            test_result['iperf_summary']['avg_throughput']) * 100
-        # Set blackbox metrics
-        if self.publish_testcase_metrics:
-            self.testcase_metric_logger.add_metric('avg_throughput',
-                                                   avg_throughput)
-            self.testcase_metric_logger.add_metric('min_throughput',
-                                                   min_throughput)
-            self.testcase_metric_logger.add_metric('std_dev_percent',
-                                                   std_dev_percent)
         # Evaluate pass/fail
         min_throughput_check = (
-            (min_throughput / avg_throughput) *
+            (test_result['iperf_summary']['min_throughput'] /
+             test_result['iperf_summary']['avg_throughput']) *
             100) > self.testclass_params['min_throughput_threshold']
-        std_deviation_check = std_dev_percent < self.testclass_params[
-            'std_deviation_threshold']
+        std_deviation_check = test_result['iperf_summary'][
+            'std_dev_percent'] < self.testclass_params[
+                'std_deviation_threshold']
 
-        llstats = (
-            'TX MCS = {0} ({1:.1f}%). '
-            'RX MCS = {2} ({3:.1f}%)'.format(
-                test_result['llstats']['summary']['common_tx_mcs'],
-                test_result['llstats']['summary']['common_tx_mcs_freq'] * 100,
-                test_result['llstats']['summary']['common_rx_mcs'],
-                test_result['llstats']['summary']['common_rx_mcs_freq'] * 100))
-
-        test_message = (
-            'Atten: {0:.2f}dB, RSSI: {1:.2f}dB. '
-            'Throughput (Mean: {2:.2f}, Std. Dev:{3:.2f}%, Min: {4:.2f} Mbps).'
-            'LLStats : {5}'.format(
-                test_result['attenuation'],
-                test_result['rssi_result']['signal_poll_rssi']['mean'],
-                avg_throughput, std_dev_percent, min_throughput, llstats))
         if min_throughput_check and std_deviation_check:
-            asserts.explicit_pass('Test Passed.' + test_message)
-        asserts.fail('Test Failed. ' + test_message)
+            asserts.explicit_pass('Test Passed.')
+        asserts.fail('Test Failed.')
 
     def post_process_results(self, test_result):
         """Extracts results and saves plots and JSON formatted results.
@@ -209,41 +185,37 @@
             avg throughput, other metrics, and other meta data
         """
         # Save output as text file
-        test_name = self.current_test_name
-        results_file_path = os.path.join(self.log_path,
-                                         '{}.txt'.format(test_name))
-        if test_result['iperf_result'].instantaneous_rates:
-            instantaneous_rates_Mbps = [
-                rate * 8 * (1.024**2)
-                for rate in test_result['iperf_result'].instantaneous_rates[
-                    self.testclass_params['iperf_ignored_interval']:-1]
-            ]
-            tput_standard_deviation = test_result[
-                'iperf_result'].get_std_deviation(
-                    self.testclass_params['iperf_ignored_interval']) * 8
-        else:
-            instantaneous_rates_Mbps = [float('nan')]
-            tput_standard_deviation = float('nan')
-        test_result['iperf_summary'] = {
-            'instantaneous_rates': instantaneous_rates_Mbps,
-            'avg_throughput': numpy.mean(instantaneous_rates_Mbps),
-            'std_deviation': tput_standard_deviation,
-            'min_throughput': min(instantaneous_rates_Mbps)
-        }
+        results_file_path = os.path.join(
+            self.log_path, '{}.txt'.format(self.current_test_name))
         with open(results_file_path, 'w') as results_file:
             json.dump(wputils.serialize_dict(test_result), results_file)
         # Plot and save
-        figure = BokehFigure(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,
-                        instantaneous_rates_Mbps,
-                        legend=self.current_test_name,
-                        marker='circle')
-        output_file_path = os.path.join(self.log_path,
-                                        '{}.html'.format(test_name))
-        figure.generate_figure(output_file_path)
+        # Set blackbox metrics
+        if self.publish_testcase_metrics:
+            self.testcase_metric_logger.add_metric(
+                'avg_throughput',
+                test_result['iperf_summary']['avg_throughput'])
+            self.testcase_metric_logger.add_metric(
+                'min_throughput',
+                test_result['iperf_summary']['min_throughput'])
+            self.testcase_metric_logger.add_metric(
+                'std_dev_percent',
+                test_result['iperf_summary']['std_dev_percent'])
+            figure = BokehFigure(self.current_test_name,
+                                 x_label='Time (s)',
+                                 primary_y_label='Throughput (Mbps)')
+            time_data = list(
+                range(
+                    0,
+                    len(test_result['iperf_summary']['instantaneous_rates'])))
+            figure.add_line(
+                time_data,
+                test_result['iperf_summary']['instantaneous_rates'],
+                legend=self.current_test_name,
+                marker='circle')
+            output_file_path = os.path.join(
+                self.log_path, '{}.html'.format(self.current_test_name))
+            figure.generate_figure(output_file_path)
         return test_result
 
     def setup_ap(self, testcase_params):
@@ -352,9 +324,11 @@
         # Run test and log result
         # Start iperf session
         self.log.info('Starting iperf test.')
+        test_result = collections.OrderedDict()
         llstats_obj = wputils.LinkLayerStats(self.dut)
         llstats_obj.update_stats()
-        if self.testbed_params['sniffer_enable']:
+        if self.testbed_params['sniffer_enable'] and len(
+                self.testclass_results) % self.sniffer_subsampling == 0:
             self.sniffer.start_capture(
                 network=testcase_params['test_network'],
                 chan=testcase_params['channel'],
@@ -375,7 +349,8 @@
         current_rssi = current_rssi.result()
         server_output_path = self.iperf_server.stop()
         # Stop sniffer
-        if self.testbed_params['sniffer_enable']:
+        if self.testbed_params['sniffer_enable'] and len(
+                self.testclass_results) % self.sniffer_subsampling == 0:
             self.sniffer.stop_capture()
         # Set attenuator to 0 dB
         for attenuator in self.attenuators:
@@ -388,16 +363,61 @@
         try:
             iperf_result = ipf.IPerfResult(iperf_file)
         except:
-            asserts.fail('Cannot get iperf result.')
+            iperf_result = ipf.IPerfResult('{}')  #empty iperf result
+            self.log.warning('Cannot get iperf result.')
+        if iperf_result.instantaneous_rates:
+            instantaneous_rates_Mbps = [
+                rate * 8 * (1.024**2)
+                for rate in iperf_result.instantaneous_rates[
+                    self.testclass_params['iperf_ignored_interval']:-1]
+            ]
+            tput_standard_deviation = iperf_result.get_std_deviation(
+                self.testclass_params['iperf_ignored_interval']) * 8
+        else:
+            instantaneous_rates_Mbps = [float('nan')]
+            tput_standard_deviation = float('nan')
+        test_result['iperf_summary'] = {
+            'instantaneous_rates':
+            instantaneous_rates_Mbps,
+            'avg_throughput':
+            numpy.mean(instantaneous_rates_Mbps),
+            'std_deviation':
+            tput_standard_deviation,
+            'min_throughput':
+            min(instantaneous_rates_Mbps),
+            'std_dev_percent':
+            (tput_standard_deviation / numpy.mean(instantaneous_rates_Mbps)) *
+            100
+        }
         llstats_obj.update_stats()
         curr_llstats = llstats_obj.llstats_incremental.copy()
-        test_result = collections.OrderedDict()
         test_result['testcase_params'] = testcase_params.copy()
         test_result['ap_settings'] = self.access_point.ap_settings.copy()
         test_result['attenuation'] = testcase_params['atten_level']
         test_result['iperf_result'] = iperf_result
         test_result['rssi_result'] = current_rssi
         test_result['llstats'] = curr_llstats
+
+        llstats = (
+            'TX MCS = {0} ({1:.1f}%). '
+            'RX MCS = {2} ({3:.1f}%)'.format(
+                test_result['llstats']['summary']['common_tx_mcs'],
+                test_result['llstats']['summary']['common_tx_mcs_freq'] * 100,
+                test_result['llstats']['summary']['common_rx_mcs'],
+                test_result['llstats']['summary']['common_rx_mcs_freq'] * 100))
+
+        test_message = (
+            'Atten: {0:.2f}dB, RSSI: {1:.2f}dB. '
+            'Throughput (Mean: {2:.2f}, Std. Dev:{3:.2f}%, Min: {4:.2f} Mbps).'
+            'LLStats : {5}'.format(
+                test_result['attenuation'],
+                test_result['rssi_result']['signal_poll_rssi']['mean'],
+                test_result['iperf_summary']['avg_throughput'],
+                test_result['iperf_summary']['std_dev_percent'],
+                test_result['iperf_summary']['min_throughput'], llstats))
+
+        self.log.info(test_message)
+
         self.testclass_results.append(test_result)
         return test_result
 
@@ -501,6 +521,7 @@
     setting turntable orientation and other chamber parameters to study
     performance in varying channel conditions
     """
+
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
         # Define metrics to be uploaded to BlackBox
@@ -605,6 +626,22 @@
             test_atten = self.testclass_params['ota_atten_levels'][band][1]
         return test_atten
 
+    def _test_throughput_stability_over_orientation(self, testcase_params):
+        """ Function that gets called for each test case
+
+        The function gets called in each test case. The function customizes
+        the test based on the test name of the test that called it
+
+        Args:
+            testcase_params: dict containing test specific parameters
+        """
+        testcase_params = self.compile_test_params(testcase_params)
+        for position in testcase_params['positions']:
+            testcase_params['position'] = position
+            self.setup_throughput_stability_test(testcase_params)
+            test_result = self.run_throughput_stability_test(testcase_params)
+            self.post_process_results(test_result)
+
     def generate_test_cases(self, channels, modes, traffic_types,
                             traffic_directions, signal_levels, chamber_mode,
                             positions):
@@ -619,8 +656,8 @@
         }
 
         test_cases = []
-        for channel, mode, signal_level, position, traffic_type, traffic_direction in itertools.product(
-                channels, modes, signal_levels, positions, traffic_types,
+        for channel, mode, signal_level, traffic_type, traffic_direction in itertools.product(
+                channels, modes, signal_levels, traffic_types,
                 traffic_directions):
             bandwidth = int(''.join([x for x in mode if x.isdigit()]))
             if channel not in allowed_configs[bandwidth]:
@@ -634,19 +671,22 @@
                 signal_level=signal_level,
                 chamber_mode=chamber_mode,
                 total_positions=len(positions),
-                position=position)
+                positions=positions)
             testcase_name = ('test_tput_stability'
-                             '_{}_{}_{}_ch{}_{}_pos{}'.format(
+                             '_{}_{}_{}_ch{}_{}'.format(
                                  signal_level, traffic_type, traffic_direction,
-                                 channel, mode, position))
-            setattr(self, testcase_name,
-                    partial(self._test_throughput_stability, testcase_params))
+                                 channel, mode))
+            setattr(
+                self, testcase_name,
+                partial(self._test_throughput_stability_over_orientation,
+                        testcase_params))
             test_cases.append(testcase_name)
         return test_cases
 
 
 class WifiOtaThroughputStability_TenDegree_Test(WifiOtaThroughputStabilityTest
                                                 ):
+
     def __init__(self, controllers):
         WifiOtaThroughputStabilityTest.__init__(self, controllers)
         self.tests = self.generate_test_cases([6, 36, 149, '6g37'],
@@ -655,8 +695,13 @@
                                               ['high', 'low'], 'orientation',
                                               list(range(0, 360, 10)))
 
+    def setup_class(self):
+        WifiOtaThroughputStabilityTest.setup_class(self)
+        self.sniffer_subsampling = 6
+
 
 class WifiOtaThroughputStability_45Degree_Test(WifiOtaThroughputStabilityTest):
+
     def __init__(self, controllers):
         WifiOtaThroughputStabilityTest.__init__(self, controllers)
         self.tests = self.generate_test_cases([6, 36, 149, '6g37'],
@@ -668,6 +713,7 @@
 
 class WifiOtaThroughputStability_SteppedStirrers_Test(
         WifiOtaThroughputStabilityTest):
+
     def __init__(self, controllers):
         WifiOtaThroughputStabilityTest.__init__(self, controllers)
         self.tests = self.generate_test_cases([6, 36, 149, '6g37'],
@@ -676,3 +722,7 @@
                                               ['high', 'low'],
                                               'stepped stirrers',
                                               list(range(100)))
+
+    def setup_class(self):
+        WifiOtaThroughputStabilityTest.setup_class(self)
+        self.sniffer_subsampling = 10
diff --git a/acts_tests/tests/google/wifi/WifiTxPowerCheckTest.py b/acts_tests/tests/google/wifi/WifiTxPowerCheckTest.py
index 706903c..d0a3334 100644
--- a/acts_tests/tests/google/wifi/WifiTxPowerCheckTest.py
+++ b/acts_tests/tests/google/wifi/WifiTxPowerCheckTest.py
@@ -95,8 +95,8 @@
             test_types=[
                 'test_tx_power',
             ],
-            country_codes=['US', 'GB', 'JP'],
-            sar_states=range(0, 13))
+            country_codes=['US', 'GB', 'JP', 'CA', 'AU'],
+            sar_states=range(-1, 13))
 
     def setup_class(self):
         self.dut = self.android_devices[-1]
@@ -139,6 +139,9 @@
         self.nvram_sar_data = self.read_nvram_sar_data()
         self.csv_sar_data = self.read_sar_csv(self.testclass_params['sar_csv'])
 
+        # Configure test retries
+        self.user_params['retry_tests'] = [self.__class__.__name__]
+
     def teardown_class(self):
         # Turn WiFi OFF and reset AP
         self.access_point.teardown()
@@ -246,11 +249,13 @@
         of SAR scenarios to NVRAM data tables.
         """
 
-        self.sar_state_mapping = collections.OrderedDict([(-1, {
+        self.sar_state_mapping = collections.OrderedDict([(-2, {
             "google_name":
-            'WIFI_POWER_SCENARIO_DISABLE'
-        }), (0, {
+            'WIFI_POWER_SCENARIO_INVALID'
+        }), (-1, {
             "google_name": 'WIFI_POWER_SCENARIO_DISABLE'
+        }), (0, {
+            "google_name": 'WIFI_POWER_SCENARIO_VOICE_CALL'
         }), (1, {
             "google_name": 'WIFI_POWER_SCENARIO_ON_HEAD_CELL_OFF'
         }), (2, {
@@ -303,24 +308,24 @@
         """
 
         sar_config = collections.OrderedDict()
-        list_of_countries = ['fcc', 'jp']
+        list_of_countries = ['fcc', 'jp', 'ca']
         try:
             sar_config['country'] = next(country
                                          for country in list_of_countries
-                                         if country in sar_line)
+                                         if country in sar_line.split('=')[0])
         except:
             sar_config['country'] = 'row'
 
         list_of_sar_states = ['grip', 'bt', 'hotspot']
         try:
             sar_config['state'] = next(state for state in list_of_sar_states
-                                       if state in sar_line)
+                                       if state in sar_line.split('=')[0])
         except:
             sar_config['state'] = 'head'
 
         list_of_bands = ['2g', '5g', '6g']
         sar_config['band'] = next(band for band in list_of_bands
-                                  if band in sar_line)
+                                  if band in sar_line.split('=')[0])
 
         sar_config['rsdb'] = 'rsdb' if 'rsdb' in sar_line else 'mimo'
         sar_config['airplane_mode'] = '_2=' in sar_line
@@ -328,9 +333,10 @@
         sar_powers = sar_line.split('=')[1].split(',')
         decoded_powers = []
         for sar_power in sar_powers:
+            # Note that core 0 and 1 are flipped in the NVRAM entries
             decoded_powers.append([
-                (int(sar_power[2:4], 16) & int('7f', 16)) / 4,
-                (int(sar_power[4:], 16) & int('7f', 16)) / 4
+                (int(sar_power[4:], 16) & int('7f', 16)) / 4,
+                (int(sar_power[2:4], 16) & int('7f', 16)) / 4
             ])
 
         return tuple(sar_config.values()), decoded_powers
@@ -353,6 +359,8 @@
             reg_domain = 'fcc'
         elif testcase_params['country_code'] == 'JP':
             reg_domain = 'jp'
+        elif testcase_params['country_code'] == 'CA':
+            reg_domain = 'ca'
         else:
             reg_domain = 'row'
         for band, channels in self.BAND_TO_CHANNEL_MAP.items():
@@ -385,6 +393,8 @@
             reg_domain = 'fcc'
         elif testcase_params['country_code'] == 'JP':
             reg_domain = 'jp'
+        elif testcase_params['country_code'] == 'CA':
+            reg_domain = 'ca'
         else:
             reg_domain = 'row'
         for band, channels in self.BAND_TO_CHANNEL_MAP.items():
@@ -597,19 +607,22 @@
                       str) and '6g' in result['testcase_params']['channel']:
             mode = 'HE' + str(result['testcase_params']['bandwidth'])
         else:
-            mode = 'VHT' + str(result['testcase_params']['bandwidth'])
+            mode = 'HE' + str(result['testcase_params']['bandwidth'])
         regulatory_power = result['wl_curpower']['regulatory_limits'][(mode, 0,
                                                                        2)]
-        if result['testcase_params']['sar_state'] == 0:
-            #get from wl_curpower
-            csv_powers = [30, 30]
-            nvram_powers = [30, 30]
-            sar_config = 'SAR DISABLED'
-        else:
-            sar_config, nvram_powers = self.get_sar_power_from_nvram(
-                result['testcase_params'])
+        board_power = result['wl_curpower']['board_limits'][(mode, str(0), 2)]
+        # try:
+        sar_config, nvram_powers = self.get_sar_power_from_nvram(
+            result['testcase_params'])
+        # except:
+        #     nvram_powers = [99, 99]
+        #     sar_config = 'SAR DISABLED'
+        try:
             csv_config, csv_powers = self.get_sar_power_from_csv(
                 result['testcase_params'])
+        except:
+            #get from wl_curpower
+            csv_powers = [99, 99]
         self.log.info("SAR state: {} ({})".format(
             result['testcase_params']['sar_state'],
             self.sar_state_mapping[result['testcase_params']['sar_state']],
@@ -618,12 +631,12 @@
             result['testcase_params']['country_code']))
         self.log.info('BRCM SAR Table: {}'.format(sar_config))
         expected_power = [
-            min([csv_powers[0], regulatory_power]) - 1.5,
-            min([csv_powers[1], regulatory_power]) - 1.5
+            min([csv_powers[0], regulatory_power, board_power]) - 1.5,
+            min([csv_powers[1], regulatory_power, board_power]) - 1.5
         ]
-        power_str = "NVRAM Powers: {}, CSV Powers: {}, Reg Powers: {}, Expected Powers: {}, Reported Powers: {}".format(
-            nvram_powers, csv_powers, [regulatory_power] * 2, expected_power,
-            result['tx_powers'])
+        power_str = "NVRAM Powers: {}, CSV Powers: {}, Reg Powers: {}, Board Power: {}, Expected Powers: {}, Reported Powers: {}".format(
+            nvram_powers, csv_powers, [regulatory_power] * 2,
+            [board_power] * 2, expected_power, result['tx_powers'])
         max_error = max([
             abs(expected_power[idx] - result['tx_powers'][idx])
             for idx in [0, 1]
@@ -668,6 +681,12 @@
                 bw=testcase_params['bandwidth'],
                 duration=testcase_params['ping_duration'] *
                 len(testcase_params['atten_range']) + self.TEST_TIMEOUT)
+        # Set sar state
+        if testcase_params['sar_state'] == -1:
+            self.dut.adb.shell('halutil -sar disable')
+        else:
+            self.dut.adb.shell('halutil -sar enable {}'.format(
+                testcase_params['sar_state']))
         # Run ping and sweep attenuation as needed
         self.log.info('Starting ping.')
         thread_future = wputils.get_ping_stats_nb(self.ping_server,
@@ -679,18 +698,15 @@
             # Set mcs
             if isinstance(testcase_params['channel'],
                           int) and testcase_params['channel'] < 13:
-                self.dut.adb.shell('wl 2g_rate -v 0x2 -b {}'.format(
+                self.dut.adb.shell('wl 2g_rate -e 0 -s 2 -b {}'.format(
                     testcase_params['bandwidth']))
             elif isinstance(testcase_params['channel'],
                             int) and testcase_params['channel'] > 13:
-                self.dut.adb.shell('wl 5g_rate -v 0x2 -b {}'.format(
+                self.dut.adb.shell('wl 5g_rate -e 0 -s 2 -b {}'.format(
                     testcase_params['bandwidth']))
             else:
                 self.dut.adb.shell('wl 6g_rate -e 0 -s 2 -b {}'.format(
                     testcase_params['bandwidth']))
-            # Set sar state
-            self.dut.adb.shell('halutil -sar enable {}'.format(
-                testcase_params['sar_state']))
             # Refresh link layer stats
             llstats_obj.update_stats()
             # Check sar state
@@ -703,11 +719,16 @@
                 last_est_out = self.dut.adb.shell(
                     "wl curpower | grep 'Last est. power'", ignore_status=True)
                 if "Last est. power" in last_est_out:
-                    per_chain_powers = last_est_out.split(
-                        ':')[1].strip().split('  ')
-                    per_chain_powers = [
-                        float(power) for power in per_chain_powers
-                    ]
+                    try:
+                        per_chain_powers = last_est_out.split(
+                            ':')[1].strip().split('  ')
+                        per_chain_powers = [
+                            float(power) for power in per_chain_powers
+                        ]
+                    except:
+                        per_chain_powers = [0, 0]
+                        self.log.warning(
+                            'Could not parse output: {}'.format(last_est_out))
                     self.log.info(
                         'Current Tx Powers = {}'.format(per_chain_powers))
                     if per_chain_powers[0] > 0:
@@ -786,8 +807,11 @@
             self.access_point.set_region(self.testbed_params['DFS_region'])
         else:
             self.access_point.set_region(self.testbed_params['default_region'])
-        self.access_point.set_channel(band, testcase_params['channel'])
-        self.access_point.set_bandwidth(band, testcase_params['mode'])
+        self.access_point.set_channel_and_bandwidth(band,
+                                                    testcase_params['channel'],
+                                                    testcase_params['mode'])
+        #self.access_point.set_channel(band, testcase_params['channel'])
+        #self.access_point.set_bandwidth(band, testcase_params['mode'])
         if 'low' in testcase_params['ap_power']:
             self.log.info('Setting low AP power.')
             self.access_point.set_power(
@@ -825,9 +849,17 @@
         if self.testbed_params.get('txbf_off', False):
             wputils.disable_beamforming(self.dut)
         wutils.set_wifi_country_code(self.dut, testcase_params['country_code'])
+        current_country = self.dut.adb.shell('wl country')
+        self.log.info('Current country code: {}'.format(current_country))
+        if testcase_params['country_code'] not in current_country:
+            asserts.fail('Country code not correct.')
+        chan_list = self.dut.adb.shell('wl chan_info_list')
+        if str(testcase_params['channel']) not in chan_list:
+            asserts.skip('Channel {} not supported in {}'.format(
+                testcase_params['channel'], testcase_params['country_code']))
         wutils.wifi_connect(self.dut,
                             testcase_params['test_network'],
-                            num_of_tries=1,
+                            num_of_tries=5,
                             check_connectivity=True)
         self.dut_ip = self.dut.droid.connectivityGetIPv4Addresses('wlan0')[0]
 
@@ -925,3 +957,23 @@
                     partial(self._test_ping, testcase_params))
             test_cases.append(testcase_name)
         return test_cases
+
+
+class WifiTxPowerCheck_BasicSAR_Test(WifiTxPowerCheckTest):
+
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.testcase_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_case())
+        self.testclass_metric_logger = (
+            BlackboxMappedMetricLogger.for_test_class())
+        self.publish_testcase_metrics = True
+        self.tests = self.generate_test_cases(
+            ap_power='standard',
+            channels=[6, 36, 52, 100, 149, '6g37'],
+            modes=['bw20', 'bw160'],
+            test_types=[
+                'test_tx_power',
+            ],
+            country_codes=['US', 'GB', 'JP', 'CA'],
+            sar_states=[-1, 0, 1, 2, 3, 4])
diff --git a/acts_tests/tests/google/wifi/aware/functional/AttachTest.py b/acts_tests/tests/google/wifi/aware/functional/AttachTest.py
index eee0f9d..8b16dc7 100644
--- a/acts_tests/tests/google/wifi/aware/functional/AttachTest.py
+++ b/acts_tests/tests/google/wifi/aware/functional/AttachTest.py
@@ -166,3 +166,18 @@
         # try enabling Aware again (attach)
         dut.droid.wifiAwareAttach()
         autils.wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACHED)
+
+    @test_tracker_info(uuid="")
+    def test_attach_detach_attach_again(self):
+        """Validated there is no delay between Disable Aware and re-enable Aware
+        """
+        dut = self.android_devices[0]
+
+        # enable Aware (attach)
+        dut.droid.wifiAwareAttach()
+        autils.wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACHED)
+
+        dut.droid.wifiAwareDestroyAll()
+        # Restart Aware immediately
+        dut.droid.wifiAwareAttach()
+        autils.wait_for_event(dut, aconsts.EVENT_CB_ON_ATTACHED, timeout=1)
diff --git a/acts_tests/tests/google/wifi/aware/functional/DiscoveryTest.py b/acts_tests/tests/google/wifi/aware/functional/DiscoveryTest.py
index 39f009d..69dc42c 100644
--- a/acts_tests/tests/google/wifi/aware/functional/DiscoveryTest.py
+++ b/acts_tests/tests/google/wifi/aware/functional/DiscoveryTest.py
@@ -14,6 +14,7 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
+import queue
 import string
 import time
 
@@ -32,6 +33,7 @@
     PAYLOAD_SIZE_MIN = 0
     PAYLOAD_SIZE_TYPICAL = 1
     PAYLOAD_SIZE_MAX = 2
+    EVENT_TIMEOUT = 3
 
     # message strings
     query_msg = "How are you doing? 你好嗎?"
@@ -1111,6 +1113,18 @@
             event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING], s_to_p_msg,
             "Message on service %s from Subscriber to Publisher "
             "not received correctly" % session_name["pub"][p_disc_id])
+        try:
+            event = p_dut.ed.pop_event(autils.decorate_event(aconsts.SESSION_CB_ON_MESSAGE_RECEIVED,
+                                             p_disc_id), self.EVENT_TIMEOUT)
+            p_dut.log.info("re-transmit message received: "
+                           + event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING])
+            asserts.assert_equal(
+                event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING], s_to_p_msg,
+                "Message on service %s from Subscriber to Publisher "
+                "not received correctly" % session_name["pub"][p_disc_id])
+        except queue.Empty:
+            p_dut.log.info("no re-transmit message")
+
         peer_id_on_pub = event["data"][aconsts.SESSION_CB_KEY_PEER_ID]
 
         # Message send from Publisher to Subscriber
@@ -1129,6 +1143,17 @@
             event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING], p_to_s_msg,
             "Message on service %s from Publisher to Subscriber"
             "not received correctly" % session_name["sub"][s_disc_id])
+        try:
+            event = s_dut.ed.pop_event(autils.decorate_event(aconsts.SESSION_CB_ON_MESSAGE_RECEIVED,
+                                                             s_disc_id), self.EVENT_TIMEOUT)
+            s_dut.log.info("re-transmit message received: "
+                           + event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING])
+            asserts.assert_equal(
+                event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING], p_to_s_msg,
+                "Message on service %s from Publisher to Subscriber"
+                "not received correctly" % session_name["sub"][s_disc_id])
+        except queue.Empty:
+            s_dut.log.info("no re-transmit message")
 
     def run_multiple_concurrent_services_same_name_diff_ssi(self, type_x, type_y):
         """Validate same service name with multiple service specific info on publisher
diff --git a/acts_tests/tests/google/wifi/aware/performance/LatencyTest.py b/acts_tests/tests/google/wifi/aware/performance/LatencyTest.py
index 871602b..9c44ca6 100644
--- a/acts_tests/tests/google/wifi/aware/performance/LatencyTest.py
+++ b/acts_tests/tests/google/wifi/aware/performance/LatencyTest.py
@@ -33,7 +33,7 @@
     # take some time
     WAIT_FOR_CLUSTER = 5
 
-    def start_discovery_session(self, dut, session_id, is_publish, dtype):
+    def start_discovery_session(self, dut, session_id, is_publish, dtype, instant_mode = None):
         """Start a discovery session
 
     Args:
@@ -41,6 +41,7 @@
       session_id: ID of the Aware session in which to start discovery
       is_publish: True for a publish session, False for subscribe session
       dtype: Type of the discovery session
+      instant_mode: set the channel to use instant communication mode.
 
     Returns:
       Discovery session started event.
@@ -48,6 +49,9 @@
         config = {}
         config[aconsts.DISCOVERY_KEY_DISCOVERY_TYPE] = dtype
         config[aconsts.DISCOVERY_KEY_SERVICE_NAME] = "GoogleTestServiceXY"
+        if instant_mode is not None:
+            config[aconsts.DISCOVERY_KEY_INSTANT_COMMUNICATION_MODE] = instant_mode
+
 
         if is_publish:
             disc_id = dut.droid.wifiAwarePublish(session_id, config)
@@ -74,6 +78,7 @@
                               solicited/active.
       dw_24ghz: DW interval in the 2.4GHz band.
       dw_5ghz: DW interval in the 5GHz band.
+      num_iterations: number of the iterations.
       startup_offset: The start-up gap (in seconds) between the two devices
       timeout_period: Time period over which to measure synchronization
     """
@@ -160,6 +165,7 @@
                               solicited/active.
       dw_24ghz: DW interval in the 2.4GHz band.
       dw_5ghz: DW interval in the 5GHz band.
+      num_iterations: number of the iterations.
     """
         key = "%s_dw24_%d_dw5_%d" % ("unsolicited_passive"
                                      if do_unsolicited_passive else
@@ -237,13 +243,15 @@
         p_dut.droid.wifiAwareDestroyAll()
         s_dut.droid.wifiAwareDestroyAll()
 
-    def run_message_latency(self, results, dw_24ghz, dw_5ghz, num_iterations):
+    def run_message_latency(self, results, dw_24ghz, dw_5ghz, num_iterations, instant_mode=None):
         """Run the message tx latency test with the specified DW intervals.
 
     Args:
       results: Result array to be populated - will add results (not erase it)
       dw_24ghz: DW interval in the 2.4GHz band.
       dw_5ghz: DW interval in the 5GHz band.
+      num_iterations: number of the iterations.
+      instant_mode: set the band to use instant communication mode, 2G or 5G
     """
         key = "dw24_%d_dw5_%d" % (dw_24ghz, dw_5ghz)
         results[key] = {}
@@ -262,9 +270,9 @@
              p_dut,
              s_dut,
              p_config=autils.create_discovery_config(
-                 self.SERVICE_NAME, aconsts.PUBLISH_TYPE_UNSOLICITED),
+                 self.SERVICE_NAME, aconsts.PUBLISH_TYPE_UNSOLICITED, instant_mode=instant_mode),
              s_config=autils.create_discovery_config(
-                 self.SERVICE_NAME, aconsts.SUBSCRIBE_TYPE_PASSIVE),
+                 self.SERVICE_NAME, aconsts.SUBSCRIBE_TYPE_PASSIVE, instant_mode=instant_mode),
              device_startup_offset=self.device_startup_offset)
 
         latencies = []
@@ -330,6 +338,7 @@
       results: Result array to be populated - will add results (not erase it)
       dw_24ghz: DW interval in the 2.4GHz band.
       dw_5ghz: DW interval in the 5GHz band.
+      num_iterations: number of the iterations.
     """
         key_avail = "on_avail_dw24_%d_dw5_%d" % (dw_24ghz, dw_5ghz)
         key_link_props = "link_props_dw24_%d_dw5_%d" % (dw_24ghz, dw_5ghz)
@@ -435,21 +444,21 @@
         results[key_avail]["ndp_setup_failures"] = ndp_setup_failures
 
     def run_end_to_end_latency(self, results, dw_24ghz, dw_5ghz,
-                               num_iterations, startup_offset, include_setup):
+                               num_iterations, startup_offset, include_setup, instant_mode = None):
         """Measure the latency for end-to-end communication link setup:
     - Start Aware
     - Discovery
-    - Message from Sub -> Pub
-    - Message from Pub -> Sub
     - NDP setup
 
     Args:
       results: Result array to be populated - will add results (not erase it)
       dw_24ghz: DW interval in the 2.4GHz band.
       dw_5ghz: DW interval in the 5GHz band.
+      num_iterations: number of the iterations.
       startup_offset: The start-up gap (in seconds) between the two devices
       include_setup: True to include the cluster setup in the latency
                     measurements.
+      instant_mode: set the band to use instant communication mode, 2G or 5G
     """
         key = "dw24_%d_dw5_%d" % (dw_24ghz, dw_5ghz)
         results[key] = {}
@@ -492,11 +501,19 @@
 
                 # start publish
                 p_disc_id, p_disc_event = self.start_discovery_session(
-                    p_dut, p_id, True, aconsts.PUBLISH_TYPE_UNSOLICITED)
+                    p_dut, p_id, True, aconsts.PUBLISH_TYPE_UNSOLICITED, instant_mode)
 
                 # start subscribe
                 s_disc_id, s_session_event = self.start_discovery_session(
-                    s_dut, s_id, False, aconsts.SUBSCRIBE_TYPE_PASSIVE)
+                    s_dut, s_id, False, aconsts.SUBSCRIBE_TYPE_PASSIVE, instant_mode)
+
+                # create NDP
+
+                # Publisher: request network
+                p_req_key = autils.request_network(
+                    p_dut,
+                    p_dut.droid.wifiAwareCreateNetworkSpecifier(
+                        p_disc_id, None, None))
 
                 # wait for discovery (allow for failures here since running lots of
                 # samples and would like to get the partial data even in the presence of
@@ -516,82 +533,6 @@
                     failures = failures + 1
                     break
 
-                # message from Sub -> Pub
-                msg_s2p = "Message Subscriber -> Publisher #%d" % i
-                next_msg_id = self.get_next_msg_id()
-                s_dut.droid.wifiAwareSendMessage(s_disc_id, peer_id_on_sub,
-                                                 next_msg_id, msg_s2p, 0)
-
-                # wait for Tx confirmation
-                try:
-                    s_dut.ed.pop_event(aconsts.SESSION_CB_ON_MESSAGE_SENT,
-                                       autils.EVENT_TIMEOUT)
-                except queue.Empty:
-                    s_dut.log.info("[Subscriber] Timed out while waiting for "
-                                   "SESSION_CB_ON_MESSAGE_SENT")
-                    failures = failures + 1
-                    break
-
-                # wait for Rx confirmation (and validate contents)
-                try:
-                    event = p_dut.ed.pop_event(
-                        aconsts.SESSION_CB_ON_MESSAGE_RECEIVED,
-                        autils.EVENT_TIMEOUT)
-                    peer_id_on_pub = event['data'][
-                        aconsts.SESSION_CB_KEY_PEER_ID]
-                    if (event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING]
-                            != msg_s2p):
-                        p_dut.log.info(
-                            "[Publisher] Corrupted input message - %s", event)
-                        failures = failures + 1
-                        break
-                except queue.Empty:
-                    p_dut.log.info("[Publisher] Timed out while waiting for "
-                                   "SESSION_CB_ON_MESSAGE_RECEIVED")
-                    failures = failures + 1
-                    break
-
-                # message from Pub -> Sub
-                msg_p2s = "Message Publisher -> Subscriber #%d" % i
-                next_msg_id = self.get_next_msg_id()
-                p_dut.droid.wifiAwareSendMessage(p_disc_id, peer_id_on_pub,
-                                                 next_msg_id, msg_p2s, 0)
-
-                # wait for Tx confirmation
-                try:
-                    p_dut.ed.pop_event(aconsts.SESSION_CB_ON_MESSAGE_SENT,
-                                       autils.EVENT_TIMEOUT)
-                except queue.Empty:
-                    p_dut.log.info("[Publisher] Timed out while waiting for "
-                                   "SESSION_CB_ON_MESSAGE_SENT")
-                    failures = failures + 1
-                    break
-
-                # wait for Rx confirmation (and validate contents)
-                try:
-                    event = s_dut.ed.pop_event(
-                        aconsts.SESSION_CB_ON_MESSAGE_RECEIVED,
-                        autils.EVENT_TIMEOUT)
-                    if (event["data"][aconsts.SESSION_CB_KEY_MESSAGE_AS_STRING]
-                            != msg_p2s):
-                        s_dut.log.info(
-                            "[Subscriber] Corrupted input message - %s", event)
-                        failures = failures + 1
-                        break
-                except queue.Empty:
-                    s_dut.log.info("[Subscriber] Timed out while waiting for "
-                                   "SESSION_CB_ON_MESSAGE_RECEIVED")
-                    failures = failures + 1
-                    break
-
-                # create NDP
-
-                # Publisher: request network
-                p_req_key = autils.request_network(
-                    p_dut,
-                    p_dut.droid.wifiAwareCreateNetworkSpecifier(
-                        p_disc_id, peer_id_on_pub, None))
-
                 # Subscriber: request network
                 s_req_key = autils.request_network(
                     s_dut,
@@ -613,6 +554,8 @@
                          cconsts.NETWORK_CB_LINK_PROPERTIES_CHANGED),
                         (cconsts.NETWORK_CB_KEY_ID, s_req_key))
                 except:
+                    s_dut.log.info("[Subscriber] Timed out while waiting for "
+                                   "EVENT_NETWORK_CALLBACK")
                     failures = failures + 1
                     break
 
@@ -742,6 +685,34 @@
         asserts.explicit_pass(
             "test_message_latency_default_dws finished", extras=results)
 
+    def test_message_latency_default_dws_instant_mode_2g(self):
+        """Measure the send message latency with the default DW configuration. Test
+    performed on non-queued message transmission - i.e. waiting for confirmation
+    of reception (ACK) before sending the next message."""
+        results = {}
+        self.run_message_latency(
+            results=results,
+            dw_24ghz=aconsts.POWER_DW_24_INTERACTIVE,
+            dw_5ghz=aconsts.POWER_DW_5_INTERACTIVE,
+            num_iterations=100,
+            instant_mode="2G")
+        asserts.explicit_pass(
+            "test_message_latency_default_dws finished", extras=results)
+
+    def test_message_latency_default_dws_instant_mode_5g(self):
+        """Measure the send message latency with the default DW configuration. Test
+    performed on non-queued message transmission - i.e. waiting for confirmation
+    of reception (ACK) before sending the next message."""
+        results = {}
+        self.run_message_latency(
+            results=results,
+            dw_24ghz=aconsts.POWER_DW_24_INTERACTIVE,
+            dw_5ghz=aconsts.POWER_DW_5_INTERACTIVE,
+            num_iterations=100,
+            instant_mode="5G")
+        asserts.explicit_pass(
+            "test_message_latency_default_dws finished", extras=results)
+
     def test_message_latency_non_interactive_dws(self):
         """Measure the send message latency with the DW configuration for
     non-interactive mode. Test performed on non-queued message transmission -
@@ -787,8 +758,6 @@
         """Measure the latency for end-to-end communication link setup:
       - Start Aware
       - Discovery
-      - Message from Sub -> Pub
-      - Message from Pub -> Sub
       - NDP setup
     """
         results = {}
@@ -802,14 +771,48 @@
         asserts.explicit_pass(
             "test_end_to_end_latency_default_dws finished", extras=results)
 
+    def test_end_to_end_latency_default_dws_instant_mode_2g(self):
+        """Measure the latency for end-to-end communication link setup:
+      - Start Aware
+      - Discovery
+      - NDP setup
+    """
+        results = {}
+        self.run_end_to_end_latency(
+            results,
+            dw_24ghz=aconsts.POWER_DW_24_INTERACTIVE,
+            dw_5ghz=aconsts.POWER_DW_5_INTERACTIVE,
+            num_iterations=10,
+            startup_offset=0,
+            include_setup=True,
+            instant_mode="2G")
+        asserts.explicit_pass(
+            "test_end_to_end_latency_default_dws finished", extras=results)
+
+    def test_end_to_end_latency_default_dws_instant_mode_5g(self):
+        """Measure the latency for end-to-end communication link setup:
+      - Start Aware
+      - Discovery
+      - NDP setup
+    """
+        results = {}
+        self.run_end_to_end_latency(
+            results,
+            dw_24ghz=aconsts.POWER_DW_24_INTERACTIVE,
+            dw_5ghz=aconsts.POWER_DW_5_INTERACTIVE,
+            num_iterations=10,
+            startup_offset=0,
+            include_setup=True,
+            instant_mode="5G")
+        asserts.explicit_pass(
+            "test_end_to_end_latency_default_dws finished", extras=results)
+
     def test_end_to_end_latency_post_attach_default_dws(self):
         """Measure the latency for end-to-end communication link setup without
     the initial synchronization:
       - Start Aware & synchronize initially
       - Loop:
         - Discovery
-        - Message from Sub -> Pub
-        - Message from Pub -> Sub
         - NDP setup
     """
         results = {}
@@ -823,3 +826,59 @@
         asserts.explicit_pass(
             "test_end_to_end_latency_post_attach_default_dws finished",
             extras=results)
+
+    def test_end_to_end_latency_post_attach_default_dws_instant_mode_2g(self):
+        """Measure the latency for end-to-end communication link setup without
+    the initial synchronization:
+        - Start Aware & synchronize initially
+        - Loop:
+        - Discovery
+        - NDP setup
+    """
+        asserts.skip_if(not self.android_devices[0].droid.isSdkAtLeastT(),
+                        "instant communication mode is only supported on T+")
+        asserts.skip_if(not (self.android_devices[0].aware_capabilities[aconsts
+                             .CAP_SUPPORTED_INSTANT_COMMUNICATION_MODE]
+                             and self.android_devices[0].aware_capabilities[aconsts
+                             .CAP_SUPPORTED_INSTANT_COMMUNICATION_MODE]),
+                        "Device doesn't support instant communication mode")
+        results = {}
+        self.run_end_to_end_latency(
+            results,
+            dw_24ghz=aconsts.POWER_DW_24_INTERACTIVE,
+            dw_5ghz=aconsts.POWER_DW_5_INTERACTIVE,
+            num_iterations=10,
+            startup_offset=0,
+            include_setup=False,
+            instant_mode="2G")
+        asserts.explicit_pass(
+            "test_end_to_end_latency_post_attach_default_dws_instant_mode finished",
+            extras=results)
+
+    def test_end_to_end_latency_post_attach_default_dws_instant_mode_5g(self):
+        """Measure the latency for end-to-end communication link setup without
+    the initial synchronization:
+        - Start Aware & synchronize initially
+        - Loop:
+        - Discovery
+        - NDP setup
+    """
+        asserts.skip_if(not self.android_devices[0].droid.isSdkAtLeastT(),
+                        "instant communication mode is only supported on T+")
+        asserts.skip_if(not (self.android_devices[0].aware_capabilities[aconsts
+                             .CAP_SUPPORTED_INSTANT_COMMUNICATION_MODE]
+                             and self.android_devices[0].aware_capabilities[aconsts
+                             .CAP_SUPPORTED_INSTANT_COMMUNICATION_MODE]),
+                        "Device doesn't support instant communication mode")
+        results = {}
+        self.run_end_to_end_latency(
+            results,
+            dw_24ghz=aconsts.POWER_DW_24_INTERACTIVE,
+            dw_5ghz=aconsts.POWER_DW_5_INTERACTIVE,
+            num_iterations=10,
+            startup_offset=0,
+            include_setup=False,
+            instant_mode="5G")
+        asserts.explicit_pass(
+            "test_end_to_end_latency_post_attach_default_dws_instant_mode finished",
+            extras=results)
diff --git a/acts_tests/tests/google/wifi/rtt/functional/RangeAwareTest.py b/acts_tests/tests/google/wifi/rtt/functional/RangeAwareTest.py
index 1051fc4..84ad88e 100644
--- a/acts_tests/tests/google/wifi/rtt/functional/RangeAwareTest.py
+++ b/acts_tests/tests/google/wifi/rtt/functional/RangeAwareTest.py
@@ -339,19 +339,6 @@
 
     #############################################################################
 
-    @test_tracker_info(uuid="9e4e7ab4-2254-498c-9788-21e15ed9a370")
-    def test_rtt_oob_discovery_one_way(self):
-        """Perform RTT between 2 Wi-Fi Aware devices. Use out-of-band discovery
-        to communicate the MAC addresses to the peer. Test one-direction RTT only.
-        Functionality test: Only evaluate success rate.
-        """
-        rtt_results = self.run_rtt_oob_discovery_set(
-            do_both_directions=False,
-            iter_count=self.NUM_ITER,
-            time_between_iterations=self.TIME_BETWEEN_ITERATIONS,
-            time_between_roles=self.TIME_BETWEEN_ROLES)
-        self.verify_results(rtt_results)
-
     @test_tracker_info(uuid="22edba77-eeb2-43ee-875a-84437550ad84")
     def test_rtt_oob_discovery_both_ways(self):
         """Perform RTT between 2 Wi-Fi Aware devices. Use out-of-band discovery
@@ -393,19 +380,6 @@
             time_between_roles=self.TIME_BETWEEN_ROLES)
         self.verify_results(rtt_results1, rtt_results2)
 
-    @test_tracker_info(uuid="3a1d19a2-7241-49e0-aaf2-0a1da4c29783")
-    def test_rtt_oob_discovery_one_way_with_accuracy_evaluation(self):
-        """Perform RTT between 2 Wi-Fi Aware devices. Use out-of-band discovery
-        to communicate the MAC addresses to the peer. Test one-direction RTT only.
-        Performance test: evaluate success rate and accuracy.
-        """
-        rtt_results = self.run_rtt_oob_discovery_set(
-            do_both_directions=False,
-            iter_count=self.NUM_ITER,
-            time_between_iterations=self.TIME_BETWEEN_ITERATIONS,
-            time_between_roles=self.TIME_BETWEEN_ROLES)
-        self.verify_results(rtt_results, accuracy_evaluation=True)
-
     @test_tracker_info(uuid="82f954a5-c0ec-4bc6-8940-3b72199328ac")
     def test_rtt_oob_discovery_both_ways_with_accuracy_evaluation(self):
         """Perform RTT between 2 Wi-Fi Aware devices. Use out-of-band discovery
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiEnterprise11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiEnterprise11axTest.py
index 7f0f121..94313b2 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiEnterprise11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiEnterprise11axTest.py
@@ -15,7 +15,7 @@
 
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiEnterpriseTest import WifiEnterpriseTest
+from ..WifiEnterpriseTest import WifiEnterpriseTest
 
 WifiEnums = wutils.WifiEnums
 # EAP Macros
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiEnterpriseRoaming11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiEnterpriseRoaming11axTest.py
index ec70da9..781e5af 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiEnterpriseRoaming11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiEnterpriseRoaming11axTest.py
@@ -15,7 +15,7 @@
 
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiEnterpriseRoamingTest import WifiEnterpriseRoamingTest
+from ..WifiEnterpriseRoamingTest import WifiEnterpriseRoamingTest
 
 WifiEnums = wutils.WifiEnums
 # EAP Macros
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiManager11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiManager11axTest.py
index d2fb981..fd906a0 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiManager11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiManager11axTest.py
@@ -14,7 +14,7 @@
 #   limitations under the License.
 
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiManagerTest import WifiManagerTest
+from ..WifiManagerTest import WifiManagerTest
 
 
 class WifiManager11axTest(WifiManagerTest):
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiNetworkSuggestion11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiNetworkSuggestion11axTest.py
index 2c45e8c..ab08cce 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiNetworkSuggestion11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiNetworkSuggestion11axTest.py
@@ -15,7 +15,7 @@
 
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiNetworkSuggestionTest import WifiNetworkSuggestionTest
+from ..WifiNetworkSuggestionTest import WifiNetworkSuggestionTest
 
 WifiEnums = wutils.WifiEnums
 # EAP Macros
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiPno11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiPno11axTest.py
index 7509d28..63b43ef 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiPno11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiPno11axTest.py
@@ -15,7 +15,7 @@
 
 import time
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiPnoTest import WifiPnoTest
+from ..WifiPnoTest import WifiPnoTest
 
 MAX_ATTN = 95
 
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiSoftApAcs11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiSoftApAcs11axTest.py
index 0155be4..1a4a2c8 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiSoftApAcs11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiSoftApAcs11axTest.py
@@ -17,7 +17,7 @@
 from acts.controllers.ap_lib import hostapd_constants
 import acts_contrib.test_utils.wifi.wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiSoftApAcsTest import WifiSoftApAcsTest
+from ..WifiSoftApAcsTest import WifiSoftApAcsTest
 
 
 class WifiSoftApAcs11axTest(WifiSoftApAcsTest):
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiStaApConcurrency11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiStaApConcurrency11axTest.py
index bdb9dc1..88b1e90 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiStaApConcurrency11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiStaApConcurrency11axTest.py
@@ -17,7 +17,7 @@
 import acts.signals as signals
 import acts_contrib.test_utils.wifi.wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiStaApConcurrencyTest import WifiStaApConcurrencyTest
+from ..WifiStaApConcurrencyTest import WifiStaApConcurrencyTest
 
 
 class WifiStaApConcurrency11axTest(WifiStaApConcurrencyTest):
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiStaConcurrencyNetworkRequest11axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiStaConcurrencyNetworkRequest11axTest.py
index 215a6e6..8dd76627b 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiStaConcurrencyNetworkRequest11axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiStaConcurrencyNetworkRequest11axTest.py
@@ -16,7 +16,7 @@
 import acts.utils as utils
 import acts_contrib.test_utils.wifi.wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiStaConcurrencyNetworkRequestTest import WifiStaConcurrencyNetworkRequestTest
+from ..WifiStaConcurrencyNetworkRequestTest import WifiStaConcurrencyNetworkRequestTest
 
 
 class WifiStaConcurrencyNetworkRequest11axTest(
diff --git a/acts_tests/tests/google/wifi/wifi6/WifiWpa311axTest.py b/acts_tests/tests/google/wifi/wifi6/WifiWpa311axTest.py
index 57ab366..cfe7987 100644
--- a/acts_tests/tests/google/wifi/wifi6/WifiWpa311axTest.py
+++ b/acts_tests/tests/google/wifi/wifi6/WifiWpa311axTest.py
@@ -14,7 +14,7 @@
 #   limitations under the License.
 
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiWpa3OweTest import WifiWpa3OweTest
+from ..WifiWpa3OweTest import WifiWpa3OweTest
 
 
 class WifiWpa311axTest(WifiWpa3OweTest):
diff --git a/acts_tests/tests/google/wifi/wifi6e/WifiNetworkSelector6eTest.py b/acts_tests/tests/google/wifi/wifi6e/WifiNetworkSelector6eTest.py
index 4a0ecf3..fa51247 100644
--- a/acts_tests/tests/google/wifi/wifi6e/WifiNetworkSelector6eTest.py
+++ b/acts_tests/tests/google/wifi/wifi6e/WifiNetworkSelector6eTest.py
@@ -17,7 +17,7 @@
 from acts.test_decorators import test_tracker_info
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiNetworkSelectorTest import WifiNetworkSelectorTest
+from ..WifiNetworkSelectorTest import WifiNetworkSelectorTest
 
 # WifiNetworkSelector imposes a 10 seconds gap between two selections
 NETWORK_SELECTION_TIME_GAP = 12
diff --git a/acts_tests/tests/google/wifi/wifi6e/WifiPno6eTest.py b/acts_tests/tests/google/wifi/wifi6e/WifiPno6eTest.py
index 0bfc3da..fbccc88 100644
--- a/acts_tests/tests/google/wifi/wifi6e/WifiPno6eTest.py
+++ b/acts_tests/tests/google/wifi/wifi6e/WifiPno6eTest.py
@@ -17,7 +17,7 @@
 from acts.test_decorators import test_tracker_info
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiPnoTest import WifiPnoTest
+from ..WifiPnoTest import WifiPnoTest
 
 MAX_ATTN = 95
 
diff --git a/acts_tests/tests/google/wifi/wifi6e/WifiStaApConcurrency6eTest.py b/acts_tests/tests/google/wifi/wifi6e/WifiStaApConcurrency6eTest.py
index 15a3264..5ba6dec 100644
--- a/acts_tests/tests/google/wifi/wifi6e/WifiStaApConcurrency6eTest.py
+++ b/acts_tests/tests/google/wifi/wifi6e/WifiStaApConcurrency6eTest.py
@@ -18,7 +18,7 @@
 from acts.test_decorators import test_tracker_info
 from acts_contrib.test_utils.wifi import wifi_test_utils as wutils
 from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest
-from WifiStaApConcurrencyTest import WifiStaApConcurrencyTest
+from ..WifiStaApConcurrencyTest import WifiStaApConcurrencyTest
 
 WifiEnums = wutils.WifiEnums
 WIFI_CONFIG_SOFTAP_BAND_2G = WifiEnums.WIFI_CONFIG_SOFTAP_BAND_2G
diff --git a/acts_tests/tests/google/wifi/wifi6e/WifiTeleCoex6eTest.py b/acts_tests/tests/google/wifi/wifi6e/WifiTeleCoex6eTest.py
index 066abdd..9a35f4e 100644
--- a/acts_tests/tests/google/wifi/wifi6e/WifiTeleCoex6eTest.py
+++ b/acts_tests/tests/google/wifi/wifi6e/WifiTeleCoex6eTest.py
@@ -16,7 +16,7 @@
 
 from acts.test_decorators import test_tracker_info
 from acts_contrib.test_utils.tel.TelephonyBaseTest import TelephonyBaseTest
-from WifiTeleCoexTest import WifiTeleCoexTest
+from ..WifiTeleCoexTest import WifiTeleCoexTest
 
 
 class WifiTeleCoex6eTest(WifiTeleCoexTest):