1#!/usr/bin/env python3
2#
3#   Copyright 2020 - The Android Open Source Project
4#
5#   Licensed under the Apache License, Version 2.0 (the "License");
6#   you may not use this file except in compliance with the License.
7#   You may obtain a copy of 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,
13#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14#   See the License for the specific language governing permissions and
15#   limitations under the License.
16
17import itertools
18
19from acts import utils
20from acts.controllers.ap_lib import hostapd_constants
21from acts.controllers.ap_lib import hostapd_config
22from acts.test_utils.abstract_devices.wlan_device import create_wlan_device
23from acts.test_utils.abstract_devices.utils_lib.wlan_utils import validate_setup_ap_and_associate
24from acts.test_utils.wifi.WifiBaseTest import WifiBaseTest
25from acts.utils import rand_ascii_str
26
27# 12, 13 outside the US
28# 14 in Japan, DSSS and CCK only
29CHANNELS_24 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
30
31# 32, 34 for 20 and 40 mhz in Europe
32# 34, 42, 46 have mixed international support
33# 144 is supported by some chips
34CHANNELS_5 = [
35    36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128,
36    132, 136, 140, 144, 149, 153, 157, 161, 165
37]
38
39BANDWIDTH_24 = [20, 40]
40BANDWIDTH_5 = [20, 40, 80]
41
42N_CAPABILITIES_DEFAULT = [
43    hostapd_constants.N_CAPABILITY_LDPC, hostapd_constants.N_CAPABILITY_SGI20,
44    hostapd_constants.N_CAPABILITY_SGI40,
45    hostapd_constants.N_CAPABILITY_TX_STBC,
46    hostapd_constants.N_CAPABILITY_RX_STBC1
47]
48
49AC_CAPABILITIES_DEFAULT = [
50    hostapd_constants.AC_CAPABILITY_MAX_MPDU_11454,
51    hostapd_constants.AC_CAPABILITY_RXLDPC,
52    hostapd_constants.AC_CAPABILITY_SHORT_GI_80,
53    hostapd_constants.AC_CAPABILITY_TX_STBC_2BY1,
54    hostapd_constants.AC_CAPABILITY_RX_STBC_1,
55    hostapd_constants.AC_CAPABILITY_MAX_A_MPDU_LEN_EXP7,
56    hostapd_constants.AC_CAPABILITY_RX_ANTENNA_PATTERN,
57    hostapd_constants.AC_CAPABILITY_TX_ANTENNA_PATTERN
58]
59
60
61def generate_test_name(settings):
62    """Generates a string test name based on the channel and band
63
64    Args:
65        settings: A dict with 'channel' and 'bandwidth' keys.
66
67    Returns:
68        A string that represents a test case name.
69    """
70    return 'test_channel_%s_bandwidth_%smhz' % (settings['channel'],
71                                                settings['bandwidth'])
72
73
74class ChannelSweepTest(WifiBaseTest):
75    """Tests for associating with all 2.5 and 5 channels and bands.
76
77    Testbed Requirement:
78    * One ACTS compatible device (dut)
79    * One Access Point
80    """
81    def __init__(self, controllers):
82        WifiBaseTest.__init__(self, controllers)
83        self.tests = ['test_24ghz_channels', 'test_5ghz_channels']
84        if 'debug_channel_sweep_tests' in self.user_params:
85            self.tests.append('test_channel_sweep_debug')
86
87    def setup_class(self):
88        super().setup_class()
89        if 'dut' in self.user_params:
90            if self.user_params['dut'] == 'fuchsia_devices':
91                self.dut = create_wlan_device(self.fuchsia_devices[0])
92            elif self.user_params['dut'] == 'android_devices':
93                self.dut = create_wlan_device(self.android_devices[0])
94            else:
95                raise ValueError('Invalid DUT specified in config. (%s)' %
96                                 self.user_params['dut'])
97        else:
98            # Default is an android device, just like the other tests
99            self.dut = create_wlan_device(self.android_devices[0])
100
101        self.android_devices = getattr(self, 'android_devices', [])
102        self.access_point = self.access_points[0]
103        self.access_point.stop_all_aps()
104
105    def setup_test(self):
106        for ad in self.android_devices:
107            ad.droid.wakeLockAcquireBright()
108            ad.droid.wakeUpNow()
109        self.dut.wifi_toggle_state(True)
110
111    def teardown_test(self):
112        for ad in self.android_devices:
113            ad.droid.wakeLockRelease()
114            ad.droid.goToSleepNow()
115        self.dut.turn_location_off_and_scan_toggle_off()
116        self.dut.disconnect()
117        self.dut.reset_wifi()
118        self.access_point.stop_all_aps()
119
120    def on_fail(self, test_name, begin_time):
121        self.dut.take_bug_report(test_name, begin_time)
122        self.dut.get_log(test_name, begin_time)
123
124    def setup_and_connect(self, settings):
125        """Generates a hostapd config based on the provided channel and
126        bandwidth, starts AP with that config, and attempts to associate the
127        dut.
128
129        Args:
130            settings: A dict with 'channel' and 'bandwidth' keys.
131        """
132        channel = settings['channel']
133        bandwidth = settings['bandwidth']
134        if channel > 14:
135            vht_bandwidth = bandwidth
136        else:
137            vht_bandwidth = None
138
139        if bandwidth == 20:
140            n_capabilities = N_CAPABILITIES_DEFAULT + [
141                hostapd_constants.N_CAPABILITY_HT20
142            ]
143        elif bandwidth == 40 or bandwidth == 80:
144            if hostapd_config.ht40_plus_allowed(channel):
145                extended_channel = [hostapd_constants.N_CAPABILITY_HT40_PLUS]
146            elif hostapd_config.ht40_minus_allowed(channel):
147                extended_channel = [hostapd_constants.N_CAPABILITY_HT40_MINUS]
148            else:
149                raise ValueError('Invalid Channel: %s' % channel)
150            n_capabilities = N_CAPABILITIES_DEFAULT + extended_channel
151        else:
152            raise ValueError('Invalid Bandwidth: %s' % bandwidth)
153
154        validate_setup_ap_and_associate(access_point=self.access_point,
155                                        client=self.dut,
156                                        profile_name='whirlwind',
157                                        channel=channel,
158                                        n_capabilities=n_capabilities,
159                                        ac_capabilities=None,
160                                        force_wmm=True,
161                                        ssid=utils.rand_ascii_str(20),
162                                        vht_bandwidth=vht_bandwidth)
163
164    def create_test_settings(self, channels, bandwidths):
165        """Creates a list of test configurations to run from the product of the
166        given channels list and bandwidths list.
167
168        Args:
169            channels: A list of ints representing the channels to test.
170            bandwidths: A list of ints representing the bandwidths to test on
171                those channels.
172
173        Returns:
174            A list of dictionaries containing 'channel' and 'bandwidth' keys,
175                one for each test combination to be run.
176        """
177        test_list = []
178        for combination in itertools.product(channels, bandwidths):
179            test_settings = {
180                'channel': combination[0],
181                'bandwidth': combination[1]
182            }
183            test_list.append(test_settings)
184        return test_list
185
186    def test_24ghz_channels(self):
187        """Runs setup_and_connect for 2.4GHz channels on 20 and 40 MHz bands."""
188        test_list = self.create_test_settings(CHANNELS_24, BANDWIDTH_24)
189        self.run_generated_testcases(self.setup_and_connect,
190                                     settings=test_list,
191                                     name_func=generate_test_name)
192
193    def test_5ghz_channels(self):
194        """Runs setup_and_connect for 5GHz channels on 20, 40, and 80 MHz bands.
195        """
196        test_list = self.create_test_settings(CHANNELS_5, BANDWIDTH_5)
197        # Channel 165 is 20mhz only
198        test_list.remove({'channel': 165, 'bandwidth': 40})
199        test_list.remove({'channel': 165, 'bandwidth': 80})
200        self.run_generated_testcases(self.setup_and_connect,
201                                     settings=test_list,
202                                     name_func=generate_test_name)
203
204    def test_channel_sweep_debug(self):
205        """Runs test cases defined in the ACTS config file for debugging
206        purposes.
207
208        Usage:
209            1. Add 'debug_channel_sweep_tests' to ACTS config with list of
210                tests to run matching pattern:
211                test_channel_<CHANNEL>_bandwidth_<BANDWIDTH>
212            2. Run test_channel_sweep_debug test.
213        """
214        allowed_channels = CHANNELS_24 + CHANNELS_5
215        chan_band_pattern = re.compile(
216            r'.*channel_([0-9]*)_.*bandwidth_([0-9]*)')
217        test_list = []
218        for test_title in self.user_params['debug_channel_sweep_tests']:
219            test = re.match(chan_band_pattern, test_title)
220            if test:
221                channel = int(test.group(1))
222                bandwidth = int(test.group(2))
223                if channel not in allowed_channels:
224                    raise ValueError("Invalid channel: %s" % channel)
225                if channel <= 14 and bandwidth not in BANDWIDTH_24:
226                    raise ValueError(
227                        "Channel %s does not support bandwidth %s" %
228                        (channel, bandwidth))
229                test_settings = {'channel': channel, 'bandwidth': bandwidth}
230                test_list.append(test_settings)
231
232        self.run_generated_testcases(self.setup_and_connect,
233                                     settings=test_list,
234                                     name_func=generate_test_name)
235