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 collections.abc
18from acts.controllers import access_point
19from acts.controllers.ap_lib import bridge_interface
20from acts.controllers.ap_lib import hostapd_security
21from acts.controllers.ap_lib import hostapd_ap_preset
22from acts_contrib.test_utils.wifi.wifi_retail_ap import WifiRetailAP
23
24
25class GoogleWifiAP(WifiRetailAP):
26    """ Class that implements Google Wifi AP.
27
28    This class is a work in progress
29    """
30    def __init__(self, ap_settings):
31        super().__init__(ap_settings)
32        # Initialize AP
33        if self.ap_settings['2G']['status'] and self.ap_settings['5G_1'][
34                'status']:
35            raise ValueError('Error initializing Google Wifi AP. '
36                             'Only one interface can be enabled at a time.')
37
38        self.capabilities = {
39            'interfaces': ['2G', '5G_1'],
40            'channels': {
41                '2G': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
42                '5G_1': [
43                    36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116,
44                    120, 124, 128, 132, 136, 140, 149, 153, 157, 161, 165
45                ]
46            },
47            'modes': {
48                '2G': ['VHT20', 'VHT40'],
49                '5G_1': ['VHT20', 'VHT40', 'VHT80']
50            },
51            'default_mode': 'VHT'
52        }
53        for interface in self.capabilities['interfaces']:
54            self.ap_settings.setdefault(interface, {})
55
56        self.BW_MODE_MAP = {
57            'legacy': 20,
58            'VHT20': 20,
59            'VHT40': 40,
60            'VHT80': 80
61        }
62        self.default_settings = {
63            'region': 'United States',
64            'brand': 'Google',
65            'model': 'Wifi',
66            'hostapd_profile': 'whirlwind',
67            '2G': {
68                'status': 0,
69                'ssid': 'GoogleWifi_2G',
70                'channel': 11,
71                'bandwidth': 'VHT20',
72                'power': 'auto',
73                'mode': None,
74                'num_streams': None,
75                'rate': 'auto',
76                'short_gi': 0,
77                'security_type': 'Open',
78                'password': 'password',
79                'subnet': '192.168.1.0/24'
80            },
81            '5G_1': {
82                'status': 0,
83                'ssid': 'GoogleWifi_2G',
84                'channel': 11,
85                'bandwidth': 'VHT20',
86                'power': 'auto',
87                'mode': None,
88                'num_streams': None,
89                'rate': 'auto',
90                'short_gi': 0,
91                'security_type': 'Open',
92                'password': 'password',
93                'subnet': '192.168.9.0/24'
94            }
95        }
96
97        for interface in self.capabilities['interfaces']:
98            for setting in self.default_settings[interface].keys():
99                if setting not in self.ap_settings[interface]:
100                    self.log.debug(
101                        '{0} {1} not found during init. Setting {1} = {2}'.
102                        format(interface, setting,
103                               self.default_settings[interface][setting]))
104                    self.ap_settings[interface][
105                        setting] = self.default_settings[interface][setting]
106        init_settings = self.ap_settings.copy()
107        init_settings['ap_subnet'] = {
108            '2g': self.ap_settings['2G']['subnet'],
109            '5g': self.ap_settings['5G_1']['subnet']
110        }
111        self.access_point = access_point.AccessPoint(init_settings)
112        self.configure_ap()
113
114    def read_ap_settings(self):
115        """Function that reads current ap settings."""
116        return self.ap_settings.copy()
117
118    def update_ap_settings(self, dict_settings={}, **named_settings):
119        """Function to update settings of existing AP.
120
121        Function copies arguments into ap_settings and calls configure_ap
122        to apply them.
123
124        Args:
125            dict_settings: single dictionary of settings to update
126            **named_settings: named settings to update
127            Note: dict and named_settings cannot contain the same settings.
128        """
129        settings_to_update = dict(dict_settings, **named_settings)
130        if len(settings_to_update) != len(dict_settings) + len(named_settings):
131            raise KeyError('The following keys were passed twice: {}'.format(
132                (set(dict_settings.keys()).intersection(
133                    set(named_settings.keys())))))
134
135        updating_2G = '2G' in settings_to_update.keys()
136        updating_5G_1 = '5G_1' in settings_to_update.keys()
137        if updating_2G and updating_5G_1:
138            raise ValueError(
139                'Error updating Google WiFi AP. '
140                'One interface can be activated and updated at a time')
141        elif updating_2G:
142            # If updating an interface and not explicitly setting its status,
143            # it is assumed that the interface is to be ENABLED and updated
144            if 'status' not in settings_to_update['2G']:
145                settings_to_update['2G']['status'] = 1
146                settings_to_update['5G_1'] = {'status': 0}
147        elif updating_5G_1:
148            if 'status' not in settings_to_update['5G_1']:
149                settings_to_update['2G'] = {'status': 0}
150                settings_to_update['5G_1']['status'] = 1
151        self.ap_settings, updates_requested, status_toggle_flag = self._update_settings_dict(
152            self.ap_settings, settings_to_update)
153        if updates_requested:
154            self.configure_ap()
155
156    def configure_ap(self):
157        """Function to configure Google Wifi."""
158        self.log.info('Stopping Google Wifi interfaces.')
159        print(self.ap_settings)
160        self.access_point.stop_all_aps()
161
162        if self.ap_settings['2G']['status'] == 1:
163            interface = '2G'
164            self.log.info('Bringing up 2.4 GHz interface.')
165        elif self.ap_settings['5G_1']['status'] == 1:
166            interface = '5G_1'
167            self.log.info('Bringing up 5 GHz interface.')
168        else:
169            return
170
171        bss_settings = []
172        ssid = self.ap_settings[interface]['ssid']
173        security_mode = self.ap_settings[interface]['security_type'].lower()
174        if 'wpa' in security_mode:
175            password = self.ap_settings[interface]['password']
176            security = hostapd_security.Security(security_mode=security_mode,
177                                                 password=password)
178        else:
179            security = hostapd_security.Security(security_mode=None,
180                                                 password=None)
181        channel = int(self.ap_settings[interface]['channel'])
182        bandwidth = self.BW_MODE_MAP[self.ap_settings[interface]['bandwidth']]
183        config = hostapd_ap_preset.create_ap_preset(
184            channel=channel,
185            ssid=ssid,
186            security=security,
187            bss_settings=bss_settings,
188            vht_bandwidth=bandwidth,
189            profile_name=self.ap_settings['hostapd_profile'],
190            iface_wlan_2g=self.access_point.wlan_2g,
191            iface_wlan_5g=self.access_point.wlan_5g)
192        config_bridge = self.access_point.generate_bridge_configs(channel)
193        brconfigs = bridge_interface.BridgeInterfaceConfigs(
194            config_bridge[0], 'lan0', config_bridge[2])
195        self.access_point.bridge.startup(brconfigs)
196        self.access_point.start_ap(config)
197        self.set_power(interface, self.ap_settings[interface]['power'])
198        self.set_rate(interface,
199                      mode=self.ap_settings[interface]['mode'],
200                      num_streams=self.ap_settings[interface]['num_streams'],
201                      rate=self.ap_settings[interface]['rate'],
202                      short_gi=self.ap_settings[interface]['short_gi'])
203        self.log.info('AP started on channel {} with SSID {}'.format(
204            channel, ssid))
205
206    def set_power(self, interface, power):
207        """Function that sets interface transmit power.
208
209        Args:
210            interface: string containing interface identifier (2G, 5G_1)
211            power: power level in dBm
212        """
213        if power == 'auto':
214            power_string = 'auto'
215        else:
216            if not float(power).is_integer():
217                self.log.info(
218                    'Power in dBm must be an integer. Setting to {}'.format(
219                        int(power)))
220            power = int(power)
221            power_string = 'fixed {}'.format(int(power) * 100)
222
223        if '2G' in interface:
224            interface_long = self.access_point.wlan_2g
225            self.ap_settings[interface]['power'] = power
226        elif '5G_1' in interface:
227            interface_long = self.access_point.wlan_5g
228            self.ap_settings[interface]['power'] = power
229        self.access_point.ssh.run('iw dev {} set txpower {}'.format(
230            interface_long, power_string))
231
232    def set_rate(self,
233                 interface,
234                 mode=None,
235                 num_streams=None,
236                 rate='auto',
237                 short_gi=0):
238        """Function that sets rate.
239
240        Args:
241            interface: string containing interface identifier (2G, 5G_1)
242            mode: string indicating the WiFi standard to use
243            num_streams: number of MIMO streams. used only for VHT
244            rate: data rate of MCS index to use
245            short_gi: boolean controlling the use of short guard interval
246        """
247        if '2G' in interface:
248            interface_long = self.access_point.wlan_2g
249            interface_short = '2.4'
250        elif '5G_1' in interface:
251            interface_long = self.access_point.wlan_5g
252            interface_short = '5'
253        self.ap_settings[interface]['mode'] = mode
254        self.ap_settings[interface]['num_streams'] = num_streams
255        self.ap_settings[interface]['rate'] = rate
256        self.ap_settings[interface]['short_gi'] = short_gi
257
258        if rate == 'auto':
259            cmd_string = 'iw dev {0} set bitrates'.format(interface_long)
260        elif 'legacy' in mode.lower():
261            cmd_string = 'iw dev {0} set bitrates legacy-{1} {2} ht-mcs-{1} vht-mcs-{1}'.format(
262                interface_long, interface_short, rate)
263        elif 'vht' in mode.lower():
264            cmd_string = 'iw dev {0} set bitrates legacy-{1} ht-mcs-{1} vht-mcs-{1} {2}:{3}'.format(
265                interface_long, interface_short, num_streams, rate)
266            if short_gi:
267                cmd_string = cmd_string + ' sgi-{}'.format(interface_short)
268        elif 'ht' in mode.lower():
269            cmd_string = 'iw dev {0} set bitrates legacy-{1} ht-mcs-{1} {2} vht-mcs-{1}'.format(
270                interface_long, interface_short, rate)
271            if short_gi:
272                cmd_string = cmd_string + ' sgi-{}'.format(interface_short)
273        self.access_point.ssh.run(cmd_string)
274