Merge "ginstall:  Output progress to /tmp/ginstall/progress."
diff --git a/cmds/Makefile b/cmds/Makefile
index 4778036..45580b2 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -33,6 +33,7 @@
 	dnsck \
 	freemegs \
 	gfhd254_reboot \
+	gflldpd \
 	gstatic \
 	http_bouncer \
 	ionice \
@@ -52,6 +53,7 @@
 LIB_TARGETS=\
 	stdoutline.so
 HOST_TEST_TARGETS=\
+	host-gflldpd_test \
 	host-netusage_test \
 	host-utils_test \
 	host-isoping_test
@@ -272,6 +274,10 @@
 anonid: anonid.o
 host-anonid: host-anonid.o
 anonid host-anonid: LIBS += -lcrypto
+host-gflldpd_test.o: CXXFLAGS += -D WVTEST_CONFIGURED -I ../wvtest/cpp
+host-gflldpd_test.o: gflldpd.c
+host-gflldpd_test: LIBS+=$(HOST_LIBS) -lm -lstdc++
+host-gflldpd_test: host-gflldpd_test.o host-wvtestmain.o host-wvtest.o
 
 
 TESTS = $(wildcard test-*.sh) $(wildcard test-*.py) $(wildcard *_test.py) $(TEST_TARGETS)
diff --git a/cmds/gflldpd.c b/cmds/gflldpd.c
new file mode 100644
index 0000000..44c2dd2
--- /dev/null
+++ b/cmds/gflldpd.c
@@ -0,0 +1,246 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013 Keichi Takahashi keichi.t@me.com
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+/* Substantially derived from * https://github.com/keichi/tiny-lldpd
+ * also under the MIT license */
+
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
+
+#include <net/ethernet.h>
+#include <net/if.h>
+#include <netinet/if_ether.h>
+#include <netinet/in.h>
+#include <netpacket/packet.h>
+#include <sys/socket.h>
+
+#define MAXINTERFACES 8
+const char *ifnames[MAXINTERFACES] = {0};
+int ninterfaces = 0;
+
+uint8_t sendbuf[1024];
+
+const uint8_t lldpaddr[ETH_ALEN] = {0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e};
+#define ETH_P_LLDP            0x88cc
+#define TLV_END               0
+#define TLV_CHASSIS_ID        1
+#define TLV_PORT_ID           2
+#define TLV_TTL               3
+#define TLV_PORT_DESCRIPTION  4
+#define TLV_SYSTEM_NAME       5
+
+#define CHASSIS_ID_MAC_ADDRESS  4
+#define PORT_ID_MAC_ADDRESS     3
+
+static int write_lldp_tlv_header(void *p, int type, int length)
+{
+  *((uint16_t *)p) = htons((type & 0x7f) << 9 | (length & 0x1ff));
+  return 2;
+}
+
+
+static int write_lldp_type_subtype_tlv(size_t offset,
+    uint8_t type, uint8_t subtype, int length, const void *data)
+{
+  uint8_t *p = sendbuf + offset;
+
+  if ((offset + 2 + 1 + length) > sizeof(sendbuf)) {
+    fprintf(stderr, "LLDP frame too large %zd > %zd\n",
+        (offset + 2 + 1 + length), sizeof(sendbuf));
+    exit(1);
+  }
+
+  p += write_lldp_tlv_header(p, type, length + 1);
+  *p++ = subtype;
+  memcpy(p, data, length);
+  p += length;
+
+  return (p - sendbuf);
+}
+
+
+static int write_lldp_type_tlv(size_t offset, uint8_t type,
+    int length, const void *data)
+{
+  uint8_t *p = sendbuf + offset;
+
+  if ((offset + 2 + length) > sizeof(sendbuf)) {
+    fprintf(stderr, "LLDP frame too large %zd > %zd\n",
+        (offset + 2 + length), sizeof(sendbuf));
+    exit(1);
+  }
+
+  p += write_lldp_tlv_header(p, type, length);
+  memcpy(p, data, length);
+  p += length;
+
+  return (p - sendbuf);
+}
+
+
+static int write_lldp_end_tlv(size_t offset)
+{
+  uint8_t *p = sendbuf + offset;
+
+  if ((offset + 2) > sizeof(sendbuf)) {
+    fprintf(stderr, "LLDP frame too large %zd > %zd\n",
+        (offset + 2), sizeof(sendbuf));
+    exit(1);
+  }
+
+  offset += write_lldp_tlv_header(p, TLV_END, 0);
+  return offset;
+}
+
+
+static void mac_str_to_bytes(const char *macstr, uint8_t *mac)
+{
+  if (sscanf(macstr, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
+        &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) != 6) {
+    fprintf(stderr, "Invalid MAC address: %s\n", macstr);
+    exit(1);
+  }
+}
+
+
+static size_t format_lldp_packet(const char *macaddr, const char *ifname,
+    const char *serial)
+{
+  uint8_t saddr[ETH_ALEN];
+  size_t offset = 0;
+  struct ether_header *eh = (struct ether_header *)sendbuf;
+  uint16_t ttl;
+
+  mac_str_to_bytes(macaddr, saddr);
+  memset(sendbuf, 0, sizeof(sendbuf));
+
+  eh = (struct ether_header *)sendbuf;
+  memcpy(eh->ether_shost, saddr, sizeof(eh->ether_shost));
+  memcpy(eh->ether_dhost, lldpaddr, sizeof(eh->ether_dhost));
+  eh->ether_type = htons(ETH_P_LLDP);
+  offset = sizeof(*eh);
+
+  offset = write_lldp_type_subtype_tlv(offset,
+      TLV_CHASSIS_ID, CHASSIS_ID_MAC_ADDRESS, ETH_ALEN, saddr);
+  offset = write_lldp_type_subtype_tlv(offset,
+      TLV_PORT_ID, PORT_ID_MAC_ADDRESS, ETH_ALEN, saddr);
+
+  ttl = htons(120);
+  offset = write_lldp_type_tlv(offset, TLV_TTL, sizeof(ttl), &ttl);
+
+  offset = write_lldp_type_tlv(offset,
+      TLV_PORT_DESCRIPTION, strlen(ifname), ifname);
+  offset = write_lldp_type_tlv(offset,
+      TLV_SYSTEM_NAME, strlen(serial), serial);
+  offset = write_lldp_end_tlv(offset);
+
+  return offset;
+}
+
+
+#ifndef UNIT_TESTS
+static void send_lldp_packet(int s, size_t len, const char *ifname)
+{
+  struct sockaddr_ll sll;
+
+  memset(&sll, 0, sizeof(sll));
+  sll.sll_family = PF_PACKET;
+  sll.sll_ifindex = if_nametoindex(ifname);
+  sll.sll_hatype = ARPHRD_ETHER;
+  sll.sll_halen = ETH_ALEN;
+  sll.sll_pkttype = PACKET_OTHERHOST;
+  memcpy(sll.sll_addr, lldpaddr, ETH_ALEN);
+  if (sendto(s, sendbuf, len, 0, (struct sockaddr*)&sll, sizeof(sll)) < 0) {
+    fprintf(stderr, "LLDP sendto failed\n");
+    exit(1);
+  }
+}
+
+
+static void usage(const char *progname)
+{
+  fprintf(stderr, "usage: %s -i eth# -m 00:11:22:33:44:55 -s G0123456789\n",
+      progname);
+  exit(1);
+}
+
+
+int main(int argc, char *argv[])
+{
+  const char *macaddr = NULL;
+  const char *serial = NULL;
+  int c;
+  int s;
+
+  while ((c = getopt(argc, argv, "i:m:s:")) != -1) {
+    switch (c) {
+      case 'i':
+        if (ninterfaces == (MAXINTERFACES - 1)) {
+          usage(argv[0]);
+        }
+        ifnames[ninterfaces++] = optarg;
+        break;
+      case 'm':
+        macaddr = optarg;
+        break;
+      case 's':
+        serial = optarg;
+        break;
+      default:
+        usage(argv[0]);
+        break;
+    }
+  }
+
+  if (ninterfaces == 0 || macaddr == NULL || serial == NULL) {
+    usage(argv[0]);
+  }
+
+  if ((s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) {
+    fprintf(stderr, "socket(PF_PACKET) failed\n");
+    exit(1);
+  }
+
+  while (1) {
+    int i;
+
+    for (i = 0; i < ninterfaces; ++i) {
+      if (ifnames[i] != NULL) {
+        size_t len = format_lldp_packet(macaddr, ifnames[i], serial);
+        send_lldp_packet(s, len, ifnames[i]);
+      }
+      usleep(10000 + (rand() % 80000));
+    }
+
+    usleep(500000 + (rand() % 1000000));
+  }
+
+  return 0;
+}
+#endif  /* UNIT_TESTS */
diff --git a/cmds/gflldpd_test.cc b/cmds/gflldpd_test.cc
new file mode 100644
index 0000000..21f33df
--- /dev/null
+++ b/cmds/gflldpd_test.cc
@@ -0,0 +1,61 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2013 Keichi Takahashi keichi.t@me.com
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <netinet/if_ether.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <wvtest.h>
+
+
+#define UNIT_TESTS
+#include "gflldpd.c"
+
+
+WVTEST_MAIN("mac_str_to_bytes") {
+  uint8_t expected_mac[] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55 };
+  uint8_t mac[ETH_ALEN];
+
+  mac_str_to_bytes("00:11:22:33:44:55", mac);
+  WVPASSEQ(memcmp(mac, expected_mac, ETH_ALEN), 0);
+}
+
+
+WVTEST_MAIN("format_lldp_packet") {
+  size_t siz;
+  uint8_t expected[] = {
+    0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e, 0x00, 0x11,
+    0x22, 0x33, 0x44, 0x55, 0x88, 0xcc, 0x02, 0x07,
+    0x04, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x04,
+    0x07, 0x03, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
+    0x06, 0x02, 0x00, 0x78, 0x08, 0x04, 0x65, 0x74,
+    0x68, 0x30, 0x0a, 0x0b, 0x47, 0x30, 0x31, 0x32,
+    0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x00,
+    0x00
+  };
+
+  siz = format_lldp_packet("00:11:22:33:44:55", "eth0", "G0123456789");
+  WVPASSEQ(siz, sizeof(expected));
+  WVPASSEQ(memcmp(sendbuf, expected, siz), 0);
+}
diff --git a/cmds/is-secure-boot.gfch100 b/cmds/is-secure-boot.gfch100
new file mode 120000
index 0000000..cf37783
--- /dev/null
+++ b/cmds/is-secure-boot.gfch100
@@ -0,0 +1 @@
+is-secure-boot.stub
\ No newline at end of file
diff --git a/cmds/is-secure-boot.gfrg240 b/cmds/is-secure-boot.gfrg240
new file mode 120000
index 0000000..cf37783
--- /dev/null
+++ b/cmds/is-secure-boot.gfrg240
@@ -0,0 +1 @@
+is-secure-boot.stub
\ No newline at end of file
diff --git a/cmds/is-secure-boot.gfch100 b/cmds/is-secure-boot.stub
similarity index 100%
rename from cmds/is-secure-boot.gfch100
rename to cmds/is-secure-boot.stub
diff --git a/cmds/statpitcher.cc b/cmds/statpitcher.cc
index 0a19443..27dd171 100644
--- a/cmds/statpitcher.cc
+++ b/cmds/statpitcher.cc
@@ -173,7 +173,7 @@
   if (pipe) {
     char buffer[128];
     if (fgets(buffer, 128, pipe.get()) != NULL) {
-      std::istringstream(buffer) >> ret;
+      std::istringstream(buffer) >> std::hex >> ret;
     }
   }
   return ret;
diff --git a/conman/Makefile b/conman/Makefile
index 6126f84..2914b4a 100644
--- a/conman/Makefile
+++ b/conman/Makefile
@@ -9,7 +9,7 @@
       echo 'echo "(gpylint-missing)" >&2'; \
     fi \
 )
-NOINSTALL=%_test.py options.py experiment.py experiment_testutils.py
+NOINSTALL=%_test.py options.py experiment.py experiment_testutils.py test_common.py
 
 all:
 
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index b4c3887..326b946 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -26,6 +26,8 @@
 import ratchet
 import status
 
+logger = logging.getLogger(__name__)
+
 try:
   import monotime  # pylint: disable=unused-import,g-import-not-at-top
 except ImportError:
@@ -74,16 +76,16 @@
   WIFI_SETCLIENT = ['wifi', 'setclient', '--persist']
   WIFI_STOPCLIENT = ['wifi', 'stopclient', '--persist']
 
-  def __init__(self, band, wifi, command_lines, wpa_control_interface):
+  def __init__(self, band, wifi, command_lines):
     self.band = band
     self.wifi = wifi
+    self.logger = self.wifi.logger.getChild(self.band)
     self.command = command_lines.splitlines()
     self.access_point_up = False
     self.ssid = None
     self.passphrase = None
     self.interface_suffix = None
     self.access_point = None
