Snap for 4813226 from 87c31720385357aa1c23df4affa32aca8e234468 to pi-release

Change-Id: I02e9e1b6f15d13b74b1df3c4fa6aa6bdd6d3ba23
diff --git a/acts/framework/acts/controllers/__init__.py b/acts/framework/acts/controllers/__init__.py
index 2cc6f43..41e48df 100644
--- a/acts/framework/acts/controllers/__init__.py
+++ b/acts/framework/acts/controllers/__init__.py
@@ -25,5 +25,5 @@
 """This is a list of all the top level controller modules"""
 __all__ = [
     "android_device", "attenuator", "monsoon", "access_point", "iperf_server",
-    "packet_sender"
+    "packet_sender", "arduino_wifi_dongle"
 ]
diff --git a/acts/framework/acts/controllers/arduino_wifi_dongle.py b/acts/framework/acts/controllers/arduino_wifi_dongle.py
new file mode 100644
index 0000000..7978f30
--- /dev/null
+++ b/acts/framework/acts/controllers/arduino_wifi_dongle.py
@@ -0,0 +1,392 @@
+#!/usr/bin/env python3
+#
+#   Copyright 2018 - The Android Open Source Project
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+import logging
+import os
+import re
+import serial
+import subprocess
+import threading
+import time
+
+from acts import logger
+from acts import signals
+from acts import tracelogger
+from acts import utils
+from acts.test_utils.wifi import wifi_test_utils as wutils
+
+from datetime import datetime
+
+ACTS_CONTROLLER_CONFIG_NAME = "ArduinoWifiDongle"
+ACTS_CONTROLLER_REFERENCE_NAME = "arduino_wifi_dongles"
+
+WIFI_DONGLE_EMPTY_CONFIG_MSG = "Configuration is empty, abort!"
+WIFI_DONGLE_NOT_LIST_CONFIG_MSG = "Configuration should be a list, abort!"
+
+DEV = "/dev/"
+IP = "IP: "
+STATUS = "STATUS: "
+SSID = "SSID: "
+RSSI = "RSSI: "
+PING = "PING: "
+SCAN_BEGIN = "Scan Begin"
+SCAN_END = "Scan End"
+READ_TIMEOUT = 10
+BAUD_RATE = 9600
+TMP_DIR = "tmp/"
+SSID_KEY = wutils.WifiEnums.SSID_KEY
+PWD_KEY = wutils.WifiEnums.PWD_KEY
+
+
+class ArduinoWifiDongleError(signals.ControllerError):
+    pass
+
+class DoesNotExistError(ArduinoWifiDongleError):
+    """Raised when something that does not exist is referenced."""
+
+def create(configs):
+    """Creates ArduinoWifiDongle objects.
+
+    Args:
+        configs: A list of dicts or a list of serial numbers, each representing
+                 a configuration of a arduino wifi dongle.
+
+    Returns:
+        A list of Wifi dongle objects.
+    """
+    wcs = []
+    if not configs:
+        raise ArduinoWifiDongleError(WIFI_DONGLE_EMPTY_CONFIG_MSG)
+    elif not isinstance(configs, list):
+        raise ArduinoWifiDongleError(WIFI_DONGLE_NOT_LIST_CONFIG_MSG)
+    elif isinstance(configs[0], str):
+        # Configs is a list of serials.
+        wcs = get_instances(configs)
+    else:
+        # Configs is a list of dicts.
+        wcs = get_instances_with_configs(configs)
+
+    return wcs
+
+def destroy(wcs):
+    for wc in wcs:
+        wc.clean_up()
+
+def get_instances(configs):
+    wcs = []
+    for s in configs:
+        wcs.append(ArduinoWifiDongle(s))
+    return wcs
+
+def get_instances_with_configs(configs):
+    wcs = []
+    for c in configs:
+        try:
+            s = c.pop("serial")
+        except KeyError:
+            raise ArduinoWifiDongleError(
+                "'serial' is missing for ArduinoWifiDongle config %s." % c)
+        wcs.append(ArduinoWifiDongle(s))
+    return wcs
+
+class ArduinoWifiDongle(object):
+    """Class representing an arduino wifi dongle.
+
+    Each object of this class represents one wifi dongle in ACTS.
+
+    Attribtues:
+        serial: Short serial number of the wifi dongle in string.
+        port: The terminal port the dongle is connected to in string.
+        log: A logger adapted from root logger with added token specific to an
+             ArduinoWifiDongle instance.
+        log_file_fd: File handle of the log file.
+        set_logging: Logging for the dongle is enabled when this param is set
+        lock: Lock to acquire and release set_logging variable
+        ssid: SSID of the wifi network the dongle is connected to.
+        ip_addr: IP address on the wifi interface.
+        scan_results: Most recent scan results.
+        ping: Ping status in bool - ping to www.google.com
+    """
+    def __init__(self, serial=''):
+        """Initializes the ArduinoWifiDongle object."""
+        self.serial = serial
+        self.port = self._get_serial_port()
+        self.log = logger.create_tagged_trace_logger(
+            "ArduinoWifiDongle|%s" % self.serial)
+        log_path_base = getattr(logging, "log_path", "/tmp/logs")
+        self.log_file_path = os.path.join(
+            log_path_base, "ArduinoWifiDongle_%s_serial_log.txt" % self.serial)
+        self.log_file_fd = open(self.log_file_path, "a")
+
+        self.set_logging = True
+        self.lock = threading.Lock()
+        self.start_controller_log()
+
+        self.ssid = None
+        self.ip_addr = None
+        self.status = 0
+        self.scan_results = []
+        self.scanning = False
+        self.ping = False
+
+        try:
+            os.stat(TMP_DIR)
+        except:
+            os.mkdir(TMP_DIR)
+
+    def clean_up(self):
+        """Cleans up the ArduinoifiDongle object and releases any resources it
+        claimed.
+        """
+        self.stop_controller_log()
+        self.log_file_fd.close()
+
+    def _get_serial_port(self):
+        """Get the serial port for a given ArduinoWifiDongle serial number.
+
+        Returns:
+            Serial port in string if the dongle is attached.
+        """
+        if not self.serial:
+            raise ArduinoWifiDongleError(
+                "Wifi dongle's serial should not be empty")
+        cmd = "ls %s" % DEV
+        serial_ports = utils.exe_cmd(cmd).decode("utf-8", "ignore").split("\n")
+        for port in serial_ports:
+            if "USB" not in port:
+                continue
+            tty_port = "%s%s" % (DEV, port)
+            cmd = "udevadm info %s" % tty_port
+            udev_output = utils.exe_cmd(cmd).decode("utf-8", "ignore")
+            result = re.search("ID_SERIAL_SHORT=(.*)\n", udev_output)
+            if self.serial == result.group(1):
+                logging.info("Found wifi dongle %s at serial port %s" %
+                             (self.serial, tty_port))
+                return tty_port
+        raise ArduinoWifiDongleError("Wifi dongle %s is specified in config"
+                                    " but is not attached." % self.serial)
+
+    def write(self, arduino, file_path, network=None):
+        """Write an ino file to the arduino wifi dongle.
+
+        Args:
+            arduino: path of the arduino executable.
+            file_path: path of the ino file to flash onto the dongle.
+            network: wifi network to connect to.
+
+        Returns:
+            True: if the write is sucessful.
+            False: if not.
+        """
+        return_result = True
+        self.stop_controller_log("Flashing %s\n" % file_path)
+        cmd = arduino + file_path + " --upload --port " + self.port
+        if network:
+            cmd = self._update_ino_wifi_network(arduino, file_path, network)
+        self.log.info("Command is %s" % cmd)
+        proc = subprocess.Popen(cmd,
+            stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
+        out, err = proc.communicate()
+        return_code = proc.returncode
+        if return_code != 0:
+            self.log.error("Failed to write file %s" % return_code)
+            return_result = False
+        self.start_controller_log("Flashing complete\n")
+        return return_result
+
+    def _update_ino_wifi_network(self, arduino, file_path, network):
+        """Update wifi network in the ino file.
+
+        Args:
+            arduino: path of the arduino executable.
+            file_path: path of the ino file to flash onto the dongle
+            network: wifi network to update the ino file with
+
+        Returns:
+            cmd: arduino command to run to flash the ino file
+        """
+        tmp_file = "%s%s" % (TMP_DIR, file_path.split('/')[-1])
+        utils.exe_cmd("cp %s %s" % (file_path, tmp_file))
+        ssid = network[SSID_KEY]
+        pwd = network[PWD_KEY]
+        sed_cmd = "sed -i 's/\"wifi_tethering_test\"/\"%s\"/' %s" % (ssid, tmp_file)
+        utils.exe_cmd(sed_cmd)
+        sed_cmd = "sed -i  's/\"password\"/\"%s\"/' %s" % (pwd, tmp_file)
+        utils.exe_cmd(sed_cmd)
+        cmd = "%s %s --upload --port %s" %(arduino, tmp_file, self.port)
+        return cmd
+
+    def start_controller_log(self, msg=None):
+        """Reads the serial port and writes the data to ACTS log file.
+
+        This method depends on the logging enabled in the .ino files. The logs
+        are read from the serial port and are written to the ACTS log after
+        adding a timestamp to the data.
+
+        Args:
+            msg: Optional param to write to the log file.
+        """
+        if msg:
+            curr_time = str(datetime.now())
+            self.log_file_fd.write(curr_time + " INFO: " + msg)
+        t = threading.Thread(target=self._start_log)
+        t.daemon = True
+        t.start()
+
+    def stop_controller_log(self, msg=None):
+        """Stop the controller log.
+
+        Args:
+            msg: Optional param to write to the log file.
+        """
+        with self.lock:
+            self.set_logging = False
+        if msg:
+            curr_time = str(datetime.now())
+            self.log_file_fd.write(curr_time + " INFO: " + msg)
+
+    def _start_log(self):
+        """Target method called by start_controller_log().
+
+        This method is called as a daemon thread, which continously reads the
+        serial port. Stops when set_logging is set to False or when the test
+        ends.
+        """
+        self.set_logging = True
+        ser = serial.Serial(self.port, BAUD_RATE)
+        while True:
+            curr_time = str(datetime.now())
+            data = ser.readline().decode("utf-8", "ignore")
+            self._set_vars(data)
+            with self.lock:
+                if not self.set_logging:
+                    break
+            self.log_file_fd.write(curr_time + " " + data)
+
+    def _set_vars(self, data):
+        """Sets the variables by reading from the serial port.
+
+        Wifi dongle data such as wifi status, ip address, scan results
+        are read from the serial port and saved inside the class.
+
+        Args:
+            data: New line from the serial port.
+        """
+        # 'data' represents each line retrieved from the device's serial port.
+        # since we depend on the serial port logs to get the attributes of the
+        # dongle, every line has the format of {ino_file: method: param: value}.
+        # We look for the attribute in the log and retrieve its value.
+        # Ex: data = "connect_wifi: loop(): STATUS: 3" then val = "3"
+        # Similarly, we check when the scan has begun and ended and get all the
+        # scan results in between.
+        if data.count(":") != 3:
+            return
+        val = data.split(":")[-1].lstrip().rstrip()
+        if SCAN_BEGIN in data:
+            self.scan_results = []
+            self.scanning = True
+        elif SCAN_END in data:
+            self.scanning = False
+        elif self.scanning:
+            self.scan_results.append(data)
+        elif IP in data:
+            self.ip_addr = None if val == "0.0.0.0" else val
+        elif SSID in data:
+            self.ssid = val
+        elif STATUS in data:
+            self.status = int(val)
+        elif PING in data:
+            self.ping = False if int(val) == 0 else True
+
+    def ip_address(self, exp_result=True, timeout=READ_TIMEOUT):
+        """Get the ip address of the wifi dongle.
+
+        Args:
+            exp_result: True if IP address is expected (wifi connected).
+            timeout: Optional param that specifies the wait time for the IP
+                     address to come up on the dongle.
+
+        Returns:
+            IP: addr in string, if wifi connected.
+                None if not connected.
+        """
+        curr_time = time.time()
+        while time.time() < curr_time + timeout:
+            if (exp_result and self.ip_addr) or \
+                (not exp_result and not self.ip_addr):
+                  break
+            time.sleep(1)
+        return self.ip_addr
+
+    def wifi_status(self, exp_result=True, timeout=READ_TIMEOUT):
+        """Get wifi status on the dongle.
+
+        Returns:
+            True: if wifi is connected.
+            False: if not connected.
+        """
+        curr_time = time.time()
+        while time.time() < curr_time + timeout:
+            if (exp_result and self.status == 3) or \
+                (not exp_result and not self.status):
+                  break
+            time.sleep(1)
+        return self.status == 3
+
+    def wifi_scan(self, exp_result=True, timeout=READ_TIMEOUT):
+        """Get the wifi scan results.
+
+        Args:
+            exp_result: True if scan results are expected.
+            timeout: Optional param that specifies the wait time for the scan
+                     results to come up on the dongle.
+
+        Returns:
+            list of dictionaries each with SSID and RSSI of the network
+            found in the scan.
+        """
+        scan_networks = []
+        d = {}
+        curr_time = time.time()
+        while time.time() < curr_time + timeout:
+            if (exp_result and self.scan_results) or \
+                (not exp_result and not self.scan_results):
+                  break
+            time.sleep(1)
+        for i in range(len(self.scan_results)):
+            if SSID in self.scan_results[i]:
+                d = {}
+                d[SSID] = self.scan_results[i].split(":")[-1].rstrip()
+            elif RSSI in self.scan_results[i]:
+                d[RSSI] = self.scan_results[i].split(":")[-1].rstrip()
+                scan_networks.append(d)
+
+        return scan_networks
+
+    def ping_status(self, exp_result=True, timeout=READ_TIMEOUT):
+        """ Get ping status on the dongle.
+
+        Returns:
+            True: if ping is successful
+            False: if not successful
+        """
+        curr_time = time.time()
+        while time.time() < curr_time + timeout:
+            if (exp_result and self.ping) or \
+                (not exp_result and not self.ping):
+                  break
+            time.sleep(1)
+        return self.ping
diff --git a/acts/framework/acts/keys.py b/acts/framework/acts/keys.py
index 526ef71..a0aa52a 100644
--- a/acts/framework/acts/keys.py
+++ b/acts/framework/acts/keys.py
@@ -47,6 +47,7 @@
     key_packet_sender = "PacketSender"
     key_monsoon = "Monsoon"
     key_sniffer = "Sniffer"
+    key_arduino_wifi_dongle = "ArduinoWifiDongle"
     # Internal keys, used internally, not exposed to user's config files.
     ikey_user_param = "user_params"
     ikey_testbed_name = "testbed_name"
@@ -64,6 +65,7 @@
     m_key_iperf_server = "iperf_server"
     m_key_packet_sender = "packet_sender"
     m_key_sniffer = "sniffer"
+    m_key_arduino_wifi_dongle = "arduino_wifi_dongle"
 
     # A list of keys whose values in configs should not be passed to test
     # classes without unpacking first.
@@ -81,6 +83,7 @@
         key_monsoon,
         key_sniffer,
         key_chameleon_device,
+        key_arduino_wifi_dongle,
     ]
 
     # Keys that are file or folder paths.
