1#!/usr/bin/env python3 2# 3# Copyright (C) 2021 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may not 6# use this file except in compliance with the License. You may obtain a copy of 7# the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations under 15# the License. 16""" 17Tests STA handling of channel switch announcements. 18""" 19 20import random 21import time 22 23from acts import asserts 24from acts.controllers.access_point import setup_ap 25from acts.controllers.ap_lib import hostapd_constants 26from acts.utils import rand_ascii_str 27from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest 28from acts_contrib.test_utils.abstract_devices.wlan_device import create_wlan_device 29from typing import Sequence 30 31 32class ChannelSwitchTest(WifiBaseTest): 33 # Time to wait between issuing channel switches 34 WAIT_BETWEEN_CHANNEL_SWITCHES_S = 15 35 36 # For operating class 115 tests. 37 GLOBAL_OPERATING_CLASS_115_CHANNELS = [36, 40, 44, 48] 38 # A channel outside the operating class. 39 NON_GLOBAL_OPERATING_CLASS_115_CHANNEL = 52 40 41 # For operating class 124 tests. 42 GLOBAL_OPERATING_CLASS_124_CHANNELS = [149, 153, 157, 161] 43 # A channel outside the operating class. 44 NON_GLOBAL_OPERATING_CLASS_124_CHANNEL = 52 45 46 def setup_class(self) -> None: 47 super().setup_class() 48 self.ssid = rand_ascii_str(10) 49 if 'dut' in self.user_params: 50 if self.user_params['dut'] == 'fuchsia_devices': 51 self.dut = create_wlan_device(self.fuchsia_devices[0]) 52 elif self.user_params['dut'] == 'android_devices': 53 self.dut = create_wlan_device(self.android_devices[0]) 54 else: 55 raise ValueError('Invalid DUT specified in config. (%s)' % 56 self.user_params['dut']) 57 else: 58 # Default is an android device, just like the other tests 59 self.dut = create_wlan_device(self.android_devices[0]) 60 self.access_point = self.access_points[0] 61 self._stop_all_soft_aps() 62 self.in_use_interface = None 63 64 def teardown_test(self) -> None: 65 self.dut.disconnect() 66 self.dut.reset_wifi() 67 self.download_ap_logs() 68 self.access_point.stop_all_aps() 69 70 # TODO(fxbug.dev/85738): Change band type to an enum. 71 def channel_switch(self, 72 band: str, 73 starting_channel: int, 74 channel_switches: Sequence[int], 75 test_with_soft_ap: bool = False) -> None: 76 """Setup and run a channel switch test with the given parameters. 77 78 Creates an AP, associates to it, and then issues channel switches 79 through the provided channels. After each channel switch, the test 80 checks that the DUT is connected for a period of time before considering 81 the channel switch successful. If directed to start a SoftAP, the test 82 will also check that the SoftAP is on the expected channel after each 83 channel switch. 84 85 Args: 86 band: band that AP will use, must be a valid band (e.g. 87 hostapd_constants.BAND_2G) 88 starting_channel: channel number that AP will use at startup 89 channel_switches: ordered list of channels that the test will 90 attempt to switch to 91 test_with_soft_ap: whether to start a SoftAP before beginning the 92 channel switches (default is False); note that if a SoftAP is 93 started, the test will also check that the SoftAP handles 94 channel switches correctly 95 """ 96 asserts.assert_true( 97 band in [hostapd_constants.BAND_2G, hostapd_constants.BAND_5G], 98 'Failed to setup AP, invalid band {}'.format(band)) 99 100 self.current_channel_num = starting_channel 101 if band == hostapd_constants.BAND_5G: 102 self.in_use_interface = self.access_point.wlan_5g 103 elif band == hostapd_constants.BAND_2G: 104 self.in_use_interface = self.access_point.wlan_2g 105 asserts.assert_true( 106 self._channels_valid_for_band([self.current_channel_num], band), 107 'starting channel {} not a valid channel for band {}'.format( 108 self.current_channel_num, band)) 109 110 setup_ap(access_point=self.access_point, 111 profile_name='whirlwind', 112 channel=self.current_channel_num, 113 ssid=self.ssid) 114 if test_with_soft_ap: 115 self._start_soft_ap() 116 self.log.info('sending associate command for ssid %s', self.ssid) 117 self.dut.associate(target_ssid=self.ssid) 118 asserts.assert_true(self.dut.is_connected(), 'Failed to connect.') 119 120 asserts.assert_true(channel_switches, 121 'Cannot run test, no channels to switch to') 122 asserts.assert_true( 123 self._channels_valid_for_band(channel_switches, band), 124 'channel_switches {} includes invalid channels for band {}'.format( 125 channel_switches, band)) 126 127 for channel_num in channel_switches: 128 if channel_num == self.current_channel_num: 129 continue 130 self.log.info('channel switch: {} -> {}'.format( 131 self.current_channel_num, channel_num)) 132 self.access_point.channel_switch(self.in_use_interface, 133 channel_num) 134 channel_num_after_switch = self.access_point.get_current_channel( 135 self.in_use_interface) 136 asserts.assert_equal(channel_num_after_switch, channel_num, 137 'AP failed to channel switch') 138 self.current_channel_num = channel_num 139 140 # Check periodically to see if DUT stays connected. Sometimes 141 # CSA-induced disconnects occur seconds after last channel switch. 142 for _ in range(self.WAIT_BETWEEN_CHANNEL_SWITCHES_S): 143 asserts.assert_true( 144 self.dut.is_connected(), 145 'Failed to stay connected after channel switch.') 146 client_channel = self._client_channel() 147 asserts.assert_equal( 148 client_channel, channel_num, 149 'Client interface on wrong channel ({})'.format( 150 client_channel)) 151 if test_with_soft_ap: 152 soft_ap_channel = self._soft_ap_channel() 153 asserts.assert_equal( 154 soft_ap_channel, channel_num, 155 'SoftAP interface on wrong channel ({})'.format( 156 soft_ap_channel)) 157 time.sleep(1) 158 159 def test_channel_switch_2g(self) -> None: 160 """Channel switch through all (US only) channels in the 2 GHz band.""" 161 self.channel_switch( 162 band=hostapd_constants.BAND_2G, 163 starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G, 164 channel_switches=hostapd_constants.US_CHANNELS_2G) 165 166 def test_channel_switch_2g_with_soft_ap(self) -> None: 167 """Channel switch through (US only) 2 Ghz channels with SoftAP up.""" 168 self.channel_switch( 169 band=hostapd_constants.BAND_2G, 170 starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G, 171 channel_switches=hostapd_constants.US_CHANNELS_2G, 172 test_with_soft_ap=True) 173 174 def test_channel_switch_2g_shuffled_with_soft_ap(self) -> None: 175 """Switch through shuffled (US only) 2 Ghz channels with SoftAP up.""" 176 channels = hostapd_constants.US_CHANNELS_2G 177 random.shuffle(channels) 178 self.log.info('Shuffled channel switch sequence: {}'.format(channels)) 179 self.channel_switch( 180 band=hostapd_constants.BAND_2G, 181 starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_2G, 182 channel_switches=channels, 183 test_with_soft_ap=True) 184 185 # TODO(fxbug.dev/84777): This test fails. 186 def test_channel_switch_5g(self) -> None: 187 """Channel switch through all (US only) channels in the 5 GHz band.""" 188 self.channel_switch( 189 band=hostapd_constants.BAND_5G, 190 starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G, 191 channel_switches=hostapd_constants.US_CHANNELS_5G) 192 193 # TODO(fxbug.dev/84777): This test fails. 194 def test_channel_switch_5g_with_soft_ap(self) -> None: 195 """Channel switch through (US only) 5 GHz channels with SoftAP up.""" 196 self.channel_switch( 197 band=hostapd_constants.BAND_5G, 198 starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G, 199 channel_switches=hostapd_constants.US_CHANNELS_5G, 200 test_with_soft_ap=True) 201 202 def test_channel_switch_5g_shuffled_with_soft_ap(self) -> None: 203 """Switch through shuffled (US only) 5 Ghz channels with SoftAP up.""" 204 channels = hostapd_constants.US_CHANNELS_5G 205 random.shuffle(channels) 206 self.log.info('Shuffled channel switch sequence: {}'.format(channels)) 207 self.channel_switch( 208 band=hostapd_constants.BAND_5G, 209 starting_channel=hostapd_constants.AP_DEFAULT_CHANNEL_5G, 210 channel_switches=channels, 211 test_with_soft_ap=True) 212 213 # TODO(fxbug.dev/84777): This test fails. 214 def test_channel_switch_regression_global_operating_class_115( 215 self) -> None: 216 """Channel switch into, through, and out of global op. class 115 channels. 217 218 Global operating class 115 is described in IEEE 802.11-2016 Table E-4. 219 Regression test for fxbug.dev/84777. 220 """ 221 channels = self.GLOBAL_OPERATING_CLASS_115_CHANNELS + [ 222 self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL 223 ] 224 self.channel_switch( 225 band=hostapd_constants.BAND_5G, 226 starting_channel=self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL, 227 channel_switches=channels) 228 229 # TODO(fxbug.dev/84777): This test fails. 230 def test_channel_switch_regression_global_operating_class_115_with_soft_ap( 231 self) -> None: 232 """Test global operating class 124 channel switches, with SoftAP. 233 234 Regression test for fxbug.dev/84777. 235 """ 236 channels = self.GLOBAL_OPERATING_CLASS_115_CHANNELS + [ 237 self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL 238 ] 239 self.channel_switch( 240 band=hostapd_constants.BAND_5G, 241 starting_channel=self.NON_GLOBAL_OPERATING_CLASS_115_CHANNEL, 242 channel_switches=channels, 243 test_with_soft_ap=True) 244 245 # TODO(fxbug.dev/84777): This test fails. 246 def test_channel_switch_regression_global_operating_class_124( 247 self) -> None: 248 """Switch into, through, and out of global op. class 124 channels. 249 250 Global operating class 124 is described in IEEE 802.11-2016 Table E-4. 251 Regression test for fxbug.dev/64279. 252 """ 253 channels = self.GLOBAL_OPERATING_CLASS_124_CHANNELS + [ 254 self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL 255 ] 256 self.channel_switch( 257 band=hostapd_constants.BAND_5G, 258 starting_channel=self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL, 259 channel_switches=channels) 260 261 # TODO(fxbug.dev/84777): This test fails. 262 def test_channel_switch_regression_global_operating_class_124_with_soft_ap( 263 self) -> None: 264 """Test global operating class 124 channel switches, with SoftAP. 265 266 Regression test for fxbug.dev/64279. 267 """ 268 channels = self.GLOBAL_OPERATING_CLASS_124_CHANNELS + [ 269 self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL 270 ] 271 self.channel_switch( 272 band=hostapd_constants.BAND_5G, 273 starting_channel=self.NON_GLOBAL_OPERATING_CLASS_124_CHANNEL, 274 channel_switches=channels, 275 test_with_soft_ap=True) 276 277 def _channels_valid_for_band(self, channels: Sequence[int], 278 band: str) -> bool: 279 """Determine if the channels are valid for the band (US only). 280 281 Args: 282 channels: channel numbers 283 band: a valid band (e.g. hostapd_constants.BAND_2G) 284 """ 285 if band == hostapd_constants.BAND_2G: 286 band_channels = frozenset(hostapd_constants.US_CHANNELS_2G) 287 elif band == hostapd_constants.BAND_5G: 288 band_channels = frozenset(hostapd_constants.US_CHANNELS_5G) 289 else: 290 asserts.fail('Invalid band {}'.format(band)) 291 channels_set = frozenset(channels) 292 if channels_set <= band_channels: 293 return True 294 return False 295 296 def _start_soft_ap(self) -> None: 297 """Start a SoftAP on the DUT. 298 299 Raises: 300 EnvironmentError: if the SoftAP does not start 301 """ 302 ssid = rand_ascii_str(10) 303 security_type = 'none' 304 password = '' 305 connectivity_mode = 'local_only' 306 operating_band = 'any' 307 308 self.log.info('Starting SoftAP on DUT') 309 310 response = self.dut.device.sl4f.wlan_ap_policy_lib.wlanStartAccessPoint( 311 ssid, security_type, password, connectivity_mode, operating_band) 312 if response.get('error'): 313 raise EnvironmentError('SL4F: Failed to setup SoftAP. Err: %s' % 314 response['error']) 315 self.log.info('SoftAp network (%s) is up.' % ssid) 316 317 def _stop_all_soft_aps(self) -> None: 318 """Stops all SoftAPs on Fuchsia Device. 319 320 Raises: 321 EnvironmentError: if SoftAP stop call fails 322 """ 323 response = self.dut.device.sl4f.wlan_ap_policy_lib.wlanStopAllAccessPoint( 324 ) 325 if response.get('error'): 326 raise EnvironmentError( 327 'SL4F: Failed to stop all SoftAPs. Err: %s' % 328 response['error']) 329 330 def _client_channel(self) -> int: 331 """Determine the channel of the DUT client interface. 332 333 If the interface is not connected, the method will assert a test 334 failure. 335 336 Returns: channel number 337 338 Raises: 339 EnvironmentError: if client interface channel cannot be 340 determined 341 """ 342 status = self.dut.status() 343 if status['error']: 344 raise EnvironmentError('Could not determine client channel') 345 346 result = status['result'] 347 if isinstance(result, dict): 348 if result.get('Connected'): 349 return result['Connected']['channel']['primary'] 350 asserts.fail('Client interface not connected') 351 raise EnvironmentError('Could not determine client channel') 352 353 def _soft_ap_channel(self) -> int: 354 """Determine the channel of the DUT SoftAP interface. 355 356 If the interface is not connected, the method will assert a test 357 failure. 358 359 Returns: channel number 360 361 Raises: 362 EnvironmentError: if SoftAP interface channel cannot be determined. 363 """ 364 iface_ids = self.dut.get_wlan_interface_id_list() 365 for iface_id in iface_ids: 366 query = self.dut.device.sl4f.wlan_lib.wlanQueryInterface(iface_id) 367 if query['error']: 368 continue 369 query_result = query['result'] 370 if type(query_result) is dict and query_result.get('role') == 'Ap': 371 status = self.dut.device.sl4f.wlan_lib.wlanStatus(iface_id) 372 if status['error']: 373 continue 374 status_result = status['result'] 375 if isinstance(status_result, dict): 376 if status_result.get('Connected'): 377 return status_result['Connected']['channel']['primary'] 378 asserts.fail('SoftAP interface not connected') 379 raise EnvironmentError('Could not determine SoftAP channel') 380