-    self._wpa_control_interface = wpa_control_interface
 
     binwifi_option_attrs = {
         '-s': 'ssid',
@@ -106,16 +108,12 @@
     if self.ssid is None:
       raise ValueError('Command file does not specify SSID')
 
-    if self.wifi.initial_ssid == self.ssid:
-      logging.info('Connected to WLAN at startup')
+    if self.client_up:
+      self.logger.info('Connected to WLAN at startup')
 
   @property
   def client_up(self):
-    wpa_status = self.wifi.wpa_status()
-    return (wpa_status.get('wpa_state') == 'COMPLETED'
-            # NONE indicates we're on a provisioning network; anything else
-            # suggests we're already on the WLAN.
-            and wpa_status.get('key_mgmt') != 'NONE')
+    return self.ssid and self.ssid == self.wifi.current_secure_ssid()
 
   def start_access_point(self):
     """Start an access point."""
@@ -131,9 +129,9 @@
     try:
       subprocess.check_output(self.command, stderr=subprocess.STDOUT)
       self.access_point_up = True
-      logging.info('Started %s GHz AP', self.band)
+      self.logger.info('Started %s GHz AP', self.band)
     except subprocess.CalledProcessError as e:
-      logging.error('Failed to start access point: %s', e.output)
+      self.logger.error('Failed to start access point: %s', e.output)
 
   def stop_access_point(self):
     if not self.access_point_up:
@@ -146,24 +144,25 @@
     try:
       subprocess.check_output(command, stderr=subprocess.STDOUT)
       self.access_point_up = False
-      logging.info('Stopped %s GHz AP', self.band)
+      self.logger.info('Stopped %s GHz AP', self.band)
     except subprocess.CalledProcessError as e:
-      logging.error('Failed to stop access point: %s', e.output)
+      self.logger.error('Failed to stop access point: %s', e.output)
       return
 
   def start_client(self):
     """Join the WLAN as a client."""
     if experiment.enabled('WifiNo2GClient') and self.band == '2.4':
-      logging.info('WifiNo2GClient enabled; not starting 2.4 GHz client.')
+      self.logger.info('WifiNo2GClient enabled; not starting 2.4 GHz client.')
       return
 
     up = self.client_up
     if up:
-      logging.debug('Wifi client already started on %s GHz', self.band)
+      self.logger.debug('Wifi client already started on %s GHz', self.band)
       return
 
     if self._actually_start_client():
-      self._post_start_client()
+      self.wifi.status.connected_to_wlan = True
+      self.logger.info('Started wifi client on %s GHz', self.band)
 
   def _actually_start_client(self):
     """Actually run wifi setclient.
@@ -181,34 +180,26 @@
       self.wifi.status.trying_wlan = True
       subprocess.check_output(command, stderr=subprocess.STDOUT, env=env)
     except subprocess.CalledProcessError as e:
-      logging.error('Failed to start wifi client: %s', e.output)
+      self.logger.error('Failed to start wifi client: %s', e.output)
       self.wifi.status.wlan_failed = True
       return False
 
     return True
 
-  def _post_start_client(self):
-    self.wifi.handle_wpa_events()
-    self.wifi.status.connected_to_wlan = True
-    logging.info('Started wifi client on %s GHz', self.band)
-    self.wifi.attach_wpa_control(self._wpa_control_interface)
-
   def stop_client(self):
     if not self.client_up:
-      logging.debug('Wifi client already stopped on %s GHz', self.band)
+      self.logger.debug('Wifi client already stopped on %s GHz', self.band)
       return
 
-    self.wifi.detach_wpa_control()
-
     try:
       subprocess.check_output(self.WIFI_STOPCLIENT + ['-b', self.band],
                               stderr=subprocess.STDOUT)
       # TODO(rofrankel): Make this work for dual-radio devices.
       self.wifi.status.connected_to_wlan = False
-      logging.info('Stopped wifi client on %s GHz', self.band)
-      self.wifi.handle_wpa_events()
+      self.logger.info('Stopped wifi client on %s GHz', self.band)
+      self.wifi.update()
     except subprocess.CalledProcessError as e:
-      logging.error('Failed to stop wifi client: %s', e.output)
+      self.logger.error('Failed to stop wifi client: %s', e.output)
 
 
 class ConnectionManager(object):
@@ -242,7 +233,6 @@
                tmp_dir='/tmp/conman',
                config_dir='/config/conman',
                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=120, associate_wait_s=15,
                dhcp_wait_s=10, acs_connection_check_wait_s=1,
@@ -254,7 +244,6 @@
     self._interface_status_dir = os.path.join(tmp_dir, 'interfaces')
     self._status_dir = os.path.join(tmp_dir, 'status')
     self._moca_tmp_dir = moca_tmp_dir
-    self._wpa_control_interface = wpa_control_interface
     self._run_duration_s = run_duration_s
     self._interface_update_period = interface_update_period
     self._wifi_scan_period_s = wifi_scan_period_s
@@ -273,7 +262,7 @@
                       self._interface_status_dir, self._moca_tmp_dir):
       if not os.path.exists(directory):
         os.makedirs(directory)
-        logging.info('Created monitored directory: %s', directory)
+        logger.info('Created monitored directory: %s', directory)
 
     acs_autoprov_filepath = os.path.join(self._tmp_dir,
                                          'acs_autoprovisioning')
@@ -287,8 +276,8 @@
       status_dir = os.path.join(self._status_dir, ifc.name)
       if not os.path.exists(status_dir):
         os.makedirs(status_dir)
-      ifc.status = status.Status(status_dir)
-    self._status = status.CompositeStatus(self._status_dir,
+      ifc.status = status.Status(ifc.name, status_dir)
+    self._status = status.CompositeStatus(__name__, self._status_dir,
                                           [i.status for i in self.interfaces()])
 
     wm = pyinotify.WatchManager()
@@ -310,17 +299,13 @@
       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.
+    # Do the same for wifi interfaces.
     for wifi in self.wifi:
       wifi_up = self.is_interface_up(wifi.name)
+      wifi.wpa_supplicant = wifi_up
       if not os.path.exists(
           os.path.join(self._interface_status_dir, wifi.name)):
         self.ifplugd_action(wifi.name, wifi_up)
-      if wifi_up:
-        wifi.status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
-            self._wpa_control_interface)
 
     for path, prefix in ((self._tmp_dir, self.GATEWAY_FILE_PREFIX),
                          (self._tmp_dir, self.SUBNET_FILE_PREFIX),
@@ -334,7 +319,7 @@
     # the routing table.
     for ifc in self.interfaces():
       ifc.initialize()
-      logging.info('%s initialized', ifc.name)
+      logger.info('%s initialized', ifc.name)
 
     # Make sure no unwanted APs or clients are running.
     for wifi in self.wifi:
@@ -343,20 +328,20 @@
         if config:
           if config.access_point and self.bridge.internet():
             # If we have a config and want an AP, we don't want a client.
-            logging.info('Stopping pre-existing %s client on %s',
-                         band, wifi.name)
+            logger.info('Stopping pre-existing %s client on %s',
+                        band, wifi.name)
             self._stop_wifi(band, False, True)
           else:
             # If we have a config but don't want an AP, make sure we aren't
             # running one.
-            logging.info('Stopping pre-existing %s AP on %s', band, wifi.name)
+            logger.info('Stopping pre-existing %s AP on %s', band, wifi.name)
             self._stop_wifi(band, True, False)
           break
       else:
         # If we have no config for this radio, neither a client nor an AP should
         # be running.
-        logging.info('Stopping pre-existing %s AP and clienton %s',
-                     band, wifi.name)
+        logger.info('Stopping pre-existing %s AP and clienton %s',
+                    band, wifi.name)
         self._stop_wifi(wifi.bands[0], True, True)
 
     self._interface_update_counter = 0
@@ -438,8 +423,6 @@
       while True:
         self.run_once()
     finally:
-      for wifi in self.wifi:
-        wifi.detach_wpa_control()
       self.notifier.stop()
 
   def run_once(self):
@@ -479,7 +462,7 @@
 
     for wifi in self.wifi:
       if self.currently_provisioning(wifi):
-        logging.debug('Currently provisioning, nothing else to do.')
+        logger.debug('Currently provisioning, nothing else to do.')
         continue
 
       provisioning_failed = self.provisioning_failed(wifi)
@@ -510,22 +493,17 @@
           if wlan_configuration.access_point_up:
             continue_wifi = True
 
-      if not wifi.attached():
-        logging.debug('Attempting to attach to wpa control interface for %s',
-                      wifi.name)
-        wifi.status.attached_to_wpa_supplicant = wifi.attach_wpa_control(
-            self._wpa_control_interface)
-      wifi.handle_wpa_events()
+      wifi.update()
 
       if continue_wifi:
-        logging.debug('Running AP on %s, nothing else to do.', wifi.name)
+        logger.debug('Running AP on %s, nothing else to do.', wifi.name)
         continue
 
       # If this interface is connected to the user's WLAN, there is nothing else
       # to do.
       if self._connected_to_wlan(wifi):
         wifi.status.connected_to_wlan = True
-        logging.debug('Connected to WLAN on %s, nothing else to do.', wifi.name)
+        logger.debug('Connected to WLAN on %s, nothing else to do.', wifi.name)
         continue
 
       # This interface is not connected to the WLAN, so scan for potential
@@ -533,7 +511,7 @@
       if ((not self.acs() or provisioning_failed) and
           not getattr(wifi, 'last_successful_bss_info', None) and
           _gettime() > wifi.last_wifi_scan_time + self._wifi_scan_period_s):
-        logging.debug('Performing scan on %s.', wifi.name)
+        logger.debug('Performing scan on %s.', wifi.name)
         self._wifi_scan(wifi)
 
       # Periodically retry rejoining the WLAN.  If the WLAN configuration is
@@ -544,15 +522,15 @@
       for band in wifi.bands:
         wlan_configuration = self._wlan_configuration.get(band, None)
         if wlan_configuration and _gettime() >= self._try_wlan_after[band]:
-          logging.info('Trying to join WLAN on %s.', wifi.name)
+          logger.info('Trying to join WLAN on %s.', wifi.name)
           wlan_configuration.start_client()
           if self._connected_to_wlan(wifi):
-            logging.info('Joined WLAN on %s.', wifi.name)
+            logger.info('Joined WLAN on %s.', wifi.name)
             wifi.status.connected_to_wlan = True
             self._try_wlan_after[band] = 0
             break
           else:
-            logging.error('Failed to connect to WLAN on %s.', wifi.name)
+            logger.error('Failed to connect to WLAN on %s.', wifi.name)
             wifi.status.connected_to_wlan = False
             self._try_wlan_after[band] = _gettime() + self._wlan_retry_s
       else:
@@ -562,10 +540,10 @@
         # 1) The configuration didn't change, and we should retry connecting.
         # 2) cwmpd isn't writing a configuration, possibly because the device
         #    isn't registered to any accounts.
-        logging.debug('Unable to join WLAN on %s', wifi.name)
+        logger.debug('Unable to join WLAN on %s', wifi.name)
         wifi.status.connected_to_wlan = False
         if self.acs():
-          logging.debug('Connected to ACS')
+          logger.debug('Connected to ACS')
 
           if wifi.acs():
             wifi.last_successful_bss_info = getattr(wifi,
@@ -576,8 +554,8 @@
 
           now = _gettime()
           if self._wlan_configuration:
-            logging.info('ACS has not updated WLAN configuration; will retry '
-                         ' with old config.')
+            logger.info('ACS has not updated WLAN configuration; will retry '
+                        ' with old config.')
             for w in self.wifi:
               for b in w.bands:
                 self._try_wlan_after[b] = now
@@ -587,18 +565,18 @@
           elif (hasattr(wifi, 'complain_about_acs_at')
                 and now >= wifi.complain_about_acs_at):
             wait = wifi.complain_about_acs_at - self.provisioning_since(wifi)
-            logging.info('Can ping ACS, but no WLAN configuration for %ds.',
-                         wait)
+            logger.info('Can ping ACS, but no WLAN configuration for %ds.',
+                        wait)
             wifi.complain_about_acs_at += wait
         # If we didn't manage to join the WLAN, and we don't have an ACS
         # connection or the ACS session failed, we should try another open AP.
         if not self.acs() or provisioning_failed:
           now = _gettime()
           if self._connected_to_open(wifi) and not provisioning_failed:
-            logging.debug('Waiting for provisioning for %ds.',
-                          now - self.provisioning_since(wifi))
+            logger.debug('Waiting for provisioning for %ds.',
+                         now - self.provisioning_since(wifi))
           else:
-            logging.debug('Not connected to ACS or provisioning failed')
+            logger.debug('Not connected to ACS or provisioning failed')
             self._try_next_bssid(wifi)
 
     time.sleep(max(0, self._run_duration_s - (_gettime() - start_time)))
@@ -662,8 +640,8 @@
     if lowest_metric_interface:
       ip = lowest_metric_interface[1].get_ip_address()
       ip_line = '%s %s\n' % (ip, HOSTNAME) if ip else ''
-      logging.info('Lowest metric default route is on dev %r',
-                   lowest_metric_interface[1].name)
+      logger.info('Lowest metric default route is on dev %r',
+                  lowest_metric_interface[1].name)
 
     new_tmp_hosts = '%s127.0.0.1 localhost' % ip_line
 
@@ -712,7 +690,7 @@
       if e.errno == errno.ENOENT:
         # Logging about failing to open .tmp files results in spammy logs.
         if not filename.endswith('.tmp'):
-          logging.error('Not a file: %s', filepath)
+          logger.error('Not a file: %s', filepath)
         return
       else:
         raise
@@ -721,10 +699,10 @@
       if filename == self.ETHERNET_STATUS_FILE:
         try:
           self.bridge.ethernet = bool(int(contents))
-          logging.info('Ethernet %s', 'up' if self.bridge.ethernet else 'down')
+          logger.info('Ethernet %s', 'up' if self.bridge.ethernet else 'down')
         except ValueError:
-          logging.error('Status file contents should be 0 or 1, not %s',
-                        contents)
+          logger.error('Status file contents should be 0 or 1, not %s',
+                       contents)
           return
 
     elif path == self._config_dir:
@@ -735,8 +713,7 @@
           wifi = self.wifi_for_band(band)
           if wifi:
             self._update_wlan_configuration(
-                self.WLANConfiguration(band, wifi, contents,
-                                       self._wpa_control_interface))
+                self.WLANConfiguration(band, wifi, contents))
       elif filename.startswith(self.ACCESS_POINT_FILE_PREFIX):
         match = re.match(self.ACCESS_POINT_FILE_REGEXP, filename)
         if match:
@@ -744,7 +721,7 @@
           wifi = self.wifi_for_band(band)
           if wifi and band in self._wlan_configuration:
             self._wlan_configuration[band].access_point = True
-          logging.info('AP enabled for %s GHz', band)
+          logger.info('AP enabled for %s GHz', band)
 
     elif path == self._tmp_dir:
       if filename.startswith(self.GATEWAY_FILE_PREFIX):
@@ -756,16 +733,16 @@
         ifc = self.interface_by_name(interface_name)
         if ifc:
           ifc.set_gateway_ip(contents)
-          logging.info('Received gateway %r for interface %s', contents,
-                       ifc.name)
+          logger.info('Received gateway %r for interface %s', contents,
+                      ifc.name)
 
       if filename.startswith(self.SUBNET_FILE_PREFIX):
         interface_name = filename.split(self.SUBNET_FILE_PREFIX)[-1]
         ifc = self.interface_by_name(interface_name)
         if ifc:
           ifc.set_subnet(contents)
-          logging.info('Received subnet %r for interface %s', contents,
-                       ifc.name)
+          logger.info('Received subnet %r for interface %s', contents,
+                      ifc.name)
 
     elif path == self._moca_tmp_dir:
       match = re.match(r'^%s\d+$' % self.MOCA_NODE_FILE_PREFIX, filename)
@@ -773,7 +750,7 @@
         try:
           json_contents = json.loads(contents)
         except ValueError:
-          logging.error('Cannot parse %s as JSON.', filepath)
+          logger.error('Cannot parse %s as JSON.', filepath)
           return
         node = json_contents['NodeId']
         had_moca = self.bridge.moca
@@ -795,9 +772,9 @@
       if band in wifi.bands:
         return wifi
 
-    logging.error('No wifi interface for %s GHz.  wlan interfaces:\n%s',
-                  band, '\n'.join('%s: %r' %
-                                  (w.name, w.bands) for w in self.wifi))
+    logger.error('No wifi interface for %s GHz.  wlan interfaces:\n%s',
+                 band, '\n'.join('%s: %r' %
+                                 (w.name, w.bands) for w in self.wifi))
 
   def ifplugd_action(self, interface_name, up):
     subprocess.call(self.IFPLUGD_ACTION + [interface_name,
@@ -805,13 +782,13 @@
 
   def _wifi_scan(self, wifi):
     """Perform a wifi scan and update wifi.cycler."""
-    logging.info('Scanning on %s...', wifi.name)
+    logger.info('Scanning on %s...', wifi.name)
     wifi.last_wifi_scan_time = _gettime()
     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.
     items = self._find_bssids(wifi.bands[0])
-    logging.info('Done scanning on %s', wifi.name)
+    logger.info('Done scanning on %s', wifi.name)
     if not hasattr(wifi, 'cycler'):
       wifi.cycler = cycler.AgingPriorityCycler(
           cycle_length_s=self._bssid_cycle_length_s)
@@ -839,17 +816,16 @@
     last_successful_bss_info = getattr(wifi, 'last_successful_bss_info', None)
     bss_info = last_successful_bss_info or wifi.cycler.next()
     if bss_info is not None:
-      logging.info('Attempting to connect to SSID %s (%s) for provisioning',
-                   bss_info.ssid, bss_info.bssid)
+      logger.info('Attempting to connect to SSID %s (%s) for provisioning',
+                  bss_info.ssid, bss_info.bssid)
       self.start_provisioning(wifi)
       connected = self._try_bssid(wifi, bss_info)
       if connected:
-        wifi.attach_wpa_control(self._wpa_control_interface)
-        wifi.handle_wpa_events()
+        wifi.update()
         wifi.status.connected_to_open = True
         now = _gettime()
         wifi.complain_about_acs_at = now + 5
-        logging.info('Attempting to provision via SSID %s', bss_info.ssid)
+        logger.info('Attempting to provision via SSID %s', bss_info.ssid)
         self._try_to_upload_logs = True
       # If we can no longer connect to this, it's no longer successful.
       else:
@@ -863,7 +839,7 @@
       # Relatedly, once we find ACS access on an open network we may want to
       # save that SSID/BSSID and that first in future.  If we do that then we
       # can declare that provisioning has failed much more aggressively.
-      logging.info('Ran out of BSSIDs to try on %s', wifi.name)
+      logger.info('Ran out of BSSIDs to try on %s', wifi.name)
       wifi.status.provisioning_failed = True
 
     return False
@@ -890,7 +866,7 @@
     band = wlan_configuration.band
     current = self._wlan_configuration.get(band, None)
     if current is None or wlan_configuration.command != current.command:
-      logging.debug('Received new WLAN configuration for band %s', band)
+      logger.debug('Received new WLAN configuration for band %s', band)
       if current is not None:
         wlan_configuration.access_point = current.access_point
       else:
@@ -901,7 +877,7 @@
         wlan_configuration.access_point = os.path.exists(ap_file)
       self._wlan_configuration[band] = wlan_configuration
       self.wifi_for_band(band).status.have_config = True
-      logging.info('Updated WLAN configuration for %s GHz', band)
+      logger.info('Updated WLAN configuration for %s GHz', band)
       self._update_access_point(wlan_configuration)
 
   def _update_access_point(self, wlan_configuration):
@@ -938,7 +914,7 @@
     try:
       self._binwifi(*full_command)
     except subprocess.CalledProcessError as e:
-      logging.error('wifi %s failed: "%s"', ' '.join(full_command), e.output)
+      logger.error('wifi %s failed: "%s"', ' '.join(full_command), e.output)
 
   def _binwifi(self, *command):
     """Test seam for calls to /bin/wifi.
@@ -956,13 +932,13 @@
                             stderr=subprocess.STDOUT)
 
   def _try_upload_logs(self):