diff --git a/acts/framework/acts/logger.py b/acts/framework/acts/logger.py
index 92f1e5e..2f94f83 100755
--- a/acts/framework/acts/logger.py
+++ b/acts/framework/acts/logger.py
@@ -255,3 +255,17 @@
             >>> lambda log_message: return 'string'
     """
     return tracelogger.TraceLogger(LoggerAdapter(logging_lambda))
+
+
+def create_tagged_trace_logger(tag=''):
+    """Returns a logger that logs each line with the given prefix.
+
+    Args:
+        tag: The tag of the log line, E.g. if tag == tag123, the output
+            line would be:
+
+            <TESTBED> <TIME> <LOG_LEVEL> [tag123] logged message
+    """
+    def logging_lambda(msg):
+        return '[%s] %s' % (tag, msg)
+    return create_logger(logging_lambda)
diff --git a/acts/framework/acts/test_utils/net/arduino_test_utils.py b/acts/framework/acts/test_utils/net/arduino_test_utils.py
new file mode 100644
index 0000000..af0b3da
--- /dev/null
+++ b/acts/framework/acts/test_utils/net/arduino_test_utils.py
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3.4
+#
+# Copyright 2018 Google, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+import time
+import pprint
+
+from enum import IntEnum
+from queue import Empty
+
+from acts import asserts
+from acts import signals
+from acts import utils
+from acts.controllers import attenuator
+from acts.test_utils.wifi import wifi_constants
+from acts.test_utils.tel import tel_defines
+from acts.test_utils.wifi import wifi_test_utils as wutils
+
+ARDUINO = "/root/arduino/arduino-1.8.5/arduino "
+CONNECT_WIFI = "/arduino/connect_wifi/connect_wifi.ino"
+DISCONNECT_WIFI = "/arduino/disconnect_wifi/disconnect_wifi.ino"
+SSID = wutils.WifiEnums.SSID_KEY
+PWD = wutils.WifiEnums.PWD_KEY
+
+def connect_wifi(wd, network=None):
+    """Connect wifi on arduino wifi dongle
+
+    Args:
+        wd - wifi dongle object
+        network - wifi network to connect to
+    """
+    wd.log.info("Flashing connect_wifi.ino onto dongle")
+    cmd = "locate %s" % CONNECT_WIFI
+    file_path = utils.exe_cmd(cmd).decode("utf-8", "ignore").rstrip()
+    write_status = wd.write(ARDUINO, file_path, network)
+    asserts.assert_true(write_status, "Failed to flash connect wifi")
+    wd.log.info("Flashing complete")
+    wifi_status = wd.wifi_status()
+    asserts.assert_true(wifi_status, "Failed to connect to %s" % network)
+    ping_status = wd.ping_status()
+    asserts.assert_true(ping_status, "Failed to connect to internet")
+
+def disconnect_wifi(wd):
+    """Disconnect wifi on arduino wifi dongle
+
+    Args:
+        wd - wifi dongle object
+
+    Returns:
+        True - if wifi is disconnected
+        False - if not
+    """
+    wd.log.info("Flashing disconnect_wifi.ino onto dongle")
+    cmd = "locate %s" % DISCONNECT_WIFI
+    file_path = utils.exe_cmd(cmd).decode("utf-8", "ignore").rstrip()
+    write_status = wd.write(ARDUINO, file_path)
+    asserts.assert_true(write_status, "Failed to flash disconnect wifi")
+    wd.log.info("Flashing complete")
+    wifi_status = wd.wifi_status(False)
+    asserts.assert_true(not wifi_status, "Failed to disconnect wifi")
diff --git a/acts/framework/acts/test_utils/net/connectivity_const.py b/acts/framework/acts/test_utils/net/connectivity_const.py
index 64bd8b0..bea0060 100644
--- a/acts/framework/acts/test_utils/net/connectivity_const.py
+++ b/acts/framework/acts/test_utils/net/connectivity_const.py
@@ -63,6 +63,24 @@
 MULTIPATH_PREFERENCE_RELIABILITY = 1 << 1
 MULTIPATH_PREFERENCE_PERFORMANCE = 1 << 2
 
+# IpSec constants
+SOCK_STREAM = 1
+SOCK_DGRAM = 2
+AF_INET = 2
+AF_INET6 = 10
+DIRECTION_IN = 0
+DIRECTION_OUT = 1
+MODE_TRANSPORT = 0
+MODE_TUNNEL = 1
+CRYPT_NULL = "ecb(cipher_null)"
+CRYPT_AES_CBC = "cbc(aes)"
+AUTH_HMAC_MD5 = "hmac(md5)"
+AUTH_HMAC_SHA1 = "hmac(sha1)"
+AUTH_HMAC_SHA256 = "hmac(sha256)"
+AUTH_HMAC_SHA384 = "hmac(sha384)"
+AUTH_HMAC_SHA512 = "hmac(sha512)"
+AUTH_CRYPT_AES_GCM = "rfc4106(gcm(aes))"
+
 # Constants for VpnProfile
 class VpnProfile(object):
     """ This class contains all the possible
