Merge "platform: change jsonpoll port to match glaukus"
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index eaaf7cf..7379d88 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -85,6 +85,10 @@
     if self.ssid is None:
       raise ValueError('Command file does not specify SSID')
 
+    if self.wifi.initial_ssid == self.ssid:
+      logging.debug('Connected to WLAN at startup')
+      self.client_up = True
+
   def start_access_point(self):
     """Start an access point."""
 
@@ -237,11 +241,21 @@
 
     # If the ethernet file doesn't exist for any reason when conman starts,
     # check explicitly and run ifplugd.action to create the file.
-    if not os.path.exists(os.path.join(self._interface_status_dir,
-                                       self.ETHERNET_STATUS_FILE)):
-      ethernet_up = self.is_ethernet_up()
-      self.bridge.ethernet = ethernet_up
+    if not os.path.exists(os.path.join(self._interface_status_dir, 'eth0')):
+      ethernet_up = self.is_interface_up('eth0')
       self.ifplugd_action('eth0', ethernet_up)
+      self.bridge.ethernet = ethernet_up
+
+    # Do the same for wifi interfaces , but rather than explicitly setting that
+    # the wpa_supplicant link is up, attempt to attach to the wpa_supplicant
+    # control interface.
+    for wifi in self.wifi:
+      if not os.path.exists(
+          os.path.join(self._interface_status_dir, wifi.name)):
+        wifi_up = self.is_interface_up(wifi.name)
+        self.ifplugd_action(wifi.name, wifi_up)
+        if wifi_up:
+          wifi.attach_wpa_control(self._wpa_control_interface)
 
     for path, prefix in ((self._status_dir, self.GATEWAY_FILE_PREFIX),
                          (self._interface_status_dir, ''),
@@ -259,13 +273,16 @@
     self._interface_update_counter = 0
     self._try_wlan_after = {'5': 0, '2.4': 0}
 
-  def is_ethernet_up(self):
-    """Explicitly check whether ethernet is up.
+  def is_interface_up(self, interface_name):
+    """Explicitly check whether an interface is up.
 
     Only used on startup, and only if ifplugd file is missing.
 
+    Args:
+      interface_name:  The name of the interface to check.
+
     Returns:
-      Whether the ethernet link is up.
+      Whether the interface is up.
     """
     try:
       lines = subprocess.check_output(self.IP_LINK).splitlines()
@@ -273,7 +290,7 @@
       raise EnvironmentError('Failed to call "ip link": %r', e.message)
 
     for line in lines:
-      if 'eth0' in line and 'LOWER_UP' in line:
+      if interface_name in line and 'LOWER_UP' in line:
         return True
 
     return False
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 4ebd48b..c22fbeb 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -193,13 +193,39 @@
   WIFI_SETCLIENT = ['echo', 'setclient']
   IFUP = ['echo', 'ifup']
   IFPLUGD_ACTION = ['echo', 'ifplugd.action']
-  # This simulates the output of 'ip link' when eth0 is up.
-  IP_LINK = ['echo', 'eth0 LOWER_UP']
 
   def __init__(self, *args, **kwargs):
+    self.interfaces_already_up = kwargs.pop('__test_interfaces_already_up',
+                                            ['eth0'])
+
+    wifi_interfaces_already_up = [ifc for ifc in self.interfaces_already_up
+                                  if ifc.startswith('wcli')]
+    for wifi in wifi_interfaces_already_up:
+      # wcli1 is always 5 GHz.  wcli0 always *includes* 2.4.
+      band = '5' if wifi == 'wcli1' else '2.4'
+      # This will happen in the super function, but in order for
+      # write_wlan_config to work we have to do it now.  This has to happen
+      # before the super function so that the files exist before the inotify
+      # registration.
+      self._config_dir = kwargs['config_dir']
+      self.write_wlan_config(band, 'my ssid', 'passphrase')
+
+      # Also create the wpa_supplicant socket to which to attach.
+      open(os.path.join(kwargs['wpa_control_interface'], wifi), 'w')
+
     super(ConnectionManager, self).__init__(*args, **kwargs)
+
+    for wifi in wifi_interfaces_already_up:
+      # pylint: disable=protected-access
+      self.interface_by_name(wifi)._initially_connected = True
+
     self.scan_has_results = False
 
+  @property
+  def IP_LINK(self):
+    return ['echo'] + ['%s LOWER_UP' % ifc
+                       for ifc in self.interfaces_already_up]
+
   def _update_access_point(self, wlan_configuration):
     client_was_up = wlan_configuration.client_up
     super(ConnectionManager, self)._update_access_point(wlan_configuration)
@@ -356,7 +382,7 @@
       self.run_once()
 
 
-def connection_manager_test(radio_config):
+def connection_manager_test(radio_config, **cm_kwargs):
   """Returns a decorator that does ConnectionManager test boilerplate."""
   def inner(f):
     """The actual decorator."""
@@ -388,7 +414,8 @@
                               wpa_control_interface=wpa_control_interface,
                               run_duration_s=run_duration_s,
                               interface_update_period=interface_update_period,
-                              wifi_scan_period_s=wifi_scan_period_s)
+                              wifi_scan_period_s=wifi_scan_period_s,
+                              **cm_kwargs)
 
         c.test_interface_update_period = interface_update_period
         c.test_wifi_scan_period = wifi_scan_period
@@ -720,6 +747,7 @@
   wvtest.WVPASS(c.wifi_for_band('5').current_route())
 
 
+
 @wvtest.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO_NO_5GHZ)
 def connection_manager_test_one_radio_no_5ghz(c):
@@ -757,5 +785,18 @@
   wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
 
 
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO,
+                         __test_interfaces_already_up=['eth0', 'wcli0'])
+def connection_manager_test_wifi_already_up(c):
+  """Test ConnectionManager when wifi is already up.
+
+  Args:
+    c:  The ConnectionManager set up by @connection_manager_test.
+  """
+  wvtest.WVPASS(c._connected_to_wlan(c.wifi_for_band('2.4')))
+  wvtest.WVPASS(c.wifi_for_band('2.4').current_route)
+
+
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/conman/interface.py b/conman/interface.py
index 0a26959..9b9c653 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -306,6 +306,7 @@
     self.bands = kwargs.pop('bands', [])
     super(Wifi, self).__init__(*args, **kwargs)
     self._wpa_control = None
+    self.initial_ssid = None
 
   @property
   def wpa_supplicant(self):
@@ -331,8 +332,14 @@
         logging.error('Error attaching to wpa_supplicant: %s', e)
         return
 
-      self.wpa_supplicant = ('wpa_state=COMPLETED' in
-                             self._wpa_control.request('STATUS'))
+      for line in self._wpa_control.request('STATUS').splitlines():
+        if '=' not in line:
+          continue
+        key, value = line.split('=', 1)
+        if key == 'wpa_state':
+          self.wpa_supplicant = value == 'COMPLETED'
+        elif key == 'ssid' and not self._initialized:
+          self.initial_ssid = value
 
   def get_wpa_control(self, socket):
     return wpactrl.WPACtrl(socket)
@@ -367,3 +374,10 @@
             break
 
         self.update_routes()
+
+  def initialize(self):
+    """Unset self.initial_ssid, which is only relevant during initialization."""
+
+    self.initial_ssid = None
+    super(Wifi, self).initialize()
+
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 6368a83..1e86125 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -89,7 +89,8 @@
 
   def request(self, request_type):
     if request_type == 'STATUS':
-      return 'foo\nwpa_state=COMPLETED\nbar' if self.connected else 'foo'
+      return ('foo\nwpa_state=COMPLETED\nssid=my ssid\nbar' if self.connected
+              else 'foo')
     else:
       raise ValueError('Invalid request_type %s' % request_type)
 
@@ -219,11 +220,15 @@
     w.detach_wpa_control()
     # pylint: disable=protected-access
     w._initially_connected = True
+    w._initialized = False
     w.attach_wpa_control(wpa_path)
     wpa_control = w._wpa_control
 
     # wpa_supplicant was already connected when we attached.
     wvtest.WVPASS(w.wpa_supplicant)
+    wvtest.WVPASSEQ(w.initial_ssid, 'my ssid')
+    w.initialize()
+    wvtest.WVPASSEQ(w.initial_ssid, None)
 
     # The wpa_supplicant process disconnects and terminates.
     wpa_control.add_event(Wifi.DISCONNECTED_EVENT)
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index f8966f3..d0aaf45 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -61,6 +61,7 @@
     '7c:61:93': ['htc'],
     '84:7a:88': ['htc'],
     '90:e7:c4': ['htc'],
+    'a0:f4:50': ['htc'],
     'b4:ce:f6': ['htc'],
     'd8:b3:77': ['htc'],
     'e8:99:c4': ['htc'],
@@ -110,6 +111,7 @@
     '14:7d:c5': ['murata'],
     '1c:99:4c': ['murata'],
     '20:02:af': ['murata'],
+    '40:f3:08': ['murata'],
     '44:a7:cf': ['murata'],
     '5c:da:d4': ['murata'],
     '78:4b:87': ['murata'],
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index a44a7b6..c7863fb 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -25,6 +25,9 @@
   ('Unknown', './testdata/pcaps/HTC Evo 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Incredible 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Inspire 2.4GHz.pcap'),
+  ('Unknown', './testdata/pcaps/HTC One V 2.4GHz.pcap'),
+  ('Unknown', './testdata/pcaps/HTC One X 2.4GHz.pcap'),
+  ('Unknown', './testdata/pcaps/HTC One X 5GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Sensation 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Thunderbolt 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Titan 2.4GHz.pcap'),
@@ -46,7 +49,6 @@
   ('Unknown', './testdata/pcaps/Samsung Exhibit 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Fascinate 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Galaxy Tab 2 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/Samsung Infuse 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Infuse 5GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Vibrant 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Sony Xperia Z5 2.4GHz.pcap'),
@@ -57,6 +59,10 @@
   # work for these, instead we add them explicitly.
   ('iPad (1st/2nd gen)', './testdata/pcaps/iPad 1st gen 5GHz.pcap'),
   ('iPad (1st/2nd gen)', './testdata/pcaps/iPad 2nd gen 5GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad (4th gen) 5GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad (4th gen) 2.4GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad Air 5GHz.pcap'),
+  ('iPad (4th gen or Air)', './testdata/pcaps/iPad Air 2.4GHz.pcap'),
   ('iPhone 6/6+', './testdata/pcaps/iPhone 6 5GHz.pcap'),
   ('iPhone 6/6+', './testdata/pcaps/iPhone 6+ 5GHz.pcap'),
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 2.4GHz.pcap'),
@@ -65,6 +71,8 @@
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 5GHz.pcap'),
   ('iPod Touch 1st/2nd gen', './testdata/pcaps/iPod Touch 1st gen 2.4GHz.pcap'),
   ('Nest Thermostat v1/v2', './testdata/pcaps/Nest Thermostat 2.4GHz.pcap'),
+  ('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap'),
+  ('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Infuse 2.4GHz.pcap'),
 ]
 
 
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index 4bbfb3d..64aa1ca 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -45,3 +45,4 @@
 1432237016 04:0c:ce:cf:40:2c 192.168.42.35 MacbookAir2010
 1432237016 8c:2d:aa:9c:ce:0f 192.168.42.36 iPood-5
 1432237016 dc:86:d8:a0:c8:de 192.168.42.37 iPhoone-5c
+1432237016 54:ae:27:32:ef:7f 192.168.42.38 iPaad-Air-1
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 5a1ef1d..4831315 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -37,3 +37,4 @@
 04:0c:ce:cf:40:2c 1,3,6,15,119,95,252,44,46
 8c:2d:aa:9c:ce:0f 1,3,6,15,119,252
 dc:86:d8:a0:c8:de 1,3,6,15,119,252
+54:ae:27:32:ef:7f 1,3,6,15,119,252
diff --git a/taxonomy/testdata/pcaps/HTC One V 2.4GHz.pcap b/taxonomy/testdata/pcaps/HTC One V 2.4GHz.pcap
new file mode 100644
index 0000000..5efec50
--- /dev/null
+++ b/taxonomy/testdata/pcaps/HTC One V 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/HTC One X 2.4GHz.pcap b/taxonomy/testdata/pcaps/HTC One X 2.4GHz.pcap
new file mode 100644
index 0000000..e57a6b2
--- /dev/null
+++ b/taxonomy/testdata/pcaps/HTC One X 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/HTC One X 5GHz.pcap b/taxonomy/testdata/pcaps/HTC One X 5GHz.pcap
new file mode 100644
index 0000000..9a25012
--- /dev/null
+++ b/taxonomy/testdata/pcaps/HTC One X 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 2.4GHz Broadcast.pcap b/taxonomy/testdata/pcaps/Moto X 2.4GHz Broadcast.pcap
new file mode 100644
index 0000000..045ec57
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 2.4GHz Broadcast.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 2.4GHz Specific.pcap b/taxonomy/testdata/pcaps/Moto X 2.4GHz Specific.pcap
new file mode 100644
index 0000000..0399b6c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 2.4GHz Specific.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 2.4GHz.pcap b/taxonomy/testdata/pcaps/Moto X 2.4GHz.pcap
new file mode 100644
index 0000000..40e0ca1
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Moto X 5GHz.pcap b/taxonomy/testdata/pcaps/Moto X 5GHz.pcap
new file mode 100644
index 0000000..dffcc63
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Moto X 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap
new file mode 100644
index 0000000..e772fa8
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S2 5GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S2 5GHz.pcap
new file mode 100644
index 0000000..035fc85
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S2 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S4 2.4GHz I9505.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S4 2.4GHz I9505.pcap
new file mode 100644
index 0000000..83832ba
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S4 2.4GHz I9505.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S4 5GHz I9505.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S4 5GHz I9505.pcap
new file mode 100644
index 0000000..ed78fc2
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S4 5GHz I9505.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap" "b/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
new file mode 100644
index 0000000..7eb0924
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap
new file mode 100644
index 0000000..6d6fabb
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap
new file mode 100644
index 0000000..fac188d
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index b7cfaa6..c312c05 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -179,21 +179,21 @@
         ('BCM4330', 'iPad (3rd gen)', '2.4GHz'),
 
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '5GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:0f,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
     'wifi3|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,intwrk:ff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('BCM4334', 'iPad (4th gen)', '2.4GHz'),
+        ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
 
     'wifi|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe|os:ios':
         ('BCM43241', 'iPad Air (1st gen)', '5GHz'),
@@ -382,6 +382,8 @@
 
     'wifi3|probe:0,1,45,221(00904c,51),htcap:09ef,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(00904c,51),221(0050f2,2),cap:0011,htcap:09ef,htagg:1b,htmcs:0000ffff,txpow:0005|os:macos':
         ('BCM4331', 'MacBook Pro 17" late 2011 (A1297)', '5GHz'),
+    'wifi3|probe:0,1,3,45,221(00904c,51),htcap:09ef,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(00904c,51),221(0050f2,2),cap:0011,htcap:09ef,htagg:1b,htmcs:0000ffff,txpow:0005|os:macos':
+        ('BCM4331', 'MacBook Pro 17" late 2011 (A1297)', '5GHz'),
     'wifi3|probe:0,1,50,3,45,221(00904c,51),htcap:19ad,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(00904c,51),221(0050f2,2),cap:0431,htcap:19ad,htagg:1b,htmcs:0000ffff,txpow:1305|os:macos':
         ('BCM4331', 'MacBook Pro 17" late 2011 (A1297)', '2.4GHz'),
 
@@ -413,9 +415,11 @@
     'wifi3|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,cap:8431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02|oui:motorola':
         ('QCA_WCN3620', 'Moto G or Moto X', '2.4GHz'),
 
-    'wifi|probe:0,1,45,221(0050f2,8),191,htcap:016e,vhtcap:31800120|assoc:0,1,33,36,48,45,221(0050f2,2),191,127,htcap:016e,vhtcap:31800120|oui:motorola':
+    'wifi3|probe:0,1,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,48,45,221(0050f2,2),191,127,127,cap:0431,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a02|oui:motorola':
         ('QCA_WCN3680', 'Moto X', '5GHz'),
-    'wifi|probe:0,1,50,3,45,221(0050f2,8),191,htcap:012c,vhtcap:31800120|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c|oui:motorola':
+    'wifi3|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,cap:0431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02|oui:motorola':
+        ('QCA_WCN3680', 'Moto X', '2.4GHz'),
+    'wifi3|probe:0,1,50,3,45,221(0050f2,8),191,htcap:012c,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,50,48,45,221(0050f2,2),127,cap:0431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02|oui:motorola':
         ('QCA_WCN3680', 'Moto X', '2.4GHz'),
 
     'wifi3|probe:0,1,3,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,48,45,221(0050f2,2),191,127,127,cap:8431,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a02|oui:motorola':
@@ -658,6 +662,23 @@
     'wifi3|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,intwrk:0f,extcap:00080f8401400040|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),cap:0431,htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1202|oui:murata':
         ('BCM4359', 'Samsung Galaxy Note 5', '2.4GHz'),
 
+    'wifi3|probe:0,1,45,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,45,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:murata':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:murata':
+        ('', 'Samsung Galaxy S2', '5GHz'),
+    'wifi3|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:samsung':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+    'wifi3|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:murata':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+    'wifi3|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:samsung':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+    'wifi3|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0431,htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:murata':
+        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
+
     'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f09|oui:samsung':
         ('', 'Samsung Galaxy S2+', '5GHz'),
     'wifi3|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),cap:0011,htcap:000c,htagg:19,htmcs:000000ff,txpow:0f09|oui:murata':
@@ -712,6 +733,10 @@
         ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
     'wifi3|probe:0,1,50,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),cap:0431,htcap:102d,htagg:17,htmcs:000000ff,txpow:1201|oui:murata':
         ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
+    'wifi3|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,intwrk:0f,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),cap:0431,htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,intwrk:0f,extcap:000000800040|oui:samsung':
+        ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
+    'wifi3|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,intwrk:0f,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),cap:0431,htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,intwrk:0f,extcap:000000800040|oui:murata':
+        ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
 
     'wifi3|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,intwrk:0f,extcap:0000088001400040|assoc:0,1,33,36,48,45,127,107,191,221(00904c,4),221(001018,2),221(0050f2,2),cap:0011,htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e20b,intwrk:0f,extcap:0000088001400040|oui:samsung':
         ('BCM4354', 'Samsung Galaxy S5', '5GHz'),
diff --git a/waveguide/fake/taxonomy/9c:d9:17:00:00:02 b/waveguide/fake/taxonomy/9c:d9:17:00:00:02
index 43be77d..8d6402c 100644
--- a/waveguide/fake/taxonomy/9c:d9:17:00:00:02
+++ b/waveguide/fake/taxonomy/9c:d9:17:00:00:02
@@ -1 +1 @@
-wifi|probe:0,1,45,221(0050f2,8),191,htcap:016e,vhtcap:31800120|assoc:0,1,33,36,48,45,221(0050f2,2),191,127,htcap:016e,vhtcap:31800120
+wifi3|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,cap:0431,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 7ad4079..10af967 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -9,29 +9,52 @@
 import utils
 
 
+ALREADY_MEMBER_FMT = ('device %s is already a member of a bridge; '
+                      "can't enslave it to bridge %s.")
+NOT_MEMBER_FMT = 'device %s is not a slave of %s'
+
+
 def _get_interface():
-  return subprocess.check_output(['get-quantenna-interface']).strip() or None
+  return subprocess.check_output(['get-quantenna-interface']).strip()
 
 
-def _get_qcsapi():
-  # qcsapi_pcie_static runs on PCIe hosts, e.g. GFRG250.
-  # call_qcsapi runs on the LHOST, e.g. GFEX250.
-  return next((qcsapi for qcsapi in ['qcsapi_pcie_static', 'call_qcsapi']
-               if utils.subprocess_quiet(['runnable', qcsapi]) == 0), None)
-
-
-def _get_mac_address():
-  var = {'wlan0': 'MAC_ADDR_WIFI', 'wlan1': 'MAC_ADDR_WIFI2'}[_get_interface()]
+def _get_mac_address(interface):
+  try:
+    var = {'wlan0': 'MAC_ADDR_WIFI', 'wlan1': 'MAC_ADDR_WIFI2'}[interface]
+  except KeyError:
+    raise utils.BinWifiException('no MAC address for %s in hnvram' % interface)
   return subprocess.check_output(['hnvram', '-rq', var]).strip()
 
 
 def _qcsapi(*args):
-  return subprocess.check_output([_get_qcsapi()] + list(args)).strip()
+  return subprocess.check_output(['qcsapi'] + list(args)).strip()
+
+
+def _brctl(*args):
+  return subprocess.check_output(['brctl'] + list(args),
+                                 stderr=subprocess.STDOUT).strip()
+
+
+def _set_interface_in_bridge(bridge, interface, want_in_bridge):
+  """Add/remove Quantenna interface from/to the bridge."""
+  if want_in_bridge:
+    command = 'addif'
+    error_fmt = ALREADY_MEMBER_FMT
+  else:
+    command = 'delif'
+    error_fmt = NOT_MEMBER_FMT
+
+  try:
+    _brctl(command, bridge, interface)
+  except subprocess.CalledProcessError as e:
+    if error_fmt % (interface, bridge) not in e.output:
+      raise utils.BinWifiException(e.output)
 
 
 def _set(mode, opt):
   """Enable wifi."""
-  if not _get_interface() or not _get_qcsapi():
+  interface = _get_interface()
+  if not interface:
     return False
 
   _qcsapi('rfenable', '0')
@@ -47,7 +70,7 @@
   for param, value in config.iteritems():
     _qcsapi('update_config_param', 'wifi0', param, value)
 
-  _qcsapi('set_mac_addr', 'wifi0', _get_mac_address())
+  _qcsapi('set_mac_addr', 'wifi0', _get_mac_address(interface))
 
   if int(_qcsapi('is_startprod_done')):
     _qcsapi('reload_in_mode', 'wifi0', mode)
@@ -61,12 +84,14 @@
       raise utils.BinWifiException('startprod timed out')
 
   if mode == 'ap':
+    _set_interface_in_bridge(opt.bridge, interface, True)
     _qcsapi('set_ssid', 'wifi0', opt.ssid)
     _qcsapi('set_passphrase', 'wifi0', '0', os.environ['WIFI_PSK'])
     _qcsapi('set_option', 'wifi0', 'ssid_broadcast',
             '0' if opt.hidden_mode else '1')
     _qcsapi('rfenable', '1')
   elif mode == 'sta':
+    _set_interface_in_bridge(opt.bridge, interface, False)
     _qcsapi('create_ssid', 'wifi0', opt.ssid)
     _qcsapi('ssid_set_passphrase', 'wifi0', opt.ssid, '0',
             os.environ['WIFI_CLIENT_PSK'])
@@ -79,7 +104,7 @@
 
 def _stop(_):
   """Disable wifi."""
-  if not _get_interface() or not _get_qcsapi():
+  if not _get_interface():
     return False
 
   _qcsapi('rfenable', '0')
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index 1f99d41..72f0333 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -3,6 +3,8 @@
 """Tests for quantenna.py."""
 
 import os
+from subprocess import CalledProcessError
+
 from configs_test import FakeOptDict
 import quantenna
 from wvtest import wvtest
@@ -17,14 +19,39 @@
     return '1\n' if ['startprod', 'wifi0'] in calls else '0\n'
 
 
-def set_fakes(interface='wlan1', qcsapi='qcsapi_pcie_static'):
+bridge_interfaces = set()
+
+
+def fake_brctl(*args):
+  bridge = args[-2]
+  wvtest.WVPASS(bridge == 'br0')
+  interface = args[-1]
+  if 'addif' in args:
+    if interface in bridge_interfaces:
+      raise CalledProcessError(
+          returncode=1, cmd=['brctl'] + list(args),
+          output=quantenna.ALREADY_MEMBER_FMT % (interface, bridge))
+    bridge_interfaces.add(interface)
+    return
+
+  if 'delif' in args:
+    if interface not in bridge_interfaces:
+      raise CalledProcessError(
+          returncode=1, cmd=['brctl'] + list(args),
+          output=quantenna.NOT_MEMBER_FMT % (interface, bridge))
+    bridge_interfaces.remove(interface)
+    return
+
+
+def set_fakes(interface='wlan1'):
   del calls[:]
+  bridge_interfaces.clear()
   os.environ['WIFI_PSK'] = 'wifi_psk'
   os.environ['WIFI_CLIENT_PSK'] = 'wifi_client_psk'
   quantenna._get_interface = lambda: interface
-  quantenna._get_qcsapi = lambda: qcsapi
-  quantenna._get_mac_address = lambda: '00:11:22:33:44:55'
+  quantenna._get_mac_address = lambda _: '00:11:22:33:44:55'
   quantenna._qcsapi = fake_qcsapi
+  quantenna._brctl = fake_brctl
 
 
 def matching_calls_indices(accept):
@@ -40,13 +67,12 @@
   wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_client_wifi(opt))
   wvtest.WVPASSEQ(calls, [])
-  set_fakes(qcsapi='')
   wvtest.WVFAIL(quantenna.set_wifi(opt))
   wvtest.WVFAIL(quantenna.set_client_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_client_wifi(opt))
   wvtest.WVPASSEQ(calls, [])
-  set_fakes(interface='', qcsapi='')
+  set_fakes(interface='')
   wvtest.WVFAIL(quantenna.set_wifi(opt))
   wvtest.WVFAIL(quantenna.set_client_wifi(opt))
   wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
@@ -57,10 +83,12 @@
 @wvtest.wvtest
 def set_wifi_test():
   opt = FakeOptDict()
+  opt.bridge = 'br0'
   set_fakes()
 
   # Run set_wifi for the first time.
   wvtest.WVPASS(quantenna.set_wifi(opt))
+  wvtest.WVPASS('wlan1' in bridge_interfaces)
 
   # 'rfenable 0' must be run first so that a live interface is not being
   # modified.
@@ -104,6 +132,7 @@
   opt.width = '80'
   new_calls_start = len(calls)
   wvtest.WVPASS(quantenna.set_client_wifi(opt))
+  wvtest.WVFAIL('wlan1' in bridge_interfaces)
 
   # Clear old calls.
   del calls[:new_calls_start]
@@ -143,6 +172,13 @@
   wvtest.WVPASSLT(rim, i[0])
   wvtest.WVPASSLT(i[-1], calls.index(['apply_security_config', 'wifi0']))
 
+  # Make sure subsequent equivalent calls don't fail despite the redundant
+  # bridge changes.
+  wvtest.WVPASS(quantenna.set_client_wifi(opt))
+  wvtest.WVPASS(quantenna.set_client_wifi(opt))
+  wvtest.WVPASS(quantenna.set_wifi(opt))
+  wvtest.WVPASS(quantenna.set_wifi(opt))
+
 
 @wvtest.wvtest
 def stop_wifi_test():