-    logging.info('Attempting to upload logs')
+    logger.info('Attempting to upload logs')
     if subprocess.call(self.UPLOAD_LOGS_AND_WAIT) != 0:
-      logging.error('Failed to upload logs')
+      logger.error('Failed to upload logs')
 
   def cwmp_wakeup(self):
     if subprocess.call(self.CWMP_WAKEUP) != 0:
-      logging.error('cwmp wakeup failed')
+      logger.error('cwmp wakeup failed')
 
   def start_provisioning(self, wifi):
     wifi.set_gateway_ip(None)
@@ -975,12 +951,12 @@
       if wifi.provisioning_ratchet.done_after:
         wifi.status.provisioning_completed = True
         wifi.provisioning_ratchet.stop()
-        logging.info('%s successfully provisioned', wifi.name)
+        logger.info('%s successfully provisioned', wifi.name)
       return False
     except ratchet.TimeoutException:
       wifi.status.provisioning_failed = True
-      logging.info('%s failed to provision: %s', wifi.name,
-                   wifi.provisioning_ratchet.current_step().name)
+      logger.info('%s failed to provision: %s', wifi.name,
+                  wifi.provisioning_ratchet.current_step().name)
       return True
 
   def provisioning_completed(self, wifi):
@@ -999,7 +975,7 @@
   try:
     return subprocess.check_output(['wifi', 'show'])
   except subprocess.CalledProcessError as e:
-    logging.error('Failed to call "wifi show": %s', e)
+    logger.error('Failed to call "wifi show": %s', e)
     return ''
 
 
@@ -1007,7 +983,7 @@
   try:
     return subprocess.check_output(['get-quantenna-interfaces']).split()
   except subprocess.CalledProcessError:
-    logging.fatal('Failed to call get-quantenna-interfaces')
+    logger.fatal('Failed to call get-quantenna-interfaces')
     raise
 
 
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 22cf89f..e9807cd 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -15,9 +15,11 @@
 import interface_test
 import iw
 import status
+import test_common
 from wvtest import wvtest
 
-logging.basicConfig(level=logging.DEBUG)
+logger = logging.getLogger(__name__)
+
 
 FAKE_MOCA_NODE1_FILE = """{
   "NodeId": 1,
@@ -66,7 +68,7 @@
 """
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def get_client_interfaces_test():
   """Test get_client_interfaces."""
   subprocess.reset()
@@ -96,7 +98,7 @@
                   {'wcli0': {'frenzy': True, 'bands': set(['5'])}})
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def WLANConfigurationParseTest():  # pylint: disable=invalid-name
   """Test WLANConfiguration parsing."""
   subprocess.reset()
@@ -106,7 +108,7 @@
       '--bridge=br0', '-s', 'my ssid=1', '--interface-suffix', '_suffix',
   ])
   config = connection_manager.WLANConfiguration(
-      '5', interface_test.Wifi('wcli0', 20), cmd, None)
+      '5', interface_test.Wifi('wcli0', 20), cmd)
 
   wvtest.WVPASSEQ('my ssid=1', config.ssid)
   wvtest.WVPASSEQ('abcdWIFI_PSK=qwer', config.passphrase)
@@ -157,10 +159,6 @@
         subprocess.mock('wifi', 'remote_ap', band=band, ssid=ssid, psk=psk,
                         bssid='00:00:00:00:00:00')
 
-        # Also create the wpa_supplicant socket to which to attach.
-        open(os.path.join(kwargs['wpa_control_interface'], interface_name),
-             'w')
-
     super(ConnectionManager, self).__init__(*args, **kwargs)
 
   # Just looking for last_wifi_scan_time to change doesn't work because the
@@ -206,7 +204,7 @@
       self.run_once()
 
   def run_until_scan(self, band):
-    logging.debug('running until scan on band %r', band)
+    logger.debug('running until scan on band %r', band)
     wifi = self.wifi_for_band(band)
     wifi_scan_counter = wifi.wifi_scan_counter
     while wifi_scan_counter == wifi.wifi_scan_counter:
@@ -261,7 +259,6 @@
         moca_tmp_dir = tempfile.mkdtemp()
         wpa_control_interface = tempfile.mkdtemp()
         subprocess.mock('wifi', 'wpa_path', wpa_control_interface)
-        FrenzyWifi.WPACtrl.WIFIINFO_PATH = tempfile.mkdtemp()
         connection_manager.CWMP_PATH = tempfile.mkdtemp()
         subprocess.set_conman_paths(tmp_dir, config_dir,
                                     connection_manager.CWMP_PATH)
@@ -276,7 +273,6 @@
         c = ConnectionManager(tmp_dir=tmp_dir,
                               config_dir=config_dir,
                               moca_tmp_dir=moca_tmp_dir,
-                              wpa_control_interface=wpa_control_interface,
                               run_duration_s=run_duration_s,
                               interface_update_period=interface_update_period,
                               wlan_retry_s=0,
@@ -291,7 +287,7 @@
 
         f(c)
       except Exception:
-        logging.error('Uncaught exception!')
+        logger.error('Uncaught exception!')
         traceback.print_exc()
         raise
       finally:
@@ -301,7 +297,6 @@
         shutil.rmtree(config_dir)
         shutil.rmtree(moca_tmp_dir)
         shutil.rmtree(wpa_control_interface)
-        shutil.rmtree(FrenzyWifi.WPACtrl.WIFIINFO_PATH)
         shutil.rmtree(connection_manager.CWMP_PATH)
 
     actual_test.func_name = f.func_name
@@ -478,6 +473,7 @@
   ssid = 'wlan2'
   psk = 'password2'
   subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
+  # Overwrites previous one due to same BSSID.
   subprocess.mock('wifi', 'remote_ap',
                   bssid='11:22:33:44:55:66',
                   ssid=ssid, psk=psk, band=band, security='WPA2')
@@ -487,10 +483,6 @@
   wvtest.WVPASS(c._connected_to_open(c.wifi_for_band(band)))
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_attempted_bss_info.ssid, 's2')
 
-  # Overwrites previous one due to same BSSID.
-  subprocess.mock('wifi', 'remote_ap',
-                  bssid='11:22:33:44:55:66',
-                  ssid=ssid, psk=psk, band=band, security='WPA2')
   # Run once for cwmp wakeup to get called, then once more for the new config to
   # be received.
   c.run_once()
@@ -761,43 +753,43 @@
   wvtest.WVPASS(c.has_status_files([status.P.PROVISIONING_COMPLETED]))
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
 def connection_manager_test_generic_marvell8897_2g(c):
   connection_manager_test_generic(c, '2.4')
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
 def connection_manager_test_generic_marvell8897_5g(c):
   connection_manager_test_generic(c, '5')
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
 def connection_manager_test_generic_ath9k_ath10k_2g(c):
   connection_manager_test_generic(c, '2.4')
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
 def connection_manager_test_generic_ath9k_ath10k_5g(c):
   connection_manager_test_generic(c, '5')
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
 def connection_manager_test_generic_ath9k_frenzy_2g(c):
   connection_manager_test_generic(c, '2.4')
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
 def connection_manager_test_generic_ath9k_frenzy_5g(c):
   connection_manager_test_generic(c, '5')
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_FRENZY)
 def connection_manager_test_generic_frenzy_5g(c):
   connection_manager_test_generic(c, '5')
@@ -899,13 +891,13 @@
   wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
 def connection_manager_test_dual_band_two_radios_ath9k_ath10k(c):
   connection_manager_test_dual_band_two_radios(c)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
 def connection_manager_test_dual_band_two_radios_ath9k_frenzy(c):
   connection_manager_test_dual_band_two_radios(c)
@@ -991,13 +983,13 @@
   wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
 def connection_manager_test_dual_band_one_radio_marvell8897(c):
   connection_manager_test_dual_band_one_radio(c)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897_NO_5GHZ)
 def connection_manager_test_marvell8897_no_5ghz(c):
   """Test ConnectionManager for the case documented in b/27328894.
@@ -1043,7 +1035,7 @@
   wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897,
                          __test_interfaces_already_up=['eth0', 'wcli0'])
 def connection_manager_test_wifi_already_up(c):
@@ -1056,7 +1048,7 @@
   wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897, wlan_configs={'5': True})
 def connection_manager_one_radio_marvell8897_existing_config_5g_ap(c):
   wvtest.WVPASSEQ(len(c._binwifi_commands), 1)
@@ -1064,7 +1056,7 @@
                   c._binwifi_commands[0])
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897,
                          wlan_configs={'5': False})
 def connection_manager_one_radio_marvell8897_existing_config_5g_no_ap(c):
@@ -1073,7 +1065,7 @@
                   c._binwifi_commands[0])
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_ATH10K,
                          wlan_configs={'5': True})
 def connection_manager_two_radios_ath9k_ath10k_existing_config_5g_ap(c):
@@ -1083,7 +1075,7 @@
                 in c._binwifi_commands)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
 def connection_manager_conman_no_2g_wlan(c):
   unused_raii = experiment_testutils.MakeExperimentDirs()
diff --git a/conman/cycler_test.py b/conman/cycler_test.py
index de1e6c0..1493a83 100755
--- a/conman/cycler_test.py
+++ b/conman/cycler_test.py
@@ -5,10 +5,11 @@
 import time
 
 import cycler
+import test_common
 from wvtest import wvtest
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def cycler_test():
   c = cycler.AgingPriorityCycler()
   wvtest.WVPASS(c.next() is None)
diff --git a/conman/interface.py b/conman/interface.py
index b741555..245ca27 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -7,12 +7,7 @@
 import re
 import subprocess
 
-# This has to be called before another module calls it with a higher log level.
-# pylint: disable=g-import-not-at-top
-logging.basicConfig(level=logging.DEBUG)
-
 import experiment
-import wpactrl
 
 METRIC_5GHZ = 20
 METRIC_24GHZ_5GHZ = 21
@@ -37,6 +32,7 @@
 
   def __init__(self, name, base_metric):
     self.name = name
+    self.logger = logging.getLogger(self.name)
 
     # Currently connected links for this interface, e.g. ethernet.
     self.links = set()
@@ -68,18 +64,18 @@
     """
     # Until initialized, we want to act as if the interface is down.
     if not self._initialized:
-      logging.info('%s not initialized; not running connection_check%s',
-                   self.name, ' (ACS)' if check_acs else '')
+      self.logger.info('not initialized; not running connection_check%s',
+                       ' (ACS)' if check_acs else '')
       return None
 
     if not self.links:
-      logging.info('Connection check for %s failed due to no links', self.name)
+      self.logger.info('Connection check failed due to no links')
       return False
 
-    logging.debug('Gateway IP for %s is %s', self.name, self._gateway_ip)
+    self.logger.debug('Gateway IP is %s', self._gateway_ip)
     if self._gateway_ip is None:
-      logging.info('Connection check%s for %s failed due to no gateway IP',
-                   ' (ACS)' if check_acs else '', self.name)
+      self.logger.info('Connection check%s failed due to no gateway IP',
+                       ' (ACS)' if check_acs else '')
       return False
 
     self.add_routes()
