Merge "gfch100: add rxslicer chart to craftui"
diff --git a/cache_warming/cache_warming.py b/cache_warming/cache_warming.py
index e094d4a..3416b56 100644
--- a/cache_warming/cache_warming.py
+++ b/cache_warming/cache_warming.py
@@ -13,11 +13,13 @@
 import json
 import os
 import socket
+import sys
 import dns.exception
 import dns.resolver
 
 hit_log = {}
 last_fetch = datetime.min
+verbose = False
 TOP_N = 50
 FETCH_INTERVAL = 60  # seconds
 UDP_SERVER_PATH = '/tmp/dns_query_log_socket'
@@ -34,6 +36,8 @@
     log: Dictionary of top requested hosts with host key and tuple value
          containing most recent hit time and hit count.
   """
+  if verbose:
+    print 'Saving hosts in %s.' % HOSTS_JSON_PATH
   d = os.path.dirname(HOSTS_JSON_PATH)
   if not os.path.exists(d):
     os.makedirs(d)
@@ -47,6 +51,8 @@
   Loads dictionary with host key and tuple value containing most recent hit
   time and hit count as hit_log if it exists.
   """
+  if verbose:
+    print 'Loading hosts from %s.' % HOSTS_JSON_PATH
   if os.path.isfile(HOSTS_JSON_PATH):
     with open(HOSTS_JSON_PATH, 'r') as hosts_json:
       global hit_log
@@ -64,6 +70,8 @@
             '[Unix time] [host name]'.
   """
   time, _, host = qry.partition(' ')
+  if verbose:
+    print 'Received query for %s.' % host
   if host in hit_log:
     hit_log[host] = (hit_log[host][0] + 1, time)
   else:
@@ -103,9 +111,13 @@
   if len(hosts) > TOP_N:
     hosts = hosts[:TOP_N]
   for host in hosts:
+    if verbose:
+      print 'Fetching %s.' % host
     try:
       my_resolver.query(host)
     except dns.exception.DNSException:
+      if verbose:
+        print 'Failed to fetch %s.' % host
       del hit_log[host]
       hosts.remove(host)
 
@@ -121,14 +133,20 @@
   return sorted(hit_log, key=hit_log.get, reverse=True)
 
 
-def warm_cache():
+def warm_cache(port, server):
   """Warms cache with predetermined number of most requested hosts.
 
   Sorts hosts in hit log by hit count, fetches predetermined
   number of top requested hosts, updates last fetch time.