diff --git a/acts/framework/acts/test_utils/net/socket_test_utils.py b/acts/framework/acts/test_utils/net/socket_test_utils.py
new file mode 100644
index 0000000..a8a05fc
--- /dev/null
+++ b/acts/framework/acts/test_utils/net/socket_test_utils.py
@@ -0,0 +1,285 @@
+#
+#   Copyright 2018 - 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 queue
+import re
+import threading
+import time
+
+from acts.test_utils.net import connectivity_const as cconst
+from acts import asserts
+
+MSG = "Test message "
+PKTS = 5
+
+""" Methods for android.system.Os based sockets """
+def open_android_socket(ad, domain, sock_type, ip, port):
+    """ Open TCP or UDP using android.system.Os class
+
+    Args:
+      1. ad - android device object
+      2. domain - IPv4 or IPv6 type
+      3. sock_type - UDP or TCP socket
+      4. ip - IP addr on the device
+      5. port - open socket on port
+
+    Returns:
+      File descriptor key
+    """
+    fd_key = ad.droid.openSocket(domain, sock_type, ip, port)
+    ad.log.info("File descriptor: %s" % fd_key)
+    asserts.assert_true(fd_key, "Failed to open socket")
+    return fd_key
+
+def close_android_socket(ad, fd_key):
+    """ Close socket
+
+    Args:
+      1. ad - android device object
+      2. fd_key - file descriptor key
+    """
+    status = ad.droid.closeSocket(fd_key)
+    asserts.assert_true(status, "Failed to close socket")
+
+def listen_accept_android_socket(client,
+                                 server,
+                                 client_fd,
+                                 server_fd,
+                                 server_ip,
+                                 server_port):
+    """ Listen, accept TCP sockets
+
+    Args:
+      1. client : ad object for client device
+      2. server : ad object for server device
+      3. client_fd : client's socket handle
+      4. server_fd : server's socket handle
+      5. server_ip : send data to this IP
+      6. server_port : send data to this port
+    """
+    server.droid.listenSocket(server_fd)
+    client.droid.connectSocket(client_fd, server_ip, server_port)
+    sock = server.droid.acceptSocket(server_fd)
+    asserts.assert_true(sock, "Failed to accept socket")
+    return sock
+
+def send_recv_data_android_sockets(client,
+                                   server,
+                                   client_fd,
+                                   server_fd,
+                                   server_ip,
+                                   server_port):
+    """ Send TCP or UDP data over android os sockets from client to server.
+        Verify that server received the data.
+
+    Args:
+      1. client : ad object for client device
+      2. server : ad object for server device
+      3. client_fd : client's socket handle
+      4. server_fd : server's socket handle
+      5. server_ip : send data to this IP
+      6. server_port : send data to this port
+    """
+    send_list = []
+    recv_list = []
+
+    for _ in range(1, PKTS+1):
+        msg = MSG + " %s" % _
+        send_list.append(msg)
+        client.log.info("Sending message: %s" % msg)
+        client.droid.sendDataOverSocket(server_ip, server_port, msg, client_fd)
+        recv_msg = server.droid.recvDataOverSocket(server_fd)
+        server.log.info("Received message: %s" % recv_msg)
+        recv_list.append(recv_msg)
+
+    recv_list = [x.rstrip('\x00') if x else x for x in recv_list]
+    asserts.assert_true(send_list and recv_list and send_list == recv_list,
+                        "Send and recv information is incorrect")
+
+""" Methods for java.net.DatagramSocket based sockets """
+def open_datagram_socket(ad, ip, port):
+    """ Open datagram socket
+
+    Args:
+      1. ad : android device object
+      2. ip : IP addr on the device
+      3. port : socket port
+
+    Returns:
+      Hash key of the datagram socket
+    """
+    socket_key = ad.droid.openDatagramSocket(ip, port)
+    ad.log.info("Datagram socket: %s" % socket_key)
+    asserts.assert_true(socket_key, "Failed to open datagram socket")
+    return socket_key
+
+def close_datagram_socket(ad, socket_key):
+    """ Close datagram socket
+
+    Args:
+      1. socket_key : hash key of datagram socket
+    """
+    status = ad.droid.closeDatagramSocket(socket_key)
+    asserts.assert_true(status, "Failed to close datagram socket")
+
+def send_recv_data_datagram_sockets(client,
+                                    server,
+                                    client_sock,
+                                    server_sock,
+                                    server_ip,
+                                    server_port):
+    """ Send data over datagram socket from dut_a to dut_b.
+        Verify that dut_b received the data.
+
+    Args:
+      1. client : ad object for client device
+      2. server : ad object for server device
+      3. client_sock : client's socket handle
+      4. server_sock : server's socket handle
+      5. server_ip : send data to this IP
+      6. server_port : send data to this port
+    """
+    send_list = []
+    recv_list = []
+
+    for _ in range(1, PKTS+1):
+        msg = MSG + " %s" % _
+        send_list.append(msg)
+        client.log.info("Sending message: %s" % msg)
+        client.droid.sendDataOverDatagramSocket(client_sock,
+                                                msg,
+                                                server_ip,
+                                                server_port)
+        recv_msg = server.droid.recvDataOverDatagramSocket(server_sock)
+        server.log.info("Received message: %s" % recv_msg)
+        recv_list.append(recv_msg)
+
+    recv_list = [x.rstrip('\x00') if x else x for x in recv_list]
+    asserts.assert_true(send_list and recv_list and send_list == recv_list,
+                        "Send and recv information is incorrect")
+
+""" Utils methods for java.net.Socket based sockets """
+def _accept_socket(server, server_ip, server_port, server_sock, q):
+    sock = server.droid.acceptTcpSocket(server_sock)
+    server.log.info("Server socket: %s" % sock)
+    q.put(sock)
+
+def _client_socket(client, server_ip, server_port, client_ip, client_port, q):
+    time.sleep(0.5)
+    sock = client.droid.openTcpSocket(server_ip,
+                                      server_port,
+                                      client_ip,
+                                      client_port)
+    client.log.info("Client socket: %s" % sock)
+    q.put(sock)
+
+def open_connect_socket(client,
+                        server,
+                        client_ip,
+                        server_ip,
+                        client_port,
+                        server_port,
+                        server_sock):
+    """ Open tcp socket and connect to server
+
+    Args:
+      1. client : ad object for client device
+      2. server : ad object for server device
+      3. client_ip : client's socket handle
+      4. server_ip : send data to this IP
+      5. client_port : port on client socket
+      6. server_port : port on server socket
+      7. server_sock : server socket
+
+    Returns:
+      client and server socket from successful connect
+    """
+    sq = queue.Queue()
+    cq = queue.Queue()
+    s = threading.Thread(target = _accept_socket,
+                         args = (server, server_ip, server_port, server_sock,
+                                 sq))
+    c = threading.Thread(target = _client_socket,
+                         args = (client, server_ip, server_port, client_ip,
+                                 client_port, cq))
+    s.start()
+    c.start()
+    c.join()
+    s.join()
+
+    client_sock = cq.get()
+    server_sock = sq.get()
+    asserts.assert_true(client_sock and server_sock, "Failed to open sockets")
+
+    return client_sock, server_sock
+
+def open_server_socket(server, server_ip, server_port):
+    """ Open tcp server socket
+
+    Args:
+      1. server : ad object for server device
+      2. server_ip : send data to this IP
+      3. server_port : send data to this port
+    """
+    sock = server.droid.openTcpServerSocket(server_ip, server_port)
+    server.log.info("Server Socket: %s" % sock)
+    asserts.assert_true(sock, "Failed to open server socket")
+    return sock
+
+def close_socket(ad, socket):
+    """ Close socket
+
+    Args:
+      1. ad - android device object
+      2. socket - socket key
+    """
+    status = ad.droid.closeTcpSocket(socket)
+    asserts.assert_true(status, "Failed to socket")
+
+def close_server_socket(ad, socket):
+    """ Close server socket
+
+    Args:
+      1. ad - android device object
+      2. socket - server socket key
+    """
+    status = ad.droid.closeTcpServerSocket(socket)
+    asserts.assert_true(status, "Failed to socket")
+
+def send_recv_data_sockets(client, server, client_sock, server_sock):
+    """ Send data over TCP socket from client to server.
+        Verify that server received the data
+
+    Args:
+      1. client : ad object for client device
+      2. server : ad object for server device
+      3. client_sock : client's socket handle
+      4. server_sock : server's socket handle
+    """
+    send_list = []
+    recv_list = []
+
+    for _ in range(1, PKTS+1):
+        msg = MSG + " %s" % _
+        send_list.append(msg)
+        client.log.info("Sending message: %s" % msg)
+        client.droid.sendDataOverTcpSocket(client_sock, msg)
+        recv_msg = server.droid.recvDataOverTcpSocket(server_sock)
+        server.log.info("Received message: %s" % recv_msg)
+        recv_list.append(recv_msg)
+
+    recv_list = [x.rstrip('\x00') if x else x for x in recv_list]
+    asserts.assert_true(send_list and recv_list and send_list == recv_list,
+                        "Send and recv information is incorrect")
diff --git a/acts/framework/acts/test_utils/wifi/rtt/rtt_test_utils.py b/acts/framework/acts/test_utils/wifi/rtt/rtt_test_utils.py
index 29fac81..c24b406 100644
--- a/acts/framework/acts/test_utils/wifi/rtt/rtt_test_utils.py
+++ b/acts/framework/acts/test_utils/wifi/rtt/rtt_test_utils.py
@@ -143,6 +143,33 @@
   return []
 
 