@@ -92,10 +88,9 @@
 
     with open(os.devnull, 'w') as devnull:
       result = subprocess.call(cmd, stdout=devnull, stderr=devnull) == 0
-      logging.info('Connection check%s for %s %s',
-                   ' (ACS)' if check_acs else '',
-                   self.name,
-                   'passed' if result else 'failed')
+      self.logger.info('Connection check%s %s',
+                       ' (ACS)' if check_acs else '',
+                       'passed' if result else 'failed')
 
     return result
 
@@ -120,7 +115,7 @@
     Remove any stale routes and add any missing desired routes.
     """
     if self.metric is None:
-      logging.info('Cannot add route for %s without a metric.', self.name)
+      self.logger.info('Cannot add route without a metric.')
       return
 
     # If the current routes are the same, there is nothing to do.  If either
@@ -133,7 +128,7 @@
     if self._subnet:
       if ((subnet.get('route', None), subnet.get('metric', None)) !=
           (self._subnet, str(self.metric))):
-        logging.debug('Adding subnet route for dev %s', self.name)
+        self.logger.debug('Adding subnet route')
         to_add.append(('subnet', ('add', self._subnet, 'dev', self.name,
                                   'metric', str(self.metric))))
         subnet = self._subnet
@@ -146,7 +141,7 @@
       if (subnet and
           (default.get('via', None), default.get('metric', None)) !=
           (self._gateway_ip, str(self.metric))):
-        logging.debug('Adding default route for dev %s', self.name)
+        self.logger.debug('Adding default route')
         to_add.append(('default',
                        ('add', 'default', 'via', self._gateway_ip,
                         'dev', self.name, 'metric', str(self.metric))))
@@ -155,7 +150,7 @@
 
     # RFC2365 multicast route.
     if current.get('multicast', {}).get('metric', None) != str(self.metric):
-      logging.debug('Adding multicast route for dev %s', self.name)
+      self.logger.debug('Adding multicast route')
       to_add.append(('multicast', ('add', RFC2385_MULTICAST_ROUTE,
                                    'dev', self.name,
                                    'metric', str(self.metric))))
@@ -185,7 +180,7 @@
     # Use a sorted list to ensure that default comes before subnet.
     for route_type in sorted(list(args)):
       while route_type in self.current_routes():
-        logging.debug('Deleting %s route for dev %s', route_type, self.name)
+        self.logger.debug('Deleting %s route', route_type)
         self._ip_route('del', self.current_routes()[route_type]['route'],
                        'dev', self.name)
 
@@ -223,23 +218,23 @@
 
   def _ip_route(self, *args):
     if not self._initialized:
-      logging.info('Not initialized, not running %s %s',
-                   ' '.join(self.IP_ROUTE), ' '.join(args))
+      self.logger.info('Not initialized, not running %s %s',
+                       ' '.join(self.IP_ROUTE), ' '.join(args))
       return ''
 
     try:
-      logging.debug('%s calling ip route %s', self.name, ' '.join(args))
+      self.logger.debug('calling ip route %s', ' '.join(args))
       return subprocess.check_output(self.IP_ROUTE + list(args))
     except subprocess.CalledProcessError as e:
-      logging.error('Failed to call "ip route" with args %r: %s', args,
-                    e.message)
+      self.logger.error('Failed to call "ip route" with args %r: %s', args,
+                        e.message)
       return ''
 
   def _ip_addr_show(self):
     try:
       return subprocess.check_output(self.IP_ADDR_SHOW + [self.name])
     except subprocess.CalledProcessError as e:
-      logging.error('Could not get IP address for %s: %s', self.name, e.message)
+      self.logger.error('Could not get IP address: %s', e.message)
       return None
 
   def get_ip_address(self):
@@ -248,12 +243,12 @@
     return match and match.group('IP') or None
 
   def set_gateway_ip(self, gateway_ip):
-    logging.info('New gateway IP %s for %s', gateway_ip, self.name)
+    self.logger.info('New gateway IP %s', gateway_ip)
     self._gateway_ip = gateway_ip
     self.update_routes(expire_cache=True)
 
   def set_subnet(self, subnet):
-    logging.info('New subnet %s for %s', subnet, self.name)
+    self.logger.info('New subnet %s', subnet)
     self._subnet = subnet
     self.update_routes(expire_cache=True)
 
@@ -265,10 +260,10 @@
     had_links = bool(self.links)
 
     if is_up:
-      logging.info('%s gained link %s', self.name, link)
+      self.logger.info('gained link %s', link)
       self.links.add(link)
     else:
-      logging.info('%s lost link %s', self.name, link)
+      self.logger.info('lost link %s', link)
       self.links.remove(link)
 
     # If a link goes away, we may have lost access to something but not gained
@@ -283,7 +278,7 @@
       self.update_routes(expire_cache=False)
 
   def expire_connection_status_cache(self):
-    logging.debug('Expiring connection status cache for %s', self.name)
+    self.logger.debug('Expiring connection status cache')
     self._has_internet = self._has_acs = None
 
   def update_routes(self, expire_cache=True):
@@ -297,7 +292,7 @@
       expire_cache:  If true, force a recheck of connection status before
       deciding how to prioritize routes.
     """
-    logging.debug('Updating routes for %s', self.name)
+    self.logger.debug('Updating routes')
     if expire_cache:
       self.expire_connection_status_cache()
 
@@ -319,7 +314,7 @@
     """
     if not self._initialized:
       return
-    logging.info('%s routes have normal priority', self.name)
+    self.logger.info('routes have normal priority')
     self.metric_offset = 0
     self.add_routes()
 
@@ -331,7 +326,7 @@
     """
     if not self._initialized:
       return
-    logging.info('%s routes have low priority', self.name)
+    self.logger.info('routes have low priority')
     self.metric_offset = 50
     self.add_routes()
 
@@ -402,9 +397,10 @@
     failure_s = self._acs_session_failure_s()
     if (experiment.enabled('WifiSimulateWireless')
         and failure_s < MAX_ACS_FAILURE_S):
-      logging.info('WifiSimulateWireless: failing bridge connection check%s '
-                   '(no ACS contact for %d seconds, max %d seconds)',
-                   ' (ACS)' if check_acs else '', failure_s, MAX_ACS_FAILURE_S)
+      self.logger.info('WifiSimulateWireless: failing bridge connection check%s'
+                       ' (no ACS contact for %d seconds, max %d seconds)',
+                       ' (ACS)' if check_acs else '', failure_s,
+                       MAX_ACS_FAILURE_S)
       return False
 
     return super(Bridge, self)._connection_check(check_acs)
@@ -428,57 +424,19 @@
 class Wifi(Interface):
   """Represents a wireless interface."""
 
-  WPA_EVENT_RE = re.compile(r'<\d+>CTRL-EVENT-(?P<event>[A-Z\-]+).*')
-  # pylint: disable=invalid-name
-  WPACtrl = wpactrl.WPACtrl
-
   def __init__(self, *args, **kwargs):
     self.bands = kwargs.pop('bands', [])
     super(Wifi, self).__init__(*args, **kwargs)
-    self._wpa_control = None
-    self.initial_ssid = None
 
   @property
   def wpa_supplicant(self):
+    self.update()
     return 'wpa_supplicant' in self.links
 
   @wpa_supplicant.setter
   def wpa_supplicant(self, is_up):
     self._set_link_status('wpa_supplicant', is_up)
 
-  def attached(self):
-    return self._wpa_control and self._wpa_control.attached
-
-  def attach_wpa_control(self, path):
-    """Attach to the wpa_supplicant control interface.
-
-    Args:
-      path:  The path containing the wpa_supplicant control interface socket.
-
-    Returns:
-      Whether attaching was successful.
-    """
-    if self.attached():
-      return True
-
-    socket = os.path.join(path, self.name)
-    logging.debug('%s socket is %s', self.name, socket)
-    try:
-      self._wpa_control = self.get_wpa_control(socket)
-      self._wpa_control.attach()
-      logging.debug('%s successfully attached', self.name)
-    except (wpactrl.error, OSError) as e:
-      logging.error('Error attaching to wpa_supplicant: %s', e)
-      return False
-
-    status = self.wpa_status()
-    logging.debug('%s status after attaching is %s', self.name, status)
-    self.wpa_supplicant = status.get('wpa_state') == 'COMPLETED'
-    if not self._initialized:
-      self.initial_ssid = status.get('ssid')
-
-    return True
-
   def wpa_status(self):
     """Parse the STATUS response from the wpa_supplicant control interface.
 
@@ -488,82 +446,40 @@
     """
     status = {}
 
-    if self.attached():
-      lines = []
-      try:
-        lines = self._wpa_control.request('STATUS').splitlines()
-      except (wpactrl.error, OSError) as e:
-        logging.error('wpa_control STATUS request failed %s args %s',
-                      e.message, e.args)
-        lines = self.wpa_cli_status().splitlines()
-      for line in lines:
-        if '=' not in line:
-          continue
-        k, v = line.strip().split('=', 1)
-        status[k] = v
+    try:
+      lines = subprocess.check_output(['wpa_cli', '-i', self.name,
+                                       'status']).splitlines()
+    except subprocess.CalledProcessError:
+      self.logger.error('wpa_cli status request failed')
+      return {}
 
-    logging.debug('wpa_status is %r', status)
+    for line in lines:
+      if '=' not in line:
+        continue
+      k, v = line.strip().split('=', 1)
+      status[k] = v
+
     return status
 
-  def get_wpa_control(self, socket):
-    return self.WPACtrl(socket)
-
-  def detach_wpa_control(self):
-    if self.attached():
-      try:
-        self._wpa_control.detach()
-      except (wpactrl.error, OSError):
-        logging.error('Failed to detach from wpa_supplicant interface. This '
-                      'may mean something else killed wpa_supplicant.')
-        self._wpa_control = None
-
-      self.wpa_supplicant = False
-
-  def handle_wpa_events(self):
-    if not self.attached():
-      self.wpa_supplicant = False
-      return
-
-    # b/31261343:  Make sure we didn't miss wpa_supplicant being up.
+  def update(self):
     self.wpa_supplicant = self.wpa_status().get('wpa_state', '') == 'COMPLETED'
 
-    while self._wpa_control.pending():
-      match = self.WPA_EVENT_RE.match(self._wpa_control.recv())
-      if match:
-        event = match.group('event')
-        logging.debug('%s got wpa_supplicant event %s', self.name, event)
-        if event == 'CONNECTED':
-          self.wpa_supplicant = True
-        elif event in ('DISCONNECTED', 'TERMINATING', 'ASSOC-REJECT',
-                       'SSID-TEMP-DISABLED', 'AUTH-REJECT'):
-          self.wpa_supplicant = False
-          if event == 'TERMINATING':
-            self.detach_wpa_control()
-            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()
-
   def connected_to_open(self):
     status = self.wpa_status()
     return (status.get('wpa_state', None) == 'COMPLETED' and
             status.get('key_mgmt', None) == 'NONE')
 
-  # TODO(rofrankel):  Remove this if and when the wpactrl failures are fixed.
-  def wpa_cli_status(self):
-    """Fallback for wpa_supplicant control interface status requests."""
-    try:
-      return subprocess.check_output(['wpa_cli', '-i', self.name, 'status'])
-    except subprocess.CalledProcessError:
-      logging.error('wpa_cli status request failed')
-      return ''
+  def current_secure_ssid(self):
+    """Returns SSID if connected to a secure network, False otherwise."""
+    status = self.wpa_status()
+    return (status.get('wpa_state', None) == 'COMPLETED' and
+            # NONE indicates we're on a provisioning network; anything else
+            # suggests we're already on the WLAN.
+            status.get('key_mgmt', None) != 'NONE' and
+            status.get('ssid'))
 
 
-class FrenzyWPACtrl(object):
+class FrenzyWifi(Wifi):
   """A WPACtrl for Frenzy devices.
 
   Implements the same functions used on the normal WPACtrl, using a combination
@@ -571,100 +487,34 @@
   diffing saved state with current system state.
   """
 
-  WIFIINFO_PATH = '/tmp/wifi/wifiinfo'
-
-  def __init__(self, socket):
-    self.ctrl_iface_path, self._interface = os.path.split(socket)
-
-    # State from QCSAPI and wifi_files.
-    self._client_mode = False
-    self._ssid = None
-    self._status = None
-    self._security = None
-
-    self._events = []
-
   def _qcsapi(self, *command):
     try:
       return subprocess.check_output(['qcsapi'] + list(command)).strip()
     except subprocess.CalledProcessError as e:
-      logging.error('QCSAPI call failed: %s: %s', e, e.output)
+      self.logger.error('QCSAPI call failed: %s: %s', e, e.output)
       raise
 
-  def attach(self):
-    self._update()
-
-  @property
-  def attached(self):
-    return self._client_mode
-
-  def detach(self):
-    self._events = []
-    raise wpactrl.error('Real WPACtrl always raises this when detaching.')
-
-  def pending(self):
-    self._update()
-    return bool(self._events)
-
-  def _update(self):
+  def wpa_status(self):
     """Generate and cache events, update state."""
     try:
       client_mode = self._qcsapi('get_mode', 'wifi0') == 'Station'
       ssid = self._qcsapi('get_ssid', 'wifi0')
-      status = self._qcsapi('get_status', 'wifi0')
       security = (self._qcsapi('ssid_get_authentication_mode', 'wifi0', ssid)
                   if ssid else None)
     except subprocess.CalledProcessError:
-      # If QCSAPI failed, skip update.
-      return
+      # If QCSAPI failed, don't crash.
+      return {}
 
-    # If we have an SSID and are in client mode, and at least one of those is
-    # new, then we have just connected.
-    if client_mode and ssid and (not self._client_mode or ssid != self._ssid):
-      self._events.append('<2>CTRL-EVENT-CONNECTED')
+    up = bool(client_mode and ssid)
+    self.wpa_supplicant = up
 