+
+  Args:
+    port: Port to which to send queries (default is 53).
+    server: Alternate nameservers to query (default is None).
   """
+  if verbose:
+    print 'Warming cache.'
   sorted_hosts = sort_hit_log()
-  fetch(sorted_hosts, args.port, args.server)
+  fetch(sorted_hosts, port, server)
   global last_fetch
   last_fetch = datetime.now()
 
@@ -144,11 +162,15 @@
                       help='port to which to send queries (default is 53).')
   parser.add_argument('-s', '--server', nargs='*', type=str,
                       help='alternate nameservers to query (default is None).')
+  parser.add_argument('-v', '--verbose', action='store_true')
   return parser.parse_args()
 
 
 if __name__ == '__main__':
+  sys.stdout = os.fdopen(1, 'w', 1)
+  sys.stderr = os.fdopen(2, 'w', 1)
   args = set_args()
+  verbose = args.verbose
   load_hosts()
 
   server_address = UDP_SERVER_PATH
@@ -161,10 +183,12 @@
   sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   sock.bind(server_address)
   os.chmod(server_address, 0o777)
+  if verbose:
+    print 'Set up socket at %s.' % HOSTS_JSON_PATH
 
   while 1:
     diff = datetime.now() - last_fetch
     if diff.total_seconds() > 60:
-      warm_cache()
+      warm_cache(args.port, args.server)
     data = sock.recv(128)
     process_query(data)
diff --git a/cmds/statcatcher.cc b/cmds/statcatcher.cc
index 184c725..64db2b6 100644
--- a/cmds/statcatcher.cc
+++ b/cmds/statcatcher.cc
@@ -119,6 +119,8 @@
     if (recvsize < 0) {
       perror("Failed to receive data on socket.\n");
       exit(1);
+    } else {
+      fprintf(stderr, "received %d bytes\n", recvsize);
     }
     pkt.resize(recvsize);
 
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index 2ab4763..6ccdaa2 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -8,6 +8,7 @@
 import json
 import logging
 import os
+import random
 import re
 import subprocess
 import time
@@ -22,9 +23,6 @@
 import iw
 import status
 
-GFIBER_OUIS = ['f4:f5:e8']
-VENDOR_IE_FEATURE_ID_AUTOPROVISIONING = '01'
-
 
 class FileChangeHandler(pyinotify.ProcessEvent):
   """Connects pyinotify events to ConnectionManager."""
@@ -202,7 +200,8 @@
                moca_tmp_dir='/tmp/cwmp/monitoring/moca2',
                wpa_control_interface='/var/run/wpa_supplicant',
                run_duration_s=1, interface_update_period=5,
-               wifi_scan_period_s=120, wlan_retry_s=15, acs_update_wait_s=10):
+               wifi_scan_period_s=120, wlan_retry_s=15, acs_update_wait_s=10,
+               bssid_cycle_length_s=30):
 
     self._tmp_dir = tmp_dir
     self._config_dir = config_dir
@@ -215,6 +214,7 @@
     self._wifi_scan_period_s = wifi_scan_period_s
     self._wlan_retry_s = wlan_retry_s
     self._acs_update_wait_s = acs_update_wait_s
+    self._bssid_cycle_length_s = bssid_cycle_length_s
     self._wlan_configuration = {}
 
     # Make sure all necessary directories exist.
@@ -654,20 +654,19 @@
     subprocess.call(self.IFUP + [wifi.name])
     # /bin/wifi takes a --band option but then finds the right interface for it,
     # so it's okay to just pick the first band here.
-    with_ie, without_ie = self._find_bssids(wifi.bands[0])
+    items = self._find_bssids(wifi.bands[0])
     logging.info('Done scanning on %s', wifi.name)
-    items = [(bss_info, 3) for bss_info in with_ie]
-    items += [(bss_info, 1) for bss_info in without_ie]
-    wifi.cycler = cycler.AgingPriorityCycler(cycle_length_s=30, items=items)
+    if not hasattr(wifi, 'cycler'):
+      wifi.cycler = cycler.AgingPriorityCycler(
+          cycle_length_s=self._bssid_cycle_length_s)
+    # Shuffle items to undefined determinism in scan results + dict
+    # implementation unfairly biasing BSSID order.
+    random.shuffle(items)
+    wifi.cycler.update(items)
 
   def _find_bssids(self, band):
-    def supports_autoprovisioning(oui, vendor_ie):
-      if oui not in GFIBER_OUIS:
-        return False
-
-      return vendor_ie.startswith(VENDOR_IE_FEATURE_ID_AUTOPROVISIONING)
-
-    return iw.find_bssids(band, supports_autoprovisioning, False)
+    """Wrapper used as a unit testing seam."""
+    return iw.find_bssids(band, False)
 
   def _try_next_bssid(self, wifi):
     """Attempt to connect to the next BSSID in wifi's BSSID cycler.
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 0297ca1..954e2e3 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -6,6 +6,7 @@
 import os
 import shutil
 import tempfile
+import time
 
 import connection_manager
 import interface_test
@@ -539,6 +540,7 @@
                               run_duration_s=run_duration_s,
                               interface_update_period=interface_update_period,
                               wifi_scan_period_s=wifi_scan_period_s,
+                              bssid_cycle_length_s=0.05,
                               **cm_kwargs)
 
         c.test_interface_update_period = interface_update_period
@@ -805,8 +807,11 @@
   wvtest.WVFAIL(c.wifi_for_band(band).acs())
 
   c.can_connect_to_s2 = True
-  # Give it time to try all BSSIDs.
-  for _ in range(3):
+  # Give it time to try all BSSIDs.  This means sleeping long enough that
+  # everything in the cycler is active, then doing n+1 loops (the n+1st loop is
+  # when we decided that the SSID in the nth loop was successful).
+  time.sleep(c._bssid_cycle_length_s)
+  for _ in range(len(c.wifi_for_band(band).cycler) + 1):
     c.run_once()
   s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