+def select_best_scan_results(scans, select_count, lowest_rssi=-80):
+  """Select the strongest 'select_count' scans in the input list based on
+  highest RSSI. Exclude all very weak signals, even if results in a shorter
+  list.
+
+  Args:
+    scans: List of scan results.
+    select_count: An integer specifying how many scans to return at most.
+    lowest_rssi: The lowest RSSI to accept into the output.
+  Returns: a list of the strongest 'select_count' scan results from the scans
+           list.
+  """
+  def takeRssi(element):
+    return element['level']
+
+  result = []
+  scans.sort(key=takeRssi, reverse=True)
+  for scan in scans:
+    if len(result) == select_count:
+      break
+    if scan['level'] < lowest_rssi:
+      break # rest are lower since we're sorted
+    result.append(scan)
+
+  return result
+
+
 def validate_ap_result(scan_result, range_result):
   """Validate the range results:
   - Successful if AP (per scan result) support 802.11mc (allowed to fail
diff --git a/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py b/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py
index a05eca4..f2b41ec 100644
--- a/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py
+++ b/acts/framework/acts/test_utils/wifi/wifi_retail_ap.py
@@ -13,10 +13,11 @@
 #   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 fcntl
+import logging
+import selenium
 import splinter
 import time
-from acts.libs.proc import job
-from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
 
 BROWSER_WAIT_SHORT = 1
 BROWSER_WAIT_MED = 3
@@ -52,74 +53,90 @@
     return
 
 
-def start_chrome_browser(headless, max_allowed_sessions, timeout):
-    """Method to start chrome browser for retail AP configuration
+class BlockingBrowser(splinter.driver.webdriver.chrome.WebDriver):
+    """Class that implements a blocking browser session on top of selenium.
 
-    This function starts a chrome browser session to interface with the APs
-    web interface. The function attempts to maintain only one chromedriver
-    session by waiting until no chromedriver sessions are running on a machine.
-
-    Args:
-        headless: boolean controlling headless operation
-        max_allowed_sessions: maximum number of concurrent chrome sessions
-        timeout: maximum waiting time to launch chrome session
-    Returns:
-        browser: chrome browser session
-    Raises:
-        TimeoutError: raised when a browser session could not be started
-        withing the specified timeout
+    The class inherits from and builds upon splinter/selenium's webdriver class
+    and makes sure that only one such webdriver is active on a machine at any
+    single time. The class ensures single session operation using a lock file.
+    The class is to be used within context managers (e.g. with statements) to
+    ensure locks are always properly released.
     """
-    chrome_options = splinter.driver.webdriver.chrome.Options()
-    chrome_options.add_argument("--no-proxy-server")
-    chrome_options.add_argument("--no-sandbox")
-    chrome_options.add_argument("--allow-running-insecure-content")
-    chrome_options.add_argument("--ignore-certificate-errors")
-    chrome_capabilities = DesiredCapabilities.CHROME.copy()
-    chrome_capabilities["acceptSslCerts"] = True
-    chrome_capabilities["acceptInsecureCerts"] = True
-    if headless:
-        chrome_options.add_argument("--headless")
-        chrome_options.add_argument("--disable-gpu")
 
-    start_time = time.time()
-    end_time = start_time + timeout
-    while time.time() < end_time:
-        browsers_running = int(job.run('pgrep chromedriver | wc -l').stdout)
-        if browsers_running >= max_allowed_sessions:
-            time.sleep(BROWSER_WAIT_SHORT)
-        else:
+    def __init__(self, headless, timeout):
+        """Constructor for BlockingBrowser class.
+
+        Args:
+            headless: boolean to control visible/headless browser operation
+            timeout: maximum time allowed to launch browser
+        """
+        self.chrome_options = splinter.driver.webdriver.chrome.Options()
+        self.chrome_options.add_argument("--no-proxy-server")
+        self.chrome_options.add_argument("--no-sandbox")
+        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(
+        )
+        self.chrome_capabilities["acceptSslCerts"] = True
+        self.chrome_capabilities["acceptInsecureCerts"] = True
+        if headless:
+            self.chrome_options.add_argument("--headless")
+            self.chrome_options.add_argument("--disable-gpu")
+        self.lock_file_path = "/usr/local/bin/chromedriver"
+        self.timeout = timeout
+
+    def __enter__(self):
+        self.lock_file = open(self.lock_file_path, "r")
+        start_time = time.time()
+        while time.time() < start_time + self.timeout:
             try:
-                browser = splinter.Browser(
-                    "chrome",
-                    options=chrome_options,
-                    desired_capabilities=chrome_capabilities)
-                return browser
-            except:
+                fcntl.flock(self.lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
+                self.driver = selenium.webdriver.Chrome(
+                    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__()
+            except BlockingIOError:
                 time.sleep(BROWSER_WAIT_SHORT)
-    raise TimeoutError("Could not start chrome browser in time.")
+        raise TimeoutError("Could not start chrome browser in time.")
 
-
-def visit_config_page(browser, url, page_load_timeout, num_tries):
-    """Method to visit Netgear AP webpages.
-
-    This function visits a web page and checks the the resulting URL matches
-    the intended URL, i.e. no redirects have happened
-
-    Args:
-        browser: the splinter browser object that will visit the URL
-        url: the intended url
-        num_tries: number of tries before url is declared unreachable
-    """
-    browser.driver.set_page_load_timeout(page_load_timeout)
-    for idx in range(num_tries):
+    def __exit__(self, exc_type, exc_value, traceback):
         try:
-            browser.visit(url)
+            super(BlockingBrowser, self).__exit__(exc_type, exc_value,
+                                                  traceback)
         except:
-            browser.visit("about:blank")
-        if browser.url.split("/")[-1] == url.split("/")[-1]:
-            break
-        if idx == num_tries - 1:
-            raise RuntimeError("URL was unreachable.")
+            raise RuntimeError("Failed to quit browser. Releasing lock file.")
+        finally:
+            fcntl.flock(self.lock_file, fcntl.LOCK_UN)
+            self.lock_file.close()
+
+    def visit_persistent(self, url, page_load_timeout, num_tries):
+        """Method to visit webpages and retry upon failure.
+
+        The function visits a web page and checks the the resulting URL matches
+        the intended URL, i.e. no redirects have happened
+
+        Args:
+            url: the intended url
+            page_load_timeout: timeout for page visits
+            num_tries: number of tries before url is declared unreachable
+        """
+        self.driver.set_page_load_timeout(page_load_timeout)
+        for idx in range(num_tries):
+            try:
+                self.visit(url)
+            except:
+                self.visit("about:blank")
+            if self.url.split("/")[-1] == url.split("/")[-1]:
+                break
+            if idx == num_tries - 1:
+                logging.error("URL unreachable. Current URL: {}".format(
+                    self.url))
+                raise RuntimeError("URL unreachable.")
 
 
 class WifiRetailAP(object):
@@ -148,6 +165,7 @@
         with the assumed settings saved in the AP object. When called after AP
         configuration, this method helps ensure that our configuration was
         successful.
+        Note: Calling this function updates the stored ap_settings
 
         Raises:
             ValueError: If read AP settings do not match stored settings.
@@ -155,8 +173,9 @@
         assumed_ap_settings = self.ap_settings.copy()
         actual_ap_settings = self.read_ap_settings()
         if assumed_ap_settings != actual_ap_settings:
-            raise ValueError(
-                "Discrepancy in AP settings. Potential configuration error.")
+            logging.warning(
+                "Discrepancy in AP settings. Some settings may have been overwritten."
+            )
 
     def configure_ap(self):
         """Function that configures ap based on values of ap_settings.
@@ -166,6 +185,20 @@
         """
         raise NotImplementedError
 
+    def set_region(self, region):
+        """Function that sets AP region.
+
+        This function sets the region for the AP. Note that this may overwrite
+        channel and bandwidth settings in cases where the new region does not
+        support the current wireless configuration.
+
+        Args:
+            region: string indicating AP region
+        """
+        logging.warning("Updating region may overwrite wireless settings.")
+        setting_to_update = {"region": region}
+        self.update_ap_settings(setting_to_update)
+
     def set_radio_on_off(self, network, status):
         """Function that turns the radio on or off.
 
@@ -381,22 +414,22 @@
 
     def read_ap_settings(self):
         """Function to read ap settings."""
-        with start_chrome_browser(self.ap_settings["headless_browser"], 1,
-                                  600) as browser:
+        with BlockingBrowser(self.ap_settings["headless_browser"],
+                             600) as browser:
             # Visit URL
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
-            visit_config_page(browser, self.config_page_nologin,
-                              BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page_nologin,
+                                     BROWSER_WAIT_MED, 10)
 
             for key, value in self.config_page_fields.items():
                 if "status" in key:
-                    visit_config_page(browser, self.config_page_advanced,
-                                      BROWSER_WAIT_MED, 10)
+                    browser.visit_persistent(self.config_page_advanced,
+                                             BROWSER_WAIT_MED, 10)
                     config_item = browser.find_by_name(value)
                     self.ap_settings["{}_{}".format(key[1], key[0])] = int(
                         config_item.first.checked)
-                    visit_config_page(browser, self.config_page_nologin,
-                                      BROWSER_WAIT_MED, 10)
+                    browser.visit_persistent(self.config_page_nologin,
+                                             BROWSER_WAIT_MED, 10)
                 else:
                     config_item = browser.find_by_name(value)
                     if "bandwidth" in key:
@@ -425,12 +458,12 @@
         # Turn radios on or off
         self.configure_radio_on_off()
         # Configure radios
-        with start_chrome_browser(self.ap_settings["headless_browser"], 1,
-                                  600) as browser:
+        with BlockingBrowser(self.ap_settings["headless_browser"],
+                             600) as browser:
             # Visit URL
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
-            visit_config_page(browser, self.config_page_nologin,
-                              BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page_nologin,
+                                     BROWSER_WAIT_MED, 10)
 
             # Update region, and power/bandwidth for each network
             for key, value in self.config_page_fields.items():
@@ -443,9 +476,13 @@
                     config_item.select_by_text(self.ap_settings["region"])
                 elif "bandwidth" in key:
                     config_item = browser.find_by_name(value).first
-                    config_item.select_by_text(
-                        self.bw_mode_text[self.ap_settings["{}_{}".format(
-                            key[1], key[0])]])
+                    try:
+                        config_item.select_by_text(
+                            self.bw_mode_text[self.ap_settings["{}_{}".format(
+                                key[1], key[0])]])
+                    except AttributeError:
+                        logging.warning(
+                            "Cannot select bandwidth. Keeping AP default.")
 
             # Update security settings (passwords updated only if applicable)
             for key, value in self.config_page_fields.items():
@@ -472,9 +509,13 @@
                         key[1], key[0])])
                 elif "channel" in key:
                     config_item = browser.find_by_name(value).first
-                    config_item.select(self.ap_settings["{}_{}".format(
-                        key[1], key[0])])
-                    time.sleep(BROWSER_WAIT_SHORT)
+                    try:
+                        config_item.select(self.ap_settings["{}_{}".format(
+                            key[1], key[0])])
+                        time.sleep(BROWSER_WAIT_SHORT)
+                    except AttributeError:
+                        logging.warning(
+                            "Cannot select channel. Keeping AP default.")
                     try:
                         alert = browser.get_alert()
                         alert.accept()
@@ -490,18 +531,17 @@
                 time.sleep(BROWSER_WAIT_SHORT)
             except:
                 time.sleep(BROWSER_WAIT_SHORT)
-            visit_config_page(browser, self.config_page,
-                              BROWSER_WAIT_EXTRA_LONG, 10)
-        self.validate_ap_settings()
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
+                                     10)
 
     def configure_radio_on_off(self):
         """Helper configuration function to turn radios on/off."""
-        with start_chrome_browser(self.ap_settings["headless_browser"], 1,
-                                  600) as browser:
+        with BlockingBrowser(self.ap_settings["headless_browser"],
+                             600) as browser:
             # Visit URL
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
-            visit_config_page(browser, self.config_page_advanced,
-                              BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page_advanced,
+                                     BROWSER_WAIT_MED, 10)
 
             # Turn radios on or off
             status_toggled = False
@@ -521,8 +561,8 @@
                 time.sleep(BROWSER_WAIT_SHORT)
                 browser.find_by_name("Apply").first.click()
                 time.sleep(BROWSER_WAIT_EXTRA_LONG)
-                visit_config_page(browser, self.config_page,
-                                  BROWSER_WAIT_EXTRA_LONG, 10)
+                browser.visit_persistent(self.config_page,
+                                         BROWSER_WAIT_EXTRA_LONG, 10)
 
 
 class NetgearR7500AP(WifiRetailAP):
@@ -622,10 +662,10 @@
         self.read_radio_on_off()
         # Get radio configuration. Note that if both radios are off, the below
         # code will result in an error
-        with start_chrome_browser(self.ap_settings["headless_browser"], 1,
-                                  600) as browser:
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
+        with BlockingBrowser(self.ap_settings["headless_browser"],
+                             600) as browser:
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
             time.sleep(BROWSER_WAIT_SHORT)
             wireless_button = browser.find_by_id("wireless").first
             wireless_button.click()
@@ -667,10 +707,10 @@
         # Turn radios on or off
         self.configure_radio_on_off()
         # Configure radios
-        with start_chrome_browser(self.ap_settings["headless_browser"], 1,
-                                  600) as browser:
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
+        with BlockingBrowser(self.ap_settings["headless_browser"],
+                             600) as browser:
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
             time.sleep(BROWSER_WAIT_SHORT)
             wireless_button = browser.find_by_id("wireless").first
             wireless_button.click()
@@ -696,18 +736,29 @@
                                 48 < int(self.ap_settings["{}_{}".format(
                                     key[1], key[0])]) < 149)
                         config_item = iframe.find_by_name(value).first
-                        config_item.select_by_text(channel_string)
+                        try:
+                            config_item.select_by_text(channel_string)
+                        except AttributeError:
+                            logging.warning(
+                                "Cannot select channel. Keeping AP default.")
                     elif key == ("2G", "bandwidth"):
                         config_item = iframe.find_by_name(value).first
-                        config_item.select_by_text(
-                            str(self.bw_mode_text_2g[self.ap_settings[
-                                "{}_{}".format(key[1], key[0])]]))
+                        try:
+                            config_item.select_by_text(
+                                str(self.bw_mode_text_2g[self.ap_settings[
+                                    "{}_{}".format(key[1], key[0])]]))
+                        except AttributeError:
+                            logging.warning(
+                                "Cannot select bandwidth. Keeping AP default.")
                     elif key == ("5G_1", "bandwidth"):
                         config_item = iframe.find_by_name(value).first
-                        config_item.select_by_text(
-                            str(self.bw_mode_text_5g[self.ap_settings[
-                                "{}_{}".format(key[1], key[0])]]))
-
+                        try:
+                            config_item.select_by_text(
+                                str(self.bw_mode_text_5g[self.ap_settings[
+                                    "{}_{}".format(key[1], key[0])]]))
+                        except AttributeError:
+                            logging.warning(
+                                "Cannot select bandwidth. Keeping AP default.")
                 # Update passwords for WPA2-PSK protected networks
                 # (Must be done after security type is selected)
                 for key, value in self.config_page_fields.items():
@@ -738,17 +789,16 @@
                     pass
                 time.sleep(BROWSER_WAIT_SHORT)
             time.sleep(BROWSER_WAIT_EXTRA_LONG)
-            visit_config_page(browser, self.config_page,
-                              BROWSER_WAIT_EXTRA_LONG, 10)
-        self.validate_ap_settings()
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_EXTRA_LONG,
+                                     10)
 
     def configure_radio_on_off(self):
         """Helper configuration function to turn radios on/off."""
-        with start_chrome_browser(self.ap_settings["headless_browser"], 1,
-                                  600) as browser:
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
-            visit_config_page(browser, self.config_page_advanced,
-                              BROWSER_WAIT_MED, 10)
+        with BlockingBrowser(self.ap_settings["headless_browser"],
+                             600) as browser:
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page_advanced,
+                                     BROWSER_WAIT_MED, 10)
             time.sleep(BROWSER_WAIT_SHORT)
             wireless_button = browser.find_by_id("advanced_bt").first
             wireless_button.click()
@@ -777,16 +827,16 @@
                     time.sleep(BROWSER_WAIT_SHORT)
                     browser.find_by_name("Apply").first.click()
                     time.sleep(BROWSER_WAIT_EXTRA_LONG)
-                    visit_config_page(browser, self.config_page,
-                                      BROWSER_WAIT_EXTRA_LONG, 10)
+                    browser.visit_persistent(self.config_page,
+                                             BROWSER_WAIT_EXTRA_LONG, 10)
 
     def read_radio_on_off(self):
         """Helper configuration function to read radio status."""
-        with start_chrome_browser(self.ap_settings["headless_browser"], 1,
-                                  600) as browser:
-            visit_config_page(browser, self.config_page, BROWSER_WAIT_MED, 10)
-            visit_config_page(browser, self.config_page_advanced,
-                              BROWSER_WAIT_MED, 10)
+        with BlockingBrowser(self.ap_settings["headless_browser"],
+                             600) as browser:
+            browser.visit_persistent(self.config_page, BROWSER_WAIT_MED, 10)
+            browser.visit_persistent(self.config_page_advanced,
+                                     BROWSER_WAIT_MED, 10)
             wireless_button = browser.find_by_id("advanced_bt").first
             wireless_button.click()
             time.sleep(BROWSER_WAIT_SHORT)
diff --git a/acts/tests/google/net/arduino/connect_wifi/connect_wifi.ino b/acts/tests/google/net/arduino/connect_wifi/connect_wifi.ino
new file mode 100644
index 0000000..14bd196
--- /dev/null
+++ b/acts/tests/google/net/arduino/connect_wifi/connect_wifi.ino
@@ -0,0 +1,93 @@
+#include <ESP8266WiFi.h>
+#include <ESP8266Ping.h>
+#include <WiFiUdp.h>
+
+const char* ssid = "wifi_tethering_test";
+const char* password = "password";
+
+unsigned int localPort = 8888;
+char packetBuffer[UDP_TX_PACKET_MAX_SIZE];
+WiFiUDP Udp;
+
+
+void setup() {
+    delay(1000); // wait for a second to read from serial port after flashing
+    Serial.begin(9600);
+    Serial.println("connect: setup(): CALL: Setup Begin");
+    Serial.println("connect: setup(): INFO: Setting baud rate to 9600");
+
+    wifiStatus();
+    connectWifi();
+
+    Udp.begin(localPort);
+    Serial.println("connect: setup(): CALL: Setup End");
+}
+
+void loop() {
+    wifiStatus();
+    udpPackets();
+}
+
+void connectWifi() {
+    Serial.println("connect: connectWifi(): CALL: Connect Begin");
+    WiFi.begin(ssid, password);
+    while (WiFi.status() != WL_CONNECTED) {
+        Serial.println("connect: setup(): INFO: WiFi disconnected");
+        delay(1000);
+    }
+    Serial.println("connect: connectWifi(): CALL: Connect End");
+}
+
+void wifiStatus() {
+    Serial.println("connect: wifiStatus(): CALL: Status Begin");
+    Serial.println("connect: wifiStatus(): INFO: WiFi connected");
+    Serial.print("connect: wifiStatus(): STATUS: ");
+    Serial.println(WiFi.status());
+    Serial.print("connect: wifiStatus(): IP: ");
+    Serial.println(WiFi.localIP());
+    Serial.print("connect: wifiStatus(): SSID: ");
+    Serial.println(WiFi.SSID());
+    bool ret = Ping.ping("www.google.com", 3);
+    Serial.print("connect: wifiStatus(): PING: ");
+    if (ret) {
+        Serial.println("1");
+    } else {
+        Serial.println("0");
+    }
+
+    delay(250);
+    Serial.println("connect: wifiStatus(): CALL: Status End");
+}
+
+void udpPackets() {
+    Serial.println("connect: udpPackets(): CALL: UDP Begin");
+    int packetSize = Udp.parsePacket();
+    while(packetSize) {
+        Serial.print("connect: udpPackets(): PKTSZ: ");
+        Serial.println(packetSize);
+        Serial.print("connect: udpPackets(): REMOTEIP: ");
+        IPAddress remote = Udp.remoteIP();
+        for (int i =0; i < 4; i++) {
+            Serial.print(remote[i], DEC);
+            if (i < 3) {
+                Serial.print(".");
+            }
+        }
+        Serial.println("");
+        Serial.print("connect: udpPackets(): REMOTEPORT: ");
+        Serial.println(Udp.remotePort());
+
+        // read the packet into packetBufffer
+        Udp.read(packetBuffer,UDP_TX_PACKET_MAX_SIZE);
+        Serial.print("connect: udpPackets(): RECV: ");
+        Serial.println(packetBuffer);
+
+        // send the same message back
+        Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
+        Udp.write(packetBuffer);
+        Udp.endPacket();
+
+        packetSize = Udp.parsePacket();
+    }
+    Serial.println("connect: udpPackets(): CALL: UDP End");
+}
diff --git a/acts/tests/google/net/arduino/disconnect_wifi/disconnect_wifi.ino b/acts/tests/google/net/arduino/disconnect_wifi/disconnect_wifi.ino
new file mode 100644
index 0000000..cacef95
--- /dev/null
+++ b/acts/tests/google/net/arduino/disconnect_wifi/disconnect_wifi.ino
@@ -0,0 +1,64 @@
+#include <ESP8266WiFi.h>
+
+void setup() {
+    delay(1000); // wait for a second to read from serial port after flashing
+    Serial.begin(9600);
+    Serial.println("disconnect: setup(): CALL: Setup Begin");
+    Serial.println("disconnect: setup(): INFO: Setting baud rate to 9600");
+
+    wifiStatus();
+    disconnectWifi();
+
+    Serial.println("disconnect: setup(): CALL: Setup End");
+}
+
+void loop() {
+    wifiStatus();
+    scanNetworks();
+}
+
+void disconnectWifi() {
+    Serial.println("disconnect: setup(): CALL: Disconnect Begin");
+    WiFi.disconnect();
+    while (WiFi.status() == WL_CONNECTED) {
+        Serial.println("disconnect: setup(): INFO: WiFi connected");
+        delay(1000);
+    }
+    Serial.println("disconnect: setup(): CALL: Disconnect End");
+}
+
+void wifiStatus() {
+    Serial.println("disconnect: wifiStatus(): CALL: Status Begin");
+    Serial.println("disconnect: loop(): INFO: WiFi disconnected");
+    Serial.print("disconnect: wifiStatus(): STATUS: ");
+    Serial.println(WiFi.status());
+    Serial.print("disconnect: wifiStatus(): IP: ");
+    Serial.println(WiFi.localIP());
+    Serial.print("disconnect: wifiStatus(): SSID: ");
+    Serial.println(WiFi.SSID());
+    delay(1000);
+    Serial.println("disconnect: wifiStatus(): CALL: Status End");
+}
+
+void scanNetworks() {
+    Serial.println("disconnect: scanNetworks(): CALL: Scan Begin");
+    int n = WiFi.scanNetworks();
+    if (n == 0) {
+        Serial.println("disconnect: scanNetworks(): INFO: No networks found");
+        Serial.println("disconnect: scanNetworks(): COUNT: 0");
+    } else {
+        Serial.println("disconnect: scanNetworks(): INFO: WiFi Networks Found");
+        Serial.print("COUNT: ");
+        Serial.println(n);
+
+        for (int i = 0; i < n; ++i) {
+            Serial.print("SSID: ");
+            Serial.println(WiFi.SSID(i));
+            Serial.print("RSSI: ");
+            Serial.println(WiFi.RSSI(i));
+        }
+    }
+
+    delay(5000); // Wait a bit before scanning again
+    Serial.println("disconnect: scanNetworks(): CALL: Scan End");
+}
diff --git a/acts/tests/google/wifi/WifiRvrTest.py b/acts/tests/google/wifi/WifiRvrTest.py
index b0b2298..34a32c7 100644
--- a/acts/tests/google/wifi/WifiRvrTest.py
+++ b/acts/tests/google/wifi/WifiRvrTest.py
@@ -343,6 +343,11 @@
         rvr_result = {}
         # Configure AP
         band = self.access_point.band_lookup_by_channel(channel)
+        if wutils.WifiEnums.channel_5G_to_freq[
+                channel] in wutils.WifiEnums.DFS_5G_FREQUENCIES:
+            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, channel)
         self.access_point.set_bandwidth(band, mode)
         self.log.info("Access Point Configuration: {}".format(
@@ -469,6 +474,118 @@
     def test_rvr_TCP_UL_ch48_VHT20(self):
         self._test_rvr()
 
+    @test_tracker_info(uuid='c2e199ce-d23f-4a24-b146-74e762085620')
+    def test_rvr_TCP_DL_ch52_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='5c5943e8-9d91-4270-a5ab-e7018807c64e')
+    def test_rvr_TCP_UL_ch52_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='b52afe89-182f-4bad-8879-cbf7001d28ef')
+    def test_rvr_TCP_DL_ch56_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='f8526241-3b96-463a-9082-a749a8650d5f')
+    def test_rvr_TCP_UL_ch56_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='c3042d7e-7468-4ab8-aec3-9b3088ba3e4c')
+    def test_rvr_TCP_DL_ch60_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='80426542-b035-4fb3-9010-e997f95d4964')
+    def test_rvr_TCP_UL_ch60_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='aa0e7117-390c-4265-adf2-0990f65f8b0b')
+    def test_rvr_TCP_DL_ch64_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='b2fdda85-256b-4368-8e8b-39274062264e')
+    def test_rvr_TCP_UL_ch64_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='48b6590f-1553-4170-83a5-40d3976e9e77')
+    def test_rvr_TCP_DL_ch100_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='2d0525fe-57ce-49d3-826d-4ebedd2ca6d6')
+    def test_rvr_TCP_UL_ch100_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='52da922d-6c2f-4afa-aca3-c19438ae3217')
+    def test_rvr_TCP_DL_ch100_VHT40(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='2c7e7106-88c8-47ba-ac28-362475abec41')
+    def test_rvr_TCP_UL_ch100_VHT40(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='fd4a7118-e9fe-4931-b32c-f69efd3e6493')
+    def test_rvr_TCP_DL_ch100_VHT80(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='146502b2-9cab-4bbe-8a5c-7ec625edc2ef')
+    def test_rvr_TCP_UL_ch100_VHT80(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='a5e185d6-b523-4016-bc8a-2a32cdc67ae0')
+    def test_rvr_TCP_DL_ch104_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='886aed91-0fdc-432d-b47e-ebfa85ac27ad')
+    def test_rvr_TCP_UL_ch104_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='fda3de6e-3183-401b-b98c-1b076da139e1')
+    def test_rvr_TCP_DL_ch108_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='29cc30f5-bbc8-4b64-9789-a56154907af5')
+    def test_rvr_TCP_UL_ch108_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='5c52ccac-8c38-46fa-a7b3-d714b6a814ad')
+    def test_rvr_TCP_DL_ch112_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='cc1c2a0b-71a3-4343-b7ff-489527c839d2')
+    def test_rvr_TCP_UL_ch112_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='11c6ccc3-e347-44ce-9a79-6c90e9dfd0a0')
+    def test_rvr_TCP_DL_ch116_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='29f0fce1-005d-4ad7-97d7-6b43cbdff01b')
+    def test_rvr_TCP_UL_ch116_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='112302b1-8261-479a-b397-916b08fbbdd2')
+    def test_rvr_TCP_DL_ch132_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='3bb0efb8-ddfc-4a0b-b7cf-6d6af1dbb9f4')
+    def test_rvr_TCP_UL_ch132_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='11a4638f-d872-4730-82eb-71d9c64e0e16')
+    def test_rvr_TCP_DL_ch136_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='4d797c24-3bbe-43a6-ac9e-291db1aa732a')
+    def test_rvr_TCP_UL_ch136_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='5d433b44-0395-43cb-b85a-be138390b18b')
+    def test_rvr_TCP_DL_ch140_VHT20(self):
+        self._test_rvr()
+
+    @test_tracker_info(uuid='47061772-21b1-4330-bd4f-daec21afa0c8')
+    def test_rvr_TCP_UL_ch140_VHT20(self):
+        self._test_rvr()
+
     @test_tracker_info(uuid='24aa1e7a-3978-4803-877f-3ac5812ab0ae')
     def test_rvr_TCP_DL_ch149_VHT20(self):
         self._test_rvr()
@@ -618,6 +735,19 @@
             "test_rvr_TCP_DL_ch161_VHT20", "test_rvr_TCP_UL_ch161_VHT20")
 
 
+class WifiRvr_SampleDFS_Test(WifiRvrTest):
+    def __init__(self, controllers):
+        base_test.BaseTestClass.__init__(self, controllers)
+        self.tests = (
+            "test_rvr_TCP_DL_ch64_VHT20", "test_rvr_TCP_UL_ch64_VHT20",
+            "test_rvr_TCP_DL_ch100_VHT20", "test_rvr_TCP_UL_ch100_VHT20",
+            "test_rvr_TCP_DL_ch100_VHT40", "test_rvr_TCP_UL_ch100_VHT40",
+            "test_rvr_TCP_DL_ch100_VHT80", "test_rvr_TCP_UL_ch100_VHT80",
+            "test_rvr_TCP_DL_ch116_VHT20", "test_rvr_TCP_UL_ch116_VHT20",
+            "test_rvr_TCP_DL_ch132_VHT20", "test_rvr_TCP_UL_ch132_VHT20",
+            "test_rvr_TCP_DL_ch140_VHT20", "test_rvr_TCP_UL_ch140_VHT20")
+
+
 class WifiRvr_SampleUDP_Test(WifiRvrTest):
     def __init__(self, controllers):
         base_test.BaseTestClass.__init__(self, controllers)
diff --git a/acts/tests/google/wifi/WifiTetheringTest.py b/acts/tests/google/wifi/WifiTetheringTest.py
index fa20aa5..080f9e4 100644
--- a/acts/tests/google/wifi/WifiTetheringTest.py
+++ b/acts/tests/google/wifi/WifiTetheringTest.py
@@ -29,6 +29,8 @@
 from acts.test_utils.tel.tel_test_utils import verify_http_connection
 from acts.test_utils.tel.tel_test_utils import WIFI_CONFIG_APBAND_2G
 from acts.test_utils.tel.tel_test_utils import WIFI_CONFIG_APBAND_5G
+from acts.test_utils.net import socket_test_utils as sutils
+from acts.test_utils.net import arduino_test_utils as dutils
 from acts.test_utils.wifi import wifi_test_utils as wutils
 
 WAIT_TIME = 2
@@ -162,6 +164,56 @@
                 wutils.wifi_connect(dut, self.network)
             device_connected[dut_id] = not device_connected[dut_id]
 
+    def _connect_disconnect_android_device(self, dut_id, wifi_state):
+        """ Connect or disconnect wifi on android device depending on the
+            current wifi state
+
+        Args:
+            1. dut_id: tethered device to change the wifi state
+            2. wifi_state: current wifi state
+        """
+        ad = self.tethered_devices[dut_id]
+        if wifi_state:
+            self.log.info("Disconnecting wifi on android device")
+            wutils.wifi_forget_network(ad, self.network["SSID"])
+        else:
+            self.log.info("Connecting to wifi on android device")
+            wutils.wifi_connect(ad, self.network)
+
+    def _connect_disconnect_wifi_dongle(self, dut_id, wifi_state):
+        """ Connect or disconnect wifi on wifi dongle depending on the
+            current wifi state
+
+        Args:
+            1. dut_id: wifi dongle to change the wifi state
+            2. wifi_state: current wifi state
+        """
+        wd = self.arduino_wifi_dongles[dut_id]
+        if wifi_state:
+            self.log.info("Disconnecting wifi on dongle")
+            dutils.disconnect_wifi(wd)
+        else:
+            self.log.info("Connecting to wifi on dongle")
+            dutils.connect_wifi(wd, self.network)
+
+    def _connect_disconnect_tethered_devices(self):
+        """ Connect disconnect tethered devices to wifi hotspot """
+        num_android_devices = len(self.tethered_devices)
+        num_wifi_dongles = 0
+        if self.arduino_wifi_dongles:
+            num_wifi_dongles = len(self.arduino_wifi_dongles)
+        total_devices = num_android_devices + num_wifi_dongles
+        device_connected = [False] * total_devices
+        for _ in range(50):
+            dut_id = random.randint(0, total_devices-1)
+            wifi_state = device_connected[dut_id]
+            if dut_id < num_android_devices:
+              self._connect_disconnect_android_device(dut_id, wifi_state)
+            else:
+              self._connect_disconnect_wifi_dongle(dut_id-num_android_devices,
+                                                   wifi_state)
+            device_connected[dut_id] = not device_connected[dut_id]
+
     def _verify_ping(self, dut, ip, isIPv6=False):
         """ Verify ping works from the dut to IP/hostname
 
@@ -191,25 +243,24 @@
         return dut.droid.connectivityGetIPv4Addresses(iface_name) + \
             dut.droid.connectivityGetIPv6Addresses(iface_name)
 
-    def _test_traffic_between_two_tethered_devices(self, dut1, dut2):
+    def _test_traffic_between_two_tethered_devices(self, ad, wd):
         """ Verify pinging interfaces of one DUT from another
 
         Args:
-            1. dut1 - tethered device 1
-            2. dut2 - tethered device 2
+            1. ad - android device
+            2. wd - wifi dongle
         """
-        wutils.wifi_connect(dut1, self.network)
-        wutils.wifi_connect(dut2, self.network)
+        wutils.wifi_connect(ad, self.network)
+        dutils.connect_wifi(wd, self.network)
+        local_ip = ad.droid.connectivityGetIPv4Addresses('wlan0')[0]
+        remote_ip = wd.ip_address()
+        port = 8888
 
-        dut1_ipaddrs = dut1.droid.connectivityGetIPv4Addresses("wlan0") + \
-            dut1.droid.connectivityGetIPv6Addresses("wlan0")
-        dut2_ipaddrs = dut2.droid.connectivityGetIPv4Addresses("wlan0") + \
-            dut2.droid.connectivityGetIPv6Addresses("wlan0")
-
-        for ip in dut1_ipaddrs:
-            asserts.assert_true(self._verify_ping(dut2, ip), "%s " % ip)
-        for ip in dut2_ipaddrs:
-            asserts.assert_true(self._verify_ping(dut1, ip), "%s " % ip)
+        time.sleep(6) # wait until UDP packets method is invoked
+        socket = sutils.open_datagram_socket(ad, local_ip, port)
+        sutils.send_recv_data_datagram_sockets(
+            ad, ad, socket, socket, remote_ip, port)
+        sutils.close_datagram_socket(ad, socket)
 
     def _ping_hotspot_interfaces_from_tethered_device(self, dut):
         """ Ping hotspot interfaces from tethered device
@@ -331,7 +382,7 @@
         wutils.stop_wifi_tethering(self.hotspot_device)
 
     @test_tracker_info(uuid="110b61d1-8af2-4589-8413-11beac7a3025")
-    def wifi_tethering_2ghz_traffic_between_2tethered_devices(self):
+    def test_wifi_tethering_2ghz_traffic_between_2tethered_devices(self):
         """ Steps:
 
             1. Start wifi hotspot with 2G band
@@ -341,7 +392,7 @@
         wutils.toggle_wifi_off_and_on(self.hotspot_device)
         self._start_wifi_tethering(WIFI_CONFIG_APBAND_2G)
         self._test_traffic_between_two_tethered_devices(self.tethered_devices[0],
-                                                        self.tethered_devices[1])
+                                                        self.arduino_wifi_dongles[0])
         wutils.stop_wifi_tethering(self.hotspot_device)
 
     @test_tracker_info(uuid="953f6e2e-27bd-4b73-85a6-d2eaa4e755d5")
@@ -355,7 +406,7 @@
         wutils.toggle_wifi_off_and_on(self.hotspot_device)
         self._start_wifi_tethering(WIFI_CONFIG_APBAND_5G)
         self._test_traffic_between_two_tethered_devices(self.tethered_devices[0],
-                                                        self.tethered_devices[1])
+                                                        self.arduino_wifi_dongles[0])
         wutils.stop_wifi_tethering(self.hotspot_device)
 
     @test_tracker_info(uuid="d7d5aa51-682d-4882-a334-61966d93b68c")