-    # If we are in client mode but lost SSID, we disconnected.
-    if client_mode and self._ssid and not ssid:
-      self._events.append('<2>CTRL-EVENT-DISCONNECTED')
-
-    # If there is an auth/assoc failure, then status (above) is 'Error'.  We
-    # really want the converse of this implication (i.e. that 'Error' implies an
-    # auth/assoc failure), but due to limited documentation this will have to
-    # do.  It should be good enough:  if something else causes get_status to
-    # return 'Error', we are probably not connected, and we don't do anything
-    # special with auth/assoc failures specifically.
-    if client_mode and status == 'Error' and self._status != 'Error':
-      self._events.append('<2>CTRL-EVENT-SSID-TEMP-DISABLED')
-
-    # If we left client mode, wpa_supplicant has terminated.
-    if self._client_mode and not client_mode:
-      self._events.append('<2>CTRL-EVENT-TERMINATING')
-
-    self._client_mode = client_mode
-    self._ssid = ssid
-    self._status = status
-    self._security = security
-
-  def recv(self):
-    return self._events.pop(0)
-
-  def request(self, request_type):
-    """Partial implementation of WPACtrl.request."""
-
-    if request_type != 'STATUS':
-      return ''
-
-    self._update()
-
-    if not self._client_mode or not self._ssid:
-      return ''
-
-    return ('wpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s' %
-            (self._ssid, self._security or 'NONE'))
-
-
-class FrenzyWifi(Wifi):
-  """Represents a Frenzy wireless interface."""
-
-  # pylint: disable=invalid-name
-  WPACtrl = FrenzyWPACtrl
+    if up:
+      return {
+          'wpa_state': 'COMPLETED',
+          'ssid': ssid,
+          'key_mgmt': security or 'NONE',
+      }
+    else:
+      return {
+          'wpa_state': 'SCANNING',
+      }
diff --git a/conman/interface_test.py b/conman/interface_test.py
index 14fb795..3ab58e6 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -2,19 +2,15 @@
 
 """Tests for connection_manager.py."""
 
-import logging
 import os
 import shutil
 import subprocess
 import tempfile
 import time
 
-# This has to be called before another module calls it with a higher log level.
-# pylint: disable=g-import-not-at-top
-logging.basicConfig(level=logging.DEBUG)
-
 import experiment_testutils
 import interface
+import test_common
 from wvtest import wvtest
 
 
@@ -57,7 +53,7 @@
   pass
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def bridge_test():
   """Test Interface and Bridge."""
   tmp_dir = tempfile.mkdtemp()
@@ -176,7 +172,6 @@
 def generic_wifi_test(w, wpa_path):
   # Not currently connected.
   subprocess.wifi.WPA_PATH = wpa_path
-  w.attach_wpa_control(wpa_path)
   wvtest.WVFAIL(w.wpa_supplicant)
 
   # wpa_supplicant connects.
@@ -186,42 +181,19 @@
                   bssid='00:00:00:00:00:00', connection_check_result='succeed')
   subprocess.check_call(['wifi', 'setclient', '--ssid', ssid, '--band', '5'],
                         env={'WIFI_CLIENT_PSK': psk})
-  wvtest.WVFAIL(w.wpa_supplicant)
-  w.attach_wpa_control(wpa_path)
-  w.handle_wpa_events()
   wvtest.WVPASS(w.wpa_supplicant)
   w.set_gateway_ip('192.168.1.1')
 
   # wpa_supplicant disconnects.
   subprocess.mock('wifi', 'disconnected_event', '5')
-  w.handle_wpa_events()
   wvtest.WVFAIL(w.wpa_supplicant)
 
-  # Now, start over so we can test what happens when wpa_supplicant is already
-  # connected when we attach.
-  w.detach_wpa_control()
-  w._initialized = False
-  subprocess.check_call(['wifi', 'setclient', '--ssid', ssid, '--band', '5'],
-                        env={'WIFI_CLIENT_PSK': psk})
-  w.attach_wpa_control(wpa_path)
-
-  # wpa_supplicant was already connected when we attached.
-  wvtest.WVPASS(w.wpa_supplicant)
-  wvtest.WVPASSEQ(w.initial_ssid, ssid)
-  w.initialize()
-  wvtest.WVPASSEQ(w.initial_ssid, None)
-
-  wvtest.WVPASSNE(w.wpa_status(), {})
-  w._wpa_control.request_status_fails = True
-  wvtest.WVPASSNE(w.wpa_status(), {})
-
   # The wpa_supplicant process disconnects and terminates.
   subprocess.check_call(['wifi', 'stopclient', '--band', '5'])
-  w.handle_wpa_events()
   wvtest.WVFAIL(w.wpa_supplicant)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def wifi_test():
   """Test Wifi."""
   w = Wifi('wcli0', '21')
@@ -241,7 +213,7 @@
     shutil.rmtree(conman_path)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def frenzy_wifi_test():
   """Test FrenzyWifi."""
   w = FrenzyWifi('wlan0', '20')
@@ -254,17 +226,14 @@
     subprocess.mock('wifi', 'interfaces',
                     subprocess.wifi.MockInterface(phynum='0', bands=['5'],
                                                   driver='frenzy'))
-    FrenzyWifi.WPACtrl.WIFIINFO_PATH = wifiinfo_path = tempfile.mkdtemp()
-
     generic_wifi_test(w, wpa_path)
 
   finally:
     shutil.rmtree(wpa_path)
     shutil.rmtree(conman_path)
-    shutil.rmtree(wifiinfo_path)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def simulate_wireless_test():
   """Test the WifiSimulateWireless experiment."""
   unused_raii = experiment_testutils.MakeExperimentDirs()
@@ -329,62 +298,5 @@
     shutil.rmtree(interface.CWMP_PATH)
 
 
-@wvtest.wvtest
-def b31261343_test():
-  """Test Wifi."""
-  w = Wifi('wcli0', '21')
-  w.initialize()
-
-  try:
-    wpa_path = tempfile.mkdtemp()
-    conman_path = tempfile.mkdtemp()
-    subprocess.set_conman_paths(conman_path, None)
-    subprocess.mock('wifi', 'interfaces',
-                    subprocess.wifi.MockInterface(phynum='0', bands=['5'],
-                                                  driver='cfg80211'))
-    subprocess.wifi.WPA_PATH = wpa_path
-
-    w.attach_wpa_control(wpa_path)
-    wvtest.WVFAIL(w.wpa_supplicant)
-
-    # Set up.
-    ssid = 'my=ssid'
-    psk = 'passphrase'
-    subprocess.mock('wifi', 'remote_ap', ssid=ssid, psk=psk, band='5',
-                    bssid='00:00:00:00:00:00', connection_check_result='succeed')
-    subprocess.check_call(['wifi', 'setclient', '--ssid', ssid, '--band', '5'],
-                          env={'WIFI_CLIENT_PSK': psk})
-
-    w.set_gateway_ip('192.168.1.1')
-    w.set_subnet('192.168.1.0/24')
-    wvtest.WVFAIL(w.wpa_supplicant)
-    w.attach_wpa_control(wpa_path)
-    w.handle_wpa_events()
-
-    def check_working():
-      w.update_routes(True)
-      wvtest.WVPASS(w.wpa_supplicant)
-      wvtest.WVPASS('default' in w.current_routes())
-
-    def check_broken():
-      w.update_routes(True)
-      wvtest.WVFAIL(w.wpa_supplicant)
-      wvtest.WVFAIL('default' in w.current_routes())
-
-    check_working()
-
-    # This is the buggy state.
-    w.wpa_supplicant = False
-    check_broken()
-
-    # Should fix itself when we next run handle_wpa_events.
-    w.handle_wpa_events()
-    check_working()
-
-  finally:
-    shutil.rmtree(wpa_path)
-    shutil.rmtree(conman_path)
-
-
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/conman/iw_test.py b/conman/iw_test.py
index 202d10c..9f5f0e6 100755
--- a/conman/iw_test.py
+++ b/conman/iw_test.py
@@ -5,6 +5,7 @@
 import subprocess
 
 import iw
+import test_common
 from wvtest import wvtest
 
 
@@ -34,7 +35,7 @@
 )
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def find_bssids_test():
   """Test iw.find_bssids."""
   subprocess.mock('wifi', 'interfaces',
diff --git a/conman/ratchet.py b/conman/ratchet.py
index 61e8705..8013c3d 100644
--- a/conman/ratchet.py
+++ b/conman/ratchet.py
@@ -16,11 +16,6 @@
   _gettime = time.time
 
 
-# This has to be called before another module calls it with a higher log level.
-# pylint: disable=g-import-not-at-top
-logging.basicConfig(level=logging.DEBUG)
-
-
 class TimeoutException(Exception):
   pass
 
@@ -33,7 +28,7 @@
     if evaluate:
       self.evaluate = evaluate
     self.timeout = timeout
-    self.logger = logger or logging
+    self.logger = logger or logging.getLogger(self.name)
     self.callback = callback
     self.reset()
 
@@ -129,7 +124,7 @@
     self.name = name
     self.steps = steps
     for step in self.steps:
-      step.logger = logging.getLogger(self.name)
+      step.logger = logging.getLogger(self.name).getChild(step.name)
     self._status = status
     super(Ratchet, self).__init__(name, None, 0)
 
diff --git a/conman/ratchet_test.py b/conman/ratchet_test.py
index 48b693c..e0b831e 100755
--- a/conman/ratchet_test.py
+++ b/conman/ratchet_test.py
@@ -9,10 +9,11 @@
 
 import ratchet
 import status
+import test_common
 from wvtest import wvtest
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def condition_test():
   """Test basic Condition functionality."""
   x = y = 0
@@ -42,7 +43,7 @@
   wvtest.WVPASSEQ(len(callback_sink), 2)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def file_condition_test():
   """Test File*Condition functionality."""
   _, filename = tempfile.mkstemp()
@@ -70,7 +71,7 @@
   wvtest.WVFAIL(c_touched.check())
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def ratchet_test():
   """Test Ratchet functionality."""
 
@@ -88,7 +89,7 @@
         ratchet.Condition('x', lambda: x, 0.1),
         ratchet.Condition('y', lambda: y, 0.1),
         ratchet.Condition('z', lambda: z, 0.1),
-    ], status.Status(status_export_path))
+    ], status.Status('test ratchet', status_export_path))
     x = y = 1
 
     # Test that timeouts are not just summed, but start whenever the previous
diff --git a/conman/status.py b/conman/status.py
index c5d4187..0b6ac6a 100644
--- a/conman/status.py
+++ b/conman/status.py
@@ -30,7 +30,6 @@
   COULD_REACH_ACS = 'COULD_REACH_ACS'
   CAN_REACH_INTERNET = 'CAN_REACH_INTERNET'
   PROVISIONING_FAILED = 'PROVISIONING_FAILED'
-  ATTACHED_TO_WPA_SUPPLICANT = 'ATTACHED_TO_WPA_SUPPLICANT'
 
   WAITING_FOR_PROVISIONING = 'WAITING_FOR_PROVISIONING'
   WAITING_FOR_DHCP = 'WAITING_FOR_DHCP'
@@ -82,10 +81,6 @@
         (P.HAVE_CONFIG,),
         (),
     ),