@@ -954,8 +959,8 @@
   # The next 2.4 GHz scan will have results.
   c.interface_with_scan_results = c.wifi_for_band('2.4').name
   c.run_until_scan('2.4')
-  # Now run 3 cycles, so that s2 will have been tried.
-  for _ in range(3):
+  # Now run for enough cycles that s2 will have been tried.
+  for _ in range(len(c.wifi_for_band('2.4').cycler)):
     c.run_once()
   c.run_until_interface_update()
   wvtest.WVPASS(c.acs())
@@ -1044,10 +1049,10 @@
   wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
   wvtest.WVFAIL(c.wifi_for_band('5').current_route())
 
-  # The 2.4 GHz scan will have results that will lead to ACS access.
+  # The scan will have results that will lead to ACS access.
   c.interface_with_scan_results = c.wifi_for_band('2.4').name
   c.run_until_scan('5')
-  for _ in range(3):
+  for _ in range(len(c.wifi_for_band('2.4').cycler)):
     c.run_once()
   c.run_until_interface_update()
   wvtest.WVPASS(c.acs())
diff --git a/conman/cycler.py b/conman/cycler.py
index ff65bfc..23f2ce4 100755
--- a/conman/cycler.py
+++ b/conman/cycler.py
@@ -27,9 +27,10 @@
       queue after being automatically reinserted.
       items: Initial items for the queue, as tuples of (item, priority).
     """
-    t = time.time()
-    self._items = {item: [priority, t] for item, priority in items}
     self._min_time_in_queue_s = cycle_length_s
+    self._items = {}
+    if items:
+      self.update(items)
 
   def empty(self):
     return not self._items
@@ -79,3 +80,23 @@
 
     return result
 
+  def update(self, items):
+    """Update to the given items, adding new ones and removing old ones.
+
+    Args:
+      items:  An iterable of (item, priority).
+    """
+    now = time.time()
+    new_items = {}
+    for item, priority in items:
+      t = now
+      existing = self._items.get(item, None)
+      if existing:
+        t = existing[1]
+      new_items[item] = [priority, t]
+
+    self._items = new_items
+
+  def __len__(self):
+    return len(self._items)
+
diff --git a/conman/cycler_test.py b/conman/cycler_test.py
index c4e498b..de1e6c0 100755
--- a/conman/cycler_test.py
+++ b/conman/cycler_test.py
@@ -20,26 +20,40 @@
   # We should get all three in order, since they all have the same insertion
   # time.  They will all get slightly different insertion times, but next()
   # should be fast enough that the differences don't make much difference.
-  wvtest.WVPASS(c.peek() == 'A')
-  wvtest.WVPASS(c.next() == 'A')
-  wvtest.WVPASS(c.next() == 'B')
-  wvtest.WVPASS(c.next() == 'C')
+  wvtest.WVPASSEQ(c.peek(), 'A')
+  wvtest.WVPASSEQ(c.next(), 'A')
+  wvtest.WVPASSEQ(c.next(), 'B')
+  wvtest.WVPASSEQ(c.next(), 'C')
   wvtest.WVPASS(c.peek() is None)
   wvtest.WVPASS(c.next() is None)
   wvtest.WVPASS(c.next() is None)
 
   # Now, wait for items to be ready again and just cycle one of them.
   time.sleep(cycle_length_s)
-  wvtest.WVPASS(c.next() == 'A')
+  wvtest.WVPASSEQ(c.next(), 'A')
 
   # Now, if we wait 1.9 cycles, the aged priorities will be as follows:
   # A: 0.9 * 10 = 9
   # B: 1.9 * 5 = 9.5
   # C: 1.9 * 1 = 1.9
   time.sleep(cycle_length_s * 1.9)
-  wvtest.WVPASS(c.next() == 'B')
-  wvtest.WVPASS(c.next() == 'A')
-  wvtest.WVPASS(c.next() == 'C')
+  wvtest.WVPASSEQ(c.next(), 'B')
+  wvtest.WVPASSEQ(c.next(), 'A')
+  wvtest.WVPASSEQ(c.next(), 'C')
+
+  # Update c, keeping A as-is, removing B, updating C's priority, and adding D.
+  # Sleep for two cycles.  After the first cycle, D has priority 20 and A and C
+  # have priority 0 (since we just cycled them).  After the second cycle, the
+  # priorities are as follows:
+  # A: 1 * 10 = 10
+  # C: 1 * 20 = 20
+  # D: 2 * 20 = 40
+  c.update((('A', 10), ('C', 20), ('D', 20)))
+  time.sleep(cycle_length_s * 2)
+  wvtest.WVPASSEQ(c.next(), 'D')
+  wvtest.WVPASSEQ(c.next(), 'C')
+  wvtest.WVPASSEQ(c.next(), 'A')
+  wvtest.WVPASS(c.next() is None)
 
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/conman/interface.py b/conman/interface.py
index 82d1506..04c809c 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -2,7 +2,6 @@
 
 """Models wired and wireless interfaces."""
 
-import json
 import logging
 import os
 import re
@@ -479,27 +478,7 @@
     self._events = []
 
   def _qcsapi(self, *command):
-    try:
-      return subprocess.check_output(['qcsapi'] + list(command)).strip()
-    except subprocess.CalledProcessError:
-      return None
-
-  def _wifiinfo_filename(self):
-    return os.path.join(self.WIFIINFO_PATH, self._interface)
-
-  def _get_wifiinfo(self):
-    try:
-      return json.load(open(self._wifiinfo_filename()))
-    except IOError:
-      return None
-
-  def _get_ssid(self):
-    wifiinfo = self._get_wifiinfo()
-    if wifiinfo:
-      return wifiinfo.get('SSID')
-
-  def _check_client_mode(self):
-    return self._qcsapi('get_mode', 'wifi0') == 'Station'
+    return subprocess.check_output(['qcsapi'] + list(command)).strip()
 
   def attach(self):
     self._update()
@@ -517,9 +496,13 @@
 
   def _update(self):
     """Generate and cache events, update state."""
-    client_mode = self._check_client_mode()
-    ssid = self._get_ssid()
-    status = self._qcsapi('get_status', 'wifi0')
+    try:
+      client_mode = self._qcsapi('get_mode', 'wifi0') == 'Station'
+      ssid = self._qcsapi('get_ssid', 'wifi0')
+      status = self._qcsapi('get_status', 'wifi0')
+    except subprocess.CalledProcessError:
+      # If QCSAPI failed, skip update.
+      return
 
     # If we have an SSID and are in client mode, and at least one of those is
     # new, then we have just connected.
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 4283a2a..4c7d52b 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -2,7 +2,6 @@
 
 """Tests for connection_manager.py."""
 
-import json
 import logging
 import os
 import shutil
@@ -199,16 +198,15 @@
 
   def add_connected_event(self):
     self.fake_qcsapi['get_mode'] = 'Station'
-    json.dump({'SSID': self.ssid_testonly},
-              open(self._wifiinfo_filename(), 'w'))
+    self.fake_qcsapi['get_ssid'] = self.ssid_testonly
 
   def add_disconnected_event(self):
     self.ssid_testonly = None
-    json.dump({'SSID': ''}, open(self._wifiinfo_filename(), 'w'))
+    self.fake_qcsapi['get_ssid'] = None
 
   def add_terminating_event(self):
     self.ssid_testonly = None
-    json.dump({'SSID': ''}, open(self._wifiinfo_filename(), 'w'))
+    self.fake_qcsapi['get_ssid'] = None
     self.fake_qcsapi['get_mode'] = 'AP'
 
   def detach(self):
diff --git a/conman/iw.py b/conman/iw.py
index 300c3e2..fd56e32 100755
--- a/conman/iw.py
+++ b/conman/iw.py
@@ -17,8 +17,13 @@
     return ''
 
 
+GFIBER_OUIS = ['f4:f5:e8']
+VENDOR_IE_FEATURE_ID_AUTOPROVISIONING = '01'
+
+
 _BSSID_RE = r'BSS (?P<BSSID>([0-9a-f]{2}:?){6})\(on .*\)'
 _SSID_RE = r'SSID: (?P<SSID>.*)'
+_RSSI_RE = r'signal: (?P<RSSI>.*) dBm'
 _VENDOR_IE_RE = (r'Vendor specific: OUI (?P<OUI>([0-9a-f]{2}:?){3}), '
                  'data:(?P<data>( [0-9a-f]{2})+)')
 
@@ -26,15 +31,17 @@
 class BssInfo(object):
   """Contains info about a BSS, parsed from 'iw scan'."""
 
-  def __init__(self, bssid='', ssid='', security=None, vendor_ies=None):
+  def __init__(self, bssid='', ssid='', rssi=-100, security=None,
+               vendor_ies=None):
     self.bssid = bssid
     self.ssid = ssid
+    self.rssi = rssi
     self.vendor_ies = vendor_ies or []
     self.security = security or []
 
   def __attrs(self):
     return (self.bssid, self.ssid, tuple(sorted(self.vendor_ies)),
-            tuple(sorted(self.security)))
+            tuple(sorted(self.security)), self.rssi)
 
   def __eq__(self, other):
     # pylint: disable=protected-access
@@ -70,6 +77,10 @@
     if match:
       bss_info.ssid = match.group('SSID')
       continue
+    match = re.match(_RSSI_RE, line)
+    if match:
+      bss_info.rssi = float(match.group('RSSI'))
+      continue
     match = re.match(_VENDOR_IE_RE, line)
     if match:
       bss_info.vendor_ies.append((match.group('OUI'),
@@ -88,22 +99,20 @@
   return result
 
 
-def find_bssids(band, vendor_ie_function, include_secure):
+def find_bssids(band, include_secure):
   """Return information about interesting access points.
 
   Args:
     band:  The band on which to scan.