@@ -368,7 +419,7 @@
         """
         wutils.toggle_wifi_off_and_on(self.hotspot_device)
         self._start_wifi_tethering(WIFI_CONFIG_APBAND_2G)
-        self._connect_disconnect_devices()
+        self._connect_disconnect_tethered_devices()
         wutils.stop_wifi_tethering(self.hotspot_device)
 
     @test_tracker_info(uuid="34abd6c9-c7f1-4d89-aa2b-a66aeabed9aa")
diff --git a/acts/tests/google/wifi/rtt/functional/RangeApNonSupporting11McTest.py b/acts/tests/google/wifi/rtt/functional/RangeApNonSupporting11McTest.py
index 813102b..65b67d2 100644
--- a/acts/tests/google/wifi/rtt/functional/RangeApNonSupporting11McTest.py
+++ b/acts/tests/google/wifi/rtt/functional/RangeApNonSupporting11McTest.py
@@ -14,15 +14,12 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 
-import queue
-
 from acts import asserts
 from acts.test_decorators import test_tracker_info
-from acts.test_utils.wifi import wifi_test_utils as wutils
+from acts.test_utils.wifi.WifiBaseTest import WifiBaseTest
 from acts.test_utils.wifi.rtt import rtt_const as rconsts
 from acts.test_utils.wifi.rtt import rtt_test_utils as rutils
 from acts.test_utils.wifi.rtt.RttBaseTest import RttBaseTest
-from acts.test_utils.wifi.WifiBaseTest import WifiBaseTest
 
 
 class RangeApNonSupporting11McTest(WifiBaseTest, RttBaseTest):
@@ -47,7 +44,8 @@
   def test_rtt_non_80211mc_supporting_aps(self):
     """Scan for APs and perform RTT on non-IEEE 802.11mc supporting APs"""
     dut = self.android_devices[0]
-    non_rtt_aps = rutils.scan_with_rtt_support_constraint(dut, False)
+    non_rtt_aps = rutils.select_best_scan_results(
+      rutils.scan_with_rtt_support_constraint(dut, False), select_count=1)
     dut.log.debug("Visible non-IEEE 802.11mc APs=%s", non_rtt_aps)
     asserts.assert_true(len(non_rtt_aps) > 0, "Need at least one AP!")
     events = rutils.run_ranging(dut, non_rtt_aps, self.NUM_ITER,
@@ -84,7 +82,8 @@
     device not having privilege access (expect failures)."""
     dut = self.android_devices[0]
     rutils.config_privilege_override(dut, True)