-    P.ATTACHED_TO_WPA_SUPPLICANT: (
-        (),
-        (),
-    ),
     P.WAITING_FOR_PROVISIONING: (
         (P.CONNECTED_TO_OPEN,),
         (),
@@ -137,6 +132,7 @@
   """
 
   def __init__(self, *args, **kwargs):
+    self._scope = kwargs.pop('scope', '')
     super(Proposition, self).__init__(*args, **kwargs)
     self._value = None
     self._implications = set()
@@ -180,7 +176,8 @@
     self.export()
     for parent in self.parents:
       parent.export()
-    logging.debug('%s is now %s', self._name, self._value)
+    logging.getLogger(self._scope).getChild(self._name).debug(
+        'now %s', self._value)
 
     if value:
       for implication in self._implications:
@@ -214,7 +211,8 @@
 class Status(object):
   """Provides a convenient API for conman to describe system status."""
 
-  def __init__(self, export_path):
+  def __init__(self, name, export_path):
+    self._name = name
     if not os.path.isdir(export_path):
       os.makedirs(export_path)
 
@@ -224,7 +222,7 @@
 
   def _set_up_propositions(self):
     self._propositions = {
-        p: Proposition(p, self._export_path)
+        p: Proposition(p, self._export_path, scope=self._name)
         for p in dict(inspect.getmembers(P)) if not p.startswith('_')
     }
 
@@ -260,9 +258,9 @@
 
 class CompositeStatus(Status):
 
-  def __init__(self, export_path, children):
+  def __init__(self, name, export_path, children):
     self._children = children
-    super(CompositeStatus, self).__init__(export_path)
+    super(CompositeStatus, self).__init__(name, export_path)
 
   def _set_up_propositions(self):
     self._propositions = {
diff --git a/conman/status_test.py b/conman/status_test.py
index 38dda83..6a591aa 100755
--- a/conman/status_test.py
+++ b/conman/status_test.py
@@ -2,16 +2,14 @@
 
 """Tests for connection_manager.py."""
 
-import logging
 import os
 import shutil
 import tempfile
 
 import status
+import test_common
 from wvtest import wvtest
 
-logging.basicConfig(level=logging.DEBUG)
-
 
 def file_in(path, filename):
   return os.path.exists(os.path.join(path, filename))
@@ -21,7 +19,7 @@
   return file_in(s._export_path, filename)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def test_proposition():
   export_path = tempfile.mkdtemp()
 
@@ -73,16 +71,16 @@
     shutil.rmtree(export_path)
 
 
-@wvtest.wvtest
+@test_common.wvtest
 def test_status():
   export_path_s = tempfile.mkdtemp()
   export_path_t = tempfile.mkdtemp()
   export_path_st = tempfile.mkdtemp()
 
   try:
-    s = status.Status(export_path_s)
-    t = status.Status(export_path_t)
-    st = status.CompositeStatus(export_path_st, [s, t])
+    s = status.Status('s', export_path_s)
+    t = status.Status('t', export_path_t)
+    st = status.CompositeStatus('s_or t', export_path_st, [s, t])
 
     # Sanity check that there are no contradictions.
     for p, (want_true, want_false) in status.IMPLICATIONS.iteritems():
diff --git a/conman/test/fake_python/subprocess/ip.py b/conman/test/fake_python/subprocess/ip.py
index d8baaf3..9fcd5da 100644
--- a/conman/test/fake_python/subprocess/ip.py
+++ b/conman/test/fake_python/subprocess/ip.py
@@ -9,6 +9,8 @@
 import ifup
 
 
+logger = logging.getLogger(__name__)
+
 _ROUTING_TABLE = {}
 _IP_TABLE = {}
 
@@ -74,17 +76,17 @@
     if not can_add_route(dev):
       return (1, 'Tried to add default route without subnet route: %r' %
               _ROUTING_TABLE)
-    logging.debug('Adding route for %r', key)
+    logger.debug('Adding route for %r', key)
     _ROUTING_TABLE[key] = ' '.join(args[1:])
   elif args[0] == 'del':
     if key in _ROUTING_TABLE:
-      logging.debug('Deleting route for %r', key)
+      logger.debug('Deleting route for %r', key)
       del _ROUTING_TABLE[key]
     elif key[2] is None:
       # pylint: disable=g-builtin-op
       for k in _ROUTING_TABLE.keys():
         if k[:-1] == key[:-1]:
-          logging.debug('Deleting route for %r (generalized from %s)', k, key)
+          logger.debug('Deleting route for %r (generalized from %s)', k, key)
           del _ROUTING_TABLE[k]
           break
 
diff --git a/conman/test/fake_python/subprocess/qcsapi.py b/conman/test/fake_python/subprocess/qcsapi.py
index 3625772..a7622a5 100644
--- a/conman/test/fake_python/subprocess/qcsapi.py
+++ b/conman/test/fake_python/subprocess/qcsapi.py
@@ -14,11 +14,9 @@
 
 
 def mock(*args, **kwargs):
-  import logging
   if 'value' not in kwargs:
     raise ValueError('Must specify value for mock qcsapi call %r' % args)
   value = kwargs['value']
-  logging.debug  ('qcsapi %r mocked: %r', args, value)
   if value is None and args in STATE:
     del STATE[args]
   else:
diff --git a/conman/test/fake_python/subprocess/wifi.py b/conman/test/fake_python/subprocess/wifi.py
index 13d1be3..900b908 100644
--- a/conman/test/fake_python/subprocess/wifi.py
+++ b/conman/test/fake_python/subprocess/wifi.py
@@ -67,8 +67,9 @@
 class AccessPoint(object):
 
   def __init__(self, **kwargs):
-    for attr in ('ssid', 'psk', 'band', 'bssid', 'security', 'rssi',
-                 'vendor_ies', 'connection_check_result', 'hidden'):
+    self._attrs = ('ssid', 'psk', 'band', 'bssid', 'security', 'rssi',
+                   'vendor_ies', 'connection_check_result', 'hidden')
+    for attr in self._attrs:
       setattr(self, attr, kwargs.get(attr, None))
 
   def scan_str(self):
@@ -86,6 +87,13 @@
         rssi='%.2f dBm' % (self.rssi or 0),
         security=security_strs.get(self.security, ''))
 
+  def __str__(self):
+    return 'AccessPoint<%s>' % ' '.join('%s=%s' % (attr, getattr(self, attr))
+                                        for attr in self._attrs)
+
+  def __repr__(self):
+    return str(self)
+
 
 def call(*args, **kwargs):
   wifi_commands = {
@@ -140,14 +148,14 @@
     ap = REMOTE_ACCESS_POINTS[band].get(bssid, None)
     if not ap or ap.ssid != ssid:
       _setclient_error_not_found(interface_name, ssid, interface.driver)
-      return 1, ('AP with band %r and BSSID %r and ssid %s not found'
-                 % (band, bssid, ssid))
+      return 1, ('AP with band %r and BSSID %r and ssid %s not found: %s'
+                 % (band, bssid, ssid, REMOTE_ACCESS_POINTS))
   elif ssid:
     candidates = [ap for ap in REMOTE_ACCESS_POINTS[band].itervalues()
                   if ap.ssid == ssid]
     if not candidates:
       _setclient_error_not_found(interface_name, ssid, interface.driver)
-      return 1, 'AP with SSID %r not found' % ssid
+      return 1, 'AP with SSID %r not found: %s' % (ssid, REMOTE_ACCESS_POINTS)
     ap = random.choice(candidates)
   else:
     raise ValueError('Did not specify BSSID or SSID in %r' % args)
diff --git a/conman/test/fake_python/wpactrl.py b/conman/test/fake_python/wpactrl.py
deleted file mode 100644
index 3d8e300..0000000
--- a/conman/test/fake_python/wpactrl.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/python
-
-"""Fake WPACtrl implementation."""
-
-import os
-
-import subprocess
-import subprocess.wifi
-
-
-CONNECTED_EVENT = '<2>CTRL-EVENT-CONNECTED'
-DISCONNECTED_EVENT = '<2>CTRL-EVENT-DISCONNECTED'
-TERMINATING_EVENT = '<2>CTRL-EVENT-TERMINATING'
-
-
-# pylint: disable=invalid-name
-class error(Exception):
-  pass
-
-
-class WPACtrl(object):
-  """Fake wpactrl.WPACtrl."""
-
-  # pylint: disable=unused-argument
-  def __init__(self, wpa_socket):
-    self._socket = wpa_socket
-    self.interface_name = os.path.split(self._socket)[-1]
-    self.attached = False
-    self.connected = False
-    self.request_status_fails = False
-    self._clear_events()
-
-  def pending(self):
-    return bool(subprocess.wifi.INTERFACE_EVENTS[self.interface_name])
-
-  def recv(self):
-    return subprocess.wifi.INTERFACE_EVENTS[self.interface_name].pop(0)
-
-  def attach(self):
-    if not os.path.exists(self._socket):
-      raise error('wpactrl_attach failed')
-    self.attached = True
-
-  def detach(self):
-    self.attached = False
-    self.connected = False
-    self.check_socket_exists('wpactrl_detach failed')
-    self._clear_events()
-
-  def request(self, request_type):
-    if request_type == 'STATUS':
-      if self.request_status_fails:
-        raise error('test error')
-      try:
-        return subprocess.check_output(['wpa_cli', '-i', self.interface_name,
-                                        'status'])
-      except subprocess.CalledProcessError as e:
-        raise error(e.output)
-    else:
-      raise ValueError('Invalid request_type %s' % request_type)
-
-  @property
-  def ctrl_iface_path(self):
-    return os.path.split(self._socket)[0]
-
-  # Below methods are not part of WPACtrl.
-
-  def check_socket_exists(self, msg='Socket does not exist'):
-    if not os.path.exists(self._socket):
-      raise error(msg)
-
-  def _clear_events(self):
-    subprocess.wifi.INTERFACE_EVENTS[self.interface_name] = []
diff --git a/conman/test_common.py b/conman/test_common.py
new file mode 100644
index 0000000..202b712
--- /dev/null
+++ b/conman/test_common.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+
+"""Configures generic stuff for unit tests."""
+
+
+import collections
+import logging
+
+from wvtest import wvtest as real_wvtest
+
+
+logging.basicConfig(level=logging.DEBUG)
+logger = logging.getLogger(__name__)
+
+
+class LoggerCounter(logging.Handler):
+  """Counts the number of messages from each logger."""
+
+  def __init__(self):
+    super(LoggerCounter, self).__init__()
+    self.counts = collections.defaultdict(int)
+
+  def handle(self, record):
+    self.counts[record.name] += 1
+
+
+def num_root_messages(logger_counter):
+  logging.getLogger(__name__).debug('logger counts: %s', logger_counter.counts)
+  return logger_counter.counts['root']
+
+
+def wvtest(f):
+  @real_wvtest.wvtest
+  def inner(*args, **kwargs):
+    logger_counter = LoggerCounter()
+    logging.getLogger().addHandler(logger_counter)
+    f(*args, **kwargs)
+    real_wvtest.WVPASSEQ(num_root_messages(logger_counter), 0)
+  inner.func_name = f.func_name
+  return inner
diff --git a/experiment.py b/experiment.py
index 7ec45ef..1318974 100644
--- a/experiment.py
+++ b/experiment.py
@@ -6,6 +6,7 @@
 import os
 import subprocess
 
+logger = logging.getLogger(__name__)
 
 EXPERIMENTS_TMP_DIR = '/tmp/experiments'
 EXPERIMENTS_DIR = '/config/experiments'
@@ -18,10 +19,10 @@
   try:
     rv = subprocess.call(['register_experiment', name])
   except OSError as e:
-    logging.info('register_experiment: %s', e)
+    logger.info('register_experiment: %s', e)
   else:
     if rv:
-      logging.error('Failed to register experiment %s.', name)
+      logger.error('Failed to register experiment %s.', name)
 
 
 def enabled(name):
@@ -39,14 +40,14 @@
                                      name + '.available')):
     if name not in _experiment_warned:
       _experiment_warned.add(name)
-      logging.warning('Warning: experiment %r not registered.', name)
+      logger.warning('Warning: experiment %r not registered.', name)
   else:
     is_enabled = os.path.exists(os.path.join(EXPERIMENTS_DIR,
                                              name + '.active'))
     if is_enabled and name not in _experiment_enabled:
       _experiment_enabled.add(name)
-      logging.info('Notice: using experiment %r.', name)
+      logger.info('Notice: using experiment %r.', name)
     elif not is_enabled and name in _experiment_enabled:
       _experiment_enabled.remove(name)
-      logging.info('Notice: stopping experiment %r.', name)
+      logger.info('Notice: stopping experiment %r.', name)
     return is_enabled
diff --git a/experiment_testutils.py b/experiment_testutils.py
index f5886e5..3561ca3 100644
--- a/experiment_testutils.py
+++ b/experiment_testutils.py
@@ -9,12 +9,14 @@
 
 import experiment
 
+logger = logging.getLogger(__name__)
+
 
 def enable(name):
   """Enable an experiment.  For unit tests only."""
   open(os.path.join(experiment.EXPERIMENTS_TMP_DIR, name + '.available'), 'w')
   open(os.path.join(experiment.EXPERIMENTS_DIR, name + '.active'), 'w')
-  logging.debug('Enabled %s for unit tests', name)
+  logger.debug('Enabled %s for unit tests', name)
 
 
 def disable(name):
@@ -22,7 +24,7 @@
   filename = os.path.join(experiment.EXPERIMENTS_DIR, name + '.active')
   if os.path.exists(filename):
     os.unlink(filename)
-  logging.debug('Disabled %s for unit tests', name)
+  logger.debug('Disabled %s for unit tests', name)
 
 
 class MakeExperimentDirs(object):
diff --git a/ginstall/ginstall.py b/ginstall/ginstall.py
index 1ec1729..affd475 100755
--- a/ginstall/ginstall.py
+++ b/ginstall/ginstall.py
@@ -1239,7 +1239,7 @@
 
   # handle 'ginstall -p <partition>' separately
   if not opt.drm and not opt.tar:
-    partition = GetPartition(opt, GetOs())
+    partition = GetPartition(opt.partition, GetOs())
     if SetBootPartition(GetOs(), partition) != 0:
       VerbosePrint('Unable to set boot partition\n')
       return HNVRAM_ERR
diff --git a/gpio-mailbox/Makefile b/gpio-mailbox/Makefile
index b73d99f..f6cb77f 100644
--- a/gpio-mailbox/Makefile
+++ b/gpio-mailbox/Makefile
@@ -29,6 +29,8 @@
   CFLAGS += -DGFIBER_LT
 else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gflt200)
   CFLAGS += -DGFIBER_LT
+else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gflt400)
+  CFLAGS += -DGFIBER_LT
 else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gfmn100)
   CFLAGS += -DWINDCHARGER
 else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME),gfch100)
diff --git a/hnvram/Makefile b/hnvram/Makefile
index 2790f4d..badf27a 100644
--- a/hnvram/Makefile
+++ b/hnvram/Makefile
@@ -11,7 +11,7 @@
 BINDIR=$(DESTDIR)$(PREFIX)/bin
 
 HUMAX_UPGRADE_DIR ?= ../../../humax/misc/libupgrade
-CFLAGS += -g -Os -I$(HUMAX_UPGRADE_DIR) $(EXTRACFLAGS)
+CFLAGS += -g -Os -I$(HUMAX_UPGRADE_DIR) -I$(HUMAX_UPGRADE_DIR)/test $(EXTRACFLAGS)
 LDFLAGS += -L$(HUMAX_UPGRADE_DIR) $(EXTRALDFLAGS)
 
 all: hnvram
@@ -22,12 +22,16 @@
 	$(CC) $(CFLAGS) $(SRCS) -o $@ $(LDFLAGS) -lhmxupgrade
 
 unit_test: test
-test: hnvram_test
-	./hnvram_test
+test: clean hnvram_unit_test hnvram_integration_test
+	./hnvram_unit_test
+	./hnvram_integration_test
 
-hnvram_test: hnvram_test.cc hnvram_main.c $(INCS)
+hnvram_unit_test: hnvram_test.cc hnvram_main.c $(INCS)
 	$(CPP) $(CFLAGS) hnvram_test.cc -o $@ $(LDFLAGS) -lgtest -lpthread
 
+hnvram_integration_test: hnvram_integration_test.cc
+	$(CPP) $(CFLAGS) hnvram_integration_test.cc -o $@ $(LDFLAGS) -lgtest -lpthread
+
 install:
 	mkdir -p $(BINDIR)
 	cp hnvram $(BINDIR)/hnvram_binary
@@ -36,4 +40,4 @@
 	@echo "No libs to install."
 
 clean:
-	rm -f hnvram hnvram_test *.o
+	rm -f hnvram hnvram_unit_test hnvram_integration_test *.o
diff --git a/hnvram/hnvram_integration_test.cc b/hnvram/hnvram_integration_test.cc
new file mode 100644
index 0000000..b7ceda1
--- /dev/null
+++ b/hnvram/hnvram_integration_test.cc
@@ -0,0 +1,205 @@
+// Copyright 2016 Google Inc. All Rights Reserved.
+// Author: germuth@google.com (Aaron Germuth)
+
+// Tests external methods of hnvram_main end-to-end. No stubbing of lower-level
+// methods (Black box testing)
+
+#include <stdio.h>
+#include "gtest/gtest.h"
+#include "hmx_upgrade_nvram.h"
+
+#define TEST_MAIN
+#include "hnvram_main.c"
+#include "hmx_test_base.cc"
+#include "hmx_upgrade_nvram.c"
+#include "hmx_upgrade_flash.c"
+
+// Test constants
+const char* name = "NEW_VAR";
+const char* val = "ABCDEF";
+const char* val2 = "ZZZZZZZZZ";
+const int valLen = 6;
+const int valLen2 = 9;
+
+const char* fieldName = "MAC_ADDR_BT";
+const char* fieldVal = "\x01\x02\x03\x04\x05\x06";
+const char* fieldValStr = "01:02:03:04:05:06";
+const char* fieldVal2 = "12:34:56:78:0a:bc";
+const int fieldValLen = 6;
+
+// Test parameters
+const HMX_NVRAM_PARTITION_E partitions[] =
+  {HMX_NVRAM_PARTITION_RO, HMX_NVRAM_PARTITION_RW};
+HMX_NVRAM_PARTITION_E part;
+
+class HnvramIntegrationTest : public HnvramTest,
+  public ::testing::WithParamInterface<HMX_NVRAM_PARTITION_E> {
+  public:
+    HnvramIntegrationTest() {}
+    virtual ~HnvramIntegrationTest() {}
+
+    virtual void SetUp() {
+      part = GetParam();
+
+      libupgrade_verbose = 0;
+      can_add_flag = 0;
+
+      HnvramTest::SetUp();
+
+      HMX_NVRAM_Init(hnvramFileName);
+    }
+
+    virtual void TearDown() {
+      part = HMX_NVRAM_PARTITION_UNSPECIFIED;
+
+      // clear dlists
+      drv_NVRAM_Delete(HMX_NVRAM_PARTITION_RO, (unsigned char*)fieldName);
+      drv_NVRAM_Delete(HMX_NVRAM_PARTITION_RW, (unsigned char*)name);
+      drv_NVRAM_Delete(HMX_NVRAM_PARTITION_RO, (unsigned char*)name);
+
+      HnvramTest::TearDown();
+    }
+};
+
+TEST_P(HnvramIntegrationTest, TestWriteNvramNew) {
+  // Should fail without can_add
+  EXPECT_EQ(-1, write_nvram_new(name, val, part));
+
+  // Should fail to parse
+  can_add_flag = 1;
+  char valLarge[NVRAM_MAX_DATA + 1];
+  memset(valLarge, 1, sizeof(valLarge));
+  EXPECT_EQ(-2, write_nvram_new(name, valLarge, part));
+
+  // Should fail cleanly with bad partition
+  HMX_NVRAM_Init("/tmp/");
+  EXPECT_EQ(-3, write_nvram_new(name, val, part));
+
+  // Read back writes
+  HMX_NVRAM_Init(hnvramFileName);
+  unsigned char read[255];
+  unsigned int readLen = 0;
+  EXPECT_EQ(0, write_nvram_new(name, val, part));
+  EXPECT_EQ(DRV_OK,
+            HMX_NVRAM_Read(part, (unsigned char*)name, 0,
+                           read, sizeof(read), &readLen));
+  EXPECT_EQ(0, memcmp(val, read, valLen));
+  EXPECT_EQ(valLen, readLen);
+}
+
+TEST_P(HnvramIntegrationTest, TestWriteNvram) {
+  // Should fail with large val
+  char valLarge[NVRAM_MAX_DATA + 1];
+  memset(valLarge, 1, sizeof(valLarge));
+  EXPECT_EQ(-1, write_nvram(name, valLarge, part));
+
+  // Failure to parse
+  EXPECT_EQ(-2, write_nvram(fieldName, "not-proper-mac-addr", part));
+
+  // Variable doesn't already exist
+  EXPECT_EQ(-3, write_nvram(name, val, part));
+
+  // Variable exists in wrong partition
+  can_add_flag = 1;
+  EXPECT_EQ(0, write_nvram_new(name, val, part));
+  EXPECT_EQ(-4, write_nvram(name, val, HMX_NVRAM_PARTITION_W_RAWFS));
+
+  // Fail cleanly from lower-level write
+  HMX_NVRAM_Init("/tmp/");
+  EXPECT_EQ(-5, write_nvram(name, val, part));
+  HMX_NVRAM_Init(hnvramFileName);
+
+  // Try to specify partition with a field variable
+  EXPECT_EQ(0, write_nvram_new(fieldName, fieldVal, part));
+  HMX_NVRAM_Init(hnvramFileName);
+  EXPECT_EQ(-6, write_nvram(fieldName, fieldVal2, part));
+
+  // Failure from lower-level write w/field
+  HMX_NVRAM_Init("/tmp/");
+  char out[255];
+  EXPECT_EQ(-7, write_nvram(fieldName, fieldValStr,
+                        HMX_NVRAM_PARTITION_UNSPECIFIED));
+  HMX_NVRAM_Init(hnvramFileName);
+
+  // Read back val after changing val
+  EXPECT_EQ(0, write_nvram(name, val2, part));
+  unsigned char read[255];
+  unsigned int readLen = 0;
+  EXPECT_EQ(DRV_OK, HMX_NVRAM_Read(part, (unsigned char*)name, 0,
+                           read, sizeof(read), &readLen));
+  EXPECT_EQ(0, memcmp(read, val2, readLen));
+  EXPECT_EQ(readLen, valLen2);
+}
+
+TEST_P(HnvramIntegrationTest, TestClearNvram) {
+  // Delete non-existing variable
+  HMX_NVRAM_Init(hnvramFileName);
+  EXPECT_EQ(DRV_OK, clear_nvram(name));
+
+  can_add_flag = 1;
+  EXPECT_EQ(0, write_nvram_new(name, val, part));
+
+  // No hnvram partition
+  HMX_NVRAM_Init("/tmp/");
+  EXPECT_EQ(DRV_ERR, clear_nvram(name));
+
+  // Delete Existing
+  HMX_NVRAM_Init(hnvramFileName);
+  EXPECT_EQ(DRV_OK, clear_nvram(name));
+}
+
+TEST_P(HnvramIntegrationTest, TestReadNvram) {
+  char readR[255];
+  memset(readR, 56, 30);
+  HMX_NVRAM_PARTITION_E part_used;
+  EXPECT_TRUE(NULL == read_nvram(fieldName, readR, sizeof(readR), 0, &part_used));
+
+  // No variable to find
+  EXPECT_EQ(NULL, read_nvram(name, readR, sizeof(readR), 0, &part_used));
+
+  // Find field
+  can_add_flag = 1;
+  EXPECT_EQ(0, write_nvram_new(fieldName, fieldVal, HMX_NVRAM_PARTITION_RO));
+  EXPECT_FALSE(NULL == read_nvram(fieldName, readR, sizeof(readR), 1, &part_used));
+  EXPECT_EQ(0, memcmp(readR, fieldValStr, 18));
+  EXPECT_EQ(part_used, HMX_NVRAM_PARTITION_RO);
+
+  // Find variable
+  EXPECT_EQ(0, write_nvram_new(name, val, part));
+  EXPECT_FALSE(NULL == read_nvram(name, readR, sizeof(readR), 1, &part_used));
+  EXPECT_EQ(0, memcmp(readR, val, valLen));
+  EXPECT_EQ(part_used, part);
+}
+
+TEST_P(HnvramIntegrationTest, TestInitNvram) {
+  char readR[255];
+  HMX_NVRAM_PARTITION_E part_used;
+
+  // Set envvar to bad file
+  EXPECT_EQ(0, setenv("HNVRAM_LOCATION", "/tmp/", 1));
+  EXPECT_EQ(DRV_OK, init_nvram());
+
+  // Should fail to read
+  EXPECT_TRUE(NULL == read_nvram(name, readR, sizeof(readR), 1, &part_used));
+
+  // Set envvar to proper, empty file
+  EXPECT_EQ(0, setenv("HNVRAM_LOCATION", hnvramFileName, 1));
+  EXPECT_EQ(DRV_OK, init_nvram());
+
+  // Write and read it back
+  can_add_flag = 1;
+  EXPECT_EQ(0, write_nvram_new(name, val, part));
+
+  EXPECT_FALSE(NULL == read_nvram(name, readR, sizeof(readR), 1, &part_used));
+  EXPECT_EQ(0, memcmp(readR, val, valLen));
+  EXPECT_EQ(part_used, part);
+}
+
+INSTANTIATE_TEST_CASE_P(TryAllPartitions, HnvramIntegrationTest,
+                        ::testing::ValuesIn(partitions));
+
+int main(int argc, char** argv) {
+  ::testing::InitGoogleTest(&argc, argv);
+  ::testing::AddGlobalTestEnvironment(new HnvramEnvironment);
+  return RUN_ALL_TESTS();
+}
diff --git a/hnvram/hnvram_main.c b/hnvram/hnvram_main.c
index a406caa..e1dd780 100644
--- a/hnvram/hnvram_main.c
+++ b/hnvram/hnvram_main.c
@@ -42,6 +42,8 @@
   printf("\t-n : toggles whether -w can create new variables. Default is off\n");
   printf("\t-p [RW|RO] : toggles what partition new writes (-n) used. Default is RW\n");
   printf("\t-k VARNAME : delete existing key/value pair from NVRAM.\n");
+  printf("\t Set environment variable: $HNVRAM_LOCATION to change where read/writes are performed.");
+  printf("\t By default hnvram uses '/dev/mtd/hnvram'\n");
 }
 
 // Format of data in the NVRAM
@@ -369,7 +371,7 @@
   return NULL;
 }
 
-DRV_Error clear_nvram(char* optarg) {
+DRV_Error clear_nvram(const char* optarg) {
   DRV_Error err1 = HMX_NVRAM_Remove(HMX_NVRAM_PARTITION_RW,
                                     (unsigned char*)optarg);
   DRV_Error err2 = HMX_NVRAM_Remove(HMX_NVRAM_PARTITION_RO,
@@ -387,7 +389,8 @@
 }
 
 
-int write_nvram(char* name, char* value, HMX_NVRAM_PARTITION_E desired_part) {
+int write_nvram(const char* name, const char* value,
+                HMX_NVRAM_PARTITION_E desired_part) {
   const hnvram_field_t* field = get_nvram_field(name);
   int is_field = (field != NULL);
 
@@ -400,8 +403,8 @@
 
   if (strlen(value) > NVRAM_MAX_DATA) {
     fprintf(stderr, "Value length %d exceeds maximum data size of %d\n",
-      strlen(value), NVRAM_MAX_DATA);
-    return -2;
+      (int)strlen(value), NVRAM_MAX_DATA);
+    return -1;
   }
 
   unsigned char nvram_value[NVRAM_MAX_DATA];
@@ -444,18 +447,18 @@
 }
 
 // Adds new variable to HNVRAM in desired_partition as STRING
-int write_nvram_new(char* name, char* value,
+int write_nvram_new(const char* name, const char* value,
                     HMX_NVRAM_PARTITION_E desired_part) {
+  if (!can_add_flag) {
+    fprintf(stderr, "Key not found in NVRAM. Add -n to allow creation %s\n",
+            name);
+    return -1;
+  }
+
   char tmp[NVRAM_MAX_DATA] = {0};
   unsigned char nvram_value[NVRAM_MAX_DATA];
   unsigned int nvram_len = sizeof(nvram_value);
   if (parse_nvram(HNVRAM_STRING, value, nvram_value, &nvram_len) == NULL) {
-    return -1;
-  }
-
-  if (!can_add_flag) {
-    fprintf(stderr, "Key not found in NVRAM. Add -n to allow creation %s\n",
-            name);
     return -2;
   }
 
@@ -472,13 +475,19 @@
   return 0;
 }
 
+int init_nvram() {
+  const char* location = getenv("HNVRAM_LOCATION");
+  return (int)HMX_NVRAM_Init(location);
+}
+
 int hnvram_main(int argc, char* const argv[]) {
   DRV_Error err;
 
   libupgrade_verbose = 0;
 
-  if ((err = HMX_NVRAM_Init()) != DRV_OK) {
-    fprintf(stderr, "NVRAM Init failed: %d\n", err);
+  int ret = init_nvram();
+  if (ret != 0) {
+    fprintf(stderr, "NVRAM Init failed: %d\n", ret);
     exit(1);
   }
 
@@ -524,13 +533,13 @@
           char* value = equal + 1;
 
           int ret = write_nvram(name, value, desired_part);
-          if (ret == -3 && can_add_flag) {
-            // key not found, and we are authorized to add a new one
+          if (ret == -3) {
+            // key not found, try to add a new one
             ret = write_nvram_new(name, value, desired_part);
           }
 
           if (ret != 0) {
-            fprintf(stderr, "Unable to write %s\n", duparg);
+            fprintf(stderr, "Err %d: Unable to write %s\n", ret, duparg);
             free(duparg);
             exit(1);
           }
diff --git a/hnvram/hnvram_test.cc b/hnvram/hnvram_test.cc
index 643a436..dbc05a1 100644
--- a/hnvram/hnvram_test.cc
+++ b/hnvram/hnvram_test.cc
@@ -85,7 +85,7 @@
   return HMX_NVRAM_SetField_Return;
 }
 
-DRV_Error HMX_NVRAM_Init(void) {
+DRV_Error HMX_NVRAM_Init(const char* target_mtd) {
   return DRV_OK;
 }
 
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index 3b497e2..b2a0086 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -779,6 +779,8 @@
         ('Nexus 6', '', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,wps:Nexus_6|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209':
         ('Nexus 6', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_6|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:140b,extcap:000008800140':
+        ('Nexus 6', '', '2.4GHz'),
 
     'wifi4|probe:0,1,45,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,wps:Nexus_6P|assoc:0,1,33,36,48,45,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002':
         ('Nexus 6P', '', '5GHz'),
@@ -786,6 +788,12 @@
         ('Nexus 6P', '', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_6P|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1402,extcap:0000088001400040':
         ('Nexus 6P', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_6P|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1302,extcap:0000088001400040':
+        ('Nexus 6P', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,wps:Nexus_6P|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1302':
+        ('Nexus 6P', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,wps:Nexus_6P|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1402':
+        ('Nexus 6P', '', '2.4GHz'),
 
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff|oui:asus':
         ('Nexus 7', '2012 edition', '2.4GHz'),
@@ -816,6 +824,8 @@
         ('Nexus 7', '2013 edition', '5GHz'),
     'wifi4|probe:0,1,45,htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,33,36,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:1e0d,extcap:00000a02|oui:asus':
         ('Nexus 7', '2013 edition', '5GHz'),
+    'wifi4|probe:0,1,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,extcap:00000a02':
+        ('Nexus 7', '2013 edition', '5GHz'),
     'wifi4|probe:0,1,50,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02':
         ('Nexus 7', '2013 edition', '2.4GHz'),
     'wifi4|probe:0,1,50,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02':
@@ -828,11 +838,19 @@
         ('Nexus 7', '2013 edition', '2.4GHz'),
     'wifi4|probe:0,1,50,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff':
         ('Nexus 7', '2013 edition', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02':
+        ('Nexus 7', '2013 edition', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02':
+        ('Nexus 7', '2013 edition', '2.4GHz'),
 
     'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140,wps:Nexus_9|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:000008800140':
         ('Nexus 9', '', '5GHz'),
     'wifi4|probe:0,1,45,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,wps:Nexus_9|assoc:0,1,33,36,48,45,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009':
         ('Nexus 9', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040,wps:Nexus_9|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:0000088001400040':
+        ('Nexus 9', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140,wps:Nexus_9|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e20b,extcap:000008800140':
+        ('Nexus 9', '', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1309,extcap:000008800140':
         ('Nexus 9', '', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:150b,extcap:000008800140':
@@ -849,6 +867,8 @@
 
     'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040,wps:Nexus_Player|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:0000088001400040':
         ('Nexus Player', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140,wps:Nexus_Player|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:000008800140':
+        ('Nexus Player', '', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_Player|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
         ('Nexus Player', '', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_Player|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
diff --git a/wifi/configs.py b/wifi/configs.py
index a4f20b8..572b1c9 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -370,14 +370,15 @@
   else:
     network_block_lines = wpa_network_lines(ssid, passphrase)
 
+  freq_list = ' '.join(autochannel.get_all_frequencies(opt.band))
+
   network_block_lines.append('\tscan_ssid=1')
   if opt.bssid:
     network_block_lines.append('\tbssid=%s' %
                                utils.validate_and_sanitize_bssid(opt.bssid))
+  network_block_lines.append('\tfreq_list=' + freq_list)
   network_block = make_network_block(network_block_lines)
 
-  freq_list = ' '.join(autochannel.get_all_frequencies(opt.band))
-
   lines = [
       'ctrl_interface=/var/run/wpa_supplicant',
       'ap_scan=1',
diff --git a/wifi/configs_test.py b/wifi/configs_test.py
index 016fc27..bd28626 100755
--- a/wifi/configs_test.py
+++ b/wifi/configs_test.py
@@ -26,6 +26,7 @@
 \t#psk="some passphrase"
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
 \tscan_ssid=1
+\tfreq_list={freq_list}
 }}
 """
 
@@ -39,6 +40,7 @@
 \tpsk=41821f7ca3ea5d85beea7644ed7e0fefebd654177fa06c26fbdfdc3c599a317f
 \tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
+\tfreq_list={freq_list}
 }}
 """
 
@@ -53,6 +55,7 @@
 \tkey_mgmt=NONE
 \tscan_ssid=1
 \tbssid=12:34:56:78:90:ab
+\tfreq_list={freq_list}
 }}
 """
 