-    vendor_ie_function:  A function that takes a vendor IE and returns a bool.
     include_secure:  Whether to exclude secure networks.
 
   Returns:
-    Two lists of tuples of the form (SSID, BSSID info dict).  The first list has
-    BSSIDs which have a vendor IE accepted by vendor_ie_function, and the second
-    list has those which don't.
+    A list of (BSSID, priority) tuples, prioritizing BSSIDs with the GFiber
+    provisioning vendor IE > GFiber APs > other APs, and by RSSI within each
+    group.
   """
   parsed = scan_parsed(band)
-  result_with_ie = set()
-  result_without_ie = set()
+  bssids = set()
 
   for bss_info in parsed:
     if bss_info.security and not include_secure:
@@ -121,11 +130,16 @@
     if not bss_info.ssid and not bss_info.vendor_ies:
       bss_info.ssid = DEFAULT_GFIBERSETUP_SSID
 
-    for oui, data in bss_info.vendor_ies:
-      if vendor_ie_function(oui, data):
-        result_with_ie.add(bss_info)
-        break
-    else:
-      result_without_ie.add(bss_info)
+    bssids.add(bss_info)
 
-  return result_with_ie, result_without_ie
+  return [(bss_info, _bssid_priority(bss_info)) for bss_info in bssids]
+
+
+def _bssid_priority(bss_info):
+  result = 4 if bss_info.bssid[:8] in GFIBER_OUIS else 2
+  for oui, data in bss_info.vendor_ies:
+    if (oui in GFIBER_OUIS and
+        data.startswith(VENDOR_IE_FEATURE_ID_AUTOPROVISIONING)):
+      result = 5
+
+  return result + (100 + (max(bss_info.rssi, -100))) / 100.0
diff --git a/conman/iw_test.py b/conman/iw_test.py
index 3f80d6c..3bb3cf7 100755
--- a/conman/iw_test.py
+++ b/conman/iw_test.py
@@ -551,6 +551,7 @@
      * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
      * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
   Vendor specific: OUI 00:11:22, data: 01 23 45 67
+  Vendor specific: OUI f4:f5:e8, data: 01
   Vendor specific: OUI f4:f5:e8, data: 03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 6f 6d 61 74 69 6f 6e
 BSS f4:f5:e8:f1:36:43(on wcli0)
   TSF: 12499150000 usec (0d, 03:28:19)
@@ -629,49 +630,55 @@
 def find_bssids_test():
   """Test iw.find_bssids."""
   test_ie = ('00:11:22', '01 23 45 67')
+  provisioning_ie = ('f4:f5:e8', '01')
   ssid_ie = (
       'f4:f5:e8',
       '03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 6f 6d 61 74 69 6f 6e',
   )
   short_scan_result = iw.BssInfo(ssid='short scan result',
                                  bssid='00:23:97:57:f4:d8',
+                                 rssi=-60,
                                  security=['WEP'],
                                  vendor_ies=[test_ie])
   provisioning_bss_info = iw.BssInfo(ssid=iw.DEFAULT_GFIBERSETUP_SSID,
                                      bssid='94:b4:0f:f1:36:42',
-                                     vendor_ies=[test_ie, ssid_ie])
+                                     rssi=-66,
+                                     vendor_ies=[test_ie, provisioning_ie,
+                                                 ssid_ie])
   provisioning_bss_info_frenzy = iw.BssInfo(ssid=iw.DEFAULT_GFIBERSETUP_SSID,
-                                            bssid='f4:f5:e8:f1:36:43')
-
-  with_ie, without_ie = iw.find_bssids('wcli0', lambda o, d: o == '00:11:22',
-                                       True)
-
-  wvtest.WVPASSEQ(with_ie, set([short_scan_result, provisioning_bss_info]))
+                                            bssid='f4:f5:e8:f1:36:43',
+                                            rssi=-66)
 
   wvtest.WVPASSEQ(
-      without_ie,
-      set([provisioning_bss_info_frenzy,
-           iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41'),
-           iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1'),
-           iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61'),
-           iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:36:40',
-                      security=['WPA2']),
-           iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:3a:e0',
-                      security=['WPA2']),
-           iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:35:60',
-                      security=['WPA2']),
-           iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:02:a0',
-                      security=['WPA2'])]))
+      set(iw.find_bssids('wcli0', True)),
+      set([(short_scan_result, 2.4),
+           (provisioning_bss_info, 5.34),
+           (provisioning_bss_info_frenzy, 4.34),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', rssi=-67),
+            2.33),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', rssi=-65),
+            2.35),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', rssi=-38),
+            2.62),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:36:40', rssi=-66,
+                       security=['WPA2']), 2.34),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:3a:e0', rssi=-55,
+                       security=['WPA2']), 2.45),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:35:60', rssi=-39,
+                       security=['WPA2']), 2.61),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:02:a0', rssi=-54,
+                       security=['WPA2']), 2.46)]))
 
-  with_ie, without_ie = iw.find_bssids('wcli0', lambda o, d: o == '00:11:22',
-                                       False)
-  wvtest.WVPASSEQ(with_ie, set([provisioning_bss_info]))
   wvtest.WVPASSEQ(
-      without_ie,
-      set([provisioning_bss_info_frenzy,
-           iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41'),
-           iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1'),
-           iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61')]))
+      set(iw.find_bssids('wcli0', False)),
+      set([(provisioning_bss_info, 5.34),
+           (provisioning_bss_info_frenzy, 4.34),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', rssi=-67),
+            2.33),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', rssi=-65),
+            2.35),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', rssi=-38),
+            2.62)]))
 
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/waveguide/waveguide.py b/waveguide/waveguide.py
index cbc9fe8..87324c3 100755
--- a/waveguide/waveguide.py
+++ b/waveguide/waveguide.py
@@ -907,7 +907,8 @@
     wifiblaster.enable         Enable WiFi performance measurement.
     wifiblaster.interval       Average time between automated measurements in
                                seconds, or 0 to disable automated measurements.
-    wifiblaster.measureall     Unix time at which to measure all clients.
+    wifiblaster.measureall     Unix time at which to measure all clients, or 0
+                               to disable measureall requests.
     wifiblaster.onassociation  Enable WiFi performance measurement after clients
                                associate.
 
@@ -938,6 +939,20 @@
     except ValueError:
       return None
 
+  def _GetParameters(self):
+    """Reads and returns all parameters if valid, or Nones."""
+    duration = self._ReadParameter('duration', float)
+    enable = self._ReadParameter('enable', self._StrToBool)
+    fraction = self._ReadParameter('fraction', int)
+    interval = self._ReadParameter('interval', float)
+    measureall = self._ReadParameter('measureall', float)
+    onassociation = self._ReadParameter('onassociation', self._StrToBool)
+    size = self._ReadParameter('size', int)
+    if (duration > 0 and enable and fraction > 0 and interval >= 0
+        and measureall >= 0 and size > 0):
+      return (duration, fraction, interval, measureall, onassociation, size)
+    return (None, None, None, None, None, None)
+
   def _SaveResult(self, line):
     """Save wifiblaster result to the status file for that client."""
     g = re.search(MACADDR_REGEX, line)
@@ -975,23 +990,18 @@
     """Return the time of the next measurement event."""
     return self._next_measurement_time
 
-  def Measure(self, interface, client):
+  def Measure(self, interface, client, duration, fraction, size):
     """Measures the performance of a client."""
-    enable = self._ReadParameter('enable', self._StrToBool)
-    duration = self._ReadParameter('duration', float)
-    fraction = self._ReadParameter('fraction', int)
-    size = self._ReadParameter('size', int)
-    if enable and duration > 0 and fraction > 0 and size > 0:
-      RunProc(callback=self._HandleResults,
-              args=[WIFIBLASTER_BIN, '-i', interface, '-d', str(duration),
-                    '-f', str(fraction), '-s', str(size),
-                    helpers.DecodeMAC(client)])
+    RunProc(callback=self._HandleResults,
+            args=[WIFIBLASTER_BIN, '-i', interface, '-d', str(duration),
+                  '-f', str(fraction), '-s', str(size),
+                  helpers.DecodeMAC(client)])
 
   def MeasureOnAssociation(self, interface, client):
     """Measures the performance of a client after association."""
-    onassociation = self._ReadParameter('onassociation', self._StrToBool)
+    (duration, fraction, _, _, onassociation, size) = self._GetParameters()
     if onassociation:
-      self.Measure(interface, client)
+      self.Measure(interface, client, duration, fraction, size)
 
   def Poll(self, now):
     """Polls the state machine."""
@@ -1005,29 +1015,33 @@
       # Inter-arrival times in a Poisson process are exponentially distributed.
       # The timebase slip prevents a burst of measurements in case we fall
       # behind.
-      self._next_measurement_time = now + random.expovariate(1 / interval)
+      self._next_measurement_time = now + random.expovariate(1.0 / interval)
 
-    interval = self._ReadParameter('interval', float)
-    if interval <= 0:
+    # Read parameters.
+    (duration, fraction, interval, measureall, _, size) = self._GetParameters()
+
+    # Handle automated mode.
+    if interval > 0:
+      if self._interval != interval:
+        # Enable or change interval.
+        StartMeasurementTimer(interval)
+      elif now >= self._next_measurement_time:
+        # Measure a random client.
+        StartMeasurementTimer(interval)
+        try:
+          (interface, client) = random.choice(self._GetAllClients())
+        except IndexError:
+          pass
+        else:
+          self.Measure(interface, client, duration, fraction, size)
+    else:
       Disable()
-    elif self._interval != interval:
-      # Enable or change interval.
-      StartMeasurementTimer(interval)
-    elif now >= self._next_measurement_time:
-      # Measure a random client.
-      StartMeasurementTimer(interval)
-      try:
-        (interface, client) = random.choice(self._GetAllClients())
-      except IndexError:
-        pass
-      else:
-        self.Measure(interface, client)
 
-    measureall = self._ReadParameter('measureall', float)
+    # Handle measureall request.
     if time.time() >= measureall and measureall > self._last_measureall_time:
       self._last_measureall_time = measureall
       for (interface, client) in self._GetAllClients():
-        self.Measure(interface, client)
+        self.Measure(interface, client, duration, fraction, size)
 
     # Poll again in at most one second. This allows parameter changes (e.g. a
     # measureall request or a long interval to a short interval) to take effect
diff --git a/waveguide/wifiblaster_controller_test.py b/waveguide/wifiblaster_controller_test.py
index 6b562b1..9e300f2 100755
--- a/waveguide/wifiblaster_controller_test.py
+++ b/waveguide/wifiblaster_controller_test.py
@@ -17,6 +17,7 @@
 
 import glob
 import os
+import random
 import shutil
 import sys
 import tempfile
@@ -102,6 +103,7 @@
   d = tempfile.mkdtemp()
   old_wifiblaster_dir = waveguide.WIFIBLASTER_DIR
   waveguide.WIFIBLASTER_DIR = tempfile.mkdtemp()
+  oldexpovariate = random.expovariate
   oldpath = os.environ['PATH']
   oldtime = time.time
 
@@ -110,6 +112,7 @@
     return faketime[0]
 
   try:
+    random.expovariate = lambda lambd: random.uniform(0, 2 * 1.0 / lambd)
     time.time = FakeTime
     os.environ['PATH'] = 'fake:' + os.environ['PATH']
     sys.path.insert(0, 'fake')
@@ -141,11 +144,6 @@
       WriteConfig('measureall', '0')
       WriteConfig('size', '1470')
 
-      # Disabled. No measurements should be run.
-      print manager.GetState()
-      for t in xrange(0, 100):
-        wc.Poll(t)
-
       def CountRuns():
         try:
           v = open('fake/wifiblaster.out').readlines()
@@ -155,16 +153,24 @@
           os.unlink('fake/wifiblaster.out')
           return len(v)
 
-      CountRuns()  # get rid of any leftovers
+      # Get rid of any leftovers.
+      CountRuns()
+
+      # Disabled.
+      # No measurements should be run.
+      print manager.GetState()
+      for t in xrange(0, 100):
+        wc.Poll(t)
       wvtest.WVPASSEQ(CountRuns(), 0)
 
+      # Enabled.
       # The first measurement should be one cycle later than the start time.
       # This is not an implementation detail: it prevents multiple APs from
       # running simultaneous measurements if measurements are enabled at the
       # same time.
       WriteConfig('enable', 'True')
       wc.Poll(100)
-      wvtest.WVPASSGE(wc.NextMeasurement(), 100)
+      wvtest.WVPASSGT(wc.NextMeasurement(), 100)
       for t in xrange(101, 200):
         wc.Poll(t)
       wvtest.WVPASSGE(CountRuns(), 1)
@@ -183,20 +189,16 @@
         wc.Poll(t)
       wvtest.WVPASSGE(CountRuns(), 1)
 
-      # Run the measurement at t=400 to restart the timer.
-      wc.Poll(400)
-      wvtest.WVPASSGE(CountRuns(), 0)
-
       # Next poll should be in at most one second regardless of interval.
-      wvtest.WVPASSLE(wc.NextTimeout(), 401)
+      wvtest.WVPASSLE(wc.NextTimeout(), 400)
 
-      # Enabled with longer average interval.  The change in interval should
+      # Enabled with shorter average interval.  The change in interval should
       # trigger a change in next poll timeout.
       WriteConfig('interval', '0.5')
       old_to = wc.NextMeasurement()
-      wc.Poll(401)
+      wc.Poll(400)
       wvtest.WVPASSNE(old_to, wc.NextMeasurement())
-      for t in xrange(402, 500):
+      for t in xrange(401, 500):
         wc.Poll(t)
       wvtest.WVPASSGE(CountRuns(), 1)
 
@@ -218,6 +220,7 @@
                               manager.GetState().assoc[0].mac)
       wvtest.WVPASSEQ(CountRuns(), 1)
   finally:
+    random.expovariate = oldexpovariate
     time.time = oldtime
     shutil.rmtree(d)
     os.environ['PATH'] = oldpath
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 755fb8b..39dfabf 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -120,12 +120,12 @@
   try:
     _ensure_initialized('ap')
 
+    _qcsapi('wifi_create_bss', lif, mac)
+    _qcsapi('set_ssid', lif, opt.ssid)
     _qcsapi('set_bw', 'wifi0', opt.width)
     _qcsapi('set_channel', 'wifi0',
             149 if opt.channel == 'auto' else opt.channel)
 
-    _qcsapi('wifi_create_bss', lif, mac)
-    _qcsapi('set_ssid', lif, opt.ssid)
     if opt.encryption == 'NONE':
       _qcsapi('set_beacon_type', lif, 'Basic')
     else:
@@ -164,9 +164,9 @@
   try:
     _ensure_initialized('sta')
 
+    _qcsapi('create_ssid', lif, opt.ssid)
     _qcsapi('set_bw', 'wifi0', 80)
 
-    _qcsapi('create_ssid', lif, opt.ssid)
     if opt.bssid:
       _qcsapi('set_ssid_bssid', lif, opt.ssid, opt.bssid)
     if opt.encryption == 'NONE' or not os.environ.get('WIFI_CLIENT_PSK'):