-    non_rtt_aps = rutils.scan_with_rtt_support_constraint(dut, False)
+    non_rtt_aps = rutils.select_best_scan_results(
+      rutils.scan_with_rtt_support_constraint(dut, False), select_count=1)
     dut.log.debug("Visible non-IEEE 802.11mc APs=%s", non_rtt_aps)
     asserts.assert_true(len(non_rtt_aps) > 0, "Need at least one AP!")
     events = rutils.run_ranging(dut, non_rtt_aps, self.NUM_ITER,
@@ -114,7 +113,8 @@
     that get an error result.
     """
     dut = self.android_devices[0]
-    non_rtt_aps = rutils.scan_with_rtt_support_constraint(dut, False)
+    non_rtt_aps = rutils.select_best_scan_results(
+      rutils.scan_with_rtt_support_constraint(dut, False), select_count=1)
     dut.log.debug("Visible non-IEEE 802.11mc APs=%s", non_rtt_aps)
     asserts.assert_true(len(non_rtt_aps) > 0, "Need at least one AP!")
     non_rtt_aps = non_rtt_aps[0:1] # pick first
diff --git a/acts/tests/google/wifi/rtt/functional/RangeApSupporting11McTest.py b/acts/tests/google/wifi/rtt/functional/RangeApSupporting11McTest.py
index 98586cb..d889a22 100644
--- a/acts/tests/google/wifi/rtt/functional/RangeApSupporting11McTest.py
+++ b/acts/tests/google/wifi/rtt/functional/RangeApSupporting11McTest.py
@@ -42,8 +42,9 @@
   def test_rtt_80211mc_supporting_aps(self):
     """Scan for APs and perform RTT only to those which support 802.11mc"""
     dut = self.android_devices[0]
-    rtt_supporting_aps = rutils.scan_with_rtt_support_constraint(dut, True,
-                                                                 repeat=10)
+    rtt_supporting_aps = rutils.select_best_scan_results(
+      rutils.scan_with_rtt_support_constraint(dut, True, repeat=10),
+      select_count=2)
     dut.log.debug("RTT Supporting APs=%s", rtt_supporting_aps)
     events = rutils.run_ranging(dut, rtt_supporting_aps, self.NUM_ITER,
                                 self.TIME_BETWEEN_ITERATIONS)
@@ -87,8 +88,9 @@
     """Scan for APs and perform RTT only to those which support 802.11mc - using
     the LEGACY API!"""
     dut = self.android_devices[0]
-    rtt_supporting_aps = rutils.scan_with_rtt_support_constraint(dut, True,
-                                                                 repeat=10)
+    rtt_supporting_aps = rutils.select_best_scan_results(
+      rutils.scan_with_rtt_support_constraint(dut, True, repeat=10),
+      select_count=2)
     dut.log.debug("RTT Supporting APs=%s", rtt_supporting_aps)
 
     rtt_configs = []
diff --git a/acts/tests/google/wifi/rtt/functional/RttDisableTest.py b/acts/tests/google/wifi/rtt/functional/RttDisableTest.py
index cb8ade3..1816cd5 100644
--- a/acts/tests/google/wifi/rtt/functional/RttDisableTest.py
+++ b/acts/tests/google/wifi/rtt/functional/RttDisableTest.py
@@ -50,7 +50,8 @@
     asserts.assert_true(dut.droid.wifiIsRttAvailable(), "RTT is not available")
 
     # scan to get some APs to be used later
-    all_aps = rutils.scan_networks(dut)
+    all_aps = rutils.select_best_scan_results(rutils.scan_networks(dut),
+                                              select_count=1)
     asserts.assert_true(len(all_aps) > 0, "Need at least one visible AP!")
 
     # disable RTT and validate broadcast & API
diff --git a/acts/tests/google/wifi/rtt/functional/RttRequestManagementTest.py b/acts/tests/google/wifi/rtt/functional/RttRequestManagementTest.py
index 8370290..82c1058 100644
--- a/acts/tests/google/wifi/rtt/functional/RttRequestManagementTest.py
+++ b/acts/tests/google/wifi/rtt/functional/RttRequestManagementTest.py
@@ -57,7 +57,9 @@
     all_uids = [1000, 20, 30] # 1000 = System Server (makes requests foreground)
     some_uids = [20, 30]
 
-    aps = rutils.scan_with_rtt_support_constraint(dut, True, repeat=10)
+    aps = rutils.select_best_scan_results(
+      rutils.scan_with_rtt_support_constraint(dut, True, repeat=10),
+      select_count=1)
     dut.log.info("RTT Supporting APs=%s", aps)
 
     asserts.assert_true(
@@ -112,7 +114,9 @@
     # background uid will be throttled on the next run of this script
     fake_uid = [random.randint(10, 9999)]
 
-    aps = rutils.scan_with_rtt_support_constraint(dut, True, repeat=10)
+    aps = rutils.select_best_scan_results(
+      rutils.scan_with_rtt_support_constraint(dut, True, repeat=10),
+      select_count=1)
     dut.log.info("RTT Supporting APs=%s", aps)
 
     asserts.assert_true(