diff --git a/wifi/qca9880_cal.py b/wifi/qca9880_cal.py
index 4e5cc0c..7c18454 100755
--- a/wifi/qca9880_cal.py
+++ b/wifi/qca9880_cal.py
@@ -9,10 +9,11 @@
 import glob
 import os
 import os.path
+import struct
 import experiment
 import utils
 
-NO_CAL_EXPERIMENT = 'WifiNoCalibrationPatch'
+CAL_EXPERIMENT = 'WifiCalibrationPatch'
 PLATFORM_FILE = '/etc/platform'
 CALIBRATION_DIR = '/tmp/ath10k_cal'
 CAL_PATCH_FILE = 'cal_data_patch.bin'
@@ -23,9 +24,32 @@
 VERSION_LEN = 3
 SUSPECT_OUIS = ((0x28, 0x24, 0xff), (0x48, 0xa9, 0xd2), (0x60, 0x02, 0xb4),
                 (0xbc, 0x30, 0x7d), (0xbc, 0x30, 0x7e))
-MISCALIBRATED_VERSION_FIELD = (0x0, 0x0, 0x0)
 MODULE_PATH = '/sys/class/net/{}/device/driver/module'
 
+# Each tuple starts with an offset, followed by a list of values to be
+# patched beginning at that offset.
+CAL_PATCH = ((0x050a, (0x5c, 0x68, 0xbd, 0xcd)),
+             (0x0510, (0x5c, 0x68, 0xbd, 0xcd)),
+             (0x0516, (0x5c, 0x68, 0xbd, 0xcd)),
+             (0x051c, (0x5c, 0x68, 0xbd, 0xcd)),
+             (0x0531, (0x2a, 0x28, 0x26)),
+             (0x0535, (0x2a, 0x28, 0x26)),
+             (0x056b, (0xce, 0x8a, 0x66, 0x02, 0x68, 0x26, 0x80, 0x66)),
+             (0x05b4, (0x8a, 0x46, 0x02, 0x68, 0x24, 0x80, 0x46)),
+             (0x05c0, (0x8a, 0x46, 0x02, 0x68, 0x24, 0x80, 0x46)),
+             (0x05fc, (0x8c, 0x68, 0x02, 0x88, 0x26, 0x80)),
+             (0x0608, (0x8c, 0x68, 0x02, 0x88, 0x26, 0x80)))
+
+FCC_PATCH = ((0x0625, (0x50, 0x58, 0x5c, 0x8c, 0xbd, 0xc1, 0xcd,
+                       0x4c, 0x50, 0x58, 0x5c, 0x8c, 0xbd, 0xc1,
+                       0xcd, 0x4e, 0x56, 0x5e, 0x66, 0x8e)),
+             (0x06b4, (0x69, 0x6b, 0x6b, 0x62, 0x62, 0x6b, 0x6c,
+                       0x2d, 0x69, 0x6b, 0x6b, 0x62, 0x62, 0x6b,
+                       0x6d, 0x2d, 0x62, 0x6f, 0x68, 0x64, 0x64,
+                       0x68, 0x68, 0x2d, 0x5c, 0x60, 0x60, 0x66)))
+
+experiment.register(CAL_EXPERIMENT)
+
 
 def _log(msg):
   utils.log('ath10k calibration: {}'.format(msg))
