• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import multiprocessing
7import re
8import select
9import time
10
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib.cros.network import ping_runner
13from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
14from autotest_lib.server import site_attenuator
15from autotest_lib.server.cros.network import hostap_config
16from autotest_lib.server.cros.network import rvr_test_base
17
18class Reporter(object):
19    """Object that forwards stdout from Host.run to a pipe.
20
21    The |stdout_tee| parameter for Host.run() requires an object that looks
22    like a Python built-in file.  In particular, it needs 'flush', which a
23    multiprocessing.Connection (the object returned by multiprocessing.Pipe)
24    doesn't have.  This wrapper provides that functionaly in order to allow a
25    pipe to be the target of a stdout_tee.
26
27    """
28
29    def __init__(self, write_pipe):
30        """Initializes reporter.
31
32        @param write_pipe: the place to send output.
33
34        """
35        self._write_pipe = write_pipe
36
37
38    def flush(self):
39        """Flushes the output - not used by the pipe."""
40        pass
41
42
43    def close(self):
44        """Closes the pipe."""
45        return self._write_pipe.close()
46
47
48    def fileno(self):
49        """Returns the file number of the pipe."""
50        return self._write_pipe.fileno()
51
52
53    def write(self, string):
54        """Write to the pipe.
55
56        @param string: the string to write to the pipe.
57
58        """
59        self._write_pipe.send(string)
60
61
62    def writelines(self, sequence):
63        """Write a number of lines to the pipe.
64
65        @param sequence: the array of lines to be written.
66
67        """
68        for string in sequence:
69            self._write_pipe.send(string)
70
71
72class LaunchIwEvent(object):
73    """Calls 'iw event' and searches for a list of events in its output.
74
75    This class provides a framework for launching 'iw event' in its own
76    process and searching its output for an ordered list of events expressed
77    as regular expressions.
78
79    Expected to be called as follows:
80        launch_iw_event = LaunchIwEvent('iw',
81                                        self.context.client.host,
82                                        timeout_seconds=60.0)
83        # Do things that cause nl80211 traffic
84
85        # Now, wait for the results you want.
86        if not launch_iw_event.wait_for_events(['RSSI went below threshold',
87                                                'scan started',
88                                                # ...
89                                                'connected to']):
90            raise error.TestFail('Did not find all expected events')
91
92    """
93    # A timeout from Host.run(timeout) kills the process and that takes a
94    # few seconds.  Therefore, we need to add some margin to the select
95    # timeout (which will kill the process if Host.run(timeout) fails for some
96    # reason).
97    TIMEOUT_MARGIN_SECONDS = 5
98
99    def __init__(self, iw_command, dut, timeout_seconds):
100        """Launches 'iw event' process with communication channel for output
101
102        @param dut: Host object for the dut
103        @param timeout_seconds: timeout for 'iw event' (since it never
104        returns)
105
106        """
107        self._iw_command = iw_command
108        self._dut = dut
109        self._timeout_seconds = timeout_seconds
110        self._pipe_reader, pipe_writer = multiprocessing.Pipe()
111        self._iw_event = multiprocessing.Process(target=self.do_iw,
112                                                 args=(pipe_writer,
113                                                       self._timeout_seconds,))
114        self._iw_event.start()
115
116
117    def do_iw(self, connection, timeout_seconds):
118        """Runs 'iw event'
119
120        iw results are passed back, on the fly, through a supplied connection
121        object.  The process terminates itself after a specified timeout.
122
123        @param connection: a Connection object to which results are written.
124        @param timeout_seconds: number of seconds before 'iw event' is killed.
125
126        """
127        reporter = Reporter(connection)
128        # ignore_timeout just ignores the _exception_; the timeout is still
129        # valid.
130        self._dut.run('%s event' % self._iw_command,
131                      timeout=timeout_seconds,
132                      stdout_tee=reporter,
133                      ignore_timeout=True)
134
135
136    def wait_for_events(self, expected_events):
137        """Waits for 'expected_events' (in order) from iw.
138
139        @param expected_events: a list of strings that are regular expressions.
140            This method searches for the each expression, in the order that they
141            appear in |expected_events|, in the stream of output from iw. x
142
143        @returns: True if all events were found.  False, otherwise.
144
145        """
146        if not expected_events:
147            logging.error('No events')
148            return False
149
150        expected_event = expected_events.pop(0)
151        done_time = (time.time() + self._timeout_seconds +
152                     LaunchIwEvent.TIMEOUT_MARGIN_SECONDS)
153        received_event_log = []
154        while expected_event:
155            timeout = done_time - time.time()
156            if timeout <= 0:
157                break
158            (sread, swrite, sexec) = select.select(
159                    [self._pipe_reader], [], [], timeout)
160            if sread:
161                received_event = sread[0].recv()
162                received_event_log.append(received_event)
163                if re.search(expected_event, received_event):
164                    logging.info('Found expected event: "%s"',
165                                 received_event.rstrip())
166                    if expected_events:
167                        expected_event = expected_events.pop(0)
168                    else:
169                        expected_event = None
170                        logging.info('Found ALL expected events')
171                        break
172            else:  # Timeout.
173                break
174
175        if expected_event:
176            logging.error('Never found expected event "%s". iw log:',
177                          expected_event)
178            for event in received_event_log:
179                logging.error(event.rstrip())
180            return False
181        return True
182
183
184class network_WiFi_RoamOnLowPower(rvr_test_base.RvRTestBase):
185    """Tests roaming to an AP when the old one's signal is too weak.
186
187    This test uses a dual-radio Stumpy as the AP and configures the radios to
188    broadcast two BSS's with different frequencies on the same SSID.  The DUT
189    connects to the first radio, the test attenuates that radio, and the DUT
190    is supposed to roam to the second radio.
191
192    This test requires a particular configuration of test equipment:
193
194                                   +--------- StumpyCell/AP ----------+
195                                   | chromeX.grover.hostY.router.cros |
196                                   |                                  |
197                                   |       [Radio 0]  [Radio 1]       |
198                                   +--------A-----B----C-----D--------+
199        +------ BeagleBone ------+          |     |    |     |
200        | chromeX.grover.hostY.  |          |     X    |     X
201        | attenuator.cros      [Port0]-[attenuator]    |
202        |                      [Port1]----- | ----[attenuator]
203        |                      [Port2]-X    |          |
204        |                      [Port3]-X    +-----+    |
205        |                        |                |    |
206        +------------------------+                |    |
207                                   +--------------E----F--------------+
208                                   |             [Radio 0]            |
209                                   |                                  |
210                                   |    chromeX.grover.hostY.cros     |
211                                   +-------------- DUT ---------------+
212
213    Where antennas A, C, and E are the primary antennas for AP/radio0,
214    AP/radio1, and DUT/radio0, respectively; and antennas B, D, and F are the
215    auxilliary antennas for AP/radio0, AP/radio1, and DUT/radio0,
216    respectively.  The BeagleBone controls 2 attenuators that are connected
217    to the primary antennas of AP/radio0 and 1 which are fed into the primary
218    and auxilliary antenna ports of DUT/radio 0.  Ports 2 and 3 of the
219    BeagleBone as well as the auxillary antennae of AP/radio0 and 1 are
220    terminated.
221
222    This arrangement ensures that the attenuator port numbers are assigned to
223    the primary radio, first, and the secondary radio, second.  If this happens,
224    the ports will be numbered in the order in which the AP's channels are
225    configured (port 0 is first, port 1 is second, etc.).
226
227    This test is a de facto test that the ports are configured in that
228    arrangement since swapping Port0 and Port1 would cause us to attenuate the
229    secondary radio, providing no impetus for the DUT to switch radios and
230    causing the test to fail to connect at radio 1's frequency.
231
232    """
233
234    version = 1
235
236    FREQUENCY_0 = 2412
237    FREQUENCY_1 = 2462
238    PORT_0 = 0  # Port created first (on FREQUENCY_0)
239    PORT_1 = 1  # Port created second (on FREQUENCY_1)
240
241    # Supplicant's signal to noise threshold for roaming.  When noise is
242    # measurable and S/N is less than the threshold, supplicant will attempt
243    # to roam.  We're setting the roam threshold (and setting it so high --
244    # it's usually 18) because some of the DUTs we're using have a hard time
245    # measuring signals below -55 dBm.  A threshold of 40 roams when the
246    # signal is about -50 dBm (since the noise tends to be around -89).
247    ABSOLUTE_ROAM_THRESHOLD_DB = 40
248
249
250    def run_once(self):
251        """Test body."""
252        self.context.client.clear_supplicant_blacklist()
253
254        with self.context.client.roam_threshold(
255                self.ABSOLUTE_ROAM_THRESHOLD_DB):
256            logging.info('- Configure first AP & connect')
257            self.context.configure(hostap_config.HostapConfig(
258                    frequency=self.FREQUENCY_0,
259                    mode=hostap_config.HostapConfig.MODE_11G))
260            router_ssid = self.context.router.get_ssid()
261            self.context.assert_connect_wifi(xmlrpc_datatypes.
262                                             AssociationParameters(
263                    ssid=router_ssid))
264            self.context.assert_ping_from_dut()
265
266            # Setup background scan configuration to set a signal level, below
267            # which, supplicant will scan (3dB below the current level).  We
268            # must reconnect for these parameters to take effect.
269            logging.info('- Set background scan level')
270            bgscan_config = xmlrpc_datatypes.BgscanConfiguration(
271                    method='simple',
272                    signal=self.context.client.wifi_signal_level - 3)
273            self.context.client.shill.disconnect(router_ssid)
274            self.context.assert_connect_wifi(
275                    xmlrpc_datatypes.AssociationParameters(
276                    ssid=router_ssid, bgscan_config=bgscan_config))
277
278            logging.info('- Configure second AP')
279            self.context.configure(hostap_config.HostapConfig(
280                    ssid=router_ssid,
281                    frequency=self.FREQUENCY_1,
282                    mode=hostap_config.HostapConfig.MODE_11G),
283                                   multi_interface=True)
284
285            launch_iw_event = LaunchIwEvent('iw',
286                                            self.context.client.host,
287                                            timeout_seconds=60.0)
288
289            logging.info('- Drop the power on the first AP')
290
291            self.set_signal_to_force_roam(port=self.PORT_0,
292                                          frequency=self.FREQUENCY_0)
293
294            # Verify that the low signal event is generated, that supplicant
295            # scans as a result (or, at least, that supplicant scans after the
296            # threshold is passed), and that it connects to something.
297            logging.info('- Wait for RSSI threshold drop, scan, and connect')
298            if not launch_iw_event.wait_for_events(['RSSI went below threshold',
299                                                    'scan started',
300                                                    'connected to']):
301                raise error.TestFail('Did not find all expected events')
302
303            logging.info('- Wait for a connection on the second AP')
304            # Instead of explicitly connecting, just wait to see if the DUT
305            # connects to the second AP by itself
306            self.context.wait_for_connection(ssid=router_ssid,
307                                             freq=self.FREQUENCY_1, ap_num=1)
308
309            # Clean up.
310            self.context.router.deconfig()
311
312
313    def set_signal_to_force_roam(self, port, frequency):
314        """Adjust the AP attenuation to force the DUT to roam.
315
316        wpa_supplicant (v2.0-devel) decides when to roam based on a number of
317        factors even when we're only interested in the scenario when the roam
318        is instigated by an RSSI drop.  The gates for roaming differ between
319        systems that have drivers that measure noise and those that don't.  If
320        the driver reports noise, the S/N of both the current BSS and the
321        target BSS is capped at 30 and then the following conditions must be
322        met:
323
324            1) The S/N of the current AP must be below supplicant's roam
325               threshold.
326            2) The S/N of the roam target must be more than 3dB larger than
327               that of the current BSS.
328
329        If the driver does not report noise, the following condition must be
330        met:
331
332            3) The roam target's signal must be above the current BSS's signal
333               by a signal-dependent value (that value doesn't currently go
334               higher than 5).
335
336        This would all be enough complication.  Unfortunately, the DUT's signal
337        measurement hardware has typically not been optimized for accurate
338        measurement throughout the signal range.  Based on some testing
339        (crbug:295752), it was discovered that the DUT's measurements of signal
340        levels somewhere below -50dBm show values greater than the actual signal
341        and with quite a bit of variance.  Since wpa_supplicant uses this same
342        mechanism to read its levels, this code must iterate to find values that
343        will reliably trigger supplicant to roam to the second AP.
344
345        It was also shown that some MIMO DUTs send different signal levels to
346        their two radios (testing has shown this to be somewhere around 5dB to
347        7dB).
348
349        @param port: the beaglebone port that is desired to be attenuated.
350        @param frequency: noise needs to be read for a frequency.
351
352        """
353        # wpa_supplicant calls an S/N of 30 dB "quite good signal" and caps the
354        # S/N at this level for the purposes of roaming calculations.  We'll do
355        # the same (since we're trying to instigate behavior in supplicant).
356        GREAT_SNR = 30
357
358        # The difference between the S/Ns of APs from 2), above.
359        MIN_AP_SIGNAL_DIFF_FOR_ROAM_DB = 3
360
361        # The maximum delta for a system that doesn't measure noise, from 3),
362        # above.
363        MIN_NOISELESS_SIGNAL_DIFF_FOR_ROAM_DB = 5
364
365        # Adds a clear margin to attenuator levels to make sure that we
366        # attenuate enough to do the job in light of signal and noise levels
367        # that bounce around.  This value was reached empirically and further
368        # tweaking may be necessary if this test gets flaky.
369        SIGNAL_TO_NOISE_MARGIN_DB = 3
370
371        # The measured difference between the radios on one of our APs.
372        # TODO(wdg): dynamically measure the difference between the AP's radios
373        # (crbug:307678).
374        TEST_HW_SIGNAL_DELTA_DB = 7
375
376        # wpa_supplicant's roaming algorithm differs between systems that can
377        # measure noise and those that can't.  This code tracks those
378        # differences.
379        actual_signal_dbm = self.context.client.wifi_signal_level
380        actual_noise_dbm = self.context.client.wifi_noise_level(frequency)
381        logging.info('Radio 0 signal: %r, noise: %r', actual_signal_dbm,
382                     actual_noise_dbm)
383        if actual_noise_dbm is not None:
384            system_measures_noise = True
385            actual_snr_db = actual_signal_dbm - actual_noise_dbm
386            radio1_snr_db = actual_snr_db - TEST_HW_SIGNAL_DELTA_DB
387
388            # Supplicant will cap any S/N measurement used for roaming at
389            # GREAT_SNR so we'll do the same.
390            if radio1_snr_db > GREAT_SNR:
391                radio1_snr_db = GREAT_SNR
392
393            # In order to roam, the S/N of radio 0 must be both less than 3db
394            # below radio1 and less than the roam threshold.
395            logging.info('Radio 1 S/N = %d', radio1_snr_db)
396            delta_snr_threshold_db = (radio1_snr_db -
397                                      MIN_AP_SIGNAL_DIFF_FOR_ROAM_DB)
398            if (delta_snr_threshold_db < self.ABSOLUTE_ROAM_THRESHOLD_DB):
399                target_snr_db = delta_snr_threshold_db
400                logging.info('Target S/N = %d (delta algorithm)',
401                             target_snr_db)
402            else:
403                target_snr_db = self.ABSOLUTE_ROAM_THRESHOLD_DB
404                logging.info('Target S/N = %d (threshold algorithm)',
405                             target_snr_db)
406
407            # Add some margin.
408            target_snr_db -= SIGNAL_TO_NOISE_MARGIN_DB
409            attenuation_db = actual_snr_db - target_snr_db
410            logging.info('Noise: target S/N=%d attenuation=%r',
411                         target_snr_db, attenuation_db)
412        else:
413            system_measures_noise = False
414            # On a system that doesn't measure noise, supplicant needs the
415            # signal from radio 0 to be less than that of radio 1 minus a fixed
416            # delta value.  While we're here, subtract additional margin from
417            # the target value.
418            target_signal_dbm = (actual_signal_dbm - TEST_HW_SIGNAL_DELTA_DB -
419                                 MIN_NOISELESS_SIGNAL_DIFF_FOR_ROAM_DB -
420                                 SIGNAL_TO_NOISE_MARGIN_DB)
421            attenuation_db = actual_signal_dbm - target_signal_dbm
422            logging.info('No noise: target_signal=%r, attenuation=%r',
423                         target_signal_dbm, attenuation_db)
424
425        # Attenuate, measure S/N, repeat (due to flaky measurments) until S/N is
426        # where we want it.
427        keep_tweaking_snr = True
428        while keep_tweaking_snr:
429            # Keep attenuation values below the attenuator's maximum.
430            if attenuation_db > (site_attenuator.Attenuator.
431                                 MAX_VARIABLE_ATTENUATION):
432                attenuation_db = (site_attenuator.Attenuator.
433                                  MAX_VARIABLE_ATTENUATION)
434            logging.info('Applying attenuation=%r', attenuation_db)
435            self.context.attenuator.set_variable_attenuation_on_port(
436                    port, attenuation_db)
437            if attenuation_db >= (site_attenuator.Attenuator.
438                                    MAX_VARIABLE_ATTENUATION):
439                logging.warning('. NOTICE: Attenuation is at maximum value')
440                keep_tweaking_snr = False
441            elif system_measures_noise:
442                actual_snr_db = self.get_signal_to_noise(frequency)
443                if actual_snr_db > target_snr_db:
444                    logging.info('. S/N (%d) > target value (%d)',
445                                 actual_snr_db, target_snr_db)
446                    attenuation_db += actual_snr_db - target_snr_db
447                else:
448                    logging.info('. GOOD S/N=%r', actual_snr_db)
449                    keep_tweaking_snr = False
450            else:
451                actual_signal_dbm = self.context.client.wifi_signal_level
452                logging.info('. signal=%r', actual_signal_dbm)
453                if actual_signal_dbm > target_signal_dbm:
454                    logging.info('. Signal > target value (%d)',
455                                 target_signal_dbm)
456                    attenuation_db += actual_signal_dbm - target_signal_dbm
457                else:
458                    keep_tweaking_snr = False
459
460        logging.info('Done')
461
462
463    def get_signal_to_noise(self, frequency):
464        """Gets both the signal and the noise on the current connection.
465
466        @param frequency: noise needs to be read for a frequency.
467        @returns: signal and noise in dBm
468
469        """
470        ping_ip = self.context.get_wifi_addr(ap_num=0)
471        ping_config = ping_runner.PingConfig(target_ip=ping_ip, count=1,
472                                             ignore_status=True,
473                                             ignore_result=True)
474        self.context.client.ping(ping_config)  # Just to provide traffic.
475        signal_dbm = self.context.client.wifi_signal_level
476        noise_dbm = self.context.client.wifi_noise_level(frequency)
477        print '. signal: %r, noise: %r' % (signal_dbm, noise_dbm)
478        if noise_dbm is None:
479            return None
480        return signal_dbm - noise_dbm
481