• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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