@@ -75,14 +99,23 @@
   """Check the QCA8990 module to see if it is improperly calibrated.
 
   There are two manufacturers of the modules, Senao and Wistron of which only
-  Wistron modules are suspect. Wistron provided a list of OUIs manufactured
-  which are listed in SUSPECT_OUIS. Modules manufactured by Winstron containing
-  V02 at offset VERSION_OFFSET have been corrected, while those containing 3
-  zero's at this offset are still suspect and will be considered mis-calibrated.
+  Wistron modules are suspect. Wistron provided a list of suspect OUIs
+  which are listed in SUSPECT_OUIS.
+
+  The version field must also be checked, starting at offset VERSION_OFFSET.
+  If this fields is all zeros, then it is an implicit indication of V01,
+  otherwise it contains a version string.
+
+  V01 -- (version field contains 0's) These modules need both calibration and
+         FCC power limits patched.
+  V02 -- Only FCC power limits need to be patched
+  V03 -- No patching required.
 
   Returns:
-    True if module is mis-calibrated, None if it can't be determined, and False
-    otherwise.
+    A tuple containing one or both of: fcc, cal. Or None.
+    'fcc' -- FCC patching required.
+    'cal' -- Calibration data patching required.
+    None  -- No patching required.
   """
 
   try:
@@ -94,7 +127,7 @@
       f.seek(OUI_OFFSET)
       oui = f.read(OUI_LEN)
       f.seek(VERSION_OFFSET)
-      version = f.read(VERSION_LEN)
+      version = struct.unpack('3s', f.read(VERSION_LEN))[0]
 
   except IOError as e:
     _log('unable to open cal_data {}: {}'.format(cal_data_path, e.strerror))
@@ -102,17 +135,27 @@
 
   if oui not in (bytearray(s) for s in SUSPECT_OUIS):
     _log('OUI {} is properly calibrated.'.format(_oui_string(oui)))
-    return False
+    # Create an empty directory so this script short-circuits if run again.
+    _create_calibration_dir()
+    return None
 
-  if version != (bytearray(MISCALIBRATED_VERSION_FIELD)):
-    _log('version field {} signals proper calibration.'.
-         format(_version_string(version)))
-    return False
+  # V01 is retroactively represented not by a string, but by 3 0 value bytes.
+  if version == '\x00\x00\x00':
+    _log('version field is V01. CAL + FCC calibration required.')
+    return ('fcc', 'cal')
 
-  _log('May be mis-calibrated. OUI: {} version: {}'.
-       format(_oui_string(oui), _version_string(version)))
+  if version == 'V02':
+    _log('version field is V02. Only FCC calibration required.')
+    return ('fcc',)
 
-  return True
+  if version == 'V03':
+    _log('version field is V03. No patching required.')
+    # Create an empty directory so this script short-circuits if run again.
+    _create_calibration_dir()
+    return None
+
+  _log('version field unknown: {}'.format(version))
+  return None
 
 
 def _is_previously_calibrated():
@@ -160,9 +203,18 @@
   return glob.glob(ATH10K_CAL_DATA)[0]
 
 
-def _generate_calibration_patch():
+def _apply_patch(msg, cal_data, patch):
+  _log(msg)
+  for offset, values in patch:
+    cal_data[offset:offset + len(values)] = values
+
+
+def _generate_calibration_patch(calibration_state):
   """Create calibration patch and write to storage.
 
+  Args:
+    calibration_state: data from ath10k to be patched.
+
   Returns:
     True for success or False for failure.
   """
@@ -174,11 +226,12 @@
          format(_ath10k_cal_data_path(), e.strerror))
     return False
 
-  # Patch cal_data here once we get the actual calibration data.
-  # For now just return False until we get the data.
-  _log('patch not generated as data not supplied yet.')
-  # pylint: disable=unreachable
-  return False
+  # Actual calibration starts here.
+  if 'cal' in calibration_state:
+    _apply_patch('Applying CAL patch...', cal_data, CAL_PATCH)
+
+  if 'fcc' in calibration_state:
+    _apply_patch('Applying FCC patch...', cal_data, FCC_PATCH)
 
   if not _create_calibration_dir():
     return False
@@ -211,8 +264,9 @@
 def qca8990_calibration():
   """Main QCA8990 calibration check."""
 
-  if experiment.enabled(NO_CAL_EXPERIMENT):
-    _log('experiment {} on. Skip calibration check.'.format(NO_CAL_EXPERIMENT))
+  if not experiment.enabled(CAL_EXPERIMENT):
+    _log('experiment {} not specified. Skipping calibration check.'.
+         format(CAL_EXPERIMENT))
     return
 
   if _is_previously_calibrated():
@@ -223,15 +277,9 @@
     _log('this platform does not use ath10k.')
     return
 
-  cal_result = _is_module_miscalibrated()
-  if cal_result is None:
-    _log('unknown if miscalibrated.')
-  elif not cal_result:
-    _log('module is NOT miscalibrated.')
-    # Creating an empty directory signals that this script has already run.
-    _create_calibration_dir()
-  else:
-    if _generate_calibration_patch():
+  calibration_state = _is_module_miscalibrated()
+  if calibration_state is not None:
+    if _generate_calibration_patch(calibration_state):
       _log('generated new patch.')
       _reload_driver()
 
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index bb541f5..9e0d26b 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -11,7 +11,7 @@
 
 
 def _get_quantenna_interfaces():
-  return subprocess.check_output(['get-quantenna-interfaces']).split()
+  return utils.read_or_empty('/sys/class/net/quantenna/vlan')
 
 
 def _qcsapi(*args):
@@ -27,13 +27,6 @@
   return ':'.join(octets)
 
 
-def _get_vlan(hif):
-  m = re.search(r'VID: (\d+)', utils.read_or_empty('/proc/net/vlan/%s' % hif))
-  if m:
-    return int(m.group(1))
-  raise utils.BinWifiException('no VLAN ID for interface %s' % hif)
-
-
 def _get_interfaces(mode, suffix):
   # Each host interface (hif) maps to exactly one LHOST interface (lif) based on
   # the VLAN ID as follows: the lif is wifiX where X is the VLAN ID - 2 (VLAN
@@ -41,11 +34,12 @@
   # VLAN ID 2.
   prefix = 'wlan' if mode == 'ap' else 'wcli'
   suffix = r'.*' if suffix == 'ALL' else suffix
-  for hif in _get_quantenna_interfaces():
+  for line in _get_quantenna_interfaces().splitlines():
+    hif, vlan = line.split()
+    vlan = int(vlan)
+    lif = 'wifi%d' % (vlan - 2)
+    mac = _get_external_mac(hif)
     if re.match(r'^' + prefix + r'\d*' + suffix + r'$', hif):
-      vlan = _get_vlan(hif)
-      lif = 'wifi%d' % (vlan - 2)
-      mac = _get_external_mac(hif)
       yield hif, lif, mac, vlan
 
 
@@ -64,8 +58,11 @@
 def _parse_scan_result(line):
   # Scan result format:
   #
-  # "Quantenna1" 00:26:86:00:11:5f 60 56 1 2 1 2 0 15 80
-  # |            |                 |  |  | | | | | |  |
+  # "Quantenna1" 00:26:86:00:11:5f 60 56 1 2 1 2 0 15 80 100 1 Infrastructure
+  # |            |                 |  |  | | | | | |  |  |   | |
+  # |            |                 |  |  | | | | | |  |  |   | Mode
+  # |            |                 |  |  | | | | | |  |  |   DTIM interval
+  # |            |                 |  |  | | | | | |  |  Beacon interval
   # |            |                 |  |  | | | | | |  Maximum bandwidth
   # |            |                 |  |  | | | | | WPS flags
   # |            |                 |  |  | | | | Qhop flags
@@ -80,7 +77,7 @@
   #
   # The SSID may contain quotes and spaces. Split on whitespace from the right,
   # making at most 10 splits, to preserve spaces in the SSID.
-  sp = line.strip().rsplit(None, 10)
+  sp = line.strip().rsplit(None, 13)
   return sp[0][1:-1], sp[1], int(sp[2]), -float(sp[3]), int(sp[4]), int(sp[5])
 
 
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index b0fb485..00400ed 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -16,37 +16,25 @@
 
 
 @wvtest.wvtest
-def get_vlan_test():
-  old_read_or_empty = utils.read_or_empty
-  utils.read_or_empty = lambda _: 'wlan0  VID: 3    REORDER_HDR: 1'
-  wvtest.WVPASSEQ(quantenna._get_vlan('wlan0'), 3)
-  utils.read_or_empty = lambda _: ''
-  wvtest.WVEXCEPT(utils.BinWifiException, quantenna._get_vlan, 'wlan0')
-  utils.read_or_empty = old_read_or_empty
-
-
-@wvtest.wvtest
 def get_interface_test():
   old_get_quantenna_interfaces = quantenna._get_quantenna_interfaces
   old_get_external_mac = quantenna._get_external_mac
-  old_get_vlan = quantenna._get_vlan
-  quantenna._get_quantenna_interfaces = lambda: ['wlan0', 'wlan0_portal']
+  quantenna._get_quantenna_interfaces = lambda: 'wlan0 3\nwlan0_portal 4\n'
   quantenna._get_external_mac = lambda _: '00:00:00:00:00:00'
-  quantenna._get_vlan = lambda _: 3
   wvtest.WVPASSEQ(quantenna._get_interface('ap', ''),
                   ('wlan0', 'wifi1', '00:00:00:00:00:00', 3))
   wvtest.WVPASSEQ(quantenna._get_interface('ap', '_portal'),
-                  ('wlan0_portal', 'wifi1', '00:00:00:00:00:00', 3))
+                  ('wlan0_portal', 'wifi2', '00:00:00:00:00:00', 4))
   wvtest.WVPASSEQ(quantenna._get_interface('sta', ''),
                   (None, None, None, None))
-  quantenna._get_vlan = old_get_vlan
   quantenna._get_external_mac = old_get_external_mac
   quantenna._get_quantenna_interfaces = old_get_quantenna_interfaces
 
 
 @wvtest.wvtest
 def parse_scan_result_test():
-  result = '  " ssid with "quotes" " 00:11:22:33:44:55 40 25 0 0 0 0 0 1 40  '
+  result = ('  " ssid with "quotes" " 00:11:22:33:44:55 40 25 0 0 0 0 0 1 40 '
+            '100 1 Infrastructure')
   wvtest.WVPASSEQ(quantenna._parse_scan_result(result),
                   (' ssid with "quotes" ', '00:11:22:33:44:55', 40, -25, 0, 0))