Merge "hnvram: Added option to choose target file + tests"
diff --git a/cmds/Makefile b/cmds/Makefile
index b058618..45580b2 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -33,6 +33,7 @@
 	dnsck \
 	freemegs \
 	gfhd254_reboot \
+	gflldpd \
 	gstatic \
 	http_bouncer \
 	ionice \
@@ -52,8 +53,10 @@
 LIB_TARGETS=\
 	stdoutline.so
 HOST_TEST_TARGETS=\
+	host-gflldpd_test \
 	host-netusage_test \
-	host-utils_test
+	host-utils_test \
+	host-isoping_test
 SCRIPT_TARGETS=\
 	is-secure-boot
 ARCH_TARGETS=\
@@ -148,6 +151,9 @@
 host-%.o: %.cc
 	$(HOST_CXX) $(CXXFLAGS) $(INCS) $(HOST_INCS) -DCOMPILE_FOR_HOST=1 -o $@ -c $<
 
+host-%.o: ../wvtest/cpp/%.cc
+	$(HOST_CXX) $(CXXFLAGS) -D WVTEST_CONFIGURED -o $@ -c $<
+
 %: %.o
 	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(LIBS) -lc
 
@@ -171,8 +177,12 @@
 	echo "Building .pb.cc"
 	$(HOST_PROTOC) --cpp_out=. $<
 
-
-host-isoping isoping: LIBS+=$(RT) -lm
+host-isoping isoping: LIBS+=$(RT) -lm -lstdc++
+host-isoping: host-isoping.o host-isoping_main.o
+host-isoping_test.o: CXXFLAGS += -D WVTEST_CONFIGURED -I ../wvtest/cpp
+host-isoping_test.o: isoping.cc
+host-isoping_test: LIBS+=$(HOST_LIBS) -lm -lstdc++
+host-isoping_test: host-isoping_test.o host-isoping.o host-wvtestmain.o host-wvtest.o
 host-isostream isostream: LIBS+=$(RT)
 host-diskbench diskbench: LIBS+=-lpthread $(RT)
 host-dnsck: LIBS+=$(HOST_LIBS) -lcares $(RT)
@@ -187,7 +197,7 @@
 host-alivemonitor alivemonitor: LIBS+=$(RT)
 host-buttonmon buttonmon: LIBS+=$(RT)
 alivemonitor: alivemonitor.o
-isoping: isoping.o
+isoping: isoping.o isoping_main.o
 isostream: isostream.o
 diskbench: diskbench.o
 dnsck: LIBS+=-lcares $(RT)
@@ -264,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/anonid.c b/cmds/anonid.c
index e7854ad..106f5fc 100644
--- a/cmds/anonid.c
+++ b/cmds/anonid.c
@@ -177,6 +177,7 @@
     usage(argv[0]);
   }
 
+  memset(anonid, 0, sizeof(anonid));
   get_anonid_for_mac(addr, anonid);
   printf("%s\n", anonid);
 
diff --git a/cmds/castcheck b/cmds/castcheck
index 70080ec..40c90cc 100755
--- a/cmds/castcheck
+++ b/cmds/castcheck
@@ -13,7 +13,7 @@
 while IFS=";" read ip; do
   cast_devices="$cast_devices $ip"
 done<<EOT
-$($AVAHI -tpvlr _googlecast._tcp | grep "^=" | cut -d";" -f8 | sort)
+$(timeout 10 $AVAHI -tpvlr _googlecast._tcp | grep "^=" | cut -d";" -f8 | sort)
 EOT
 
 echo "Cast responses from:$cast_devices"
diff --git a/cmds/dialcheck.cc b/cmds/dialcheck.cc
index 17f8fbd..d5ea202 100644
--- a/cmds/dialcheck.cc
+++ b/cmds/dialcheck.cc
@@ -290,7 +290,7 @@
   int s4, s6;
 
   setlinebuf(stdout);
-  alarm(30);
+  alarm(10);
 
   while ((c = getopt(argc, argv, "t:")) != -1) {
     switch(c) {
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/isoping.c b/cmds/isoping.c
deleted file mode 100644
index a669883..0000000
--- a/cmds/isoping.c
+++ /dev/null
@@ -1,657 +0,0 @@
-/*
- * Copyright 2014 Google Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- *
- * Like ping, but sends packets isochronously (equally spaced in time) in
- * each direction.  By being clever, we can use the known timing of each
- * packet to determine, on a noisy network, which direction is dropping or
- * delaying packets and by how much.
- *
- * Also unlike ping, this requires a server (ie. another copy of this
- * program) to be running on the remote end.
- */
-#include <arpa/inet.h>
-#include <errno.h>
-#include <math.h>
-#include <memory.h>
-#include <netdb.h>
-#include <netinet/in.h>
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <sys/select.h>
-#include <sys/socket.h>
-#include <sys/types.h>
-#include <time.h>
-#include <unistd.h>
-
-#ifndef CLOCK_MONOTONIC_RAW
-#define CLOCK_MONOTONIC_RAW 4
-#endif
-
-#define MAGIC 0x424c4950
-#define SERVER_PORT 4948
-#define DEFAULT_PACKETS_PER_SEC 10.0
-
-// A 'cycle' is the amount of time we can assume our calibration between
-// the local and remote monotonic clocks is reasonably valid.  It seems
-// some of our devices have *very* fast clock skew (> 1 msec/minute) so
-// this unfortunately has to be much shorter than I'd like.  This may
-// reflect actual bugs in our ntpd and/or the kernel's adjtime()
-// implementation.  In particular, we shouldn't have to do this kind of
-// periodic correction, because that's what adjtime() *is*.  But the results
-// are way off without this.
-#define USEC_PER_CYCLE (10*1000*1000)
-
-#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
-#define DIFF(x, y) ((int32_t)((uint32_t)(x) - (uint32_t)(y)))
-#define DIV(x, y) ((y) ? ((double)(x)/(y)) : 0)
-#define _STR(n) #n
-#define STR(n) _STR(n)
-
-
-// Layout of the UDP packets exchanged between client and server.
-// All integers are in network byte order.
-// Packets have exactly the same structure in both directions.
-struct Packet {
-  uint32_t magic;     // magic number to reject bogus packets
-  uint32_t id;        // sequential packet id number
-  uint32_t txtime;    // transmitter's monotonic time when pkt was sent
-  uint32_t clockdiff; // estimate of (transmitter's clk) - (receiver's clk)
-  uint32_t usec_per_pkt; // microseconds of delay between packets
-  uint32_t num_lost;  // number of pkts transmitter expected to get but didn't
-  uint32_t first_ack; // starting index in acks[] circular buffer
-  struct {
-    // txtime==0 for empty elements in this array.
-    uint32_t id;      // id field from a received packet
-    uint32_t rxtime;  // receiver's monotonic time when pkt arrived
-  } acks[64];
-};
-
-
-int want_to_die;
-
-
-static void sighandler(int sig) {
-  want_to_die = 1;
-}
-
-
-// Returns the kernel monotonic timestamp in microseconds, truncated to
-// 32 bits.  That will wrap around every ~4000 seconds, which is okay
-// for our purposes.  We use 32 bits to save space in our packets.
-// This function never returns the value 0; it returns 1 instead, so that
-// 0 can be used as a magic value.
-#ifdef __MACH__  // MacOS X doesn't have clock_gettime()
-#include <mach/mach.h>
-#include <mach/mach_time.h>
-
-static uint64_t ustime64(void) {
-  static mach_timebase_info_data_t timebase;
-  if (!timebase.denom) mach_timebase_info(&timebase);
-  uint64_t result = (mach_absolute_time() * timebase.numer /
-                     timebase.denom / 1000);
-  return !result ? 1 : result;
-}
-#else
-static uint64_t ustime64(void) {
-  // CLOCK_MONOTONIC_RAW, when available, is not subject to NTP speed
-  // adjustments while CLOCK_MONOTONIC is.  You might expect NTP speed
-  // adjustments to make things better if we're trying to sync timings
-  // between two machines, but at least our ntpd is pretty bad at making
-  // adjustments, so it tends the vary the speed wildly in order to kind
-  // of oscillate around the right time.  Experimentally, CLOCK_MONOTONIC_RAW
-  // creates less trouble for isoping's use case.
-  struct timespec ts;
-  if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts) < 0) {
-    if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
-      perror("clock_gettime");
-      exit(98); // really should never happen, so don't try to recover
-    }
-  }
-  uint64_t result = ts.tv_sec * 1000000LL + ts.tv_nsec / 1000;
-  return !result ? 1 : result;
-}
-#endif
-
-
-static uint32_t ustime(void) {
-  return (uint32_t)ustime64();
-}
-
-
-static void usage_and_die(char *argv0) {
-  fprintf(stderr,
-          "\n"
-          "Usage: %s                          (server mode)\n"
-          "   or: %s <server-hostname-or-ip>  (client mode)\n"
-          "\n"
-          "      -f <lines/sec>  max output lines per second\n"
-          "      -r <pps>        packets per second (default=%g)\n"
-          "      -t <ttl>        packet ttl to use (default=2 for safety)\n"
-          "      -q              quiet mode (don't print packets)\n"
-          "      -T              print timestamps\n",
-          argv0, argv0, (double)DEFAULT_PACKETS_PER_SEC);
-  exit(99);
-}
-
-
-// Render the given sockaddr as a string.  (Uses a static internal buffer
-// which is overwritten each time.)
-static const char *sockaddr_to_str(struct sockaddr *sa) {
-  static char addrbuf[128];
-  void *aptr;
-
-  switch (sa->sa_family) {
-  case AF_INET:
-    aptr = &((struct sockaddr_in *)sa)->sin_addr;
-    break;
-  case AF_INET6:
-    aptr = &((struct sockaddr_in6 *)sa)->sin6_addr;
-    break;
-  default:
-    return "unknown";
-  }
-
-  if (!inet_ntop(sa->sa_family, aptr, addrbuf, sizeof(addrbuf))) {
-    perror("inet_ntop");
-    exit(98);
-  }
-  return addrbuf;
-}
-
-
-// Print the timestamp corresponding to the current time.
-// Deliberately the same format as tcpdump uses, so we can easily sort and
-// correlate messages between isoping and tcpdump.
-static void print_timestamp(uint32_t when) {
-  uint64_t now = ustime64();
-  int32_t nowdiff = DIFF(now, when);
-  uint64_t when64 = now - nowdiff;
-  time_t t = when64 / 1000000;
-  struct tm tm;
-  memset(&tm, 0, sizeof(tm));
-  localtime_r(&t, &tm);
-  printf("%02d:%02d:%02d.%06d ", tm.tm_hour, tm.tm_min, tm.tm_sec,
-         (int)(when64 % 1000000));
-}
-
-
-static double onepass_stddev(long long sumsq, long long sum, long long count) {
-  // Incremental standard deviation calculation, without needing to know the
-  // mean in advance.  See:
-  // http://mathcentral.uregina.ca/QQ/database/QQ.09.02/carlos1.html
-  long long numer = (count * sumsq) - (sum * sum);
-  long long denom = count * (count - 1);
-  return sqrt(DIV(numer, denom));
-}
-
-
-int main(int argc, char **argv) {
-  int is_server = 1;
-  struct sockaddr_in6 listenaddr, rxaddr, last_rxaddr;
-  struct sockaddr *remoteaddr = NULL;
-  socklen_t remoteaddr_len = 0, rxaddr_len = 0;
-  struct addrinfo *ai = NULL;
-  int sock = -1, want_timestamps = 0, quiet = 0, ttl = 2;
-  double packets_per_sec = DEFAULT_PACKETS_PER_SEC, prints_per_sec = -1;
-
-  setvbuf(stdout, NULL, _IOLBF, 0);
-
-  int c;
-  while ((c = getopt(argc, argv, "f:r:t:qTh?")) >= 0) {
-    switch (c) {
-    case 'f':
-      prints_per_sec = atof(optarg);
-      if (prints_per_sec <= 0) {
-        fprintf(stderr, "%s: lines per second must be >= 0\n", argv[0]);
-        return 99;
-      }
-      break;
-    case 'r':
-      packets_per_sec = atof(optarg);
-      if (packets_per_sec < 0.001 || packets_per_sec > 1e6) {
-        fprintf(stderr, "%s: packets per sec (-r) must be 0.001..1000000\n",
-                argv[0]);
-        return 99;
-      }
-      break;
-    case 't':
-      ttl = atoi(optarg);
-      if (ttl < 1) {
-        fprintf(stderr, "%s: ttl must be >= 1\n", argv[0]);
-        return 99;
-      }
-      break;
-    case 'q':
-      quiet = 1;
-      break;
-    case 'T':
-      want_timestamps = 1;
-      break;
-    case 'h':
-    case '?':
-    default:
-      usage_and_die(argv[0]);
-      break;
-    }
-  }
-
-  sock = socket(PF_INET6, SOCK_DGRAM, 0);
-  if (sock < 0) {
-    perror("socket");
-    return 1;
-  }
-
-  if (argc - optind == 0) {
-    is_server = 1;
-    memset(&listenaddr, 0, sizeof(listenaddr));
-    listenaddr.sin6_family = AF_INET6;
-    listenaddr.sin6_port = htons(SERVER_PORT);
-    if (bind(sock, (struct sockaddr *)&listenaddr, sizeof(listenaddr)) != 0) {
-      perror("bind");
-      return 1;
-    }
-    socklen_t addrlen = sizeof(listenaddr);
-    if (getsockname(sock, (struct sockaddr *)&listenaddr, &addrlen) != 0) {
-      perror("getsockname");
-      return 1;
-    }
-    fprintf(stderr, "server listening at [%s]:%d\n",
-           sockaddr_to_str((struct sockaddr *)&listenaddr),
-           ntohs(listenaddr.sin6_port));
-  } else if (argc - optind == 1) {
-    const char *remotename = argv[optind];
-    is_server = 0;
-    struct addrinfo hints = {
-      .ai_flags = AI_ADDRCONFIG | AI_V4MAPPED,
-      .ai_family = AF_INET6,
-      .ai_socktype = SOCK_DGRAM,
-    };
-    int err = getaddrinfo(remotename, STR(SERVER_PORT), &hints, &ai);
-    if (err != 0 || !ai) {
-      fprintf(stderr, "getaddrinfo(%s): %s\n", remotename, gai_strerror(err));
-      return 1;
-    }
-    fprintf(stderr, "connecting to %s...\n", sockaddr_to_str(ai->ai_addr));
-    if (connect(sock, ai->ai_addr, ai->ai_addrlen) != 0) {
-      perror("connect");
-      return 1;
-    }
-    remoteaddr = ai->ai_addr;
-    remoteaddr_len = ai->ai_addrlen;
-  } else {
-    usage_and_die(argv[0]);
-  }
-
-  fprintf(stderr, "using ttl=%d\n", ttl);
-  // IPPROTO_IPV6 is the only one that works on MacOS, and is arguably the
-  // technically correct thing to do since it's an AF_INET6 socket.
-  if (setsockopt(sock, IPPROTO_IPV6, IP_TTL, &ttl, sizeof(ttl))) {
-    perror("setsockopt(TTLv6)");
-    return 1;
-  }
-  // ...but in Linux (at least 3.13), IPPROTO_IPV6 does not actually
-  // set the TTL if the IPv6 socket ends up going over IPv4.  We have to
-  // set that separately.  On MacOS, that always returns EINVAL, so ignore
-  // the error if that happens.
-  if (setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl))) {
-    if (errno != EINVAL) {
-      perror("setsockopt(TTLv4)");
-      return 1;
-    }
-  }
-
-  int32_t usec_per_pkt = 1e6 / packets_per_sec;
-  int32_t usec_per_print = prints_per_sec > 0 ? 1e6 / prints_per_sec : 0;
-
-  // WARNING: lots of math below relies on well-defined uint32/int32
-  // arithmetic overflow behaviour, plus the fact that when we subtract
-  // two successive timestamps (for example) they will be less than 2^31
-  // microseconds apart.  It would be safer to just use 64-bit values
-  // everywhere, but that would cut the number of acks-per-packet in half,
-  // which would be unfortunate.
-  uint32_t next_tx_id = 1;       // id field for next transmit
-  uint32_t next_rx_id = 0;       // expected id field for next receive
-  uint32_t next_rxack_id = 0;    // expected ack.id field in next received ack
-  uint32_t start_rtxtime = 0;    // remote's txtime at startup
-  uint32_t start_rxtime = 0;     // local rxtime at startup
-  uint32_t last_rxtime = 0;      // local rxtime of last received packet
-  int32_t min_cycle_rxdiff = 0;  // smallest packet delay seen this cycle
-  uint32_t next_cycle = 0;       // time when next cycle begins
-  uint32_t now = ustime();       // current time
-  uint32_t next_send = now + usec_per_pkt;  // time when we'll send next pkt
-  uint32_t num_lost = 0;         // number of rx packets not received
-  int next_txack_index = 0;      // next array item to fill in tx.acks
-  struct Packet tx, rx;          // transmit and received packet buffers
-  char last_ackinfo[128] = "";   // human readable format of latest ack
-  uint32_t last_print = now - usec_per_pkt;  // time of last packet printout
-  // Packet statistics counters for transmit and receive directions.
-  long long lat_tx = 0, lat_tx_min = 0x7fffffff, lat_tx_max = 0,
-      lat_tx_count = 0, lat_tx_sum = 0, lat_tx_var_sum = 0;
-  long long lat_rx = 0, lat_rx_min = 0x7fffffff, lat_rx_max = 0,
-      lat_rx_count = 0, lat_rx_sum = 0, lat_rx_var_sum = 0;
-
-  memset(&tx, 0, sizeof(tx));
-
-  struct sigaction act = {
-    .sa_handler = sighandler,
-    .sa_flags = SA_RESETHAND,
-  };
-  sigaction(SIGINT, &act, NULL);
-
-  while (!want_to_die) {
-    fd_set rfds;
-    FD_ZERO(&rfds);
-    FD_SET(sock, &rfds);
-    struct timeval tv;
-    tv.tv_sec = 0;
-
-    now = ustime();
-    if (DIFF(next_send, now) < 0) {
-      tv.tv_usec = 0;
-    } else {
-      tv.tv_usec = DIFF(next_send, now);
-    }
-    int nfds = select(sock + 1, &rfds, NULL, NULL, remoteaddr ? &tv : NULL);
-    now = ustime();
-    if (nfds < 0 && errno != EINTR) {
-      perror("select");
-      return 1;
-    }
-
-    // time to send the next packet?
-    if (remoteaddr && DIFF(now, next_send) >= 0) {
-      tx.magic = htonl(MAGIC);
-      tx.id = htonl(next_tx_id++);
-      tx.usec_per_pkt = htonl(usec_per_pkt);
-      tx.txtime = htonl(next_send);
-      tx.clockdiff = start_rtxtime ? htonl(start_rxtime - start_rtxtime) : 0;
-      tx.num_lost = htonl(num_lost);
-      tx.first_ack = htonl(next_txack_index);
-      // note: tx.acks[] is filled in incrementally; we just transmit the
-      // current state of it here.  The reason we keep a list of the most
-      // recent acks is in case our packet gets lost, so the receiver will
-      // have more chances to receive the timing information for the packets
-      // it sent us.
-      if (is_server) {
-        if (sendto(sock, &tx, sizeof(tx), 0,
-                   remoteaddr, remoteaddr_len) < 0) {
-          perror("sendto");
-        }
-      } else {
-        if (send(sock, &tx, sizeof(tx), 0) < 0) {
-          int e = errno;
-          perror("send");
-          if (e == ECONNREFUSED) return 2;
-        }
-      }
-      if (is_server && DIFF(now, last_rxtime) > 60*1000*1000) {
-        fprintf(stderr, "client disconnected.\n");
-        remoteaddr = NULL;
-      }
-      next_send += usec_per_pkt;
-    }
-
-    if (nfds > 0) {
-      // incoming packet
-      rxaddr_len = sizeof(rxaddr);
-      ssize_t got = recvfrom(sock, &rx, sizeof(rx), 0,
-                             (struct sockaddr *)&rxaddr, &rxaddr_len);
-      if (got < 0) {
-        int e = errno;
-        perror("recvfrom");
-        if (!is_server && e == ECONNREFUSED) return 2;
-        continue;
-      }
-      if (got != sizeof(rx) || rx.magic != htonl(MAGIC)) {
-        fprintf(stderr, "got invalid packet of length %ld\n", (long)got);
-        continue;
-      }
-
-      // is it a new client?
-      if (is_server) {
-        // TODO(apenwarr): fork() for each newly connected client.
-        //  The way the code is right now, it won't work right at all
-        //  if there are two clients sending us packets at once.
-        //  That's okay for a first round of controlled tests.
-        if (!remoteaddr ||
-            memcmp(&rxaddr, &last_rxaddr, sizeof(rxaddr)) != 0) {
-          fprintf(stderr, "new client connected: %s\n",
-                  sockaddr_to_str((struct sockaddr *)&rxaddr));
-          memcpy(&last_rxaddr, &rxaddr, sizeof(rxaddr));
-          remoteaddr = (struct sockaddr *)&last_rxaddr;
-          remoteaddr_len = rxaddr_len;
-
-          next_send = now + 10*1000;
-          next_tx_id = 1;
-          next_rx_id = next_rxack_id = 0;
-          start_rtxtime = start_rxtime = 0;
-          num_lost = 0;
-          next_txack_index = 0;
-          usec_per_pkt = ntohl(rx.usec_per_pkt);
-          memset(&tx, 0, sizeof(tx));
-        }
-      }
-
-      // process the incoming packet header.
-      // Most of the complexity here comes from the fact that the remote
-      // system's clock will be skewed vs. ours.  (We use CLOCK_MONOTONIC
-      // instead of CLOCK_REALTIME, so unless we figure out the skew offset,
-      // it's essentially meaningless to compare the two values.)  We can
-      // however assume that both clocks are ticking at 1 microsecond per
-      // tick... except for inevitable clock rate errors, which we have to
-      // account for occasionally.
-
-      uint32_t txtime = ntohl(rx.txtime), rxtime = now;
-      uint32_t id = ntohl(rx.id);
-      if (!next_rx_id) {
-        // The remote txtime is told to us by the sender, so it is always
-        // perfectly correct... but it uses the sender's clock.
-        start_rtxtime = txtime - id * usec_per_pkt;
-
-        // The receive time uses our own clock and is estimated by us, so
-        // it needs to be corrected over time because:
-        //   a) the two clocks inevitably run at slightly different speeds;
-        //   b) there's an unknown, variable, network delay between tx and rx.
-        // Here, we're just assigning an initial estimate.
-        start_rxtime = rxtime - id * usec_per_pkt;
-
-        min_cycle_rxdiff = 0;
-        next_rx_id = id;
-        next_cycle = now + USEC_PER_CYCLE;
-      }
-
-      // see if we missed receiving any previous packets.
-      int32_t tmpdiff = DIFF(id, next_rx_id);
-      if (tmpdiff > 0) {
-        // arriving packet has id > expected, so something was lost.
-        // Note that we don't use the rx.acks[] structure to determine packet
-        // loss; that's because the limited size of the array means that,
-        // during a longer outage, we might not see an ack for a packet
-        // *even if that packet arrived safely* at the remote.  So we count
-        // on the remote end to count its own packet losses using sequence
-        // numbers, and send that count back to us.  We do the same here
-        // for incoming packets from the remote, and send the error count
-        // back to them next time we're ready to transmit.
-        fprintf(stderr, "lost %ld  expected=%ld  got=%ld\n",
-                (long)tmpdiff, (long)next_rx_id, (long)id);
-        num_lost += tmpdiff;
-        next_rx_id += tmpdiff + 1;
-      } else if (!tmpdiff) {
-        // exactly as expected; good.
-        next_rx_id++;
-      } else if (tmpdiff < 0) {
-        // packet before the expected one? weird.
-        fprintf(stderr, "out-of-order packets? %ld\n", (long)tmpdiff);
-      }
-
-      // fix up the clock offset if there's any drift.
-      tmpdiff = DIFF(rxtime, start_rxtime + id * usec_per_pkt);
-      if (tmpdiff < -20) {
-        // packet arrived before predicted time, so prediction was based on
-        // a packet that was "slow" before, or else one of our clocks is
-        // drifting. Use earliest legitimate start time.
-        fprintf(stderr, "time paradox: backsliding start by %ld usec\n",
-                (long)tmpdiff);
-        start_rxtime = rxtime - id * usec_per_pkt;
-      }
-      int32_t rxdiff = DIFF(rxtime, start_rxtime + id * usec_per_pkt);
-
-      // Figure out the offset between our clock and the remote's clock, so
-      // we can calculate the minimum round trip time (rtt). Then, because
-      // the consecutive packets sent in both directions are equally spaced
-      // in time, we can figure out how much a particular packet was delayed
-      // in transit - independently in each direction! This is an advantage
-      // over the normal "ping" program which has no way to tell which
-      // direction caused the delay, or which direction dropped the packet.
-
-      // Our clockdiff is
-      //   (our rx time) - (their tx time)
-      //   == (their rx time + offset) - (their tx time)
-      //   == (their tx time + offset + 1/2 rtt) - (their tx time)
-      //   == offset + 1/2 rtt
-      // and theirs (rx.clockdiff) is:
-      //   (their rx time) - (our tx time)
-      //   == (their rx time) - (their tx time + offset)
-      //   == (their tx time + 1/2 rtt) - (their tx time + offset)
-      //   == 1/2 rtt - offset
-      // So add them together and we get:
-      //   offset + 1/2 rtt + 1/2 rtt - offset
-      //   == rtt
-      // Subtract them and we get:
-      //   offset + 1/2 rtt - 1/2 rtt + offset
-      //   == 2 * offset
-      // ...but that last subtraction is dangerous because if we divide by
-      // 2 to get offset, it doesn't work with 32-bit math, which may have
-      // discarded a high-order bit somewhere along the way.  Instead,
-      // we can extract offset once we have rtt by substituting it into
-      //   clockdiff = offset + 1/2 rtt
-      //   offset = clockdiff - 1/2 rtt
-      // (Dividing rtt by 2 is safe since it's always small and positive.)
-      //
-      // (This example assumes 1/2 rtt in each direction. There's no way to
-      // determine it more accurately than that.)
-      int32_t clockdiff = DIFF(start_rxtime, start_rtxtime);
-      int32_t rtt = clockdiff + ntohl(rx.clockdiff);
-      int32_t offset = DIFF(clockdiff, rtt / 2);
-      if (!ntohl(rx.clockdiff)) {
-        // don't print the first packet: it has an invalid clockdiff since
-        // the client can't calculate the clockdiff until it receives
-        // at least one packet from us.
-        last_print = now - usec_per_print + 1;
-      } else {
-        // not the first packet, so statistics are valid.
-        lat_rx_count++;
-        lat_rx = rxdiff + rtt/2;
-        lat_rx_min = lat_rx_min > lat_rx ? lat_rx : lat_rx_min;
-        lat_rx_max = lat_rx_max < lat_rx ? lat_rx : lat_rx_max;
-        lat_rx_sum += lat_rx;
-        lat_rx_var_sum += lat_rx * lat_rx;
-      }
-
-      // Note: the way ok_to_print is structured, if there is a dropout in
-      // the connection for more than usec_per_print, we will statistically
-      // end up printing the first packet after the dropout ends.  That one
-      // should have the longest timeout, ie. a "worst case" packet, which is
-      // usually the information you want to see.
-      int ok_to_print = !quiet && DIFF(now, last_print) >= usec_per_print;
-      if (ok_to_print) {
-        if (want_timestamps) print_timestamp(rxtime);
-        printf("%12s  %6.1f ms rx  (min=%.1f)  loss: %ld/%ld tx  %ld/%ld rx\n",
-               last_ackinfo,
-               (rxdiff + rtt/2) / 1000.0,
-               (rtt/2) / 1000.0,
-               (long)ntohl(rx.num_lost),
-               (long)next_tx_id - 1,
-               (long)num_lost,
-               (long)next_rx_id - 1);
-        last_ackinfo[0] = '\0';
-        last_print = now;
-      }
-
-      if (rxdiff < min_cycle_rxdiff) min_cycle_rxdiff = rxdiff;
-      if (DIFF(now, next_cycle) >= 0) {
-        if (min_cycle_rxdiff > 0) {
-          fprintf(stderr, "clock skew: sliding start by %ld usec\n",
-                  (long)min_cycle_rxdiff);
-          start_rxtime += min_cycle_rxdiff;
-        }
-        min_cycle_rxdiff = 0x7fffffff;
-        next_cycle += USEC_PER_CYCLE;
-      }
-
-      // schedule this for an ack next time we send the packet
-      tx.acks[next_txack_index].id = htonl(id);
-      tx.acks[next_txack_index].rxtime = htonl(rxtime);
-      next_txack_index = (next_txack_index + 1) % ARRAY_LEN(tx.acks);
-
-      // see which of our own transmitted packets have been acked
-      uint32_t first_ack = ntohl(rx.first_ack);
-      for (uint32_t i = 0; i < ARRAY_LEN(rx.acks); i++) {
-        uint32_t acki = (first_ack + i) % ARRAY_LEN(rx.acks);
-        uint32_t ackid = ntohl(rx.acks[acki].id);
-        if (!ackid) continue;  // empty slot
-        if (DIFF(ackid, next_rxack_id) >= 0) {
-          // an expected ack
-          uint32_t start_txtime = next_send - next_tx_id * usec_per_pkt;
-          uint32_t txtime = start_txtime + ackid * usec_per_pkt;
-          uint32_t rrxtime = ntohl(rx.acks[acki].rxtime);
-          uint32_t rxtime = rrxtime + offset;
-          // note: already contains 1/2 rtt, unlike rxdiff
-          int32_t txdiff = DIFF(rxtime, txtime);
-          if (usec_per_print <= 0 && last_ackinfo[0]) {
-            // only print multiple acks per rx if no usec_per_print limit
-            if (want_timestamps) print_timestamp(rxtime);
-            printf("%12s\n", last_ackinfo);
-            last_ackinfo[0] = '\0';
-          }
-          if (!last_ackinfo[0]) {
-            snprintf(last_ackinfo, sizeof(last_ackinfo), "%6.1f ms tx",
-                     txdiff / 1000.0);
-          }
-          next_rxack_id = ackid + 1;
-          lat_tx_count++;
-          lat_tx = txdiff;
-          lat_tx_min = lat_tx_min > lat_tx ? lat_tx : lat_tx_min;
-          lat_tx_max = lat_tx_max < lat_tx ? lat_tx : lat_tx_max;
-          lat_tx_sum += lat_tx;
-          lat_tx_var_sum += lat_tx * lat_tx;
-        }
-      }
-
-      last_rxtime = rxtime;
-    }
-  }
-
-  printf("\n---\n");
-  printf("tx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
-         lat_tx_min / 1000.0,
-         DIV(lat_tx_sum, lat_tx_count) / 1000.0,
-         lat_tx_max / 1000.0,
-         onepass_stddev(lat_tx_var_sum, lat_tx_sum, lat_tx_count) / 1000.0);
-  printf("rx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
-         lat_rx_min / 1000.0,
-         DIV(lat_rx_sum, lat_rx_count) / 1000.0,
-         lat_rx_max / 1000.0,
-         onepass_stddev(lat_rx_var_sum, lat_rx_sum, lat_rx_count) / 1000.0);
-  printf("\n");
-
-  if (ai) freeaddrinfo(ai);
-  if (sock >= 0) close(sock);
-  return 0;
-}
diff --git a/cmds/isoping.cc b/cmds/isoping.cc
new file mode 100644
index 0000000..c6b123e
--- /dev/null
+++ b/cmds/isoping.cc
@@ -0,0 +1,663 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/*
+ *
+ * Like ping, but sends packets isochronously (equally spaced in time) in
+ * each direction.  By being clever, we can use the known timing of each
+ * packet to determine, on a noisy network, which direction is dropping or
+ * delaying packets and by how much.
+ *
+ * Also unlike ping, this requires a server (ie. another copy of this
+ * program) to be running on the remote end.
+ */
+#include "isoping.h"
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <math.h>
+#include <memory.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifndef CLOCK_MONOTONIC_RAW
+#define CLOCK_MONOTONIC_RAW 4
+#endif
+
+#define MAGIC 0x424c4950
+#define SERVER_PORT 4948
+#define DEFAULT_PACKETS_PER_SEC 10.0
+#define DEFAULT_TTL 2
+
+// A 'cycle' is the amount of time we can assume our calibration between
+// the local and remote monotonic clocks is reasonably valid.  It seems
+// some of our devices have *very* fast clock skew (> 1 msec/minute) so
+// this unfortunately has to be much shorter than I'd like.  This may
+// reflect actual bugs in our ntpd and/or the kernel's adjtime()
+// implementation.  In particular, we shouldn't have to do this kind of
+// periodic correction, because that's what adjtime() *is*.  But the results
+// are way off without this.
+#define USEC_PER_CYCLE (10*1000*1000)
+
+#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
+#define DIFF(x, y) ((int32_t)((uint32_t)(x) - (uint32_t)(y)))
+#define DIV(x, y) ((y) ? ((double)(x)/(y)) : 0)
+#define _STR(n) #n
+#define STR(n) _STR(n)
+
+// Global flag values.
+int is_server = 1;
+int quiet = 0;
+int ttl = DEFAULT_TTL;
+int want_timestamps = 0;
+double packets_per_sec = DEFAULT_PACKETS_PER_SEC;
+double prints_per_sec = -1.0;
+
+int want_to_die;
+
+
+static void sighandler(int sig) {
+  want_to_die = 1;
+}
+
+Session::Session(uint32_t now)
+    : usec_per_pkt(1e6 / packets_per_sec),
+      usec_per_print(prints_per_sec > 0 ? 1e6 / prints_per_sec : 0),
+      next_tx_id(1),
+      next_rx_id(0),
+      next_rxack_id(0),
+      start_rtxtime(0),
+      start_rxtime(0),
+      last_rxtime(0),
+      min_cycle_rxdiff(0),
+      next_cycle(0),
+      next_send(now + usec_per_pkt),
+      num_lost(0),
+      next_txack_index(0),
+      last_print(now - usec_per_pkt),
+      lat_tx(0), lat_tx_min(0x7fffffff), lat_tx_max(0),
+      lat_tx_count(0), lat_tx_sum(0), lat_tx_var_sum(0),
+      lat_rx(0), lat_rx_min(0x7fffffff), lat_rx_max(0),
+      lat_rx_count(0), lat_rx_sum(0), lat_rx_var_sum(0) {
+  memset(&tx, 0, sizeof(tx));
+  strcpy(last_ackinfo, "");
+}
+
+// Returns the kernel monotonic timestamp in microseconds, truncated to
+// 32 bits.  That will wrap around every ~4000 seconds, which is okay
+// for our purposes.  We use 32 bits to save space in our packets.
+// This function never returns the value 0; it returns 1 instead, so that
+// 0 can be used as a magic value.
+#ifdef __MACH__  // MacOS X doesn't have clock_gettime()
+#include <mach/mach.h>
+#include <mach/mach_time.h>
+
+static uint64_t ustime64(void) {
+  static mach_timebase_info_data_t timebase;
+  if (!timebase.denom) mach_timebase_info(&timebase);
+  uint64_t result = (mach_absolute_time() * timebase.numer /
+                     timebase.denom / 1000);
+  return !result ? 1 : result;
+}
+#else
+static uint64_t ustime64(void) {
+  // CLOCK_MONOTONIC_RAW, when available, is not subject to NTP speed
+  // adjustments while CLOCK_MONOTONIC is.  You might expect NTP speed
+  // adjustments to make things better if we're trying to sync timings
+  // between two machines, but at least our ntpd is pretty bad at making
+  // adjustments, so it tends the vary the speed wildly in order to kind
+  // of oscillate around the right time.  Experimentally, CLOCK_MONOTONIC_RAW
+  // creates less trouble for isoping's use case.
+  struct timespec ts;
+  if (clock_gettime(CLOCK_MONOTONIC_RAW, &ts) < 0) {
+    if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
+      perror("clock_gettime");
+      exit(98); // really should never happen, so don't try to recover
+    }
+  }
+  uint64_t result = ts.tv_sec * 1000000LL + ts.tv_nsec / 1000;
+  return !result ? 1 : result;
+}
+#endif
+
+
+static uint32_t ustime(void) {
+  return (uint32_t)ustime64();
+}
+
+
+static void usage_and_die(char *argv0) {
+  fprintf(stderr,
+          "\n"
+          "Usage: %s                          (server mode)\n"
+          "   or: %s <server-hostname-or-ip>  (client mode)\n"
+          "\n"
+          "      -f <lines/sec>  max output lines per second\n"
+          "      -r <pps>        packets per second (default=%g)\n"
+          "      -t <ttl>        packet ttl to use (default=2 for safety)\n"
+          "      -q              quiet mode (don't print packets)\n"
+          "      -T              print timestamps\n",
+          argv0, argv0, (double)DEFAULT_PACKETS_PER_SEC);
+  exit(99);
+}
+
+
+// Render the given sockaddr as a string.  (Uses a static internal buffer
+// which is overwritten each time.)
+static const char *sockaddr_to_str(struct sockaddr *sa) {
+  static char addrbuf[128];
+  void *aptr;
+
+  switch (sa->sa_family) {
+  case AF_INET:
+    aptr = &((struct sockaddr_in *)sa)->sin_addr;
+    break;
+  case AF_INET6:
+    aptr = &((struct sockaddr_in6 *)sa)->sin6_addr;
+    break;
+  default:
+    return "unknown";
+  }
+
+  if (!inet_ntop(sa->sa_family, aptr, addrbuf, sizeof(addrbuf))) {
+    perror("inet_ntop");
+    exit(98);
+  }
+  return addrbuf;
+}
+
+
+// Print the timestamp corresponding to the current time.
+// Deliberately the same format as tcpdump uses, so we can easily sort and
+// correlate messages between isoping and tcpdump.
+static void print_timestamp(uint32_t when) {
+  uint64_t now = ustime64();
+  int32_t nowdiff = DIFF(now, when);
+  uint64_t when64 = now - nowdiff;
+  time_t t = when64 / 1000000;
+  struct tm tm;
+  memset(&tm, 0, sizeof(tm));
+  localtime_r(&t, &tm);
+  printf("%02d:%02d:%02d.%06d ", tm.tm_hour, tm.tm_min, tm.tm_sec,
+         (int)(when64 % 1000000));
+}
+
+
+static double onepass_stddev(long long sumsq, long long sum, long long count) {
+  // Incremental standard deviation calculation, without needing to know the
+  // mean in advance.  See:
+  // http://mathcentral.uregina.ca/QQ/database/QQ.09.02/carlos1.html
+  long long numer = (count * sumsq) - (sum * sum);
+  long long denom = count * (count - 1);
+  return sqrt(DIV(numer, denom));
+}
+
+
+void prepare_tx_packet(struct Session *s) {
+  s->tx.magic = htonl(MAGIC);
+  s->tx.id = htonl(s->next_tx_id++);
+  s->tx.usec_per_pkt = htonl(s->usec_per_pkt);
+  s->tx.txtime = htonl(s->next_send);
+  s->tx.clockdiff = s->start_rtxtime ?
+      htonl(s->start_rxtime - s->start_rtxtime) : 0;
+  s->tx.num_lost = htonl(s->num_lost);
+  s->tx.first_ack = htonl(s->next_txack_index);
+}
+
+static int send_packet(struct Session *s,
+                       int sock,
+                       struct sockaddr *remoteaddr,
+                       socklen_t remoteaddr_len) {
+  // note: tx.acks[] is filled in incrementally; we just transmit the current
+  // state of it here.  The reason we keep a list of the most recent acks is in
+  // case our packet gets lost, so the receiver will have more chances to
+  // receive the timing information for the packets it sent us.
+  if (is_server) {
+    if (sendto(sock, &s->tx, sizeof(s->tx), 0,
+               remoteaddr, remoteaddr_len) < 0) {
+      perror("sendto");
+    }
+  } else {
+    if (send(sock, &s->tx, sizeof(s->tx), 0) < 0) {
+      int e = errno;
+      perror("send");
+      if (e == ECONNREFUSED) return 2;
+    }
+  }
+  s->next_send += s->usec_per_pkt;
+  return 0;
+}
+
+
+void handle_packet(struct Session *s, uint32_t now) {
+  // process the incoming packet header.
+  // Most of the complexity here comes from the fact that the remote
+  // system's clock will be skewed vs. ours.  (We use CLOCK_MONOTONIC
+  // instead of CLOCK_REALTIME, so unless we figure out the skew offset,
+  // it's essentially meaningless to compare the two values.)  We can
+  // however assume that both clocks are ticking at 1 microsecond per
+  // tick... except for inevitable clock rate errors, which we have to
+  // account for occasionally.
+
+  uint32_t txtime = ntohl(s->rx.txtime), rxtime = now;
+  uint32_t id = ntohl(s->rx.id);
+  if (!s->next_rx_id) {
+    // The remote txtime is told to us by the sender, so it is always perfectly
+    // correct... but it uses the sender's clock.
+    s->start_rtxtime = txtime - id * s->usec_per_pkt;
+
+    // The receive time uses our own clock and is estimated by us, so it needs
+    // to be corrected over time because:
+    //   a) the two clocks inevitably run at slightly different speeds;
+    //   b) there's an unknown, variable, network delay between tx and rx.
+    // Here, we're just assigning an initial estimate.
+    s->start_rxtime = rxtime - id * s->usec_per_pkt;
+
+    s->min_cycle_rxdiff = 0;
+    s->next_rx_id = id;
+    s->next_cycle = now + USEC_PER_CYCLE;
+  }
+
+  // see if we missed receiving any previous packets.
+  int32_t tmpdiff = DIFF(id, s->next_rx_id);
+  if (tmpdiff > 0) {
+    // arriving packet has id > expected, so something was lost.
+    // Note that we don't use the rx.acks[] structure to determine packet loss;
+    // that's because the limited size of the array means that, during a longer
+    // outage, we might not see an ack for a packet *even if that packet arrived
+    // safely* at the remote.  So we count on the remote end to count its own
+    // packet losses using sequence numbers, and send that count back to us.  We
+    // do the same here for incoming packets from the remote, and send the error
+    // count back to them next time we're ready to transmit.
+    fprintf(stderr, "lost %ld  expected=%ld  got=%ld\n",
+            (long)tmpdiff, (long)s->next_rx_id, (long)id);
+    s->num_lost += tmpdiff;
+    s->next_rx_id += tmpdiff + 1;
+  } else if (!tmpdiff) {
+    // exactly as expected; good.
+    s->next_rx_id++;
+  } else if (tmpdiff < 0) {
+    // packet before the expected one? weird.
+    fprintf(stderr, "out-of-order packets? %ld\n", (long)tmpdiff);
+  }
+
+  // fix up the clock offset if there's any drift.
+  tmpdiff = DIFF(rxtime, s->start_rxtime + id * s->usec_per_pkt);
+  if (tmpdiff < -20) {
+    // packet arrived before predicted time, so prediction was based on
+    // a packet that was "slow" before, or else one of our clocks is
+    // drifting. Use earliest legitimate start time.
+    fprintf(stderr, "time paradox: backsliding start by %ld usec\n",
+            (long)tmpdiff);
+    s->start_rxtime = rxtime - id * s->usec_per_pkt;
+  }
+  int32_t rxdiff = DIFF(rxtime, s->start_rxtime + id * s->usec_per_pkt);
+
+  // Figure out the offset between our clock and the remote's clock, so we can
+  // calculate the minimum round trip time (rtt). Then, because the consecutive
+  // packets sent in both directions are equally spaced in time, we can figure
+  // out how much a particular packet was delayed in transit - independently in
+  // each direction! This is an advantage over the normal "ping" program which
+  // has no way to tell which direction caused the delay, or which direction
+  // dropped the packet.
+
+  // Our clockdiff is
+  //   (our rx time) - (their tx time)
+  //   == (their rx time + offset) - (their tx time)
+  //   == (their tx time + offset + 1/2 rtt) - (their tx time)
+  //   == offset + 1/2 rtt
+  // and theirs (rx.clockdiff) is:
+  //   (their rx time) - (our tx time)
+  //   == (their rx time) - (their tx time + offset)
+  //   == (their tx time + 1/2 rtt) - (their tx time + offset)
+  //   == 1/2 rtt - offset
+  // So add them together and we get:
+  //   offset + 1/2 rtt + 1/2 rtt - offset
+  //   == rtt
+  // Subtract them and we get:
+  //   offset + 1/2 rtt - 1/2 rtt + offset
+  //   == 2 * offset
+  // ...but that last subtraction is dangerous because if we divide by 2 to get
+  // offset, it doesn't work with 32-bit math, which may have discarded a
+  // high-order bit somewhere along the way.  Instead, we can extract offset
+  // once we have rtt by substituting it into
+  //   clockdiff = offset + 1/2 rtt
+  //   offset = clockdiff - 1/2 rtt
+  // (Dividing rtt by 2 is safe since it's always small and positive.)
+  //
+  // (This example assumes 1/2 rtt in each direction. There's no way to
+  // determine it more accurately than that.)
+  int32_t clockdiff = DIFF(s->start_rxtime, s->start_rtxtime);
+  int32_t rtt = clockdiff + ntohl(s->rx.clockdiff);
+  int32_t offset = DIFF(clockdiff, rtt / 2);
+  if (!ntohl(s->rx.clockdiff)) {
+    // don't print the first packet: it has an invalid clockdiff since the
+    // client can't calculate the clockdiff until it receives at least one
+    // packet from us.
+    s->last_print = now - s->usec_per_print + 1;
+  } else {
+    // not the first packet, so statistics are valid.
+    s->lat_rx_count++;
+    s->lat_rx = rxdiff + rtt/2;
+    s->lat_rx_min = s->lat_rx_min > s->lat_rx ? s->lat_rx : s->lat_rx_min;
+    s->lat_rx_max = s->lat_rx_max < s->lat_rx ? s->lat_rx : s->lat_rx_max;
+    s->lat_rx_sum += s->lat_rx;
+    s->lat_rx_var_sum += s->lat_rx * s->lat_rx;
+  }
+
+  // Note: the way ok_to_print is structured, if there is a dropout in the
+  // connection for more than usec_per_print, we will statistically end up
+  // printing the first packet after the dropout ends.  That one should have the
+  // longest timeout, ie. a "worst case" packet, which is usually the
+  // information you want to see.
+  int ok_to_print = !quiet && DIFF(now, s->last_print) >= s->usec_per_print;
+  if (ok_to_print) {
+    if (want_timestamps) print_timestamp(rxtime);
+    printf("%12s  %6.1f ms rx  (min=%.1f)  loss: %ld/%ld tx  %ld/%ld rx\n",
+           s->last_ackinfo,
+           (rxdiff + rtt/2) / 1000.0,
+           (rtt/2) / 1000.0,
+           (long)ntohl(s->rx.num_lost),
+           (long)s->next_tx_id - 1,
+           (long)s->num_lost,
+           (long)s->next_rx_id - 1);
+    s->last_ackinfo[0] = '\0';
+    s->last_print = now;
+  }
+
+  if (rxdiff < s->min_cycle_rxdiff) s->min_cycle_rxdiff = rxdiff;
+  if (DIFF(now, s->next_cycle) >= 0) {
+    if (s->min_cycle_rxdiff > 0) {
+      fprintf(stderr, "clock skew: sliding start by %ld usec\n",
+              (long)s->min_cycle_rxdiff);
+      s->start_rxtime += s->min_cycle_rxdiff;
+    }
+    s->min_cycle_rxdiff = 0x7fffffff;
+    s->next_cycle += USEC_PER_CYCLE;
+  }
+
+  // schedule this for an ack next time we send the packet
+  s->tx.acks[s->next_txack_index].id = htonl(id);
+  s->tx.acks[s->next_txack_index].rxtime = htonl(rxtime);
+  s->next_txack_index = (s->next_txack_index + 1) % ARRAY_LEN(s->tx.acks);
+
+  // see which of our own transmitted packets have been acked
+  uint32_t first_ack = ntohl(s->rx.first_ack);
+  for (uint32_t i = 0; i < ARRAY_LEN(s->rx.acks); i++) {
+    uint32_t acki = (first_ack + i) % ARRAY_LEN(s->rx.acks);
+    uint32_t ackid = ntohl(s->rx.acks[acki].id);
+    if (!ackid) continue;  // empty slot
+    if (DIFF(ackid, s->next_rxack_id) >= 0) {
+      // an expected ack
+      uint32_t start_txtime = s->next_send - s->next_tx_id * s->usec_per_pkt;
+      uint32_t txtime = start_txtime + ackid * s->usec_per_pkt;
+      uint32_t rrxtime = ntohl(s->rx.acks[acki].rxtime);
+      uint32_t rxtime = rrxtime + offset;
+      // note: already contains 1/2 rtt, unlike rxdiff
+      int32_t txdiff = DIFF(rxtime, txtime);
+      if (s->usec_per_print <= 0 && s->last_ackinfo[0]) {
+        // only print multiple acks per rx if no usec_per_print limit
+        if (want_timestamps) print_timestamp(rxtime);
+        printf("%12s\n", s->last_ackinfo);
+        s->last_ackinfo[0] = '\0';
+      }
+      if (!s->last_ackinfo[0]) {
+        snprintf(s->last_ackinfo, sizeof(s->last_ackinfo), "%6.1f ms tx",
+                 txdiff / 1000.0);
+      }
+      s->next_rxack_id = ackid + 1;
+      s->lat_tx_count++;
+      s->lat_tx = txdiff;
+      s->lat_tx_min = s->lat_tx_min > s->lat_tx ? s->lat_tx : s->lat_tx_min;
+      s->lat_tx_max = s->lat_tx_max < s->lat_tx ? s->lat_tx : s->lat_tx_max;
+      s->lat_tx_sum += s->lat_tx;
+      s->lat_tx_var_sum += s->lat_tx * s->lat_tx;
+    }
+  }
+
+  s->last_rxtime = rxtime;
+}
+
+
+int isoping_main(int argc, char **argv) {
+  struct sockaddr_in6 listenaddr, rxaddr, last_rxaddr;
+  struct sockaddr *remoteaddr = NULL;
+  socklen_t remoteaddr_len = 0, rxaddr_len = 0;
+  struct addrinfo *ai = NULL;
+  int sock = -1;
+
+  setvbuf(stdout, NULL, _IOLBF, 0);
+
+  int c;
+  while ((c = getopt(argc, argv, "f:r:t:qTh?")) >= 0) {
+    switch (c) {
+    case 'f':
+      prints_per_sec = atof(optarg);
+      if (prints_per_sec <= 0) {
+        fprintf(stderr, "%s: lines per second must be >= 0\n", argv[0]);
+        return 99;
+      }
+      break;
+    case 'r':
+      packets_per_sec = atof(optarg);
+      if (packets_per_sec < 0.001 || packets_per_sec > 1e6) {
+        fprintf(stderr, "%s: packets per sec (-r) must be 0.001..1000000\n",
+                argv[0]);
+        return 99;
+      }
+      break;
+    case 't':
+      ttl = atoi(optarg);
+      if (ttl < 1) {
+        fprintf(stderr, "%s: ttl must be >= 1\n", argv[0]);
+        return 99;
+      }
+      break;
+    case 'q':
+      quiet = 1;
+      break;
+    case 'T':
+      want_timestamps = 1;
+      break;
+    case 'h':
+    case '?':
+    default:
+      usage_and_die(argv[0]);
+      break;
+    }
+  }
+
+  sock = socket(PF_INET6, SOCK_DGRAM, 0);
+  if (sock < 0) {
+    perror("socket");
+    return 1;
+  }
+
+  if (argc - optind == 0) {
+    is_server = 1;
+    memset(&listenaddr, 0, sizeof(listenaddr));
+    listenaddr.sin6_family = AF_INET6;
+    listenaddr.sin6_port = htons(SERVER_PORT);
+    if (bind(sock, (struct sockaddr *)&listenaddr, sizeof(listenaddr)) != 0) {
+      perror("bind");
+      return 1;
+    }
+    socklen_t addrlen = sizeof(listenaddr);
+    if (getsockname(sock, (struct sockaddr *)&listenaddr, &addrlen) != 0) {
+      perror("getsockname");
+      return 1;
+    }
+    fprintf(stderr, "server listening at [%s]:%d\n",
+           sockaddr_to_str((struct sockaddr *)&listenaddr),
+           ntohs(listenaddr.sin6_port));
+  } else if (argc - optind == 1) {
+    const char *remotename = argv[optind];
+    is_server = 0;
+    struct addrinfo hints;
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_flags = AI_ADDRCONFIG | AI_V4MAPPED;
+    hints.ai_family = AF_INET6;
+    hints.ai_socktype = SOCK_DGRAM;
+    int err = getaddrinfo(remotename, STR(SERVER_PORT), &hints, &ai);
+    if (err != 0 || !ai) {
+      fprintf(stderr, "getaddrinfo(%s): %s\n", remotename, gai_strerror(err));
+      return 1;
+    }
+    fprintf(stderr, "connecting to %s...\n", sockaddr_to_str(ai->ai_addr));
+    if (connect(sock, ai->ai_addr, ai->ai_addrlen) != 0) {
+      perror("connect");
+      return 1;
+    }
+    remoteaddr = ai->ai_addr;
+    remoteaddr_len = ai->ai_addrlen;
+  } else {
+    usage_and_die(argv[0]);
+  }
+
+  fprintf(stderr, "using ttl=%d\n", ttl);
+  // IPPROTO_IPV6 is the only one that works on MacOS, and is arguably the
+  // technically correct thing to do since it's an AF_INET6 socket.
+  if (setsockopt(sock, IPPROTO_IPV6, IP_TTL, &ttl, sizeof(ttl))) {
+    perror("setsockopt(TTLv6)");
+    return 1;
+  }
+  // ...but in Linux (at least 3.13), IPPROTO_IPV6 does not actually
+  // set the TTL if the IPv6 socket ends up going over IPv4.  We have to
+  // set that separately.  On MacOS, that always returns EINVAL, so ignore
+  // the error if that happens.
+  if (setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl))) {
+    if (errno != EINVAL) {
+      perror("setsockopt(TTLv4)");
+      return 1;
+    }
+  }
+
+  uint32_t now = ustime();       // current time
+
+  struct sigaction act;
+  memset(&act, 0, sizeof(act));
+  act.sa_handler = sighandler;
+  act.sa_flags = SA_RESETHAND;
+  sigaction(SIGINT, &act, NULL);
+
+  struct Session s(now);
+
+  while (!want_to_die) {
+    fd_set rfds;
+    FD_ZERO(&rfds);
+    FD_SET(sock, &rfds);
+    struct timeval tv;
+    tv.tv_sec = 0;
+
+    now = ustime();
+    if (DIFF(s.next_send, now) < 0) {
+      tv.tv_usec = 0;
+    } else {
+      tv.tv_usec = DIFF(s.next_send, now);
+    }
+    int nfds = select(sock + 1, &rfds, NULL, NULL, remoteaddr ? &tv : NULL);
+    now = ustime();
+    if (nfds < 0 && errno != EINTR) {
+      perror("select");
+      return 1;
+    }
+
+    // time to send the next packet?
+    if (remoteaddr && DIFF(now, s.next_send) >= 0) {
+      prepare_tx_packet(&s);
+      int err = send_packet(&s, sock, remoteaddr, remoteaddr_len);
+      if (err != 0) {
+        return err;
+      }
+      // TODO(pmccurdy): Track disconnections across multiple clients.  Use
+      // recvmsg with the MSG_ERRQUEUE flag to detect connection refused.
+      if (is_server && DIFF(now, s.last_rxtime) > 60*1000*1000) {
+        fprintf(stderr, "client disconnected.\n");
+        remoteaddr = NULL;
+      }
+    }
+
+    if (nfds > 0) {
+      // incoming packet
+      rxaddr_len = sizeof(rxaddr);
+      ssize_t got = recvfrom(sock, &s.rx, sizeof(s.rx), 0,
+                             (struct sockaddr *)&rxaddr, &rxaddr_len);
+      if (got < 0) {
+        int e = errno;
+        perror("recvfrom");
+        if (!is_server && e == ECONNREFUSED) return 2;
+        continue;
+      }
+      if (got != sizeof(s.rx) || s.rx.magic != htonl(MAGIC)) {
+        fprintf(stderr, "got invalid packet of length %ld\n", (long)got);
+        continue;
+      }
+
+      // is it a new client?
+      if (is_server) {
+        // TODO(pmccurdy): Maintain a hash table of Sessions, look up based
+        // on rxaddr, create a new one if necessary, remove this resetting code.
+        if (!remoteaddr ||
+            memcmp(&rxaddr, &last_rxaddr, sizeof(rxaddr)) != 0) {
+          fprintf(stderr, "new client connected: %s\n",
+                  sockaddr_to_str((struct sockaddr *)&rxaddr));
+          memcpy(&last_rxaddr, &rxaddr, sizeof(rxaddr));
+          remoteaddr = (struct sockaddr *)&last_rxaddr;
+          remoteaddr_len = rxaddr_len;
+
+          s.next_send = now + 10*1000;
+          s.next_tx_id = 1;
+          s.next_rx_id = s.next_rxack_id = 0;
+          s.start_rtxtime = s.start_rxtime = 0;
+          s.num_lost = 0;
+          s.next_txack_index = 0;
+          s.usec_per_pkt = ntohl(s.rx.usec_per_pkt);
+          memset(&s.tx, 0, sizeof(s.tx));
+        }
+      }
+
+      handle_packet(&s, now);
+    }
+  }
+
+  // TODO(pmccurdy): Separate out per-client and global stats.
+  printf("\n---\n");
+  printf("tx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
+         s.lat_tx_min / 1000.0,
+         DIV(s.lat_tx_sum, s.lat_tx_count) / 1000.0,
+         s.lat_tx_max / 1000.0,
+         onepass_stddev(
+             s.lat_tx_var_sum, s.lat_tx_sum, s.lat_tx_count) / 1000.0);
+  printf("rx: min/avg/max/mdev = %.2f/%.2f/%.2f/%.2f ms\n",
+         s.lat_rx_min / 1000.0,
+         DIV(s.lat_rx_sum, s.lat_rx_count) / 1000.0,
+         s.lat_rx_max / 1000.0,
+         onepass_stddev(
+             s.lat_rx_var_sum, s.lat_rx_sum, s.lat_rx_count) / 1000.0);
+  printf("\n");
+
+  if (ai) freeaddrinfo(ai);
+  if (sock >= 0) close(sock);
+  return 0;
+}
diff --git a/cmds/isoping.h b/cmds/isoping.h
new file mode 100644
index 0000000..52fcccb
--- /dev/null
+++ b/cmds/isoping.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#ifndef ISOPING_H
+#define ISOPING_H
+
+#include <stdint.h>
+
+// Layout of the UDP packets exchanged between client and server.
+// All integers are in network byte order.
+// Packets have exactly the same structure in both directions.
+struct Packet {
+  uint32_t magic;     // magic number to reject bogus packets
+  uint32_t id;        // sequential packet id number
+  uint32_t txtime;    // transmitter's monotonic time when pkt was sent
+  uint32_t clockdiff; // estimate of (transmitter's clk) - (receiver's clk)
+  uint32_t usec_per_pkt; // microseconds of delay between packets
+  uint32_t num_lost;  // number of pkts transmitter expected to get but didn't
+  uint32_t first_ack; // starting index in acks[] circular buffer
+  struct {
+    // txtime==0 for empty elements in this array.
+    uint32_t id;      // id field from a received packet
+    uint32_t rxtime;  // receiver's monotonic time when pkt arrived
+  } acks[64];
+};
+
+
+// Data we track per session.
+struct Session {
+  Session(uint32_t now);
+  int32_t usec_per_pkt;
+  int32_t usec_per_print;
+
+  // WARNING: lots of math below relies on well-defined uint32/int32
+  // arithmetic overflow behaviour, plus the fact that when we subtract
+  // two successive timestamps (for example) they will be less than 2^31
+  // microseconds apart.  It would be safer to just use 64-bit values
+  // everywhere, but that would cut the number of acks-per-packet in half,
+  // which would be unfortunate.
+  uint32_t next_tx_id;       // id field for next transmit
+  uint32_t next_rx_id;       // expected id field for next receive
+  uint32_t next_rxack_id;    // expected ack.id field in next received ack
+  uint32_t start_rtxtime;    // remote's txtime at startup
+  uint32_t start_rxtime;     // local rxtime at startup
+  uint32_t last_rxtime;      // local rxtime of last received packet
+  int32_t min_cycle_rxdiff;  // smallest packet delay seen this cycle
+  uint32_t next_cycle;       // time when next cycle begins
+  uint32_t next_send;        // time when we'll send next pkt
+  uint32_t num_lost;         // number of rx packets not received
+  int next_txack_index;      // next array item to fill in tx.acks
+  struct Packet tx, rx;      // transmit and received packet buffers
+  char last_ackinfo[128];    // human readable format of latest ack
+  uint32_t last_print;       // time of last packet printout
+  // Packet statistics counters for transmit and receive directions.
+  long long lat_tx, lat_tx_min, lat_tx_max,
+      lat_tx_count, lat_tx_sum, lat_tx_var_sum;
+  long long lat_rx, lat_rx_min, lat_rx_max,
+      lat_rx_count, lat_rx_sum, lat_rx_var_sum;
+};
+
+// Process the Session's incoming packet, from s->rx.
+void handle_packet(struct Session *s, uint32_t now);
+
+// Sets all the elements of s->tx to be ready to be sent to the other side.
+void prepare_tx_packet(struct Session *s);
+
+// Parses arguments and runs the main loop.  Distinct from main() for unit test
+// purposes.
+int isoping_main(int argc, char **argv);
+
+#endif
diff --git a/cmds/isoping_main.cc b/cmds/isoping_main.cc
new file mode 100644
index 0000000..521b5e1
--- /dev/null
+++ b/cmds/isoping_main.cc
@@ -0,0 +1,5 @@
+#include "isoping.h"
+
+int main(int argc, char **argv) {
+  return isoping_main(argc, argv);
+}
diff --git a/cmds/isoping_test.cc b/cmds/isoping_test.cc
new file mode 100644
index 0000000..84edc46
--- /dev/null
+++ b/cmds/isoping_test.cc
@@ -0,0 +1,420 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <arpa/inet.h>
+#include <limits.h>
+#include <stdio.h>
+
+#include <wvtest.h>
+
+#include "isoping.h"
+
+uint32_t send_next_packet(Session *from, uint32_t from_base,
+                          Session *to, uint32_t to_base, uint32_t latency) {
+  uint32_t t = from->next_send - from_base;
+  prepare_tx_packet(from);
+  to->rx = from->tx;
+  from->next_send += from->usec_per_pkt;
+  t += latency;
+  handle_packet(to, to_base + t);
+  fprintf(stderr,
+          "**Sent packet: txtime=%d, start_txtime=%d, rxtime=%d, "
+          "start_rxtime=%d, latency=%d, t_from=%d, t_to=%d\n",
+          from->next_send,
+          to->start_rtxtime,
+          to_base + t,
+          to->start_rxtime,
+          latency,
+          t - latency,
+          t);
+
+  return t;
+}
+
+WVTEST_MAIN("isoping algorithm logic") {
+  // Establish a positive base time for client and server.  This is conceptually
+  // the instant when the client sends its first message to the server, as
+  // measured by the clocks on each side (note: this is before the server
+  // receives the message).
+  uint32_t cbase = 400 * 1000;
+  uint32_t sbase = 600 * 1000;
+  uint32_t real_clockdiff = sbase - cbase;
+
+  // The states of the client and server.
+  struct Session c(cbase);
+  struct Session s(sbase);
+
+  // One-way latencies: cs_latency is the latency from client to server;
+  // sc_latency is from server to client.
+  uint32_t cs_latency = 24 * 1000;
+  uint32_t sc_latency = 25 * 1000;
+  uint32_t half_rtt = (sc_latency + cs_latency) / 2;
+
+  // Elapsed time, relative to the base time for each clock.
+  uint32_t t = 0;
+
+  // Send the initial packet from client to server.  This isn't enough to let us
+  // draw any useful latency conclusions.
+  // TODO(pmccurdy): Setting next_send is duplicating some work done in the main
+  // loop / send_packet.  Extract that into somewhere testable, then test it.
+  c.next_send = cbase;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+  uint32_t rxtime = sbase + t;
+  s.next_send = rxtime + 10 * 1000;
+
+  printf("last_rxtime: %d\n", s.last_rxtime);
+  printf("min_cycle_rxdiff: %d\n", s.min_cycle_rxdiff);
+  WVPASSEQ(s.rx.clockdiff, 0);
+  WVPASSEQ(s.last_rxtime, rxtime);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+  WVPASSEQ(ntohl(s.tx.acks[0].id), 1);
+  WVPASSEQ(s.next_txack_index, 1);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 1);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(s.start_rxtime, rxtime - c.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(s.next_send, rxtime + 10 * 1000);
+
+  // Reply to the client.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency);
+
+  // Now we have enough data to figure out latencies on the client.
+  rxtime = cbase + t;
+  WVPASSEQ(c.start_rxtime, rxtime - s.usec_per_pkt);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - s.usec_per_pkt);
+  WVPASSEQ(c.min_cycle_rxdiff, 0);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].id), 1);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(c.num_lost, 0);
+  WVPASSEQ(c.lat_tx_count, 1);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.lat_rx_count, 1);
+  WVPASSEQ(c.lat_rx, half_rtt);
+  WVPASSEQ(c.num_lost, 0);
+
+  // Round 2
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+  rxtime = sbase + t;
+
+  // Now the server also knows latencies.
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 2);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(s.num_lost, 0);
+  WVPASSEQ(s.lat_tx_count, 1);
+  WVPASSEQ(s.lat_tx, half_rtt);
+  WVPASSEQ(s.lat_rx_count, 1);
+  WVPASSEQ(s.lat_rx, half_rtt);
+  WVPASSEQ(s.num_lost, 0);
+
+  // Increase the latencies in both directions, reply to client.
+  int32_t latency_diff = 10 * 1000;
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_diff);
+
+  rxtime = cbase + t;
+  WVPASSEQ(ntohl(s.tx.clockdiff), real_clockdiff + cs_latency);
+  WVPASSEQ(c.start_rxtime,
+           rxtime - ntohl(s.tx.id) * s.usec_per_pkt - latency_diff);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - s.usec_per_pkt);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].id), 2);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(c.num_lost, 0);
+  WVPASSEQ(c.lat_tx_count, 2);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.lat_rx_count, 2);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_diff);
+  WVPASSEQ(c.num_lost, 0);
+
+  // Client replies with increased latency, server notices.
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_diff);
+
+  rxtime = sbase + t;
+  WVPASSEQ(ntohl(c.tx.clockdiff), - real_clockdiff + sc_latency);
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 3);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(s.num_lost, 0);
+  WVPASSEQ(s.lat_tx_count, 2);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(s.lat_rx_count, 2);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_diff);
+  WVPASSEQ(s.num_lost, 0);
+
+  // Lose a server->client packet, send the next client->server packet, verify
+  // only the received packets were acked.
+  s.next_send += s.usec_per_pkt;
+  s.next_tx_id++;
+
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_diff);
+
+  rxtime = sbase + t;
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].id), 3);
+  WVPASSEQ(ntohl(s.tx.acks[ntohl(s.tx.first_ack)].rxtime),
+           rxtime - s.usec_per_pkt);
+  WVPASSEQ(s.num_lost, 0);
+  WVPASSEQ(s.lat_tx_count, 2);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(s.lat_rx_count, 3);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_diff);
+  WVPASSEQ(s.num_lost, 0);
+
+  // Remove the extra latency from server->client, send the next packet, have
+  // the client receive it and notice the lost packet and reduced latency.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency);
+
+  rxtime = cbase + t;
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].id), 4);
+  WVPASSEQ(ntohl(c.tx.acks[ntohl(c.tx.first_ack)].rxtime), rxtime);
+  WVPASSEQ(c.num_lost, 1);
+  WVPASSEQ(c.lat_tx_count, 4);
+  WVPASSEQ(c.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(c.lat_rx_count, 3);
+  WVPASSEQ(c.lat_rx, half_rtt);
+  WVPASSEQ(c.num_lost, 1);
+
+  // A tiny reduction in latency shows up in min_cycle_rxdiff.
+  latency_diff = 0;
+  int32_t latency_mini_diff = -15;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_mini_diff);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.min_cycle_rxdiff, latency_mini_diff);
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.lat_tx, half_rtt);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_mini_diff);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_mini_diff);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(c.min_cycle_rxdiff, latency_mini_diff);
+  WVPASSEQ(c.lat_tx, half_rtt + latency_mini_diff);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_mini_diff);
+
+  // Reduce the latency dramatically, verify that both sides see it, and the
+  // start time is modified (not the min_cycle_rxdiff).
+  latency_diff = -22 * 1000;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + latency_diff);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.min_cycle_rxdiff, latency_mini_diff);
+  // We see half the latency diff applied to each side of the connection because
+  // the reduction in latency creates a time paradox, rebasing the start time
+  // and recalculating the RTT.
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency + latency_diff - s.usec_per_pkt);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff/2 + latency_mini_diff);
+  WVPASSEQ(s.lat_rx, half_rtt + latency_diff/2);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_diff);
+
+  // Now we see the new latency applied to both sides.
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(c.min_cycle_rxdiff, latency_mini_diff);
+  WVPASSEQ(c.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_diff);
+
+  // Restore latency on one side of the connection, verify that we track it on
+  // only one side and we've improved our clock sync.
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency + latency_diff);
+  WVPASSEQ(s.lat_tx, half_rtt + latency_diff);
+  WVPASSEQ(s.lat_rx, half_rtt);
+
+  // And double-check that the other side also sees the improved clock sync and
+  // one-sided latency on the correct side.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency + latency_diff);
+
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency + latency_diff);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.lat_rx, half_rtt + latency_diff);
+}
+
+// Verify that isoping handles clocks ticking at different rates.
+WVTEST_MAIN("isoping clock drift") {
+  uint32_t cbase = 1400 * 1000;
+  uint32_t sbase = 1600 * 1000;
+
+  // The states of the client and server.
+  struct Session c(cbase);
+  struct Session s(sbase);
+  // Send packets infrequently, to get new cycles more often.
+  s.usec_per_pkt = 1 * 1000 * 1000;
+  c.usec_per_pkt = 1 * 1000 * 1000;
+
+  // One-way latencies: cs_latency is the latency from client to server;
+  // sc_latency is from server to client.
+  int32_t cs_latency = 4 * 1000;
+  int32_t sc_latency = 5 * 1000;
+  int32_t drift_per_round = 15;
+  uint32_t half_rtt = (sc_latency + cs_latency) / 2;
+
+  // Perform the initial setup.
+  c.next_send = cbase;
+  uint32_t t = send_next_packet(&c, cbase, &s, sbase, cs_latency);
+  s.next_send = sbase + t + 10 * 1000;
+
+  uint32_t orig_server_start_rxtime = s.start_rxtime;
+  WVPASSEQ(s.start_rxtime, sbase + cs_latency - s.usec_per_pkt);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), 0);
+  WVPASSEQ(s.lat_rx, 0);
+  WVPASSEQ(s.lat_tx, 0);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency);
+
+  uint32_t orig_client_start_rxtime = c.start_rxtime;
+  WVPASSEQ(c.start_rxtime, cbase + 2 * half_rtt + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt);
+  WVPASSEQ(c.lat_tx, half_rtt);
+  WVPASSEQ(c.min_cycle_rxdiff, 0);
+
+  // Clock drift shows up as symmetric changes in one-way latency.
+  int32_t total_drift = drift_per_round;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift);
+  WVPASSEQ(s.lat_tx, half_rtt);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  WVPASSEQ(c.start_rxtime, cbase + 2 * half_rtt + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(c.start_rtxtime,
+           sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt - total_drift);
+  WVPASSEQ(c.lat_tx, half_rtt + total_drift);
+  WVPASSEQ(c.min_cycle_rxdiff, -drift_per_round);
+
+  // Once we exceed -20us of drift, we adjust the client's start_rxtime.
+  total_drift += drift_per_round;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift);
+  WVPASSEQ(s.lat_tx, half_rtt - drift_per_round);
+  WVPASSEQ(s.min_cycle_rxdiff, 0);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  int32_t clock_adj = total_drift;
+  WVPASSEQ(c.start_rxtime,
+           cbase + 2 * half_rtt + 10 * 1000 - c.usec_per_pkt - total_drift);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt - drift_per_round);
+  WVPASSEQ(c.lat_tx, half_rtt + drift_per_round);
+  WVPASSEQ(c.min_cycle_rxdiff, -drift_per_round);
+
+  // Skip ahead to the next cycle.
+  int packets_to_skip = 8;
+  s.next_send += packets_to_skip * s.usec_per_pkt;
+  s.next_rx_id += packets_to_skip;
+  s.next_tx_id += packets_to_skip;
+  c.next_send += packets_to_skip * c.usec_per_pkt;
+  c.next_rx_id += packets_to_skip;
+  c.next_tx_id += packets_to_skip;
+  total_drift += packets_to_skip * drift_per_round;
+
+  // At first we blame the rx latency for most of the drift.
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  // start_rxtime doesn't change here as the first cycle suppresses positive
+  // min_cycle_rxdiff values.
+  // TODO(pmccurdy): Should it?
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency - clock_adj);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift - drift_per_round);
+  WVPASSEQ(s.lat_tx, half_rtt - drift_per_round);
+  WVPASSEQ(s.min_cycle_rxdiff, INT_MAX);
+
+  // After one round-trip, we divide the blame for the latency diff evenly.
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  WVPASSEQ(c.start_rxtime, orig_client_start_rxtime - total_drift);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency);
+  WVPASSEQ(c.lat_rx, half_rtt - total_drift / 2);
+  WVPASSEQ(c.lat_tx, half_rtt + total_drift / 2);
+  WVPASSEQ(c.min_cycle_rxdiff, INT_MAX);
+
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff), cbase - sbase + sc_latency - total_drift);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift / 2);
+  WVPASSEQ(s.lat_tx, half_rtt - total_drift / 2);
+  // We also notice the difference in expected arrival times on the server...
+  WVPASSEQ(s.min_cycle_rxdiff, total_drift);
+
+  total_drift += drift_per_round;
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+  // And on the client.  The client doesn't notice the total_drift rxdiff as it
+  // was swallowed by the new cycle.
+  WVPASSEQ(c.min_cycle_rxdiff, -drift_per_round);
+
+  // Skip ahead to the next cycle.
+  packets_to_skip = 8;
+  s.next_send += packets_to_skip * s.usec_per_pkt;
+  s.next_rx_id += packets_to_skip;
+  s.next_tx_id += packets_to_skip;
+  c.next_send += packets_to_skip * c.usec_per_pkt;
+  c.next_rx_id += packets_to_skip;
+  c.next_tx_id += packets_to_skip;
+  total_drift += packets_to_skip * drift_per_round;
+  int32_t drift_per_cycle = 10 * drift_per_round;
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+  // The clock drift has worked its way into the RTT calculation.
+  half_rtt = (cs_latency + sc_latency - drift_per_cycle) / 2;
+
+  // Now start_rxtime has updated.
+  WVPASSEQ(s.start_rxtime, orig_server_start_rxtime + drift_per_cycle);
+  WVPASSEQ(s.start_rtxtime, cbase - c.usec_per_pkt);
+  WVPASSEQ(ntohl(s.rx.clockdiff),
+           cbase - sbase + sc_latency - drift_per_cycle);
+  WVPASSEQ(s.lat_rx, half_rtt + total_drift);
+  WVPASSEQ(s.lat_tx, half_rtt - drift_per_round);
+  WVPASSEQ(s.min_cycle_rxdiff, INT_MAX);
+
+  t = send_next_packet(&s, sbase, &c, cbase, sc_latency - total_drift);
+
+  WVPASSEQ(c.start_rxtime, orig_client_start_rxtime - total_drift);
+  WVPASSEQ(c.start_rtxtime, sbase + cs_latency + 10 * 1000 - c.usec_per_pkt);
+  WVPASSEQ(ntohl(c.rx.clockdiff), sbase - cbase + cs_latency + drift_per_cycle);
+  WVPASSEQ(c.lat_rx, half_rtt + drift_per_round / 2);
+  WVPASSEQ(c.lat_tx, half_rtt + total_drift / 2 + 1);
+  WVPASSEQ(c.min_cycle_rxdiff, INT_MAX);
+
+  t = send_next_packet(&c, cbase, &s, sbase, cs_latency + total_drift);
+
+}
diff --git a/cmds/soft_rc.py b/cmds/soft_rc.py
index 78bf5bc..685a069 100755
--- a/cmds/soft_rc.py
+++ b/cmds/soft_rc.py
@@ -142,7 +142,8 @@
 LOG_VERB = 3
 LOG_ALL  = 99
 
-SLEEP_BEFORE_RELEASE_TIME = 0.1  # secs
+SLEEP_BEFORE_RELEASE_TIME = 0.1   # secs
+SLEEP_BETWEEN_DIGITS_TIME = 0.25  # secs
 
 optspec = """
 soft_rc.py [options]
@@ -523,6 +524,7 @@
           for d in token:
             tok = "DIGIT_" + d
             self.SendKeyCode(tok, keymap.get(tok))
+            time.sleep(SLEEP_BETWEEN_DIGITS_TIME)
           self.SendKeyCode("OK", keymap.get("OK"))
 
         # regular key
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 0faf301..6126f84 100644
--- a/conman/Makefile
+++ b/conman/Makefile
@@ -15,7 +15,7 @@
 
 %.test: %_test.py
 	echo ./$<
-	PYTHONPATH=..:./test/fake_wpactrl:$(PYTHONPATH) ./$<
+	PYTHONPATH=..:./test/fake_python:$(PYTHONPATH) ./$<
 
 runtests: \
 	$(patsubst %_test.py,%.test,$(wildcard *_test.py))
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index f43ee8d..e484e67 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -74,7 +74,7 @@
   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.command = command_lines.splitlines()
@@ -83,7 +83,6 @@
     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 +105,12 @@
     if self.ssid is None:
       raise ValueError('Command file does not specify SSID')
 
-    if self.wifi.initial_ssid == self.ssid:
+    if self.client_up:
       logging.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."""
@@ -163,7 +158,8 @@
       return
 
     if self._actually_start_client():
-      self._post_start_client()
+      self.wifi.status.connected_to_wlan = True
+      logging.info('Started wifi client on %s GHz', self.band)
 
   def _actually_start_client(self):
     """Actually run wifi setclient.
@@ -187,26 +183,18 @@
 
     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)
       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.wifi.update()
     except subprocess.CalledProcessError as e:
       logging.error('Failed to stop wifi client: %s', e.output)
 
@@ -242,10 +230,10 @@
                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_start_wait_s=20, acs_finish_wait_s=120,
+               dhcp_wait_s=10, acs_connection_check_wait_s=1,
+               acs_start_wait_s=20, acs_finish_wait_s=120,
                bssid_cycle_length_s=30):
 
     self._tmp_dir = tmp_dir
@@ -253,13 +241,13 @@
     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
     self._wlan_retry_s = wlan_retry_s
     self._associate_wait_s = associate_wait_s
     self._dhcp_wait_s = dhcp_wait_s
+    self._acs_connection_check_wait_s = acs_connection_check_wait_s
     self._acs_start_wait_s = acs_start_wait_s
     self._acs_finish_wait_s = acs_finish_wait_s
     self._bssid_cycle_length_s = bssid_cycle_length_s
@@ -308,17 +296,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)):
-        wifi_up = self.is_interface_up(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),
@@ -366,7 +350,10 @@
           ratchet.Condition('trying_open', wifi.connected_to_open,
                             self._associate_wait_s,
                             callback=wifi.expire_connection_status_cache),
-          ratchet.Condition('waiting_for_dhcp', wifi.gateway, self._dhcp_wait_s,
+          ratchet.Condition('waiting_for_dhcp', wifi.gateway,
+                            self._dhcp_wait_s),
+          ratchet.Condition('acs_connection_check', wifi.acs,
+                            self._acs_connection_check_wait_s,
                             callback=self.cwmp_wakeup),
           ratchet.FileTouchedCondition('waiting_for_cwmp_wakeup',
                                        os.path.join(CWMP_PATH, 'acscontact'),
@@ -433,8 +420,6 @@
       while True:
         self.run_once()
     finally:
-      for wifi in self.wifi:
-        wifi.detach_wpa_control()
       self.notifier.stop()
 
   def run_once(self):
@@ -445,16 +430,18 @@
     1. Process any changes in watched files.
     2. Check interfaces for changed connectivity, if
        update_interfaces_and_routes is true.
-    3. Start, stop, or restart access points as appropriate.  If running an
+    3. Try to upload logs, if we just joined a new open network.
+    4. Start, stop, or restart access points as appropriate.  If running an
        access point, skip all remaining wifi steps for that band.
-    3. Handle any wpa_supplicant events.
-    4. Periodically, perform a wifi scan.
-    5. If not connected to the WLAN or to the ACS, try to connect to something.
-    6. If connected to the ACS but not the WLAN, and enough time has passed
+    5. Handle any wpa_supplicant events.
+    6. Periodically, perform a wifi scan.
+    7. If not connected to the WLAN or to the ACS, try to connect to something.
+    8. If connected to the ACS but not the WLAN, and enough time has passed
        since connecting that we should expect a current WLAN configuration, try
        to join the WLAN again.
-    7. Sleep for the rest of the duration of _run_duration_s.
+    9. Sleep for the rest of the duration of _run_duration_s.
     """
+
     start_time = _gettime()
     self.notifier.process_events()
     while self.notifier.check_events():
@@ -466,13 +453,22 @@
       self._interface_update_counter = 0
       self._update_interfaces_and_routes()
 
+    if self.acs() and self._try_to_upload_logs:
+      self._try_upload_logs()
+      self._try_to_upload_logs = False
+
     for wifi in self.wifi:
-      continue_wifi = False
-      provisioning_failed = self.provisioning_failed(wifi)
       if self.currently_provisioning(wifi):
+        logging.debug('Currently provisioning, nothing else to do.')
         continue
 
       provisioning_failed = self.provisioning_failed(wifi)
+      if provisioning_failed and (
+          getattr(wifi, 'last_attempted_bss_info', None) ==
+          getattr(wifi, 'last_successful_bss_info', None)):
+        wifi.last_successful_bss_info = None
+
+      continue_wifi = False
 
       # Only one wlan_configuration per interface will have access_point ==
       # True.  Try 5 GHz first, then 2.4 GHz.  If both bands are supported by
@@ -494,12 +490,7 @@
           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)
@@ -550,9 +541,6 @@
         wifi.status.connected_to_wlan = False
         if self.acs():
           logging.debug('Connected to ACS')
-          if self._try_to_upload_logs:
-            self._try_upload_logs()
-            self._try_to_upload_logs = False
 
           if wifi.acs():
             wifi.last_successful_bss_info = getattr(wifi,
@@ -722,8 +710,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:
@@ -831,8 +818,7 @@
       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
@@ -859,7 +845,7 @@
     wifi.last_attempted_bss_info = bss_info
     return subprocess.call(self.WIFI_SETCLIENT +
                            ['--ssid', bss_info.ssid,
-                            '--band', wifi.bands[0],
+                            '--band', bss_info.band,
                             '--bssid', bss_info.bssid]) == 0
 
   def _connected_to_wlan(self, wifi):
@@ -877,6 +863,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)
       if current is not None:
         wlan_configuration.access_point = current.access_point
       else:
@@ -960,6 +947,7 @@
       wifi.provisioning_ratchet.check()
       if wifi.provisioning_ratchet.done_after:
         wifi.status.provisioning_completed = True
+        wifi.provisioning_ratchet.stop()
         logging.info('%s successfully provisioned', wifi.name)
       return False
     except ratchet.TimeoutException:
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index d7d0a40..1e447d0 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -5,8 +5,10 @@
 import logging
 import os
 import shutil
+import subprocess  # Fake subprocess module in test/fake_python.
 import tempfile
 import time
+import traceback
 
 import connection_manager
 import experiment_testutils
@@ -29,92 +31,26 @@
 }
 """
 
-WIFI_SHOW_OUTPUT_MARVELL8897 = """Band: 2.4
-RegDomain: US
-Interface: wlan0  # 2.4 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 2.4
 
-Client Interface: wcli0  # 2.4 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-
-Band: 5
-RegDomain: US
-Interface: wlan0  # 5 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 5
-
-Client Interface: wcli0  # 5 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-"""
-
-WIFI_SHOW_OUTPUT_ATH9K_ATH10K = """Band: 2.4
-RegDomain: US
-Interface: wlan0  # 2.4 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 2.4
-
-Client Interface: wcli0  # 2.4 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-
-Band: 5
-RegDomain: US
-Interface: wlan1  # 5 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 5
-
-Client Interface: wcli1  # 5 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-"""
-
+WIFI_SHOW_OUTPUT_MARVELL8897 = (
+    subprocess.wifi.MockInterface(phynum='0', bands=['2.4', '5'],
+                                  driver='cfg80211'),
+)
+WIFI_SHOW_OUTPUT_ATH9K_ATH10K = (
+    subprocess.wifi.MockInterface(phynum='0', bands=['2.4'], driver='cfg80211'),
+    subprocess.wifi.MockInterface(phynum='1', bands=['5'], driver='cfg80211'),
+)
 # See b/27328894.
-WIFI_SHOW_OUTPUT_MARVELL8897_NO_5GHZ = """Band: 2.4
-RegDomain: 00
-Interface: wlan0  # 2.4 GHz ap
-BSSID: 00:50:43:02:fe:01
-AutoChannel: False
-Station List for band: 2.4
-
-Client Interface: wcli0  # 2.4 GHz client
-Client BSSID: 00:50:43:02:fe:02
-
-Band: 5
-RegDomain: 00
-"""
-
-WIFI_SHOW_OUTPUT_ATH9K_FRENZY = """Band: 2.4
-RegDomain: US
-Interface: wlan0  # 2.4 GHz ap
-Channel: 149
-BSSID: f4:f5:e8:81:1b:a0
-AutoChannel: True
-AutoType: NONDFS
-Station List for band: 2.4
-
-Client Interface: wcli0  # 2.4 GHz client
-Client BSSID: f4:f5:e8:81:1b:a1
-
-Band: 5
-RegDomain: 00
-"""
-
-WIFI_SHOW_OUTPUT_FRENZY = """Band: 2.4
-RegDomain: 00
-Band: 5
-RegDomain: 00
-"""
+WIFI_SHOW_OUTPUT_MARVELL8897_NO_5GHZ = (
+    subprocess.wifi.MockInterface(phynum='0', bands=['2.4'], driver='cfg80211'),
+)
+WIFI_SHOW_OUTPUT_ATH9K_FRENZY = (
+    subprocess.wifi.MockInterface(phynum='0', bands=['2.4'], driver='cfg80211'),
+    subprocess.wifi.MockInterface(phynum='1', bands=['5'], driver='frenzy'),
+)
+WIFI_SHOW_OUTPUT_FRENZY = (
+    subprocess.wifi.MockInterface(phynum='0', bands=['5'], driver='frenzy'),
+)
 
 IW_SCAN_DEFAULT_OUTPUT = """BSS 00:11:22:33:44:55(on wcli0)
   SSID: s1
@@ -133,22 +69,13 @@
 @wvtest.wvtest
 def get_client_interfaces_test():
   """Test get_client_interfaces."""
-  wifi_show = None
-  quantenna_interfaces = None
+  subprocess.reset()
 
-  # pylint: disable=protected-access
-  old_wifi_show = connection_manager._wifi_show
-  old_get_quantenna_interfaces = connection_manager._get_quantenna_interfaces
-  connection_manager._wifi_show = lambda: wifi_show
-  connection_manager._get_quantenna_interfaces = lambda: quantenna_interfaces
-
-  wifi_show = WIFI_SHOW_OUTPUT_MARVELL8897
-  quantenna_interfaces = []
+  subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_MARVELL8897)
   wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
                   {'wcli0': {'bands': set(['2.4', '5'])}})
 
-  wifi_show = WIFI_SHOW_OUTPUT_ATH9K_ATH10K
-  quantenna_interfaces = []
+  subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_ATH9K_ATH10K)
   wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
       'wcli0': {'bands': set(['2.4'])},
       'wcli1': {'bands': set(['5'])}
@@ -156,113 +83,30 @@
 
   # Test Quantenna devices.
 
-  # 2.4 GHz cfg80211 radio + 5 GHz Frenzy (Optimus Prime).
-  wifi_show = WIFI_SHOW_OUTPUT_ATH9K_FRENZY
-  quantenna_interfaces = ['wlan1', 'wlan1_portal', 'wcli1']
+  # 2.4 GHz cfg80211 radio + 5 GHz Frenzy (e.g. Optimus Prime).
+  subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_ATH9K_FRENZY)
   wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
       'wcli0': {'bands': set(['2.4'])},
       'wcli1': {'frenzy': True, 'bands': set(['5'])}
   })
 
   # Only Frenzy (e.g. Lockdown).
-  wifi_show = WIFI_SHOW_OUTPUT_FRENZY
-  quantenna_interfaces = ['wlan0', 'wlan0_portal', 'wcli0']
+  subprocess.mock('wifi', 'interfaces', *WIFI_SHOW_OUTPUT_FRENZY)
   wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
                   {'wcli0': {'frenzy': True, 'bands': set(['5'])}})
 
-  connection_manager._wifi_show = old_wifi_show
-  connection_manager._get_quantenna_interfaces = old_get_quantenna_interfaces
-
-
-class WLANConfiguration(connection_manager.WLANConfiguration):
-  """WLANConfiguration subclass for testing."""
-
-  WIFI_STOPAP = ['echo', 'stopap']
-  WIFI_SETCLIENT = ['echo', 'setclient']
-  WIFI_STOPCLIENT = ['echo', 'stopclient']
-
-  def __init__(self, *args, **kwargs):
-    super(WLANConfiguration, self).__init__(*args, **kwargs)
-    self.stale = False
-
-  def _actually_start_client(self):
-    self.client_was_up = self.client_up
-    self.was_attached = self.wifi.attached()
-    self.wifi._secure_testonly = True
-
-    super(WLANConfiguration, self)._actually_start_client()
-
-    if not self.client_was_up and not self.was_attached:
-      self.wifi._initial_ssid_testonly = self.ssid
-      self.wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
-
-    if self.wifi._wpa_control:
-      self.wifi._wpa_control.connected = not self.stale
-    return not self.stale
-
-  def _post_start_client(self):
-    if not self.client_was_up:
-      self.wifi.set_connection_check_result('succeed')
-
-      if self.was_attached:
-        self.wifi._wpa_control.ssid_testonly = self.ssid
-        self.wifi._wpa_control.secure_testonly = True
-        self.wifi.add_connected_event()
-
-      # Normally, wpa_supplicant would bring up the client interface, which
-      # would trigger ifplugd, which would run ifplugd.action, which would do
-      # two things:
-      #
-      # 1)  Write an interface status file.
-      # 2)  Call run-dhclient, which would call dhclient-script, which would
-      #     call ipapply, which would write gateway and subnet files.
-      #
-      # Fake both of these things instead.
-      self.write_interface_status_file('1')
-      self.write_gateway_file()
-      self.write_subnet_file()
-
-  def stop_client(self):
-    client_was_up = self.client_up
-
-    super(WLANConfiguration, self).stop_client()
-
-    if client_was_up:
-      self.wifi.add_terminating_event()
-      self.wifi.set_connection_check_result('fail')
-
-    # See comments in start_client.
-    self.write_interface_status_file('0')
-
-  def write_gateway_file(self):
-    gateway_file = os.path.join(self.tmp_dir,
-                                self.gateway_file_prefix + self.wifi.name)
-    with open(gateway_file, 'w') as f:
-      # This value doesn't matter to conman, so it's fine to hard code it here.
-      f.write('192.168.1.1')
-
-  def write_subnet_file(self):
-    subnet_file = os.path.join(self.tmp_dir,
-                               self.subnet_file_prefix + self.wifi.name)
-    with open(subnet_file, 'w') as f:
-      # This value doesn't matter to conman, so it's fine to hard code it here.
-      f.write('192.168.1.0/24')
-
-  def write_interface_status_file(self, value):
-    status_file = os.path.join(self.interface_status_dir, self.wifi.name)
-    with open(status_file, 'w') as f:
-      # This value doesn't matter to conman, so it's fine to hard code it here.
-      f.write(value)
-
 
 @wvtest.wvtest
 def WLANConfigurationParseTest():  # pylint: disable=invalid-name
   """Test WLANConfiguration parsing."""
+  subprocess.reset()
+
   cmd = '\n'.join([
       'WIFI_PSK=abcdWIFI_PSK=qwer', 'wifi', 'set', '-P', '-b', '5',
       '--bridge=br0', '-s', 'my ssid=1', '--interface-suffix', '_suffix',
   ])
-  config = WLANConfiguration('5', interface_test.Wifi('wcli0', 20), cmd, None)
+  config = connection_manager.WLANConfiguration(
+      '5', interface_test.Wifi('wcli0', 20), cmd)
 
   wvtest.WVPASSEQ('my ssid=1', config.ssid)
   wvtest.WVPASSEQ('abcdWIFI_PSK=qwer', config.passphrase)
@@ -290,150 +134,37 @@
   Bridge = interface_test.Bridge
   Wifi = Wifi
   FrenzyWifi = FrenzyWifi
-  WLANConfiguration = WLANConfiguration
-
-  WIFI_SETCLIENT = ['echo', 'setclient']
-  IFUP = ['echo', 'ifup']
-  IFPLUGD_ACTION = ['echo', 'ifplugd.action']
-  BINWIFI = ['echo', 'wifi']
-  UPLOAD_LOGS_AND_WAIT = ['echo', 'upload-logs-and-wait']
-  CWMP_WAKEUP = ['echo', 'cwmp', 'wakeup']
 
   def __init__(self, *args, **kwargs):
     self._binwifi_commands = []
 
-    self.interfaces_already_up = kwargs.pop('__test_interfaces_already_up',
-                                            ['eth0'])
-
-    self.wifi_interfaces_already_up = [ifc for ifc in self.interfaces_already_up
-                                       if ifc.startswith('w')]
-    for wifi in self.wifi_interfaces_already_up:
-      # wcli1 is always 5 GHz.  wcli0 always *includes* 2.4.  wlan* client
-      # interfaces are Frenzy interfaces and therefore 5 GHz-only.
-      band = '5' if wifi in ('wlan0', 'wlan1', 'wcli1') else '2.4'
-      # This will happen in the super function, but in order for
-      # write_wlan_config to work we have to do it now.  This has to happen
-      # before the super function so that the files exist before the inotify
-      # registration.
-      self._config_dir = kwargs['config_dir']
-      self.write_wlan_config(band, 'my ssid', 'passphrase')
-
-      # Also create the wpa_supplicant socket to which to attach.
-      open(os.path.join(kwargs['wpa_control_interface'], wifi), 'w')
+    for interface_name in kwargs.pop('__test_interfaces_already_up', ['eth0']):
+      subprocess.call(['ifup', interface_name])
+      if interface_name.startswith('w'):
+        phynum = interface_name[-1]
+        for band, interface in subprocess.wifi.INTERFACE_FOR_BAND.iteritems():
+          if interface.phynum == phynum:
+            break
+        else:
+          raise ValueError('Could not find matching interface for '
+                           '__test_interfaces_already_up')
+        ssid = 'my ssid'
+        psk = 'passphrase'
+        # If band is undefined then a ValueError will be raised above.  pylint
+        # isn't smart enough to figure that out.
+        # pylint: disable=undefined-loop-variable
+        subprocess.mock('cwmp', band, ssid=ssid, psk=psk, write_now=True)
+        subprocess.mock('wifi', 'remote_ap', band=band, ssid=ssid, psk=psk,
+                        bssid='00:00:00:00:00:00')
 
     super(ConnectionManager, self).__init__(*args, **kwargs)
 
-    self.interface_with_scan_results = None
-    self.scan_results_include_hidden = False
-    # Should we be able to connect to open network s2?
-    self.can_connect_to_s2 = True
-    self.can_connect_to_s3 = True
-    # Will s2 fail rather than providing ACS access?
-    self.s2_fail = False
-    # Will s3 fail to acquire a DHCP lease?
-    self.dhcp_failure_on_s3 = False
-    self.log_upload_count = 0
-    self.acs_session_fails = False
-    for wifi in self.wifi:
-      wifi.bssids_tried_testonly = 0
-
-  def create_wifi_interfaces(self):
-    super(ConnectionManager, self).create_wifi_interfaces()
-    for wifi in self.wifi_interfaces_already_up:
-      # pylint: disable=protected-access
-      self.interface_by_name(wifi)._initial_ssid_testonly = 'my ssid'
-      self.interface_by_name(wifi)._secure_testonly = True
-
-  @property
-  def IP_LINK(self):
-    return ['echo'] + ['%s LOWER_UP' % ifc
-                       for ifc in self.interfaces_already_up]
-
-  def _update_access_point(self, wlan_configuration):
-    client_was_up = wlan_configuration.client_up
-    super(ConnectionManager, self)._update_access_point(wlan_configuration)
-    if wlan_configuration.access_point_up:
-      if client_was_up:
-        wifi = self.wifi_for_band(wlan_configuration.band)
-        wifi.add_terminating_event()
-
-  def _try_bssid(self, wifi, bss_info):
-    if wifi.wpa_status().get('wpa_state', None) == 'COMPLETED':
-      wifi.add_disconnected_event()
-    self.last_provisioning_attempt = bss_info
-
-    super(ConnectionManager, self)._try_bssid(wifi, bss_info)
-
-    wifi.bssids_tried_testonly += 1
-
-    def connect(connection_check_result, dhcp_failure=False):
-      # pylint: disable=protected-access
-      if wifi.attached():
-        wifi._wpa_control.ssid_testonly = bss_info.ssid
-        wifi._wpa_control.secure_testonly = False
-        wifi.add_connected_event()
-      else:
-        wifi._initial_ssid_testonly = bss_info.ssid
-        wifi._secure_testonly = False
-        wifi.start_wpa_supplicant_testonly(self._wpa_control_interface)
-      wifi.set_connection_check_result(connection_check_result)
-      self.ifplugd_action(wifi.name, True, dhcp_failure)
-
-    if bss_info and bss_info.ssid == 's1':
-      connect('fail')
-      return True
-
-    if bss_info and bss_info.ssid == 's2' and self.can_connect_to_s2:
-      connect('fail' if self.s2_fail else 'succeed')
-      return True
-
-    if bss_info and bss_info.ssid == 's3' and self.can_connect_to_s3:
-      connect('restricted', self.dhcp_failure_on_s3)
-      return True
-
-    return False
-
-  # pylint: disable=unused-argument,protected-access
-  def _find_bssids(self, band):
-    scan_output = ''
-    if (self.interface_with_scan_results and
-        band in self.interface_by_name(self.interface_with_scan_results).bands):
-      scan_output = IW_SCAN_DEFAULT_OUTPUT
-      if self.scan_results_include_hidden:
-        scan_output += IW_SCAN_HIDDEN_OUTPUT
-    iw._scan = lambda interface: scan_output
-    return super(ConnectionManager, self)._find_bssids(band)
-
-  def _update_wlan_configuration(self, wlan_configuration):
-    wlan_configuration.command.insert(0, 'echo')
-    wlan_configuration._wpa_control_interface = self._wpa_control_interface
-    wlan_configuration.tmp_dir = self._tmp_dir
-    wlan_configuration.interface_status_dir = self._interface_status_dir
-    wlan_configuration.gateway_file_prefix = self.GATEWAY_FILE_PREFIX
-    wlan_configuration.subnet_file_prefix = self.SUBNET_FILE_PREFIX
-
-    super(ConnectionManager, self)._update_wlan_configuration(
-        wlan_configuration)
-
   # Just looking for last_wifi_scan_time to change doesn't work because the
   # tests run too fast.
   def _wifi_scan(self, wifi):
     super(ConnectionManager, self)._wifi_scan(wifi)
     wifi.wifi_scan_counter += 1
 
-  def ifplugd_action(self, interface_name, up, dhcp_failure=False):
-    # Typically, when moca comes up, conman calls ifplugd.action, which writes
-    # this file.  Also, when conman starts, it calls ifplugd.action for eth0.
-    self.write_interface_status_file(interface_name, '1' if up else '0')
-
-    # ifplugd calls run-dhclient, which results in a gateway file if the link is
-    # up (and working).
-    if up and not dhcp_failure:
-      self.write_gateway_file('br0' if interface_name in ('eth0', 'moca0')
-                              else interface_name)
-      self.write_subnet_file('br0' if interface_name in ('eth0', 'moca0')
-                             else interface_name)
-
   def _binwifi(self, *command):
     super(ConnectionManager, self)._binwifi(*command)
     self._binwifi_commands.append(command)
@@ -452,50 +183,7 @@
 
     return self._wlan_configuration[band].client_up
 
-  def _try_upload_logs(self):
-    self.log_upload_count += 1
-    return super(ConnectionManager, self)._try_upload_logs()
-
-  # Test methods
-
-  def tried_to_upload_logs(self):
-    result = getattr(self, 'last_log_upload_count', 0) < self.log_upload_count
-    self.last_log_upload_count = self.log_upload_count
-    return result
-
-  def delete_wlan_config(self, band):
-    delete_wlan_config(self._config_dir, band)
-
-  def write_wlan_config(self, *args, **kwargs):
-    write_wlan_config(self._config_dir, *args, **kwargs)
-
-  def enable_access_point(self, band):
-    enable_access_point(self._config_dir, band)
-
-  def disable_access_point(self, band):
-    disable_access_point(self._config_dir, band)
-
-  def write_gateway_file(self, interface_name):
-    gateway_file = os.path.join(self._tmp_dir,
-                                self.GATEWAY_FILE_PREFIX + interface_name)
-    with open(gateway_file, 'w') as f:
-      logging.debug('Writing gateway file %s', gateway_file)
-      # This value doesn't matter to conman, so it's fine to hard code it here.
-      f.write('192.168.1.1')
-
-  def write_subnet_file(self, interface_name):
-    subnet_file = os.path.join(self._tmp_dir,
-                               self.SUBNET_FILE_PREFIX + interface_name)
-    with open(subnet_file, 'w') as f:
-      logging.debug('Writing subnet file %s', subnet_file)
-      # This value doesn't matter to conman, so it's fine to hard code it here.
-      f.write('192.168.1.0/24')
-
-  def write_interface_status_file(self, interface_name, value):
-    status_file = os.path.join(self._interface_status_dir, interface_name)
-    with open(status_file, 'w') as f:
-      # This value doesn't matter to conman, so it's fine to hard code it here.
-      f.write(value)
+  # # Test methods
 
   def set_ethernet(self, up):
     self.ifplugd_action('eth0', up)
@@ -514,6 +202,7 @@
       self.run_once()
 
   def run_until_scan(self, band):
+    logging.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:
@@ -529,58 +218,12 @@
   def has_status_files(self, files):
     return not set(files) - set(os.listdir(self._status_dir))
 
-  def cwmp_wakeup(self):
-    super(ConnectionManager, self).cwmp_wakeup()
-    self.write_acscontact()
-    if self.acs():
-      self.write_acsconnected()
-
-  def write_acscontact(self):
-    open(os.path.join(connection_manager.CWMP_PATH, 'acscontact'), 'w')
-
-  def write_acsconnected(self):
-    if not self.acs_session_fails:
-      open(os.path.join(connection_manager.CWMP_PATH, 'acsconnected'), 'w')
-
-
-def wlan_config_filename(path, band):
-  return os.path.join(path, 'command.%s' % band)
-
-
-def access_point_filename(path, band):
-  return os.path.join(path, 'access_point.%s' % band)
-
-
-def write_wlan_config(path, band, ssid, psk, atomic=False):
-  final_filename = wlan_config_filename(path, band)
-  filename = final_filename + ('.tmp' if atomic else '')
-  with open(filename, 'w') as f:
-    f.write('\n'.join(['env', 'WIFI_PSK=%s' % psk,
-                       'wifi', 'set', '-b', band, '--ssid', ssid]))
-  if atomic:
-    os.rename(filename, final_filename)
-
-
-def delete_wlan_config(path, band):
-  os.unlink(wlan_config_filename(path, band))
-
-
-def enable_access_point(path, band):
-  open(access_point_filename(path, band), 'w')
-
-
-def disable_access_point(path, band):
-  ap_filename = access_point_filename(path, band)
-  if os.path.isfile(ap_filename):
-    os.unlink(ap_filename)
-
 
 def check_tmp_hosts(expected_contents):
   wvtest.WVPASSEQ(open(connection_manager.TMP_HOSTS).read(), expected_contents)
 
 
-def connection_manager_test(radio_config, wlan_configs=None,
-                            quantenna_interfaces=None, **cm_kwargs):
+def connection_manager_test(radio_config, wlan_configs=None, **cm_kwargs):
   """Returns a decorator that does ConnectionManager test boilerplate."""
   if wlan_configs is None:
     wlan_configs = {}
@@ -589,38 +232,38 @@
     """The actual decorator."""
     def actual_test():
       """The actual test function."""
+      subprocess.reset()
+
       run_duration_s = .01
       interface_update_period = 5
       wifi_scan_period = 15
       wifi_scan_period_s = run_duration_s * wifi_scan_period
       associate_wait_s = 0
       dhcp_wait_s = .5
+      acs_cc_wait_s = 0
       acs_start_wait_s = 0
-      acs_finish_wait_s = 0
+      acs_finish_wait_s = 0.25
 
-      # pylint: disable=protected-access
-      old_wifi_show = connection_manager._wifi_show
-      connection_manager._wifi_show = lambda: radio_config
-
-      old_gqi = connection_manager._get_quantenna_interfaces
-      connection_manager._get_quantenna_interfaces = (
-          lambda: quantenna_interfaces or [])
+      subprocess.mock('wifi', 'interfaces', *radio_config)
 
       try:
         # No initial state.
         connection_manager.TMP_HOSTS = tempfile.mktemp()
         tmp_dir = tempfile.mkdtemp()
         config_dir = tempfile.mkdtemp()
-        os.mkdir(os.path.join(tmp_dir, 'interfaces'))
+        interfaces_dir = os.path.join(tmp_dir, 'interfaces')
+        if not os.path.exists(interfaces_dir):
+          os.mkdir(interfaces_dir)
         moca_tmp_dir = tempfile.mkdtemp()
         wpa_control_interface = tempfile.mkdtemp()
-        FrenzyWifi.WPACtrl.WIFIINFO_PATH = tempfile.mkdtemp()
+        subprocess.mock('wifi', 'wpa_path', wpa_control_interface)
         connection_manager.CWMP_PATH = tempfile.mkdtemp()
+        subprocess.set_conman_paths(tmp_dir, config_dir,
+                                    connection_manager.CWMP_PATH)
 
         for band, access_point in wlan_configs.iteritems():
-          write_wlan_config(config_dir, band, 'initial ssid', 'initial psk')
-          if access_point:
-            open(os.path.join(config_dir, 'access_point.%s' % band), 'w')
+          subprocess.mock('cwmp', band, ssid='initial ssid', psk='initial psk',
+                          access_point=access_point, write_now=True)
 
         # Test that missing directories are created by ConnectionManager.
         shutil.rmtree(tmp_dir)
@@ -628,19 +271,23 @@
         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,
                               wifi_scan_period_s=wifi_scan_period_s,
                               associate_wait_s=associate_wait_s,
                               dhcp_wait_s=dhcp_wait_s,
+                              acs_connection_check_wait_s=acs_cc_wait_s,
                               acs_start_wait_s=acs_start_wait_s,
                               acs_finish_wait_s=acs_finish_wait_s,
                               bssid_cycle_length_s=1,
                               **cm_kwargs)
 
         f(c)
+      except Exception:
+        logging.error('Uncaught exception!')
+        traceback.print_exc()
+        raise
       finally:
         if os.path.exists(connection_manager.TMP_HOSTS):
           os.unlink(connection_manager.TMP_HOSTS)
@@ -648,11 +295,7 @@
         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)
-        # pylint: disable=protected-access
-        connection_manager._wifi_show = old_wifi_show
-        connection_manager._get_quantenna_interfaces = old_gqi
 
     actual_test.func_name = f.func_name
     return actual_test
@@ -660,6 +303,20 @@
   return inner
 
 
+def _enable_basic_scan_results(band):
+  for bssid, ssid, ccr in (('00:11:22:33:44:55', 's1', 'fail'),
+                           ('66:77:88:99:aa:bb', 's1', 'fail'),
+                           ('01:23:45:67:89:ab', 's2', 'succeed')):
+    subprocess.mock('wifi', 'remote_ap', bssid=bssid, ssid=ssid,
+                    band=band, security=None, connection_check_result=ccr)
+
+
+def _disable_basic_scan_results(band):
+  for bssid in (('00:11:22:33:44:55'), ('66:77:88:99:aa:bb'),
+                ('01:23:45:67:89:ab')):
+    subprocess.mock('wifi', 'remote_ap_remove', bssid=bssid, band=band)
+
+
 def connection_manager_test_generic(c, band):
   """Test ConnectionManager for things independent of radio configuration.
 
@@ -754,16 +411,28 @@
   check_tmp_hosts('127.0.0.1 localhost')
 
   # Now there are some scan results.
-  c.interface_with_scan_results = c.wifi_for_band(band).name
-  # Wait for a scan, plus 3 cycles, so that s2 will have been tried.
+  _enable_basic_scan_results(band)
+
+  # Create a WLAN configuration which should eventually be connected to.
+  ssid = 'wlan'
+  psk = 'password'
+  subprocess.mock('wifi', 'remote_ap',
+                  bssid='11:22:33:44:55:66',
+                  ssid=ssid, psk=psk, band=band, security='WPA2')
+  subprocess.mock('cwmp', band, ssid=ssid, psk=psk, access_point=False)
+
+  wvtest.WVFAIL(subprocess.upload_logs_and_wait.uploaded_logs())
+  # Wait for a scan, then until s2 is tried.
   c.run_until_scan(band)
-  wvtest.WVPASSEQ(c.log_upload_count, 0)
-  c.wifi_for_band(band).ip_testonly = '192.168.1.100'
+  subprocess.call(['ip', 'addr', 'add', '192.168.1.100',
+                   'dev', c.wifi_for_band(band).name])
   for _ in range(len(c.wifi_for_band(band).cycler)):
     c.run_once()
     wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
+    last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+    if last_bss_info.ssid == 's2':
+      break
 
-  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
   wvtest.WVPASSEQ(last_bss_info.ssid, 's2')
   wvtest.WVPASSEQ(last_bss_info.bssid, '01:23:45:67:89:ab')
 
@@ -773,17 +442,10 @@
   wvtest.WVPASS(c.internet())
   wvtest.WVFAIL(c.client_up(band))
   wvtest.WVPASS(c.wifi_for_band(band).current_routes())
-  wvtest.WVPASS(c.tried_to_upload_logs())
-  # Disable scan results again.
-  c.interface_with_scan_results = None
+  wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
   c.run_until_interface_update()
   check_tmp_hosts('192.168.1.100 %s\n127.0.0.1 localhost' % hostname)
 
-  # Now, create a WLAN configuration which should be connected to.
-  ssid = 'wlan'
-  psk = 'password'
-  c.write_wlan_config(band, ssid, psk)
-  c.disable_access_point(band)
   c.run_once()
   wvtest.WVPASS(c.client_up(band))
   wvtest.WVPASS(c.wifi_for_band(band).current_routes())
@@ -792,7 +454,7 @@
   # Kill wpa_supplicant.  conman should restart it.
   wvtest.WVPASS(c.client_up(band))
   wvtest.WVPASS(c._connected_to_wlan(c.wifi_for_band(band)))
-  c.wifi_for_band(band).kill_wpa_supplicant_testonly(c._wpa_control_interface)
+  subprocess.mock('wifi', 'kill_wpa_supplicant', band)
   wvtest.WVFAIL(c.client_up(band))
   wvtest.WVFAIL(c._connected_to_wlan(c.wifi_for_band(band)))
   # Make sure we stay connected to s2, rather than disconnecting and
@@ -806,18 +468,22 @@
 
   # The AP restarts with a new configuration, kicking us off.  We should
   # reprovision.
-  c._wlan_configuration[band].stale = True
-  c.wifi_for_band(band).add_disconnected_event()
+  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')
+  subprocess.mock('wifi', 'disconnected_event', band)
   c.run_once()
   wvtest.WVFAIL(c.client_up(band))
   wvtest.WVPASS(c._connected_to_open(c.wifi_for_band(band)))
-  wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's2')
+  wvtest.WVPASSEQ(c.wifi_for_band(band).last_attempted_bss_info.ssid, 's2')
 
-  # Now that we're on the provisioning network, create the new WLAN
-  # configuration, which should be connected to.
-  ssid = 'wlan2'
-  psk = 'password2'
-  c.write_wlan_config(band, ssid, psk)
+  # Run once for cwmp wakeup to get called, then once more for the new config to
+  # be received.
+  c.run_once()
   c.run_once()
   wvtest.WVPASS(c.client_up(band))
   wvtest.WVPASS(c.wifi_for_band(band).wpa_status()['ssid'] == ssid)
@@ -829,23 +495,26 @@
   # add the user's WLAN to the scan results, and scan again.  This time, the
   # first SSID tried should be 's3', which is now present in the scan results
   # (with its SSID hidden, but included via vendor IE).
-  c.delete_wlan_config(band)
-  c.can_connect_to_s2 = False
-  c.interface_with_scan_results = c.wifi_for_band(band).name
-  c.scan_results_include_hidden = True
-  c.run_until_interface_update_and_scan(band)
-  wvtest.WVFAIL(c.has_status_files([status.P.CONNECTED_TO_WLAN]))
-  c.run_until_interface_update()
-  wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
-  wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's3')
-  wvtest.WVPASSEQ(c.last_provisioning_attempt.bssid, 'ff:ee:dd:cc:bb:aa')
-  # The log upload happens on the next main loop after joining s3.
-  c.run_once()
-  wvtest.WVPASS(c.tried_to_upload_logs())
+  subprocess.mock('cwmp', band, delete_config=True, write_now=True)
+  del c.wifi_for_band(band).cycler
+  _enable_basic_scan_results(band)
+  # Remove s2.
+  subprocess.mock('wifi', 'remote_ap_remove',
+                  bssid='01:23:45:67:89:ab', band=band)
+  # Create s3.
+  subprocess.mock('wifi', 'remote_ap', bssid='ff:ee:dd:cc:bb:aa', ssid='s3',
+                  band=band, security=None, hidden=True,
+                  vendor_ies=(('f4:f5:e8', '01'), ('f4:f5:e8', '03 73 33')))
+  #### Now, recreate the same WLAN configuration, which should be connected to.
+  subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
 
-  # Now, recreate the same WLAN configuration, which should be connected to.
-  # Also, test that atomic writes/renames work.
-  c.write_wlan_config(band, ssid, psk, atomic=True)
+  c.run_until_interface_update_and_scan(band)
+  c.run_until_interface_update()
+  wvtest.WVPASSEQ(c.wifi_for_band(band).last_attempted_bss_info.ssid, 's3')
+  wvtest.WVPASSEQ(c.wifi_for_band(band).last_attempted_bss_info.bssid,
+                  'ff:ee:dd:cc:bb:aa')
+  wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
+
   c.run_once()
   wvtest.WVPASS(c.client_up(band))
   wvtest.WVPASS(c.wifi_for_band(band).current_routes())
@@ -853,7 +522,7 @@
 
   # Now enable the AP.  Since we have no wired connection, this should have no
   # effect.
-  c.enable_access_point(band)
+  subprocess.mock('cwmp', band, access_point=True, write_now=True)
   c.run_once()
   wvtest.WVPASS(c.client_up(band))
   wvtest.WVPASS(c.wifi_for_band(band).current_routes())
@@ -865,7 +534,7 @@
   # an AP.
   c.set_ethernet(True)
   c.bridge.set_connection_check_result('succeed')
-  c.bridge.ip_testonly = '192.168.1.101'
+  subprocess.call(['ip', 'addr', 'add', '192.168.1.101', 'dev', c.bridge.name])
   c.run_until_interface_update()
   wvtest.WVPASS(c.access_point_up(band))
   wvtest.WVFAIL(c.client_up(band))
@@ -876,7 +545,7 @@
   # Now move (rather than delete) the configuration file.  The AP should go
   # away, and we should not be able to join the WLAN.  Routes should not be
   # affected.
-  filename = wlan_config_filename(c._config_dir, band)
+  filename = subprocess.cwmp.wlan_config_filename(band)
   other_filename = filename + '.bak'
   os.rename(filename, other_filename)
   c.run_once()
@@ -896,7 +565,7 @@
 
   # Now delete the config and bring down the bridge and make sure we reprovision
   # via the last working BSS.
-  c.delete_wlan_config(band)
+  subprocess.mock('cwmp', band, delete_config=True, write_now=True)
   c.bridge.set_connection_check_result('fail')
   scan_count_for_band = c.wifi_for_band(band).wifi_scan_counter
   c.run_until_interface_update()
@@ -907,6 +576,7 @@
   check_tmp_hosts('192.168.1.101 %s\n127.0.0.1 localhost' % hostname)
   # s3 is not what the cycler would suggest trying next.
   wvtest.WVPASSNE('s3', c.wifi_for_band(band).cycler.peek())
+  subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
   # Run only once, so that only one BSS can be tried.  It should be the s3 one,
   # since that worked previously.
   c.run_once()
@@ -914,18 +584,23 @@
   # Make sure we didn't scan on `band`.
   wvtest.WVPASSEQ(scan_count_for_band, c.wifi_for_band(band).wifi_scan_counter)
   c.run_once()
-  wvtest.WVPASS(c.tried_to_upload_logs())
+  wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
 
-  # Now re-create the WLAN config, connect to the WLAN, and make sure that s3 is
-  # unset as last_successful_bss_info, since it is no longer available.
-  c.write_wlan_config(band, ssid, psk)
+  # Now wait for the WLAN config to be created, connect to the WLAN, and make
+  # sure that s3 is unset as last_successful_bss_info, since it is no longer
+  # available.
   c.run_once()
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
 
-  c.can_connect_to_s3 = False
-  c.scan_results_include_hidden = False
-  c.delete_wlan_config(band)
+  # Remove s3.
+  subprocess.mock('wifi', 'remote_ap_remove',
+                  bssid='ff:ee:dd:cc:bb:aa', band=band)
+  # Bring s2 back.
+  subprocess.mock('wifi', 'remote_ap', bssid='01:23:45:67:89:ab', ssid='s2',
+                  band=band, security=None)
+
+  subprocess.mock('cwmp', band, delete_config=True, write_now=True)
   c.run_once()
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, None)
 
@@ -939,7 +614,7 @@
   #    disconnecting.
   # 4) Connect to s2 but get no ACS access; see that last_successful_bss_info is
   #    unset.
-  c.write_wlan_config(band, ssid, psk)
+  subprocess.mock('cwmp', band, ssid=ssid, psk=psk, write_now=True)
   # Connect
   c.run_once()
   # Process DHCP results
@@ -947,31 +622,37 @@
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
 
-  c.delete_wlan_config(band)
+  subprocess.mock('cwmp', band, delete_config=True, write_now=True)
   c.run_once()
   wvtest.WVFAIL(c.wifi_for_band(band).acs())
 
-  c.can_connect_to_s2 = True
   # Give it time to try all BSSIDs.  This means sleeping long enough that
   # everything in the cycler is active, then doing n+1 loops (the n+1st loop is
   # when we decide that the SSID in the nth loop was successful).
   time.sleep(c._bssid_cycle_length_s)
+  subprocess.mock('cwmp', band, ssid=ssid, psk=psk)
   for _ in range(len(c.wifi_for_band(band).cycler) + 1):
     c.run_once()
-  s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
+  s2_bss = iw.BssInfo(bssid='01:23:45:67:89:ab', ssid='s2', band=band)
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
   c.run_once()
-  wvtest.WVPASS(c.tried_to_upload_logs())
+  wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
 
-  c.s2_fail = True
-  c.write_wlan_config(band, ssid, psk)
+  # Make s2's connection check fail.
+  subprocess.mock('wifi', 'remote_ap', bssid='01:23:45:67:89:ab', ssid='s2',
+                  band=band, security=None, connection_check_result='fail')
+
   c.run_once()
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
-
   wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
-  c._wlan_configuration[band].stale = True
-  c.wifi_for_band(band).add_disconnected_event()
+  # Disconnect.
+  ssid = 'wlan3'
+  psk = 'password3'
+  subprocess.mock('wifi', 'remote_ap',
+                  bssid='11:22:33:44:55:66',
+                  ssid=ssid, psk=psk, band=band, security='WPA2')
+  subprocess.mock('wifi', 'disconnected_event', band)
   # Run once so that c will reconnect to s2.
   c.run_once()
   wvtest.WVPASS(c.wifi_for_band(band).connected_to_open())
@@ -986,16 +667,18 @@
   # which lets us force a timeout and proceed to the next AP.  Having a stale
   # WLAN configuration shouldn't interrupt provisioning.
   del c.wifi_for_band(band).cycler
-  c.interface_with_scan_results = c.wifi_for_band(band).name
-  c.scan_results_include_hidden = True
-  c.can_connect_to_s3 = True
-  c.dhcp_failure_on_s3 = True
+  subprocess.mock('wifi', 'remote_ap', bssid='ff:ee:dd:cc:bb:aa', ssid='s3',
+                  band=band, security=None, hidden=True,
+                  vendor_ies=(('f4:f5:e8', '01'), ('f4:f5:e8', '03 73 33')))
+  subprocess.mock('run-dhclient', c.wifi_for_band(band).name, failure=True)
+
   # First iteration: check that we try s3.
   c.run_until_scan(band)
   last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
   wvtest.WVPASSEQ(last_bss_info.ssid, 's3')
   wvtest.WVPASSEQ(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
-  c.write_wlan_config(band, ssid, psk)
+  # Attempt to interrupt provisioning, make sure it doesn't work.
+  c._try_wlan_after[band] = 0
   # Second iteration: check that we try s3 again since there's no gateway yet.
   c.run_once()
   last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
@@ -1011,7 +694,8 @@
   wvtest.WVPASSNE(last_bss_info.bssid, 'ff:ee:dd:cc:bb:aa')
 
   # We can delete the stale WLAN config now, to simplify subsequent tests.
-  c.delete_wlan_config(band)
+  # c.delete_wlan_config(band)
+  subprocess.mock('cwmp', band, delete_config=True, write_now=True)
 
   # Now repeat the above, but for an ACS session that takes a while.  We don't
   # necessarily want to leave if it fails (so we don't want the third check),
@@ -1020,12 +704,13 @@
   # Unlike DHCP, which we can always simulate working immediately above, it is
   # wrong to simulate ACS sessions working for connections without ACS access.
   # This means we can either always wait for the ACS session timeout in every
-  # test above, making the tests much slower, or we can set that timeout to 0
-  # and then be a little gross here and change it.  The latter is unfortunately
-  # the lesser evil, because slow tests are bad.
+  # test above, making the tests much slower, or we can set that timeout very
+  # low and then be a little gross here and change it.  The latter is
+  # unfortunately the lesser evil, because slow tests are bad.
   del c.wifi_for_band(band).cycler
-  c.dhcp_failure_on_s3 = False
-  c.acs_session_fails = True
+  subprocess.mock('run-dhclient', c.wifi_for_band(band).name, failure=False)
+  subprocess.mock('cwmp', band, acs_session_fails=True)
+
   # First iteration: check that we try s3.
   c.run_until_scan(band)
   last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
@@ -1050,8 +735,7 @@
 
   # Finally, test successful provisioning.
   del c.wifi_for_band(band).cycler
-  c.dhcp_failure_on_s3 = False
-  c.acs_session_fails = False
+  subprocess.mock('cwmp', band, acs_session_fails=False)
   # First iteration: check that we try s3.
   c.run_until_scan(band)
   last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
@@ -1092,25 +776,19 @@
 
 
 @wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
-                         quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
-                        )
+@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
-@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
-                         quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
-                        )
+@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
-@connection_manager_test(WIFI_SHOW_OUTPUT_FRENZY,
-                         quantenna_interfaces=['wlan0', 'wlan0_portal', 'wcli0']
-                        )
+@connection_manager_test(WIFI_SHOW_OUTPUT_FRENZY)
 def connection_manager_test_generic_frenzy_5g(c):
   connection_manager_test_generic(c, '5')
 
@@ -1123,24 +801,28 @@
   Args:
     c:  The ConnectionManager set up by @connection_manager_test.
   """
+  ssid = 'my ssid'
+  psk = 'passphrase'
+
   wvtest.WVPASSEQ(len(c._binwifi_commands), 2)
+
   for band in ['2.4', '5']:
     wvtest.WVPASS(('stop', '--band', band, '--persist') in c._binwifi_commands)
 
+    subprocess.mock('wifi', 'remote_ap',
+                    bssid='11:22:33:44:55:66',
+                    ssid=ssid, psk=psk, band=band, security='WPA2')
+
   # Bring up ethernet, access.
   c.set_ethernet(True)
   c.run_once()
   wvtest.WVPASS(c.acs())
   wvtest.WVPASS(c.internet())
 
-  ssid = 'my ssid'
-  psk = 'passphrase'
-
   # Bring up both access points.
-  c.write_wlan_config('2.4', ssid, psk)
-  c.enable_access_point('2.4')
-  c.write_wlan_config('5', ssid, psk)
-  c.enable_access_point('5')
+  for band in ('2.4', '5'):
+    subprocess.mock('cwmp', band, ssid=ssid, psk=psk, access_point=True,
+                    write_now=True)
   c.run_once()
   wvtest.WVPASS(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
@@ -1152,7 +834,7 @@
 
   # Disable the 2.4 GHz AP, make sure the 5 GHz AP stays up.  2.4 GHz should
   # join the WLAN.
-  c.disable_access_point('2.4')
+  subprocess.mock('cwmp', '2.4', access_point=False, write_now=True)
   c.run_until_interface_update()
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
@@ -1163,7 +845,7 @@
 
   # Delete the 2.4 GHz WLAN configuration; it should leave the WLAN but nothing
   # else should change.
-  c.delete_wlan_config('2.4')
+  subprocess.mock('cwmp', '2.4', delete_config=True, write_now=True)
   c.run_until_interface_update()
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
@@ -1175,7 +857,7 @@
   # Disable the wired connection and remove the WLAN configurations.  Both
   # radios should scan.  Wait for 5 GHz to scan, then enable scan results for
   # 2.4. This should lead to ACS access.
-  c.delete_wlan_config('5')
+  subprocess.mock('cwmp', '5', delete_config=True, write_now=True)
   c.set_ethernet(False)
   c.run_once()
   wvtest.WVFAIL(c.acs())
@@ -1193,7 +875,7 @@
   wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # The next 2.4 GHz scan will have results.
-  c.interface_with_scan_results = c.wifi_for_band('2.4').name
+  _enable_basic_scan_results('2.4')
   c.run_until_scan('2.4')
   # Now run for enough cycles that s2 will have been tried.
   for _ in range(len(c.wifi_for_band('2.4').cycler)):
@@ -1204,7 +886,7 @@
   wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
   wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
   c.run_once()
-  wvtest.WVPASS(c.tried_to_upload_logs())
+  wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
 
 
 @wvtest.wvtest
@@ -1214,9 +896,7 @@
 
 
 @wvtest.wvtest
-@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
-                         quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
-                        )
+@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)
 
@@ -1243,10 +923,10 @@
   psk = 'passphrase'
 
   # Enable both access points.  Only 5 should be up.
-  c.write_wlan_config('2.4', ssid, psk)
-  c.enable_access_point('2.4')
-  c.write_wlan_config('5', ssid, psk)
-  c.enable_access_point('5')
+  subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, access_point=True,
+                  write_now=True)
+  subprocess.mock('cwmp', '5', ssid=ssid, psk=psk, access_point=True,
+                  write_now=True)
   c.run_once()
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
@@ -1256,7 +936,7 @@
 
   # Disable the 2.4 GHz AP; nothing should change.  The 2.4 GHz client should
   # not be up because the same radio is being used to run a 5 GHz AP.
-  c.disable_access_point('2.4')
+  subprocess.mock('cwmp', '2.4', access_point=False, write_now=True)
   c.run_until_interface_update()
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
@@ -1266,7 +946,7 @@
   wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # Delete the 2.4 GHz WLAN configuration; nothing should change.
-  c.delete_wlan_config('2.4')
+  subprocess.mock('cwmp', '2.4', delete_config=True, write_now=True)
   c.run_once()
   wvtest.WVFAIL(c.access_point_up('2.4'))
   wvtest.WVPASS(c.access_point_up('5'))
@@ -1279,7 +959,7 @@
   # should be a single scan that leads to ACS access.  (It doesn't matter which
   # band we specify in run_until_scan, since both bands point to the same
   # interface.)
-  c.delete_wlan_config('5')
+  subprocess.mock('cwmp', '5', delete_config=True, write_now=True)
   c.set_ethernet(False)
   c.run_once()
   wvtest.WVFAIL(c.acs())
@@ -1288,7 +968,7 @@
   wvtest.WVFAIL(c.wifi_for_band('5').current_routes_normal_testonly())
 
   # The scan will have results that will lead to ACS access.
-  c.interface_with_scan_results = c.wifi_for_band('2.4').name
+  _enable_basic_scan_results('2.4')
   c.run_until_scan('5')
   for _ in range(len(c.wifi_for_band('2.4').cycler)):
     c.run_once()
@@ -1298,7 +978,7 @@
   wvtest.WVPASS(c.wifi_for_band('2.4').current_routes())
   wvtest.WVPASS(c.wifi_for_band('5').current_routes())
   c.run_once()
-  wvtest.WVPASSEQ(c.log_upload_count, 1)
+  wvtest.WVPASS(subprocess.upload_logs_and_wait.uploaded_logs())
 
 
 @wvtest.wvtest
@@ -1320,6 +1000,12 @@
   """
   # Make sure we've correctly set up the test; that there is no 5 GHz wifi
   # interface.
+  ssid = 'my ssid'
+  psk = 'my psk'
+  subprocess.mock('wifi', 'remote_ap', band='2.4', ssid=ssid, psk=psk,
+                  bssid='00:00:00:00:00:00', security='WPA2',
+                  connection_check_result='succeed')
+
   wvtest.WVPASSEQ(c.wifi_for_band('5'), None)
 
   c.set_ethernet(True)
@@ -1327,17 +1013,17 @@
   wvtest.WVPASS(c.internet())
 
   # Make sure this doesn't crash.
-  c.write_wlan_config('5', 'my ssid', 'my psk')
+  subprocess.mock('cwmp', '5', ssid=ssid, psk=psk, write_now=True)
   c.run_once()
-  c.enable_access_point('5')
+  subprocess.mock('cwmp', '5', access_point=True, write_now=True)
   c.run_once()
-  c.disable_access_point('5')
+  subprocess.mock('cwmp', '5', access_point=False, write_now=True)
   c.run_once()
-  c.delete_wlan_config('5')
+  subprocess.mock('cwmp', '5', delete_config=True, write_now=True)
   c.run_once()
 
   # Make sure 2.4 still works.
-  c.write_wlan_config('2.4', 'my ssid', 'my psk')
+  subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, write_now=True)
   # Connect
   c.run_once()
   # Process DHCP results
@@ -1394,19 +1080,23 @@
 
   # First, establish that we connect on 2.4 without the experiment, to make sure
   # this test doesn't spuriously pass.
-  c.write_wlan_config('2.4', 'my ssid', 'my psk')
+  ssid = 'my ssid'
+  psk = 'my psk'
+  subprocess.mock('wifi', 'remote_ap', ssid=ssid, psk=psk, band='2.4',
+                  bssid='00:00:00:00:00:00')
+  subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, write_now=True)
   c.run_once()
   wvtest.WVPASS(c.client_up('2.4'))
 
   # Now, force a disconnect by deleting the config.
-  c.delete_wlan_config('2.4')
+  subprocess.mock('cwmp', '2.4', delete_config=True, write_now=True)
   c.run_once()
   wvtest.WVFAIL(c.client_up('2.4'))
 
   # Now enable the experiment, recreate the config, and make sure we don't
   # connect.
   experiment_testutils.enable('WifiNo2GClient')
-  c.write_wlan_config('2.4', 'my ssid', 'my psk')
+  subprocess.mock('cwmp', '2.4', ssid=ssid, psk=psk, write_now=True)
   c.run_once()
   wvtest.WVFAIL(c.client_up('2.4'))
 
diff --git a/conman/interface.py b/conman/interface.py
index e71a322..68aa35b 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -12,7 +12,6 @@
 logging.basicConfig(level=logging.DEBUG)
 
 import experiment
-import wpactrl
 
 METRIC_5GHZ = 20
 METRIC_24GHZ_5GHZ = 21
@@ -229,15 +228,12 @@
 
     try:
       logging.debug('%s calling ip route %s', self.name, ' '.join(args))
-      return self._really_ip_route(*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)
       return ''
 
-  def _really_ip_route(self, *args):
-    return subprocess.check_output(self.IP_ROUTE + list(args))
-
   def _ip_addr_show(self):
     try:
       return subprocess.check_output(self.IP_ADDR_SHOW + [self.name])
@@ -431,57 +427,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.
 
@@ -491,80 +449,41 @@
     """
     status = {}
 
-    if self._wpa_control and self._wpa_control.attached:
-      logging.debug('%s ctrl_iface_path %s',
-                    self, self._wpa_control.ctrl_iface_path)
-      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
-
-    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
-
-    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):
-    return (self.wpa_status().get('wpa_state', None) == 'COMPLETED' and
-            self.wpa_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'])
+      lines = subprocess.check_output(['wpa_cli', '-i', self.name,
+                                       'status']).splitlines()
     except subprocess.CalledProcessError:
       logging.error('wpa_cli status request failed')
-      return ''
+      return {}
+
+    for line in lines:
+      if '=' not in line:
+        continue
+      k, v = line.strip().split('=', 1)
+      status[k] = v
+
+    logging.debug('wpa_status is %r', status)
+    return status
+
+  def update(self):
+    self.wpa_supplicant = self.wpa_status().get('wpa_state', '') == 'COMPLETED'
+
+  def connected_to_open(self):
+    status = self.wpa_status()
+    return (status.get('wpa_state', None) == 'COMPLETED' and
+            status.get('key_mgmt', None) == 'NONE')
+
+  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
@@ -572,19 +491,6 @@
   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()
@@ -592,79 +498,27 @@
       logging.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):
-    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 74363b8..f080285 100755
--- a/conman/interface_test.py
+++ b/conman/interface_test.py
@@ -5,8 +5,6 @@
 import logging
 import os
 import shutil
-import socket
-import struct
 import subprocess
 import tempfile
 import time
@@ -15,11 +13,6 @@
 # pylint: disable=g-import-not-at-top
 logging.basicConfig(level=logging.DEBUG)
 
-# This is in site-packages on the device, but not when running tests, and so
-# raises lint errors.
-# pylint: disable=g-bad-import-order
-import wpactrl
-
 import experiment_testutils
 import interface
 from wvtest import wvtest
@@ -38,87 +31,13 @@
   def __init__(self, *args, **kwargs):
     super(FakeInterfaceMixin, self).__init__(*args, **kwargs)
     self.set_connection_check_result('succeed')
-    self.routing_table = {}
-    self.ip_testonly = None
-
-  def _connection_check(self, *args, **kwargs):
-    result = super(FakeInterfaceMixin, self)._connection_check(*args, **kwargs)
-    if not self.links:
-      return False
-    if (self.current_routes().get('default', {}).get('via', None) !=
-        self._gateway_ip):
-      return False
-    return result
+    subprocess.ip.register_testonly(self.name)
 
   def set_connection_check_result(self, result):
     if result in ['succeed', 'fail', 'restricted']:
-      # pylint: disable=invalid-name
-      self.CONNECTION_CHECK = './test/' + result
+      subprocess.mock(self.CONNECTION_CHECK, self.name, result)
     else:
-      raise ValueError('Invalid fake connection_check script.')
-
-  def _really_ip_route(self, *args):
-    def can_add_route():
-      def ip_to_int(ip):
-        return struct.unpack('!I', socket.inet_pton(socket.AF_INET, ip))[0]
-
-      if args[1] != 'default':
-        return True
-
-      via = ip_to_int(args[args.index('via') + 1])
-      for (ifc, route, _), _ in self.routing_table.iteritems():
-        if ifc != self.name:
-          continue
-
-        netmask = 0
-        if '/' in route:
-          route, netmask = route.split('/')
-          netmask = 32 - int(netmask)
-        route = ip_to_int(route)
-
-        if (route >> netmask) == (via >> netmask):
-          return True
-
-      return False
-
-    if not args:
-      return '\n'.join(self.routing_table.values() +
-                       ['1.2.3.4/24 dev fake0 proto kernel scope link',
-                        # Non-subnet route, e.g. to NFS host.
-                        '1.2.3.1 dev %s proto kernel scope link' % self.name,
-                        'default via 1.2.3.4 dev fake0',
-                        'random junk'])
-
-    metric = None
-    if 'metric' in args:
-      metric = args[args.index('metric') + 1]
-    if args[0] in ('add', 'del'):
-      route = args[1]
-    key = (self.name, route, metric)
-    if args[0] == 'add' and key not in self.routing_table:
-      if not can_add_route():
-        raise subprocess.CalledProcessError(
-            'Tried to add default route without subnet route: %r',
-            self.routing_table)
-      logging.debug('Adding route for %r', key)
-      self.routing_table[key] = ' '.join(args[1:])
-    elif args[0] == 'del':
-      if key in self.routing_table:
-        logging.debug('Deleting route for %r', key)
-        del self.routing_table[key]
-      elif key[2] is None:
-        # pylint: disable=g-builtin-op
-        for k in self.routing_table.keys():
-          if k[:-1] == key[:-1]:
-            logging.debug('Deleting route for %r (generalized from %s)', k, key)
-            del self.routing_table[k]
-            break
-
-  def _ip_addr_show(self):
-    if self.ip_testonly:
-      return _IP_ADDR_SHOW_TPL.format(name=self.name, ip=self.ip_testonly)
-
-    return ''
+      raise ValueError('Invalid fake connection_check value.')
 
   def current_routes_normal_testonly(self):
     result = self.current_routes()
@@ -129,244 +48,13 @@
   pass
 
 
-class FakeWPACtrl(object):
-  """Fake wpactrl.WPACtrl."""
-
-  # pylint: disable=unused-argument
-  def __init__(self, wpa_socket):
-    self._socket = wpa_socket
-    self.events = []
-    self.attached = False
-    self.connected = False
-    self.ssid_testonly = None
-    self.secure_testonly = False
-    self.request_status_fails = False
-
-  def pending(self):
-    self.check_socket_exists('pending: socket does not exist')
-    return bool(self.events)
-
-  def recv(self):
-    self.check_socket_exists('recv: socket does not exist')
-    return self.events.pop(0)
-
-  def attach(self):
-    if not os.path.exists(self._socket):
-      raise wpactrl.error('wpactrl_attach failed')
-    self.attached = True
-
-  def detach(self):
-    self.attached = False
-    self.ssid_testonly = None
-    self.secure_testonly = False
-    self.connected = False
-    self.check_socket_exists('wpactrl_detach failed')
-
-  def request(self, request_type):
-    if request_type == 'STATUS':
-      if self.request_status_fails:
-        raise wpactrl.error('test error')
-      return self.wpa_cli_status_testonly()
-    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 add_event(self, event):
-    self.events.append(event)
-
-  def add_connected_event(self):
-    self.connected = True
-    self.add_event(Wifi.CONNECTED_EVENT)
-
-  def add_disconnected_event(self):
-    self.connected = False
-    self.add_event(Wifi.DISCONNECTED_EVENT)
-
-  def add_terminating_event(self):
-    self.connected = False
-    self.add_event(Wifi.TERMINATING_EVENT)
-
-  def check_socket_exists(self, msg='Fake socket does not exist'):
-    if not os.path.exists(self._socket):
-      raise wpactrl.error(msg)
-
-  def wpa_cli_status_testonly(self):
-    if self.connected:
-      return ('foo\nwpa_state=COMPLETED\nssid=%s\nkey_mgmt=%s\nbar' %
-              (self.ssid_testonly,
-               'WPA2-PSK' if self.secure_testonly else 'NONE'))
-    else:
-      return 'wpa_state=SCANNING\naddress=12:34:56:78:90:ab'
-
-
 class Wifi(FakeInterfaceMixin, interface.Wifi):
   """Fake Wifi for testing."""
-
-  CONNECTED_EVENT = '<2>CTRL-EVENT-CONNECTED'
-  DISCONNECTED_EVENT = '<2>CTRL-EVENT-DISCONNECTED'
-  TERMINATING_EVENT = '<2>CTRL-EVENT-TERMINATING'
-
-  WPACtrl = FakeWPACtrl
-
-  def __init__(self, *args, **kwargs):
-    super(Wifi, self).__init__(*args, **kwargs)
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-
-  def attach_wpa_control(self, path):
-    if self._initial_ssid_testonly and self._wpa_control:
-      self._wpa_control.connected = True
-    super(Wifi, self).attach_wpa_control(path)
-
-  def get_wpa_control(self, *args, **kwargs):
-    result = super(Wifi, self).get_wpa_control(*args, **kwargs)
-    if self._initial_ssid_testonly:
-      result.connected = True
-      result.ssid_testonly = self._initial_ssid_testonly
-      result.secure_testonly = self._secure_testonly
-    return result
-
-  def add_connected_event(self):
-    if self.attached():
-      self._wpa_control.add_connected_event()
-
-  def add_disconnected_event(self):
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-    if self.attached():
-      self._wpa_control.add_disconnected_event()
-
-  def add_terminating_event(self):
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-    if self.attached():
-      self._wpa_control.add_terminating_event()
-
-  def detach_wpa_control(self):
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-    super(Wifi, self).detach_wpa_control()
-
-  def wpa_cli_status(self):
-    # This is just a convenient way of keeping things dry; the actual wpa_cli
-    # status makes a subprocess call which returns the same string.
-    return self._wpa_control.wpa_cli_status_testonly()
-
-  def start_wpa_supplicant_testonly(self, path):
-    wpa_socket = os.path.join(path, self.name)
-    logging.debug('Starting fake wpa_supplicant for %s: %s',
-                  self.name, wpa_socket)
-    open(wpa_socket, 'w')
-
-  def kill_wpa_supplicant_testonly(self, path):
-    logging.debug('Killing fake wpa_supplicant for %s', self.name)
-    if self.attached():
-      self.detach_wpa_control()
-      os.unlink(os.path.join(path, self.name))
-    else:
-      raise RuntimeError('Trying to kill wpa_supplicant while not attached')
-
-
-class FrenzyWPACtrl(interface.FrenzyWPACtrl):
-
-  def __init__(self, *args, **kwargs):
-    super(FrenzyWPACtrl, self).__init__(*args, **kwargs)
-    self.ssid_testonly = None
-    self.secure_testonly = False
-    self.request_status_fails = False
-
-  def _qcsapi(self, *command):
-    return self.fake_qcsapi.get(command[0], None)
-
-  def add_connected_event(self):
-    self.fake_qcsapi['get_mode'] = 'Station'
-    self.fake_qcsapi['get_ssid'] = self.ssid_testonly
-    security = 'PSKAuthentication' if self.secure_testonly else 'NONE'
-    self.fake_qcsapi['ssid_get_authentication_mode'] = security
-
-  def add_disconnected_event(self):
-    self.ssid_testonly = None
-    self.secure_testonly = False
-    self.fake_qcsapi['get_ssid'] = None
-    self.fake_qcsapi['ssid_get_authentication_mode'] = 'NONE'
-
-  def add_terminating_event(self):
-    self.ssid_testonly = None
-    self.secure_testonly = False
-    self.fake_qcsapi['get_ssid'] = None
-    self.fake_qcsapi['get_mode'] = 'AP'
-    self.fake_qcsapi['ssid_get_authentication_mode'] = 'NONE'
-
-  def detach(self):
-    self.add_terminating_event()
-    super(FrenzyWPACtrl, self).detach()
+  pass
 
 
 class FrenzyWifi(FakeInterfaceMixin, interface.FrenzyWifi):
-  WPACtrl = FrenzyWPACtrl
-
-  def __init__(self, *args, **kwargs):
-    super(FrenzyWifi, self).__init__(*args, **kwargs)
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-    self.fake_qcsapi = {}
-
-  def attach_wpa_control(self, *args, **kwargs):
-    super(FrenzyWifi, self).attach_wpa_control(*args, **kwargs)
-    if self._wpa_control:
-      self._wpa_control.ssid_testonly = self._initial_ssid_testonly
-      self._wpa_control.secure_testonly = self._secure_testonly
-      if self._initial_ssid_testonly:
-        self._wpa_control.add_connected_event()
-
-  def get_wpa_control(self, *args, **kwargs):
-    result = super(FrenzyWifi, self).get_wpa_control(*args, **kwargs)
-    result.fake_qcsapi = self.fake_qcsapi
-    if self._initial_ssid_testonly:
-      result.fake_qcsapi['get_mode'] = 'Station'
-      result.ssid_testonly = self._initial_ssid_testonly
-      result.secure_testonly = self._secure_testonly
-      result.add_connected_event()
-    return result
-
-  def add_connected_event(self):
-    if self.attached():
-      self._wpa_control.add_connected_event()
-
-  def add_disconnected_event(self):
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-    if self.attached():
-      self._wpa_control.add_disconnected_event()
-
-  def add_terminating_event(self):
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-    if self.attached():
-      self._wpa_control.add_terminating_event()
-
-  def detach_wpa_control(self):
-    self._initial_ssid_testonly = None
-    self._secure_testonly = False
-    super(FrenzyWifi, self).detach_wpa_control()
-
-  def start_wpa_supplicant_testonly(self, unused_path):
-    logging.debug('Starting fake wpa_supplicant for %s', self.name)
-    self.fake_qcsapi['get_mode'] = 'Station'
-
-  def kill_wpa_supplicant_testonly(self, unused_path):
-    logging.debug('Killing fake wpa_supplicant for %s', self.name)
-    if self.attached():
-      # This happens to do what we need.
-      self.add_terminating_event()
-      self.detach_wpa_control()
-    else:
-      raise RuntimeError('Trying to kill wpa_supplicant while not attached')
+  pass
 
 
 @wvtest.wvtest
@@ -443,7 +131,7 @@
     wvtest.WVPASS(os.path.exists(autoprov_filepath))
 
     wvtest.WVFAIL(b.get_ip_address())
-    b.ip_testonly = '192.168.1.100'
+    subprocess.call(['ip', 'addr', 'add', '192.168.1.100', 'dev', b.name])
     wvtest.WVPASSEQ(b.get_ip_address(), '192.168.1.100')
 
     # Get a new gateway/subnet (e.g. due to joining a new network).
@@ -487,49 +175,25 @@
 
 def generic_wifi_test(w, wpa_path):
   # Not currently connected.
-  w.start_wpa_supplicant_testonly(wpa_path)
-  w.attach_wpa_control(wpa_path)
+  subprocess.wifi.WPA_PATH = wpa_path
   wvtest.WVFAIL(w.wpa_supplicant)
 
-  # pylint: disable=protected-access
-  wpa_control = w._wpa_control
-
   # wpa_supplicant connects.
-  wpa_control.ssid_testonly = 'my=ssid'
-  wpa_control.add_connected_event()
-  wvtest.WVFAIL(w.wpa_supplicant)
-  w.handle_wpa_events()
+  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})
   wvtest.WVPASS(w.wpa_supplicant)
   w.set_gateway_ip('192.168.1.1')
 
   # wpa_supplicant disconnects.
-  wpa_control.add_disconnected_event()
-  w.handle_wpa_events()
+  subprocess.mock('wifi', 'disconnected_event', '5')
   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()
-  # pylint: disable=protected-access
-  w._initial_ssid_testonly = 'my=ssid'
-  w._initialized = False
-  w.attach_wpa_control(wpa_path)
-  wpa_control = w._wpa_control
-
-  # wpa_supplicant was already connected when we attached.
-  wvtest.WVPASS(w.wpa_supplicant)
-  wvtest.WVPASSEQ(w.initial_ssid, 'my=ssid')
-  w.initialize()
-  wvtest.WVPASSEQ(w.initial_ssid, None)
-
-  wvtest.WVPASSNE(w.wpa_status(), {})
-  w._wpa_control.request_status_fails = True
-  wvtest.WVPASSNE(w.wpa_status(), {})
-
   # The wpa_supplicant process disconnects and terminates.
-  wpa_control.add_disconnected_event()
-  wpa_control.add_terminating_event()
-  w.handle_wpa_events()
+  subprocess.check_call(['wifi', 'stopclient', '--band', '5'])
   wvtest.WVFAIL(w.wpa_supplicant)
 
 
@@ -537,33 +201,40 @@
 def wifi_test():
   """Test Wifi."""
   w = Wifi('wcli0', '21')
-  w.set_connection_check_result('succeed')
   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'))
     generic_wifi_test(w, wpa_path)
 
   finally:
     shutil.rmtree(wpa_path)
+    shutil.rmtree(conman_path)
 
 
 @wvtest.wvtest
 def frenzy_wifi_test():
   """Test FrenzyWifi."""
   w = FrenzyWifi('wlan0', '20')
-  w.set_connection_check_result('succeed')
   w.initialize()
 
   try:
     wpa_path = tempfile.mkdtemp()
-    FrenzyWifi.WPACtrl.WIFIINFO_PATH = wifiinfo_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='frenzy'))
     generic_wifi_test(w, wpa_path)
 
   finally:
     shutil.rmtree(wpa_path)
-    shutil.rmtree(wifiinfo_path)
+    shutil.rmtree(conman_path)
 
 
 @wvtest.wvtest
diff --git a/conman/iw.py b/conman/iw.py
index f2e15d8..8d80010 100755
--- a/conman/iw.py
+++ b/conman/iw.py
@@ -15,6 +15,7 @@
 _BSSID_RE = r'BSS (?P<BSSID>([0-9a-f]{2}:?){6})\(on .*\)'
 _SSID_RE = r'SSID: (?P<SSID>.*)'
 _RSSI_RE = r'signal: (?P<RSSI>.*) dBm'
+_FREQ_RE = r'freq: (?P<freq>\d+)'
 _VENDOR_IE_RE = (r'Vendor specific: OUI (?P<OUI>([0-9a-f]{2}:?){3}), '
                  'data:(?P<data>( [0-9a-f]{2})+)')
 
@@ -29,16 +30,17 @@
 class BssInfo(object):
   """Contains info about a BSS, parsed from 'iw scan'."""
 
-  def __init__(self, bssid='', ssid='', rssi=-100, security=None,
+  def __init__(self, bssid='', ssid='', rssi=0, band=None, security=None,
                vendor_ies=None):
     self.bssid = bssid
     self.ssid = ssid
     self.rssi = rssi
+    self.band = band
     self.vendor_ies = vendor_ies or []
     self.security = security or []
 
   def __attrs(self):
-    return (self.bssid, self.ssid, tuple(sorted(self.vendor_ies)),
+    return (self.bssid, self.ssid, self.band, tuple(sorted(self.vendor_ies)),
             tuple(sorted(self.security)), self.rssi)
 
   def __eq__(self, other):
@@ -52,9 +54,9 @@
     return hash(self.__attrs())
 
   def __repr__(self):
-    return '<BssInfo: SSID=%s BSSID=%s Security=%s Vendor IEs=%s>' % (
-        self.ssid, self.bssid, ','.join(self.security),
-        ','.join('|'.join(ie) for ie in self.vendor_ies))
+    return ('<BssInfo: SSID=%s BSSID=%s Band=%s Security=%s Vendor IEs=%s>'
+            % (self.ssid, self.bssid, self.band, ','.join(self.security),
+               ','.join('|'.join(ie) for ie in self.vendor_ies)))
 
 
 # TODO(rofrankel): waveguide also scans. Can we find a way to avoid two programs
@@ -79,6 +81,10 @@
     if match:
       bss_info.rssi = float(match.group('RSSI'))
       continue
+    match = re.match(_FREQ_RE, line)
+    if match:
+      bss_info.band = '2.4' if match.group('freq').startswith('2') else '5'
+      continue
     match = re.match(_VENDOR_IE_RE, line)
     if match:
       bss_info.vendor_ies.append((match.group('OUI'),
diff --git a/conman/iw_test.py b/conman/iw_test.py
index 55b2e7b..202d10c 100755
--- a/conman/iw_test.py
+++ b/conman/iw_test.py
@@ -2,633 +2,48 @@
 
 """Tests for iw.py."""
 
+import subprocess
+
 import iw
 from wvtest import wvtest
 
 
-SCAN_OUTPUT = """BSS 00:23:97:57:f4:d8(on wcli0)
-  TSF: 1269828266773 usec (14d, 16:43:48)
-  freq: 2437
-  beacon interval: 100 TUs
-  capability: ESS Privacy ShortSlotTime (0x0411)
-  signal: -60.00 dBm
-  last seen: 2190 ms ago
-  Information elements from Probe Response frame:
-  Vendor specific: OUI 00:11:22, data: 01 23 45 67
-  SSID: short scan result
-  Supported rates: 1.0* 2.0* 5.5* 11.0* 18.0 24.0 36.0 54.0
-  DS Parameter set: channel 6
-  ERP: <no flags>
-  ERP D4.0: <no flags>
-  Privacy:  WEP
-  Extended supported rates: 6.0 9.0 12.0 48.0
-BSS 94:b4:0f:f1:02:a0(on wcli0)
-  TSF: 16233722683 usec (0d, 04:30:33)
-  freq: 2412
-  beacon interval: 100 TUs
-  capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
-  signal: -54.00 dBm
-  last seen: 2490 ms ago
-  Information elements from Probe Response frame:
-  SSID: Google
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 1
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  RSN:   * Version: 1
-     * Group cipher: CCMP
-     * Pairwise ciphers: CCMP
-     * Authentication suites: IEEE 802.1X
-     * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
-  BSS Load:
-     * station count: 0
-     * channel utilisation: 33/255
-     * available admission capacity: 25625 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 1
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 0
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:35:60(on wcli0)
-  TSF: 1739987968 usec (0d, 00:28:59)
-  freq: 2462
-  beacon interval: 100 TUs
-  capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
-  signal: -39.00 dBm
-  last seen: 1910 ms ago
-  Information elements from Probe Response frame:
-  SSID: Google
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 11
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  RSN:   * Version: 1
-     * Group cipher: CCMP
-     * Pairwise ciphers: CCMP
-     * Authentication suites: IEEE 802.1X
-     * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
-  BSS Load:
-     * station count: 0
-     * channel utilisation: 49/255
-     * available admission capacity: 26875 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 11
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:35:61(on wcli0)
-  TSF: 1739988134 usec (0d, 00:28:59)
-  freq: 2462
-  beacon interval: 100 TUs
-  capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
-  signal: -38.00 dBm
-  last seen: 1910 ms ago
-  Information elements from Probe Response frame:
-  SSID: GoogleGuest
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 11
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  BSS Load:
-     * station count: 1
-     * channel utilisation: 49/255
-     * available admission capacity: 26875 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 11
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:3a:e0(on wcli0)
-  TSF: 24578560051 usec (0d, 06:49:38)
-  freq: 2437
-  beacon interval: 100 TUs
-  capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
-  signal: -55.00 dBm
-  last seen: 2310 ms ago
-  Information elements from Probe Response frame:
-  SSID: Google
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 6
-  TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  RSN:   * Version: 1
-     * Group cipher: CCMP
-     * Pairwise ciphers: CCMP
-     * Authentication suites: IEEE 802.1X
-     * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
-  BSS Load:
-     * station count: 1
-     * channel utilisation: 21/255
-     * available admission capacity: 28125 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 6
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:3a:e1(on wcli0)
-  TSF: 24578576547 usec (0d, 06:49:38)
-  freq: 2437
-  beacon interval: 100 TUs
-  capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
-  signal: -65.00 dBm
-  last seen: 80 ms ago
-  Information elements from Probe Response frame:
-  SSID: GoogleGuest
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 6
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  BSS Load:
-     * station count: 2
-     * channel utilisation: 21/255
-     * available admission capacity: 28125 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 6
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-
-BSS 94:b4:0f:f1:36:41(on wcli0)
-  TSF: 12499149351 usec (0d, 03:28:19)
-  freq: 2437
-  beacon interval: 100 TUs
-  capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
-  signal: -67.00 dBm
-  last seen: 80 ms ago
-  Information elements from Probe Response frame:
-  SSID: GoogleGuest
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 6
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  BSS Load:
-     * station count: 1
-     * channel utilisation: 28/255
-     * available admission capacity: 27500 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 6
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:36:40(on wcli0)
-  TSF: 12499150000 usec (0d, 03:28:19)
-  freq: 2437
-  beacon interval: 100 TUs
-  capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
-  signal: -66.00 dBm
-  last seen: 2350 ms ago
-  Information elements from Probe Response frame:
-  SSID: Google
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 6
-  TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  RSN:   * Version: 1
-     * Group cipher: CCMP
-     * Pairwise ciphers: CCMP
-     * Authentication suites: IEEE 802.1X
-     * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
-  BSS Load:
-     * station count: 0
-     * channel utilisation: 28/255
-     * available admission capacity: 27500 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 6
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-BSS 94:b4:0f:f1:36:42(on wcli0)
-  TSF: 12499150000 usec (0d, 03:28:19)
-  freq: 2437
-  beacon interval: 100 TUs
-  capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
-  signal: -66.00 dBm
-  last seen: 2350 ms ago
-  Information elements from Probe Response frame:
-  SSID:
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 6
-  TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  BSS Load:
-     * station count: 0
-     * channel utilisation: 28/255
-     * available admission capacity: 27500 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 6
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-  Vendor specific: OUI 00:11:22, data: 01 23 45 67
-  Vendor specific: OUI f4:f5:e8, data: 01
-  Vendor specific: OUI f4:f5:e8, data: 03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 6f 6d 61 74 69 6f 6e
-BSS 00:1a:11:f1:36:43(on wcli0)
-  TSF: 12499150000 usec (0d, 03:28:19)
-  freq: 2437
-  beacon interval: 100 TUs
-  capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
-  signal: -66.00 dBm
-  last seen: 2350 ms ago
-  Information elements from Probe Response frame:
-  SSID:
-  Supported rates: 36.0* 48.0 54.0
-  DS Parameter set: channel 6
-  TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
-  Country: US Environment: Indoor/Outdoor
-    Channels [1 - 11] @ 36 dBm
-  Power constraint: 0 dB
-  TPC report: TX power: 3 dBm
-  ERP: <no flags>
-  BSS Load:
-     * station count: 0
-     * channel utilisation: 28/255
-     * available admission capacity: 27500 [*32us]
-  HT capabilities:
-    Capabilities: 0x19ad
-      RX LDPC
-      HT20
-      SM Power Save disabled
-      RX HT20 SGI
-      TX STBC
-      RX STBC 1-stream
-      Max AMSDU length: 7935 bytes
-      DSSS/CCK HT40
-    Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
-    Minimum RX AMPDU time spacing: 4 usec (0x05)
-    HT RX MCS rate indexes supported: 0-23
-    HT TX MCS rate indexes are undefined
-  HT operation:
-     * primary channel: 6
-     * secondary channel offset: no secondary
-     * STA channel width: 20 MHz
-     * RIFS: 1
-     * HT protection: nonmember
-     * non-GF present: 1
-     * OBSS non-GF present: 1
-     * dual beacon: 0
-     * dual CTS protection: 0
-     * STBC beacon: 0
-     * L-SIG TXOP Prot: 0
-     * PCO active: 0
-     * PCO phase: 0
-  Overlapping BSS scan params:
-     * passive dwell: 20 TUs
-     * active dwell: 10 TUs
-     * channel width trigger scan interval: 300 s
-     * scan passive total per channel: 200 TUs
-     * scan active total per channel: 20 TUs
-     * BSS width channel transition delay factor: 5
-     * OBSS Scan Activity Threshold: 0.25 %
-  Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
-  WMM:   * Parameter version 1
-     * u-APSD
-     * BE: CW 15-1023, AIFSN 3
-     * BK: CW 15-1023, AIFSN 7
-     * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
-     * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
-"""
-
-
-# pylint: disable=unused-argument,protected-access
-def fake_scan(*args, **kwargs):
-  return SCAN_OUTPUT
-iw._scan = fake_scan
+SCAN_RESULTS = (
+    {'rssi': -60, 'ssid': 'short scan result', 'bssid': '00:23:97:57:f4:d8',
+     'vendor_ies': [('00:11:22', '01 23 45 67')], 'security': 'WEP'},
+    {'rssi': -54, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:02:a0',
+     'vendor_ies': [], 'security': 'WPA2'},
+    {'rssi': -39, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:35:60',
+     'vendor_ies': [], 'security': 'WPA2'},
+    {'rssi': -38, 'ssid': 'GoogleGuest', 'bssid': '94:b4:0f:f1:35:61',
+     'vendor_ies': [], 'security': None},
+    {'rssi': -55, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:3a:e0',
+     'vendor_ies': [], 'security': 'WPA2'},
+    {'rssi': -65, 'ssid': 'GoogleGuest', 'bssid': '94:b4:0f:f1:3a:e1',
+     'vendor_ies': [], 'security': None},
+    {'rssi': -67, 'ssid': 'GoogleGuest', 'bssid': '94:b4:0f:f1:36:41',
+     'vendor_ies': [], 'security': None},
+    {'rssi': -66, 'ssid': 'Google', 'bssid': '94:b4:0f:f1:36:40',
+     'vendor_ies': [], 'security': 'WPA2'},
+    {'rssi': -66, 'ssid': '', 'bssid': '94:b4:0f:f1:36:42',
+     'vendor_ies': [('00:11:22', '01 23 45 67'), ('f4:f5:e8', '01'),
+                    ('f4:f5:e8', '03 47 46 69 62 65 72 53 65 74 75 70 41 75 74 '
+                     '6f 6d 61 74 69 6f 6e')], 'security': None},
+    {'rssi': -66, 'ssid': '', 'bssid': '00:1a:11:f1:36:43',
+     'vendor_ies': [], 'security': None},
+)
 
 
 @wvtest.wvtest
 def find_bssids_test():
   """Test iw.find_bssids."""
+  subprocess.mock('wifi', 'interfaces',
+                  subprocess.wifi.MockInterface(phynum='0', bands=['2.4', '5'],
+                                                driver='cfg80211'))
+  subprocess.call(['ifup', 'wcli0'])
+  for scan_result in SCAN_RESULTS:
+    subprocess.mock('wifi', 'remote_ap', band='5', **scan_result)
+
   test_ie = ('00:11:22', '01 23 45 67')
   provisioning_ie = ('f4:f5:e8', '01')
   ssid_ie = (
@@ -637,48 +52,51 @@
   )
   short_scan_result = iw.BssInfo(ssid='short scan result',
                                  bssid='00:23:97:57:f4:d8',
+                                 band='5',
                                  rssi=-60,
                                  security=['WEP'],
                                  vendor_ies=[test_ie])
   provisioning_bss_info = iw.BssInfo(ssid=iw.DEFAULT_GFIBERSETUP_SSID,
                                      bssid='94:b4:0f:f1:36:42',
+                                     band='5',
                                      rssi=-66,
                                      vendor_ies=[test_ie, provisioning_ie,
                                                  ssid_ie])
   provisioning_bss_info_frenzy = iw.BssInfo(ssid=iw.DEFAULT_GFIBERSETUP_SSID,
                                             bssid='00:1a:11:f1:36:43',
+                                            band='5',
                                             rssi=-66)
 
   wvtest.WVPASSEQ(
-      set(iw.find_bssids('wcli0', True)),
+      set(iw.find_bssids('2.4', True)),
       set([(short_scan_result, 2.4),
            (provisioning_bss_info, 5.34),
            (provisioning_bss_info_frenzy, 4.34),
-           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', rssi=-67),
-            2.33),
-           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', rssi=-65),
-            2.35),
-           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', rssi=-38),
-            2.62),
-           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:36:40', rssi=-66,
-                       security=['WPA2']), 2.34),
-           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:3a:e0', rssi=-55,
-                       security=['WPA2']), 2.45),
-           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:35:60', rssi=-39,
-                       security=['WPA2']), 2.61),
-           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:02:a0', rssi=-54,
-                       security=['WPA2']), 2.46)]))
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41',
+                       band='5', rssi=-67), 2.33),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1',
+                       band='5', rssi=-65), 2.35),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61',
+                       band='5', rssi=-38), 2.62),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:36:40', band='5',
+                       rssi=-66, security=['WPA2']), 2.34),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:3a:e0', band='5',
+                       rssi=-55, security=['WPA2']), 2.45),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:35:60', band='5',
+                       rssi=-39, security=['WPA2']), 2.61),
+           (iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:02:a0', band='5',
+                       rssi=-54, security=['WPA2']), 2.46)]))
 
   wvtest.WVPASSEQ(
-      set(iw.find_bssids('wcli0', False)),
+      set(iw.find_bssids('2.4', False)),
       set([(provisioning_bss_info, 5.34),
            (provisioning_bss_info_frenzy, 4.34),
-           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', rssi=-67),
-            2.33),
-           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', rssi=-65),
-            2.35),
-           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', rssi=-38),
-            2.62)]))
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41', band='5',
+                       rssi=-67), 2.33),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1', band='5',
+                       rssi=-65), 2.35),
+           (iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61', band='5',
+                       rssi=-38), 2.62)]))
 
 if __name__ == '__main__':
   wvtest.wvtest_main()
diff --git a/conman/ratchet.py b/conman/ratchet.py
index 07e61a8..61e8705 100644
--- a/conman/ratchet.py
+++ b/conman/ratchet.py
@@ -15,6 +15,7 @@
 except AttributeError:
   _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)
@@ -69,7 +70,7 @@
     if now > self.start_at + self.timeout:
       self.timed_out = True
       self.logger.info('%s timed out after %.2f seconds',
-                       self.name, now - self.t0)
+                       self.name, now - self.start_at)
       raise TimeoutException()
 
     self.not_done_before = _gettime()
@@ -81,7 +82,7 @@
     self.done_after = self.not_done_before
     self.done_by = _gettime()
     self.logger.info('%s completed after %.2f seconds',
-                     self.name, self.done_by - self.t0)
+                     self.name, self.done_by - self.start_at)
 
     if self.callback:
       self.callback()
@@ -90,47 +91,35 @@
 class FileExistsCondition(Condition):
   """A condition that checks for the existence of a file."""
 
-  def __init__(self, name, filename, timeout):
-    self._filename = filename
+  def __init__(self, name, filepath, timeout):
+    self._filepath = filepath
     super(FileExistsCondition, self).__init__(name, None, timeout)
 
   def evaluate(self):
-    return os.path.exists(self._filename)
-
-  def mtime(self):
-    if os.path.exists(self._filename):
-      return os.stat(self._filename).st_mtime
-
-    return None
-
-  def mark_done(self):
-    super(FileExistsCondition, self).mark_done()
-    # We have to check this because the file could have been deleted while this
-    # was being called.  But this condition should almost always be true.
-    mtime = self.mtime()
-    if mtime:
-      self.done_after = self.done_by = mtime
+    return os.path.exists(self._filepath)
 
 
 class FileTouchedCondition(FileExistsCondition):
-  """A condition that checks that a file was touched after a certain time."""
+  """A condition that checks that a file is touched.
 
-  def reset(self, t0=None, start_at=None):
-    mtime = self.mtime
-    if t0 and mtime and mtime < t0:
-      self.initial_mtime = self.mtime()
-    else:
-      self.initial_mtime = None
-    super(FileTouchedCondition, self).reset(t0, start_at)
+  Because the clock may be adjusted, we can't compare the file's mtime to a
+  timestamp.  So just look for mtime changes instead.  This means that t0 and
+  start_at aren't respected; instead, look for touches after whenever the
+  FileTouchedCondition is reset.
+  """
+
+  def reset(self, *args, **kwargs):
+    super(FileTouchedCondition, self).reset(*args, **kwargs)
+    self.initial_mtime = self.mtime()
 
   def evaluate(self):
     if not super(FileTouchedCondition, self).evaluate():
       return False
+    return self.mtime() != self.initial_mtime
 
-    if self.initial_mtime:
-      return self.mtime() > self.initial_mtime
-
-    return self.mtime() >= self.t0
+  def mtime(self):
+    if os.path.exists(self._filepath):
+      return os.stat(self._filepath).st_mtime
 
 
 class Ratchet(Condition):
@@ -146,6 +135,7 @@
 
   def reset(self):
     self._current_step = 0
+    self.active = False
     for step in self.steps:
       step.reset()
       self._set_step_status(step, False)
@@ -153,11 +143,18 @@
 
   def start(self):
     self.reset()
+    self.active = True
     self._set_current_step_status(True)
 
+  def stop(self):
+    self.active = False
+
   # Override check rather than evaluate because we don't want the Ratchet to
   # time out unless one of its steps does.
   def check(self):
+    if not self.active:
+      return
+
     if not self.done_after:
       while self.current_step().check():
         if not self.advance():
diff --git a/conman/ratchet_test.py b/conman/ratchet_test.py
index 97f7c94..48b693c 100755
--- a/conman/ratchet_test.py
+++ b/conman/ratchet_test.py
@@ -45,38 +45,29 @@
 @wvtest.wvtest
 def file_condition_test():
   """Test File*Condition functionality."""
-  try:
-    _, filename = tempfile.mkstemp()
-    c_exists = ratchet.FileExistsCondition('c exists', filename, 0.1)
-    c_mtime = ratchet.FileTouchedCondition('c mtime', filename, 0.1)
-    wvtest.WVPASS(c_exists.check())
-    wvtest.WVFAIL(c_mtime.check())
-    # mtime precision is too low to notice that we're touching the file *after*
-    # capturing its initial mtime rather than at the same time, so take a short
-    # nap before touching it.
-    time.sleep(0.01)
-    open(filename, 'w')
-    wvtest.WVPASS(c_mtime.check())
+  _, filename = tempfile.mkstemp()
+  c_exists = ratchet.FileExistsCondition('c exists', filename, 0.1)
+  c_touched = ratchet.FileTouchedCondition('c touched', filename, 0.1)
+  wvtest.WVPASS(c_exists.check())
+  wvtest.WVFAIL(c_touched.check())
+  # File mtime resolution isn't fine enough to see the difference between this
+  # write and the previous one, so sleep for a short time before writing to
+  # ensure a different mtime.
+  time.sleep(0.01)
+  open(filename, 'w')
+  wvtest.WVPASS(c_touched.check())
 
-    # Test that old mtimes don't count.
-    time.sleep(0.01)
-    c_mtime.reset()
-    wvtest.WVFAIL(c_mtime.check())
-    time.sleep(0.1)
-    wvtest.WVEXCEPT(ratchet.TimeoutException, c_mtime.check)
+  # Test that pre-existing files don't count.
+  c_touched.reset()
+  wvtest.WVFAIL(c_touched.check())
+  time.sleep(0.1)
+  wvtest.WVEXCEPT(ratchet.TimeoutException, c_touched.check)
 
-    # Test t0 and start_at.
-    os.unlink(filename)
-    now = time.time()
-    c_mtime.reset(t0=now, start_at=now + 0.2)
-    wvtest.WVFAIL(c_mtime.check())
-    time.sleep(0.15)
-    wvtest.WVFAIL(c_mtime.check())
-    open(filename, 'w')
-    wvtest.WVPASS(c_mtime.check())
-
-  finally:
-    os.unlink(filename)
+  # Test that deleting files doesn't count.
+  c_touched.reset()
+  wvtest.WVFAIL(c_touched.check())
+  os.unlink(filename)
+  wvtest.WVFAIL(c_touched.check())
 
 
 @wvtest.wvtest
@@ -122,7 +113,7 @@
     wvtest.WVEXCEPT(ratchet.TimeoutException, r.check)
 
     x = y = z = 1
-    r.reset()
+    r.start()
     wvtest.WVPASS(r.check())
   finally:
     shutil.rmtree(status_export_path)
diff --git a/conman/status.py b/conman/status.py
index 8f8d3a1..b5b1bae 100644
--- a/conman/status.py
+++ b/conman/status.py
@@ -30,10 +30,10 @@
   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'
+  ACS_CONNECTION_CHECK = 'ACS_CONNECTION_CHECK'
   WAITING_FOR_CWMP_WAKEUP = 'WAITING_FOR_CWMP_WAKEUP'
   WAITING_FOR_ACS_SESSION = 'WAITING_FOR_ACS_SESSION'
   PROVISIONING_COMPLETED = 'PROVISIONING_COMPLETED'
@@ -81,10 +81,6 @@
         (P.HAVE_CONFIG,),
         (),
     ),
-    P.ATTACHED_TO_WPA_SUPPLICANT: (
-        (),
-        (),
-    ),
     P.WAITING_FOR_PROVISIONING: (
         (P.CONNECTED_TO_OPEN,),
         (),
@@ -93,13 +89,18 @@
         (P.WAITING_FOR_PROVISIONING,),
         (P.WAITING_FOR_CWMP_WAKEUP, P.WAITING_FOR_ACS_SESSION),
     ),
+    P.ACS_CONNECTION_CHECK: (
+        (P.WAITING_FOR_PROVISIONING,),
+        (P.WAITING_FOR_DHCP, P.WAITING_FOR_CWMP_WAKEUP,
+         P.WAITING_FOR_ACS_SESSION),
+    ),
     P.WAITING_FOR_CWMP_WAKEUP: (
         (P.WAITING_FOR_PROVISIONING,),
-        (P.WAITING_FOR_DHCP, P.WAITING_FOR_ACS_SESSION),
+        (P.WAITING_FOR_DHCP, P.ACS_CONNECTION_CHECK, P.WAITING_FOR_ACS_SESSION),
     ),
     P.WAITING_FOR_ACS_SESSION: (
         (P.WAITING_FOR_PROVISIONING,),
-        (P.WAITING_FOR_DHCP, P.WAITING_FOR_CWMP_WAKEUP),
+        (P.WAITING_FOR_DHCP, P.ACS_CONNECTION_CHECK, P.WAITING_FOR_CWMP_WAKEUP),
     ),
     P.PROVISIONING_COMPLETED: (
         (),
diff --git a/conman/status_test.py b/conman/status_test.py
index befebbf..38dda83 100755
--- a/conman/status_test.py
+++ b/conman/status_test.py
@@ -137,8 +137,11 @@
     check_exported(True, False, status.P.CONNECTED_TO_OPEN)
     check_exported(True, False, status.P.WAITING_FOR_PROVISIONING)
     check_exported(True, False, status.P.WAITING_FOR_DHCP)
-    s.waiting_for_cwmp_wakeup = True
+    s.acs_connection_check = True
     check_exported(False, False, status.P.WAITING_FOR_DHCP)
+    check_exported(True, False, status.P.ACS_CONNECTION_CHECK)
+    s.waiting_for_cwmp_wakeup = True
+    check_exported(False, False, status.P.ACS_CONNECTION_CHECK)
     check_exported(True, False, status.P.WAITING_FOR_CWMP_WAKEUP)
     s.waiting_for_acs_session = True
     check_exported(False, False, status.P.WAITING_FOR_DHCP)
@@ -147,6 +150,7 @@
     s.provisioning_completed = True
     check_exported(False, False, status.P.WAITING_FOR_PROVISIONING)
     check_exported(False, False, status.P.WAITING_FOR_DHCP)
+    check_exported(False, False, status.P.ACS_CONNECTION_CHECK)
     check_exported(False, False, status.P.WAITING_FOR_CWMP_WAKEUP)
     check_exported(False, False, status.P.WAITING_FOR_CWMP_WAKEUP)
 
diff --git a/conman/test/fail b/conman/test/fail
deleted file mode 100755
index 2bb8d86..0000000
--- a/conman/test/fail
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-exit 1
diff --git a/conman/test/fake_python/subprocess/__init__.py b/conman/test/fake_python/subprocess/__init__.py
new file mode 100644
index 0000000..3d73d4d
--- /dev/null
+++ b/conman/test/fake_python/subprocess/__init__.py
@@ -0,0 +1,149 @@
+#!/usr/bin/python
+
+"""subprocess replacement that implements specific programs in Python."""
+
+import importlib
+import logging
+import os
+import types
+
+logger = logging.getLogger('subprocess')
+logger.setLevel(logging.DEBUG)
+
+
+# Values are only for when the module name does not match the command name.
+_COMMAND_NAMES = {
+    'connection_check': None,
+    'cwmp': None,
+    'get_quantenna_interfaces': 'get-quantenna-interfaces',
+    'ifdown': None,
+    'ifplugd_action': '/etc/ifplugd/ifplugd.action',
+    'ifup': None,
+    'ip': None,
+    'register_experiment': None,
+    'run_dhclient': 'run-dhclient',
+    'qcsapi': None,
+    'upload_logs_and_wait': 'upload-logs-and-wait',
+    'wifi': None,
+    'wpa_cli': None,
+}
+_COMMANDS = {v or k: importlib.import_module('.' + k, __name__)
+             for k, v in _COMMAND_NAMES.iteritems()}
+
+STDOUT = 1
+STDERR = 2
+
+
+class CalledProcessError(Exception):
+
+  def __init__(self, returncode, cmd, output):
+    super(CalledProcessError, self).__init__()
+    self.returncode = returncode
+    self.cmd = cmd
+    self.output = output
+
+  def __repr__(self):
+    return ('CalledProcessError: '
+            'Command "%r" returned non-zero exit status %d: %s'
+            % (self.cmd, self.returncode, self.output))
+
+
+def _call(command, **kwargs):
+  """Fake subprocess call."""
+  if type(command) not in (tuple, list):
+    raise Exception('Fake subprocess.call only supports list/tuple commands, '
+                    'got: %s', command)
+
+  ignored_kwargs = ('stdout', 'stderr')
+  for ignored_kwarg in ignored_kwargs:
+    kwargs.pop(ignored_kwarg, None)
+  extra_env = kwargs.pop('env', {})
+  if kwargs:
+    raise Exception('Fake subprocess.call does not support these kwargs: %s'
+                    % kwargs.keys())
+
+  logger.debug('%r%s', command, (', env %r' % extra_env) if extra_env else '')
+
+  command, args = command[0], command[1:]
+
+  if command not in _COMMANDS:
+    raise Exception('Fake subprocess.call does not support %r, supports %r' %
+                    (command, _COMMANDS.keys()))
+
+  impl = _COMMANDS[command]
+  if isinstance(impl, types.ModuleType):
+    impl = impl.call
+
+  forwarded_kwargs = {}
+  if extra_env:
+    forwarded_kwargs['env'] = extra_env
+  return impl(*args, **forwarded_kwargs)
+
+
+def call(command, **kwargs):
+  rc, _ = _call(command, **kwargs)
+  return rc
+
+
+def check_call(command, **kwargs):
+  rc, output = _call(command, **kwargs)
+  if rc:
+    raise CalledProcessError(rc, command, output)
+  return True
+
+
+def check_output(command, **kwargs):
+  rc, output = _call(command, **kwargs)
+  if rc != 0:
+    raise CalledProcessError(rc, command, output)
+  return output
+
+
+def mock(command, *args, **kwargs):
+  _COMMANDS[command].mock(*args, **kwargs)
+
+
+def reset():
+  """Reset any module-level state."""
+  for command in _COMMANDS.itervalues():
+    if isinstance(command, types.ModuleType):
+      reload(command)
+
+
+def set_conman_paths(tmp_path=None, config_path=None, cwmp_path=None):
+  for command in ('run-dhclient', '/etc/ifplugd/ifplugd.action'):
+    _COMMANDS[command].CONMAN_PATH = tmp_path
+
+  for command in ('cwmp',):
+    _COMMANDS[command].CONMAN_CONFIG_PATH = config_path
+
+  for command in ('cwmp',):
+    _COMMANDS[command].CWMP_PATH = cwmp_path
+
+  # Make sure <tmp_path>/interfaces exists.
+  tmp_interfaces_path = os.path.join(tmp_path, 'interfaces')
+  if not os.path.exists(tmp_interfaces_path):
+    os.mkdir(tmp_interfaces_path)
+
+
+# Some tiny fake implementations don't need their own file.
+
+
+def echo(*s):
+  return 0, ' '.join(s)
+
+
+def env(extra_env, *command, **kwargs):
+  final_env = kwargs.get('env', {})
+  k, v = extra_env.split('=')
+  final_env[k] = v
+  kwargs['env'] = final_env
+  return _call(command, **kwargs)
+
+
+def timeout(unused_t, *command, **kwargs):
+  """Just a transparent pass-through."""
+  return _call(command, **kwargs)
+
+
+_COMMANDS.update({'echo': echo, 'env': env, 'timeout': timeout,})
diff --git a/conman/test/fake_python/subprocess/connection_check.py b/conman/test/fake_python/subprocess/connection_check.py
new file mode 100644
index 0000000..8c23c50
--- /dev/null
+++ b/conman/test/fake_python/subprocess/connection_check.py
@@ -0,0 +1,19 @@
+#!/usr/bin/python
+
+"""Fake connection_check implementation."""
+
+RESULTS = {}
+
+
+def mock(interface, result):
+  RESULTS[interface] = result
+
+
+def call(*args):
+  interface = args[args.index('-I') + 1]
+  result = RESULTS.get(interface, 'fail')
+
+  if result == 'restricted' and '-a' in args:
+    result = 'succeed'
+
+  return (0 if result == 'succeed' else 1), ''
diff --git a/conman/test/fake_python/subprocess/cwmp.py b/conman/test/fake_python/subprocess/cwmp.py
new file mode 100644
index 0000000..f34cd2f
--- /dev/null
+++ b/conman/test/fake_python/subprocess/cwmp.py
@@ -0,0 +1,148 @@
+#!/usr/bin/python
+
+"""Fake catawampus implementation."""
+
+import logging
+import os
+
+import connection_check
+
+
+logger = logging.getLogger('subprocess.cwmp')
+
+CONMAN_CONFIG_PATH = None
+CWMP_PATH = None
+CONFIG = {}
+ACCESS_POINT = {}
+ACS_SESSION_FAILS = False
+
+
+def call(command, env=None):
+  if command == 'wakeup':
+    if not CONMAN_CONFIG_PATH:
+      raise ValueError('Call subprocess.set_conman_paths before calling '
+                       '"cwmp wakeup".')
+
+    write_acscontact()
+
+    if ACS_SESSION_FAILS:
+      return 0, ''
+
+    if ((env and 'write_now_testonly' in env) or
+        [result for result in connection_check.RESULTS.itervalues()
+         if result in ('restricted', 'succeed')]):
+      for band in ('2.4', '5'):
+        if CONFIG.get(band, None):
+          write_wlan_config(band)
+        else:
+          delete_wlan_config(band)
+          disable_access_point(band)
+
+        if ACCESS_POINT.get(band, False):
+          enable_access_point(band)
+        else:
+          disable_access_point(band)
+
+      logger.debug('Fake ACS session completing')
+      write_acsconnected()
+    else:
+      logger.debug('ACS session failed due to no working connections')
+
+    return 0, ''
+
+  raise ValueError('Fake cwmp only supports "wakeup" command.')
+
+
+def wlan_config_filename(band):
+  return os.path.join(CONMAN_CONFIG_PATH, 'command.%s' % band)
+
+
+def access_point_filename(band):
+  return os.path.join(CONMAN_CONFIG_PATH, 'access_point.%s' % band)
+
+
+def write_wlan_config(band):
+  final_filename = wlan_config_filename(band)
+  logger.debug('Writing config for band %s: %s', band, final_filename)
+  # We don't care which writes are atomic, as long as some but not all are.
+  # Making it depend on band achieves this.
+  atomic = band == '2.4'
+  filename = final_filename + ('.tmp' if atomic else '')
+  with open(filename, 'w') as f:
+    f.write('\n'.join(['env', 'WIFI_PSK=%s' % CONFIG[band]['psk'],
+                       'wifi', 'set', '--band', band,
+                       '--ssid', CONFIG[band]['ssid']]))
+    logger.debug(  'wrote to filename %s', filename)
+  if atomic:
+    logger.debug(  'moving from %s to %s', filename, final_filename)
+    os.rename(filename, final_filename)
+
+
+def enable_access_point(band):
+  logger.debug('Enabling AP for band %s', band)
+  open(access_point_filename(band), 'w')
+
+
+def delete_wlan_config(band):
+  config_filename = wlan_config_filename(band)
+  if os.path.exists(config_filename):
+    logger.debug('Deleting config for band %s', band)
+    os.unlink(config_filename)
+
+
+def disable_access_point(band):
+  ap_filename = access_point_filename(band)
+  if os.path.isfile(ap_filename):
+    logger.debug('Disabling AP for band %s', band)
+    os.unlink(ap_filename)
+
+
+def write_acscontact():
+  logger.debug('ACS session started')
+  open(os.path.join(CWMP_PATH, 'acscontact'), 'w')
+
+
+def write_acsconnected():
+  logger.debug('ACS session completed')
+  open(os.path.join(CWMP_PATH, 'acsconnected'), 'w')
+
+
+def mock(band, access_point=None, delete_config=False, ssid=None, psk=None,
+         write_now=False, acs_session_fails=None):
+  """Mock the config written by catawampus.
+
+  Args:
+    band:  The band for which things are being mocked.
+    access_point:  Set to True or False to enable/disable the AP.
+    delete_config:  Set to True to delete the config.
+    ssid:  If updating config, the ssid to use.  psk must also be set.
+    psk:  If updating config, the psk to use.  ssid must also be set.
+    write_now:  If updating config, write it immediately.
+
+  Raises:
+    ValueError:  If invalid values are specified.
+  """
+  if acs_session_fails is not None:
+    global ACS_SESSION_FAILS
+    ACS_SESSION_FAILS = acs_session_fails
+
+  if access_point is not None:
+    if access_point not in (True, False):
+      raise ValueError('access_point should only be mocked as True/False')
+    ACCESS_POINT[band] = access_point
+    logger.debug('AP mocked %s', access_point)
+
+  if delete_config:
+    logger.debug('Config mock removed for band %s', band)
+    CONFIG[band] = None
+  elif ssid and psk:
+    logger.debug('Config mock updated for band %s', band)
+    CONFIG[band] = {'ssid': ssid, 'psk': psk}
+  elif ssid or psk:
+    raise ValueError('Cannot set only one of ssid (%r) and psk (%r).',
+                     ssid, psk)
+
+  if write_now:
+    call('wakeup', env={'write_now_testonly': True})
+
+
diff --git a/conman/test/fake_python/subprocess/get_quantenna_interfaces.py b/conman/test/fake_python/subprocess/get_quantenna_interfaces.py
new file mode 100644
index 0000000..7316139
--- /dev/null
+++ b/conman/test/fake_python/subprocess/get_quantenna_interfaces.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python -S
+
+"""Fake get-quantenna-interfaces implementation."""
+
+_INTERFACES = []
+
+
+def call(*unused_args, **unused_kwargs):
+  return 0, '\n'.join(_INTERFACES)
+
+
+def mock(interfaces):
+  global _INTERFACES
+  _INTERFACES = list(interfaces)
diff --git a/conman/test/fake_python/subprocess/ifdown.py b/conman/test/fake_python/subprocess/ifdown.py
new file mode 100644
index 0000000..0677e4f
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ifdown.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+"""Fake ifdown implementation."""
+
+import ifup
+
+
+def call(interface):
+  ifup.INTERFACE_STATE[interface] = False
+  return 0, ''
diff --git a/conman/test/fake_python/subprocess/ifplugd_action.py b/conman/test/fake_python/subprocess/ifplugd_action.py
new file mode 100644
index 0000000..ab6a97a
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ifplugd_action.py
@@ -0,0 +1,27 @@
+#!/usr/bin/python
+
+"""Fake ifplugd.action implementation."""
+
+import os
+
+import run_dhclient
+
+CONMAN_PATH = None
+
+
+def call(interface, state):
+  if CONMAN_PATH is None:
+    raise ValueError('Need to set subprocess.ifplugd_action.CONMAN_PATH')
+
+  if state not in ('up', 'down'):
+    raise ValueError('state should be "up" or "down"')
+
+  status_file = os.path.join(CONMAN_PATH, 'interfaces', interface)
+  with open(status_file, 'w') as f:
+    # This value doesn't matter to conman, so it's fine to hard code it here.
+    f.write('1' if state == 'up' else '0')
+
+  # ifplugd.action calls run-dhclient.
+  run_dhclient.call('br0' if interface in ('eth0', 'moca0') else interface)
+
+  return 0, ''
diff --git a/conman/test/fake_python/subprocess/ifup.py b/conman/test/fake_python/subprocess/ifup.py
new file mode 100644
index 0000000..7669555
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ifup.py
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+"""Fake ifup implementation."""
+
+INTERFACE_STATE = {}
+
+
+def call(interface):
+  INTERFACE_STATE[interface] = True
+  return 0, ''
diff --git a/conman/test/fake_python/subprocess/ip.py b/conman/test/fake_python/subprocess/ip.py
new file mode 100644
index 0000000..d8baaf3
--- /dev/null
+++ b/conman/test/fake_python/subprocess/ip.py
@@ -0,0 +1,136 @@
+#!/usr/bin/python
+
+"""Fake ip route implementation."""
+
+import logging
+import socket
+import struct
+
+import ifup
+
+
+_ROUTING_TABLE = {}
+_IP_TABLE = {}
+
+
+def call(subcommand, *args):
+  """Fake ip command."""
+  subcommands = {
+      'route': _ip_route,
+      'addr': _ip_addr,
+      'link': _link,
+  }
+
+  if subcommand not in subcommands:
+    return 1, 'ip subcommand %r not supported' % subcommand
+
+  return subcommands[subcommand](args)
+
+
+def register_testonly(interface):
+  if interface not in _IP_TABLE:
+    _IP_TABLE[interface] = set()
+
+
+def _ip_route(args):
+  def can_add_route(dev):
+    def ip_to_int(ip_addr):
+      return struct.unpack('!I', socket.inet_pton(socket.AF_INET, ip_addr))[0]
+
+    if args[1] != 'default':
+      return True
+
+    via = ip_to_int(args[args.index('via') + 1])
+    for (ifc, route, _), _ in _ROUTING_TABLE.iteritems():
+      if ifc != dev:
+        continue
+
+      netmask = 0
+      if '/' in route:
+        route, netmask = route.split('/')
+        netmask = 32 - int(netmask)
+      route = ip_to_int(route)
+
+      if (route >> netmask) == (via >> netmask):
+        return True
+
+    return False
+
+  if not args:
+    return 0, '\n'.join(_ROUTING_TABLE.values())
+
+  if 'dev' not in args:
+    raise Exception('fake ip route got no dev')
+
+  dev = args[args.index('dev') + 1]
+
+  metric = None
+  if 'metric' in args:
+    metric = args[args.index('metric') + 1]
+  if args[0] in ('add', 'del'):
+    route = args[1]
+  key = (dev, route, metric)
+  if args[0] == 'add' and key not in _ROUTING_TABLE:
+    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)
+    _ROUTING_TABLE[key] = ' '.join(args[1:])
+  elif args[0] == 'del':
+    if key in _ROUTING_TABLE:
+      logging.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)
+          del _ROUTING_TABLE[k]
+          break
+
+  return 0, ''
+
+
+# pylint: disable=line-too-long
+_IP_ADDR_SHOW_TPL = """4: {name}: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP qlen 1000
+    link/ether fe:fb:01:80:1b:74 brd ff:ff:ff:ff:ff:ff
+{ips}
+"""
+
+_IP_ADDR_SHOW_IP_TPL = """    inet {ip}/24 brd 100.100.255.255 scope global {name}
+       valid_lft forever preferred_lft forever
+"""
+
+
+def _ip_addr(args):
+  if 'dev' not in args:
+    raise Exception('fake ip addr show got no dev')
+
+  dev = args[args.index('dev') + 1]
+  if dev not in _IP_TABLE:
+    return 255, 'Device "%r" does not exist' % dev
+
+  if 'show' in args:
+    ips = '\n'.join(_IP_ADDR_SHOW_IP_TPL.format(name=dev, ip=addr)
+                    for addr in _IP_TABLE[dev])
+    return 0, _IP_ADDR_SHOW_TPL.format(name=dev, ips=ips)
+
+  if 'add' in args:
+    add = args[args.index('add') + 1]
+    _IP_TABLE[dev].add(add)
+    return 0, ''
+
+  if 'del' in args:
+    remove = args[args.index('del') + 1]
+    if remove in _IP_TABLE[dev]:
+      _IP_TABLE[dev].remove(remove)
+      return 0, ''
+    return 254, 'RTNETLINK answers: Cannot assign requested address'
+
+  raise Exception('no recognized ip addr command in %r' % args)
+
+
+def _link(args):
+  return 0, '\n'.join('%s LOWER_UP' %  interface
+                      for interface, state in ifup.INTERFACE_STATE.iteritems()
+                      if state)
diff --git a/conman/test/fake_python/subprocess/qcsapi.py b/conman/test/fake_python/subprocess/qcsapi.py
new file mode 100644
index 0000000..3625772
--- /dev/null
+++ b/conman/test/fake_python/subprocess/qcsapi.py
@@ -0,0 +1,25 @@
+#!/usr/bin/python -S
+
+"""Fake QCSAPI implementation."""
+
+
+STATE = {}
+
+
+def call(*args):
+  if args not in STATE:
+    return 1, 'No mocked value for args %r' % (args,)
+
+  return 0, STATE[args]
+
+
+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:
+    STATE[args] = value
diff --git a/conman/test/fake_python/subprocess/register_experiment.py b/conman/test/fake_python/subprocess/register_experiment.py
new file mode 100644
index 0000000..a2dab49
--- /dev/null
+++ b/conman/test/fake_python/subprocess/register_experiment.py
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+
+"""Fake register_experiment implementation."""
+
+
+REGISTERED_EXPERIMENTS = set()
+
+
+def call(experiment):
+  REGISTERED_EXPERIMENTS.add(experiment)
+  return 0, ''
diff --git a/conman/test/fake_python/subprocess/run_dhclient.py b/conman/test/fake_python/subprocess/run_dhclient.py
new file mode 100644
index 0000000..a1bffb3
--- /dev/null
+++ b/conman/test/fake_python/subprocess/run_dhclient.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+
+"""Fake run-dhclient implementation."""
+
+import os
+
+
+CONMAN_PATH = None
+_FAILURE = {}
+
+
+def mock(interface, failure=False):
+  _FAILURE[interface] = failure
+
+
+def call(interface):
+  if CONMAN_PATH is None:
+    raise ValueError('Need to set subprocess.ifplugd_action.CONMAN_PATH')
+
+  if not _FAILURE.get(interface, False):
+    _write_subnet_file(interface)
+    _write_gateway_file(interface)
+
+
+def _write_gateway_file(interface):
+  gateway_file = os.path.join(CONMAN_PATH, 'gateway.' + interface)
+  with open(gateway_file, 'w') as f:
+    # This value doesn't matter to conman, so it's fine to hard code it here.
+    f.write('192.168.1.1')
+
+
+def _write_subnet_file(interface):
+  subnet_file = os.path.join(CONMAN_PATH, 'subnet.' + interface)
+  with open(subnet_file, 'w') as f:
+    # This value doesn't matter to conman, so it's fine to hard code it here.
+    f.write('192.168.1.0/24')
diff --git a/conman/test/fake_python/subprocess/upload_logs_and_wait.py b/conman/test/fake_python/subprocess/upload_logs_and_wait.py
new file mode 100644
index 0000000..6c45f87
--- /dev/null
+++ b/conman/test/fake_python/subprocess/upload_logs_and_wait.py
@@ -0,0 +1,18 @@
+#!/usr/bin/python
+
+"""Fake upload-logs-and-wait implementation."""
+
+UPLOADED = False
+
+
+def call():
+  global UPLOADED
+  UPLOADED = True
+  return 0, ''
+
+
+def uploaded_logs():
+  global UPLOADED
+  result = UPLOADED
+  UPLOADED = False
+  return result
diff --git a/conman/test/fake_python/subprocess/wifi.py b/conman/test/fake_python/subprocess/wifi.py
new file mode 100644
index 0000000..900b908
--- /dev/null
+++ b/conman/test/fake_python/subprocess/wifi.py
@@ -0,0 +1,331 @@
+#!/usr/bin/python
+
+"""Fake /bin/wifi implementation."""
+
+import collections
+import os
+import random
+
+import connection_check
+import get_quantenna_interfaces
+import ifplugd_action
+import ifup
+import qcsapi
+import wpa_cli
+
+
+MockInterface = collections.namedtuple('MockInterface',
+                                       ['phynum', 'bands', 'driver'])
+
+
+# A randomly selceted wifi scan result with the interesting stuff templated.
+WIFI_SCAN_TPL = '''BSS {bssid}(on wcli0)
+  TSF: 1269828266773 usec (14d, 16:43:48)
+  freq: {freq}
+  beacon interval: 100 TUs
+  capability: ESS Privacy ShortSlotTime (0x0411)
+  signal: {rssi}
+  last seen: 2190 ms ago
+  Information elements from Probe Response frame:
+  {vendor_ies}
+  SSID: {ssid}
+  Supported rates: 1.0* 2.0* 5.5* 11.0* 18.0 24.0 36.0 54.0
+  DS Parameter set: channel 6
+  ERP: <no flags>
+  ERP D4.0: <no flags>
+  {security}
+  Extended supported rates: 6.0 9.0 12.0 48.0
+'''
+
+VENDOR_IE_TPL = '  Vendor specific: OUI {oui}, data: {data}'
+
+
+WIFI_SHOW_TPL = '''Band: {band}
+RegDomain: US
+Interface: wlan{phynum}  # {band} GHz ap
+BSSID: f4:f5:e8:81:1b:a0
+AutoChannel: True
+AutoType: NONDFS
+Station List for band: {band}
+
+Client Interface: wcli{phynum}  # {band} GHz client
+Client BSSID: f4:f5:e8:81:1b:a1
+'''
+
+WIFI_SHOW_NO_RADIO_TPL = '''Band: {band}
+RegDomain: 00
+'''
+
+WPA_PATH = None
+REMOTE_ACCESS_POINTS = collections.defaultdict(dict)
+INTERFACE_FOR_BAND = collections.defaultdict(lambda: None)
+INTERFACE_EVENTS = collections.defaultdict(list)
+LOCAL_ACCESS_POINTS = {}
+CLIENT_ASSOCIATIONS = {}
+
+
+class AccessPoint(object):
+
+  def __init__(self, **kwargs):
+    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):
+    security_strs = {
+        'WEP': '  Privacy:  WEP',
+        'WPA': '  WPA:',
+        'WPA2': '  RSN:   * Version: 1',
+    }
+    return WIFI_SCAN_TPL.format(
+        ssid=self.ssid if not self.hidden else '',
+        freq='2437' if self.band == '2.4' else '5160',
+        bssid=self.bssid,
+        vendor_ies='\n'.join(VENDOR_IE_TPL.format(oui=oui, data=data)
+                             for oui, data in (self.vendor_ies or [])),
+        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 = {
+      'scan': _scan,
+      'set': _set,
+      'stopap': _stopap,
+      'setclient': _setclient,
+      'stopclient': _stopclient,
+      'stop': _stop,
+      'show': _show,
+  }
+
+  if WPA_PATH is None and args[0].endswith('client'):
+    raise ValueError('Set subprocess.wifi.WPA_PATH before calling a fake '
+                     '"wifi *client" command')
+
+  if args[0] in wifi_commands:
+    return wifi_commands[args[0]](args[1:], env=kwargs.get('env', {}))
+
+  return 99, 'unrecognized command %s' % args[0]
+
+
+def _set(args, env=None):
+  band = _get_flag(args, ('-b', '--band'))
+  LOCAL_ACCESS_POINTS[band] = args, env
+  return 0, ''
+
+
+def _stopap(args, env=None):
+  bands = _get_flag(args, ('-b', '--band')) or '2.4 5'
+  for band in bands.split():
+    if band in LOCAL_ACCESS_POINTS:
+      del LOCAL_ACCESS_POINTS[band]
+
+  return 0, ''
+
+
+def _setclient(args, env=None):
+  env = env or {}
+
+  band = _get_flag(args, ('-b', '--band'))
+  bssid = _get_flag(args, ('--bssid',))
+  ssid = _get_flag(args, ('S', '--ssid',))
+
+  if band not in INTERFACE_FOR_BAND:
+    raise ValueError('No interface for band %r' % band)
+
+  interface = INTERFACE_FOR_BAND[band]
+  interface_name = 'wcli%s' % interface.phynum
+
+  if bssid:
+    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: %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: %s' % (ssid, REMOTE_ACCESS_POINTS)
+    ap = random.choice(candidates)
+  else:
+    raise ValueError('Did not specify BSSID or SSID in %r' % args)
+
+  psk = env.get('WIFI_CLIENT_PSK', None)
+  if psk != ap.psk:
+    _setclient_error_auth(interface_name, ssid, interface.driver)
+    return 1, 'Wrong PSK, got %r, expected %r' % (psk, ap.psk)
+
+  _setclient_success(interface_name, ssid, bssid, psk, interface.driver, ap,
+                     band)
+
+  return 0, ''
+
+
+def _setclient_error_not_found(interface_name, ssid, driver):
+  if driver == 'cfg80211':
+    wpa_cli.mock(interface_name, wpa_state='SCANNING')
+  elif driver == 'frenzy':
+    qcsapi.mock('get_mode', 'wifi0', value='Station')
+    qcsapi.mock('get_ssid', 'wifi0', value='')
+    qcsapi.mock('ssid_get_authentication_mode', 'wifi0', ssid, value='')
+    qcsapi.mock('get_status', 'wifi0', value='Error')
+
+  CLIENT_ASSOCIATIONS[interface_name] = None
+
+
+def _setclient_error_auth(interface_name, ssid, driver):
+  if driver == 'cfg80211':
+    # This is what our version of wpa_supplicant does for auth failures.
+    INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-SSID-TEMP-DISABLED')
+    wpa_cli.mock(interface_name, wpa_state='SCANNING')
+  elif driver == 'frenzy':
+    qcsapi.mock('get_mode', 'wifi0', value='Station')
+    qcsapi.mock('get_ssid', 'wifi0', value='')
+    qcsapi.mock('ssid_get_authentication_mode', 'wifi0', ssid, value='')
+    qcsapi.mock('get_status', 'wifi0', value='Error')
+
+  CLIENT_ASSOCIATIONS[interface_name] = None
+
+
+def _setclient_success(interface_name, ssid, bssid, psk, driver, ap, band):
+  if CLIENT_ASSOCIATIONS.get(interface_name, None):
+    _disconnected_event(band)
+  if driver == 'cfg80211':
+    # Make sure the wpa_supplicant socket exists.
+    open(os.path.join(WPA_PATH, interface_name), 'w')
+
+    # Tell wpa_cli what to return.
+    key_mgmt = 'WPA2-PSK' if psk else 'NONE'
+    wpa_cli.mock(interface_name, wpa_state='COMPLETED', ssid=ssid, bssid=bssid,
+                 key_mgmt=key_mgmt)
+
+    # Send the CONNECTED event.
+    INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-CONNECTED')
+
+  elif driver == 'frenzy':
+    qcsapi.mock('get_mode', 'wifi0', value='Station')
+    qcsapi.mock('get_ssid', 'wifi0', value=ssid)
+    qcsapi.mock('ssid_get_authentication_mode', 'wifi0', ssid,
+                value='PSKAuthentication' if psk else 'NONE')
+    qcsapi.mock('get_status', 'wifi0', value='')
+
+  CLIENT_ASSOCIATIONS[interface_name] = ap
+  connection_check.mock(interface_name, ap.connection_check_result or 'succeed')
+
+  # Call ifplugd.action for the interface coming up (wifi/quantenna.py does this
+  # manually).
+  ifplugd_action.call(interface_name, 'up')
+
+
+def _disconnected_event(band):
+  interface = INTERFACE_FOR_BAND[band]
+  interface_name = 'wcli%s' % interface.phynum
+  if interface.driver == 'cfg80211':
+    INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-DISCONNECTED')
+    wpa_cli.mock(interface_name, wpa_state='SCANNING')
+  else:
+    qcsapi.mock('get_ssid', 'wifi0', value='')
+    qcsapi.mock('get_status', 'wifi0', value='Error')
+
+  CLIENT_ASSOCIATIONS[interface_name] = None
+
+
+def _stopclient(args, env=None):
+  bands = _get_flag(args, ('-b', '--band')) or '2.4 5'
+  for band in bands.split():
+    interface = INTERFACE_FOR_BAND[band]
+    interface_name = 'wcli%s' % interface.phynum
+
+    if interface.driver == 'cfg80211':
+      # Send the DISCONNECTED and TERMINATING events.
+      INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-DISCONNECTED')
+      INTERFACE_EVENTS[interface_name].append('<2>CTRL-EVENT-TERMINATING')
+
+      # Clear the wpa_cli status response.
+      wpa_cli.mock(interface_name)
+
+      # Make sure the wpa_supplicant socket does not.
+      if os.path.exists(os.path.join(WPA_PATH, interface_name)):
+        os.unlink(os.path.join(WPA_PATH, interface_name))
+
+    elif interface.driver == 'frenzy':
+      qcsapi.mock('get_ssid', 'wifi0', value='')
+      qcsapi.mock('get_status', 'wifi0', value='Error')
+
+    CLIENT_ASSOCIATIONS[interface_name] = None
+
+  # Call ifplugd.action for the interface going down (wifi/quantenna.py does this
+  # manually).
+  ifplugd_action.call(interface_name, 'down')
+
+  return 0, ''
+
+
+def _stop(*args, **kwargs):
+  _stopap(*args, **kwargs)
+  _stopclient(*args, **kwargs)
+  return 0, ''
+
+
+def _kill_wpa_supplicant(band):
+  # From conman's perspective, there's no difference between someone running
+  # 'wifi stopclient' and the process dying for some other reason.
+  _stopclient(['--band', band])
+
+
+def _scan(args, **unused_kwargs):
+  band_flag = _get_flag(args, ('-b', '--band'))
+  interface = INTERFACE_FOR_BAND[band_flag]
+  interface_name = 'wcli%s' % interface.phynum
+  if not ifup.INTERFACE_STATE.get(interface_name, False):
+    return 1, 'interface down'
+
+  return 0, '\n'.join(ap.scan_str()
+                      for band in interface.bands
+                      for ap in REMOTE_ACCESS_POINTS[band].itervalues())
+
+
+def _show(unused_args, **unused_kwargs):
+  return 0, '\n\n'.join(WIFI_SHOW_TPL.format(band=band, **interface._asdict()) if interface
+                        else WIFI_SHOW_NO_RADIO_TPL.format(band)
+                        for band, interface in INTERFACE_FOR_BAND.iteritems())
+
+
+def _get_flag(args, flags):
+  for flag in flags:
+    if flag in args:
+      return args[args.index(flag) + 1]
+
+
+def mock(command, *args, **kwargs):
+  if command == 'remote_ap':
+    remote_ap = AccessPoint(**kwargs)
+    REMOTE_ACCESS_POINTS[kwargs['band']][kwargs['bssid']] = remote_ap
+  elif command == 'remote_ap_remove':
+    del REMOTE_ACCESS_POINTS[kwargs['band']][kwargs['bssid']]
+  elif command == 'interfaces':
+    INTERFACE_FOR_BAND.clear()
+    for interface in args:
+      for band in interface.bands:
+        INTERFACE_FOR_BAND[band] = interface
+      if interface.driver == 'frenzy':
+        get_quantenna_interfaces.mock(
+            fmt % interface.phynum
+            for fmt in ('wlan%s', 'wlan%s_portal', 'wcli%s'))
+  elif command == 'wpa_path':
+    global WPA_PATH
+    WPA_PATH = args[0]
+  elif command == 'disconnected_event':
+    _disconnected_event(args[0])
+  elif command == 'kill_wpa_supplicant':
+    _kill_wpa_supplicant(args[0])
diff --git a/conman/test/fake_python/subprocess/wpa_cli.py b/conman/test/fake_python/subprocess/wpa_cli.py
new file mode 100644
index 0000000..c1849a9
--- /dev/null
+++ b/conman/test/fake_python/subprocess/wpa_cli.py
@@ -0,0 +1,37 @@
+#!/usr/bin/python
+
+"""Fake wpa_cli implementation.  Used by fake WPACtrl too."""
+
+import ifdown
+import ifup
+
+
+_INTERFACE_STATE = {}
+
+
+def call(*args, **unused_kwargs):
+  if 'status' not in args:
+    raise ValueError('Fake wpa_cli can only do status requests.')
+
+  if '-i' not in args:
+    raise ValueError('Must specify interface with -i.')
+
+  interface = args[args.index('-i') + 1]
+
+  # Fails for not present or empty dict.
+  if not _INTERFACE_STATE.get(interface, None):
+    return 1, ('Failed to connect to non-global ctrl_ifname: %r  '
+               'error: No such file or directory' % interface)
+
+  state = _INTERFACE_STATE[interface]
+
+  return 0, '\n'.join('%s=%s' % (k, v) for k, v in state.iteritems())
+
+
+# Pass no kwargs to "kill" wpa_supplicant.
+def mock(interface, **kwargs):
+  _INTERFACE_STATE[interface] = {k: v for k, v in kwargs.iteritems() if v}
+  if kwargs:
+    ifup.call(interface)
+  else:
+    ifdown.call(interface)
diff --git a/conman/test/fake_wpactrl/wpactrl/__init__.py b/conman/test/fake_wpactrl/wpactrl/__init__.py
deleted file mode 100644
index b8ce1fd..0000000
--- a/conman/test/fake_wpactrl/wpactrl/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-class error(Exception):
-  pass
-
-class WPACtrl(object):
-  pass
diff --git a/conman/test/restricted b/conman/test/restricted
deleted file mode 100755
index 9bd8fc3..0000000
--- a/conman/test/restricted
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-
-echo "$@" | grep -q -- "-a"
-
diff --git a/conman/test/succeed b/conman/test/succeed
deleted file mode 100755
index c52d3c2..0000000
--- a/conman/test/succeed
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-exit 0
diff --git a/diags/chameleon/sfp.c b/diags/chameleon/sfp.c
index e17e02d..1974d27 100644
--- a/diags/chameleon/sfp.c
+++ b/diags/chameleon/sfp.c
@@ -175,7 +175,7 @@
     temp = value[0] + ((float)value[1]) / 256.0;
   }
   vcc = ((float)((value[2] << 8) + value[3])) / 10000.0;
-  tx_bias = ((float)((value[4] << 8) + value[5])) / 1000.0;
+  tx_bias = (((float)((value[4] << 8) + value[5])) * 2) / 1000.0;
   tx_power = ((float)((value[6] << 8) + value[7])) / 10000.0;
   rx_power = ((float)((value[8] << 8) + value[9])) / 10000.0;
   mod_curr = ((float)((value[12] << 8) + value[13])) / 1000.0;
diff --git a/ginstall/ginstall.py b/ginstall/ginstall.py
index 1fc0976..67f196a 100755
--- a/ginstall/ginstall.py
+++ b/ginstall/ginstall.py
@@ -62,10 +62,13 @@
 
 F = {
     'ETCPLATFORM': '/etc/platform',
+    'ETCOS': '/etc/os',
     'ETCVERSION': '/etc/version',
     'DEV': '/dev',
     'MMCBLK0': '/dev/mmcblk0',
+    'MMCBLK0-ANDROID': '/dev/block/mmcblk0',
     'MTD_PREFIX': '/dev/mtd',
+    'MTD_PREFIX-ANDROID': '/dev/mtd/mtd',
     'PROC_CMDLINE': '/proc/cmdline',
     'PROC_MTD': '/proc/mtd',
     'SECUREBOOT': '/tmp/gpio/ledcontrol/secure_boot',
@@ -74,12 +77,22 @@
     'SYSBLOCK': '/sys/block',
     'MMCBLK0BOOT0': '/dev/mmcblk0boot0',
     'MMCBLK0BOOT1': '/dev/mmcblk0boot1',
+    'MMCBLK0BOOT0-ANDROID': '/dev/block/mmcblk0boot0',
+    'MMCBLK0BOOT1-ANDROID': '/dev/block/mmcblk0boot1',
     'MEMINFO': '/proc/meminfo',
 }
 
+ANDROID_BSU_PARTITION = 'bsu'
+ANDROID_BOOT_PARTITIONS = ['boot_a', 'boot_b']
+ANDROID_SYSTEM_PARTITIONS = ['system_a', 'system_b']
+ANDROID_IMAGES = ['boot.img', 'system.img.raw']
+ANDROID_IMG_SUFFIX = ['a', 'b']
+
 MMC_RO_LOCK = {
     'MMCBLK0BOOT0': '/sys/block/mmcblk0boot0/force_ro',
     'MMCBLK0BOOT1': '/sys/block/mmcblk0boot1/force_ro',
+    'MMCBLK0BOOT0-ANDROID': '/sys/block/mmcblk0boot0/force_ro',
+    'MMCBLK0BOOT1-ANDROID': '/sys/block/mmcblk0boot1/force_ro',
 }
 
 # Verbosity of output
@@ -131,6 +144,26 @@
   return open(F['ETCPLATFORM']).read().strip()
 
 
+def GetOs():
+  # not all platforms provide ETCOS, default to 'fiberos' in that case
+  try:
+    return open(F['ETCOS']).read().strip()
+  except IOError:
+    return 'fiberos'
+
+
+def GetMtdPrefix():
+  if GetOs() == 'android':
+    return F['MTD_PREFIX-ANDROID']
+  return F['MTD_PREFIX']
+
+
+def GetMmcblk0Prefix():
+  if GetOs() == 'android':
+    return F['MMCBLK0-ANDROID']
+  return F['MMCBLK0']
+
+
 def GetVersion():
   return open(F['ETCVERSION']).read().strip()
 
@@ -154,17 +187,47 @@
   return None
 
 
-def SetBootPartition(partition):
-  VerbosePrint('Setting boot partition to kernel%d\n', partition)
-  cmd = [HNVRAM, '-q', '-w', 'ACTIVATED_KERNEL_NAME=kernel%d' % partition]
-  return subprocess.call(cmd)
+def SetBootPartition(target_os, partition):
+  """Set active boot partition for the given OS and switch the OS if needed.
+
+  Args:
+    target_os: 'fiberos' or 'android'
+    partition: 0 or 1
+
+  Returns:
+    0 if successful, else an error code.
+  """
+  if target_os == 'android':
+    param = 'ANDROID_ACTIVE_PARTITION=%s' % ANDROID_IMG_SUFFIX[partition]
+  else:
+    param = 'ACTIVATED_KERNEL_NAME=kernel%d' % partition
+
+  VerbosePrint('Setting boot partition: %s\n', param)
+  try:
+    ret = subprocess.call([HNVRAM, '-q', '-w', param])
+  except OSError:
+    ret = 127
+  if ret:
+    VerbosePrint('Failed setting boot partition!\n')
+    return ret
+
+  if target_os != GetOs():
+    VerbosePrint('Switch OS to %s\n', target_os)
+    try:
+      ret = subprocess.call([HNVRAM, '-q', '-w', 'BOOT_TARGET=%s' % target_os])
+    except OSError:
+      ret = 127
+    if ret:
+      VerbosePrint('Failed switching OS!\n')
+
+  return ret
 
 
 def GetBootedPartition():
   """Get the role of partition where the running system is booted from.
 
   Returns:
-    0 or 1 for rootfs0 and rootfs1, or None if not booted from flash.
+    0 or 1, or None if not booted from flash.
   """
   try:
     with open(F['PROC_CMDLINE']) as f:
@@ -184,6 +247,41 @@
         return 0
       elif partition == 'kernel1':
         return 1
+    elif arg.startswith('androidboot.gfiber_system_img='):
+      partition = arg.split('=')[1]
+      if partition == ANDROID_SYSTEM_PARTITIONS[0]:
+        return 0
+      elif partition == ANDROID_SYSTEM_PARTITIONS[1]:
+        return 1
+  return None
+
+
+def GetActivePartitionFromHNVRAM(target_os):
+  """Get the active partion for the given OS as set in HNVRAM.
+
+  Args:
+    target_os: 'fiberos' or 'android'
+
+  Returns:
+    0 or 1 if the active partition could be determined, None if not.
+  """
+  if target_os == 'fiberos':
+    cmd = [HNVRAM, '-q', '-r', 'ACTIVATED_KERNEL_NAME']
+  elif target_os == 'android':
+    cmd = [HNVRAM, '-q', '-r', 'ANDROID_ACTIVE_PARTITION']
+  else:
+    return None
+
+  try:
+    partition_name = subprocess.check_output(cmd).strip()
+  except subprocess.CalledProcessError:
+    return None
+
+  if partition_name in ['0', 'a']:
+    return 0
+  elif partition_name in ['1', 'b']:
+    return 1
+
   return None
 
 
@@ -219,12 +317,12 @@
     if len(fields) >= 4 and fields[3] == quotedname:
       assert fields[0].startswith('mtd')
       assert fields[0].endswith(':')
-      return '%s%d' % (F['MTD_PREFIX'], int(fields[0][3:-1]))
+      return '%s%d' % (GetMtdPrefix(), int(fields[0][3:-1]))
   return None  # no match
 
 
 def IsMtdNand(mtddevname):
-  mtddevname = re.sub(r'^' + F['MTD_PREFIX'], 'mtd', mtddevname)
+  mtddevname = re.sub(r'^' + GetMtdPrefix(), 'mtd', mtddevname)
   path = F['SYSCLASSMTD'] + '/{0}/type'.format(mtddevname)
   data = open(path).read()
   return 'nand' in data
@@ -275,7 +373,8 @@
   Returns:
     Device file of named partition
   """
-  cmd = [SGDISK, '-p', blk_dev]
+  # Note: Android doesn't support '-p' option, need to use '--print'
+  cmd = [SGDISK, '--print', blk_dev]
   devnull = open('/dev/null', 'w')
   try:
     p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=devnull)
@@ -641,6 +740,24 @@
   return True
 
 
+def GetOsFromManifest(manifest):
+  """Determine which OS (FiberOS, Android) the image is for from the manifest.
+
+  Args:
+    manifest: the manifest from an image file
+
+  Returns:
+    'android' if any Android specific image name is found in the manifest,
+    otherwise it returns 'fiberos' (default).
+
+  """
+  for key in manifest.keys():
+    if key.endswith('-sha1'):
+      if key[:-5] in ANDROID_IMAGES:
+        return 'android'
+  return 'fiberos'
+
+
 class ProgressBar(object):
   """Progress bar that prints one dot per 1MB."""
 
@@ -695,26 +812,37 @@
     Log('W: psback/logos unavailable for tracing.\n')
 
 
-def GetPartition(opt):
-  """Return the partiton to install to, given the command line options."""
-  if opt.partition == 'other':
-    boot = GetBootedPartition()
+def GetPartition(partition_name, target_os):
+  """Return the partition to install to.
+
+  Args:
+    partition_name: partition name from command-line
+                    {'primary', 'secondary', 'other'}
+    target_os: 'fiberos' or 'android'
+
+  Returns:
+    0 or 1
+
+  Raises:
+    Fatal: if no partition could be determined
+  """
+  if partition_name == 'other':
+    if target_os == GetOs():
+      boot = GetBootedPartition()
+    else:
+      boot = GetActivePartitionFromHNVRAM(target_os)
     assert boot in [None, 0, 1]
     if boot is None:
       # Policy decision: if we're booted from NFS, install to secondary
       return 1
     else:
       return boot ^ 1
-  elif opt.partition in ['primary', 0]:
+  elif partition_name in ['primary', 0]:
     return 0
-  elif opt.partition in ['secondary', 1]:
+  elif partition_name in ['secondary', 1]:
     return 1
-  elif opt.partition:
-    raise Fatal('--partition must be one of: primary, secondary, other')
-  elif opt.tar:
-    raise Fatal('A --partition option must be provided with --tar')
   else:
-    return None
+    raise Fatal('--partition must be one of: primary, secondary, other')
 
 
 def InstallKernel(kern, partition):
@@ -729,7 +857,7 @@
 
   partition_name = 'kernel%d' % partition
   mtd = GetMtdDevForNameOrNone(partition_name)
-  gpt = GetGptPartitionForName(F['MMCBLK0'], partition_name)
+  gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
   if mtd:
     VerbosePrint('Writing kernel to %r\n' % mtd)
     InstallToMtd(kern, mtd)
@@ -759,7 +887,7 @@
       if gpt:
         mtd = None
   else:
-    gpt = GetGptPartitionForName(F['MMCBLK0'], partition_name)
+    gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
   if mtd:
     if GetPlatform().startswith('GFMN'):
       VerbosePrint('Writing rootfs to %r\n' % mtd)
@@ -775,6 +903,72 @@
     raise Fatal('no partition named %r is available' % partition_name)
 
 
+def InstallAndroidBoot(boot, partition):
+  """Install an Android boot.img file.
+
+  Args:
+    boot: a FileWithSecureHash object.
+    partition: the partition to install to, 0 or 1.
+
+  Raises:
+    Fatal: if install fails
+  """
+
+  partition_name = ANDROID_BOOT_PARTITIONS[partition]
+  gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
+  if gpt:
+    VerbosePrint('Writing boot.img to %r\n' % gpt)
+    InstallToFile(boot, gpt)
+  else:
+    raise Fatal('no partition named %r is available' % partition_name)
+
+
+def InstallAndroidSystem(system, partition):
+  """Install an Android system.img file.
+
+  Args:
+    system: a FileWithSecureHash object.
+    partition: the partition to install to, 0 or 1.
+
+  Raises:
+    Fatal: if install fails
+  """
+
+  partition_name = ANDROID_SYSTEM_PARTITIONS[partition]
+  gpt = GetGptPartitionForName(GetMmcblk0Prefix(), partition_name)
+  if gpt:
+    VerbosePrint('Writing system.img.raw to %r\n' % gpt)
+    InstallToFile(system, gpt)
+  else:
+    raise Fatal('no partition named %r is available' % partition_name)
+
+
+def InstallAndroidBsu(bsu):
+  """Install an Android BSU file.
+
+  Args:
+    bsu: a FileWithSecureHash object.
+
+  Raises:
+    Fatal: if install fails
+  """
+
+  is_bsu_current = False
+  gpt = GetGptPartitionForName(GetMmcblk0Prefix(), ANDROID_BSU_PARTITION)
+  if gpt:
+    with open(gpt, 'rb') as gptfile:
+      VerbosePrint('Checking if android_bsu is up to date.\n')
+      is_bsu_current = IsIdentical('android_bsu', bsu.filelike, gptfile)
+    if is_bsu_current:
+      VerbosePrint('android_bsu is the latest.\n')
+    else:
+      bsu.filelike.seek(0, os.SEEK_SET)
+      VerbosePrint('Writing android_bsu.elf to %r\n' % gpt)
+      InstallToFile(bsu, gpt)
+  else:
+    raise Fatal('no partition named %r is available' % ANDROID_BSU_PARTITION)
+
+
 def UnlockMMC(mmc_name):
   if mmc_name in MMC_RO_LOCK:
     with open(MMC_RO_LOCK[mmc_name], 'w') as f:
@@ -805,7 +999,11 @@
       WriteLoaderToMtd(loader, loader_start, mtd, 'loader')
       installed = True
   # For hd254 we also write the loader to the emmc boot partitions.
-  for emmc_name in ['MMCBLK0BOOT0', 'MMCBLK0BOOT1']:
+  if GetOs() == 'android':
+    emmc_list = ['MMCBLK0BOOT0-ANDROID', 'MMCBLK0BOOT1-ANDROID']
+  else:
+    emmc_list = ['MMCBLK0BOOT0', 'MMCBLK0BOOT1']
+  for emmc_name in emmc_list:
     emmc_dev = F[emmc_name]
     if os.path.exists(emmc_dev):
       UnlockMMC(emmc_name)
@@ -843,19 +1041,23 @@
     WriteLoaderToMtd(uloader, uloader_start, mtd, 'uloader')
 
 
-def InstallImage(f, partition, skiploader=False, skiploadersig=False):
+def InstallImage(opt):
   """Install an image.
 
   Args:
-    f: a file-like objected expected to provide a stream in tar format
-    partition: integer 0 or 1 of the partition to install into
-    skiploader: skip installation of a bootloader
-    skiploadersig: skip checking of bootloader signature
+    opt: command-line options
 
+  Returns:
+    0 for success, else an error code
   Raises:
     Fatal: if install fails
   """
 
+  if not opt.partition:
+    # default to the safe option if not given
+    opt.partition = 'other'
+
+  f = OpenPathOrUrl(opt.tar)
   tar = tarfile.open(mode='r|*', fileobj=f)
   first = tar.next()
 
@@ -879,15 +1081,30 @@
   CheckMinimumVersion(manifest)
   CheckMisc(manifest)
 
+  target_os = GetOsFromManifest(manifest)
+  partition = GetPartition(opt.partition, target_os)
+
   loader_bin_list = ['loader.img', 'loader.bin']
   loader_sig_list = ['loader.sig']
   if CheckMultiLoader(manifest):
     loader_bin_list = ['loader.%s.bin' % GetPlatform().lower()]
     loader_sig_list = ['loader.%s.sig' % GetPlatform().lower()]
 
-  uloader = loader = None
+  uloader = loader = android_bsu = None
   uloadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
-  loadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
+
+  # TODO(cgibson): Modern ginstall images contain a loadersig. However, some
+  # releases, such as 42.33 for the FiberJack, do not have a loadersig. In 42.33
+  # this was okay since cwmp calls ginstall with the '--skiploadersig' flag.
+  # However, in later versions this flag was removed. Now if a new ginstall
+  # were to be used to downgrade to an older ginstall image, the install would
+  # fail. This seems to only affect the FiberJack platform, which is still
+  # running 42.33. This can safely be removed once all FiberJacks have been
+  # upgraded to gfiber-47 and are not anticipated to need to be downgraded back
+  # to 42.33.
+  loadersig = None
+  if not GetPlatform().startswith('GFLT'):
+    loadersig = FileWithSecureHash(StringIO.StringIO(''), 'badsig')
 
   for ti in tar:
     secure_hash = manifest.get('%s-sha1' % ti.name)
@@ -895,11 +1112,29 @@
       # already processed
       pass
     elif ti.name in ['kernel.img', 'vmlinuz', 'vmlinux', 'uImage']:
-      fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
-      InstallKernel(fh, partition)
+      if target_os != 'fiberos':
+        VerbosePrint('Cannot install kernel img in Android!\n')
+      else:
+        fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+        InstallKernel(fh, partition)
     elif ti.name.startswith('rootfs.'):
-      fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
-      InstallRootfs(fh, partition)
+      if target_os != 'fiberos':
+        VerbosePrint('Cannot install rootfs img in Android!\n')
+      else:
+        fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+        InstallRootfs(fh, partition)
+    elif ti.name == 'boot.img':
+      if target_os != 'android':
+        VerbosePrint('Cannot install boot img in FiberOS!\n')
+      else:
+        fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+        InstallAndroidBoot(fh, partition)
+    elif ti.name == 'system.img.raw':
+      if target_os != 'android':
+        VerbosePrint('Cannot install system img in FiberOS!\n')
+      else:
+        fh = FileWithSecureHash(tar.extractfile(ti), secure_hash)
+        InstallAndroidSystem(fh, partition)
     elif ti.name in loader_bin_list:
       buf = StringIO.StringIO(tar.extractfile(ti).read())
       loader = FileWithSecureHash(buf, secure_hash)
@@ -912,34 +1147,49 @@
     elif ti.name == 'uloader.sig':
       buf = StringIO.StringIO(tar.extractfile(ti).read())
       uloadersig = FileWithSecureHash(buf, secure_hash)
+    elif ti.name == 'android_bsu.elf':
+      buf = StringIO.StringIO(tar.extractfile(ti).read())
+      android_bsu = FileWithSecureHash(buf, secure_hash)
     else:
       print 'Unknown install file %s' % ti.name
 
-  if skiploadersig:
+  if opt.skiploadersig:
     loadersig = uloadersig = None
 
   key = GetKey()
-  if loadersig and loader and not skiploader:
+  if loadersig and loader and not opt.skiploader:
     if not Verify(loader.filelike, loadersig.filelike, key):
       raise Fatal('Loader signing check failed.')
     loader.filelike.seek(0, os.SEEK_SET)
-  if uloadersig and uloader and not skiploader:
+  if uloadersig and uloader and not opt.skiploader:
     if not Verify(uloader.filelike, uloadersig.filelike, key):
       raise Fatal('Uloader signing check failed.')
     uloader.filelike.seek(0, os.SEEK_SET)
 
   if loader:
-    if skiploader:
+    if opt.skiploader:
       VerbosePrint('Skipping loader installation.\n')
     else:
       InstallLoader(loader)
 
   if uloader:
-    if skiploader:
+    if opt.skiploader:
       VerbosePrint('Skipping uloader installation.\n')
     else:
       InstallUloader(uloader)
 
+  if android_bsu:
+    if opt.skiploader:
+      VerbosePrint('Skipping android_bsu installation.\n')
+    else:
+      InstallAndroidBsu(android_bsu)
+
+  if SetBootPartition(target_os, partition) != 0:
+    VerbosePrint('Unable to set boot partition\n')
+    return HNVRAM_ERR
+
+  return 0
+
 
 def OpenPathOrUrl(path):
   """Try to open path as a URL and as a local file."""
@@ -968,6 +1218,16 @@
   if not (opt.drm or opt.tar or opt.partition):
     o.fatal('Expected at least one of --partition, --tar, or --drm')
 
+  # handle 'ginstall -p <partition>' separately
+  if not opt.drm and not opt.tar:
+    partition = GetPartition(opt, GetOs())
+    if SetBootPartition(GetOs(), partition) != 0:
+      VerbosePrint('Unable to set boot partition\n')
+      return HNVRAM_ERR
+    return 0
+
+  # from here: ginstall [-t <tarfile>] [--drm <blob>] [options...]
+
   quiet = opt.quiet
 
   if opt.basepath:
@@ -977,21 +1237,11 @@
   if opt.drm:
     WriteDrm(opt)
 
-  if opt.tar and not opt.partition:
-    # default to the safe option if not given
-    opt.partition = 'other'
-
-  partition = GetPartition(opt)
+  ret = 0
   if opt.tar:
-    f = OpenPathOrUrl(opt.tar)
-    InstallImage(f, partition, skiploader=opt.skiploader,
-                 skiploadersig=opt.skiploadersig)
+    ret = InstallImage(opt)
 
-  if partition is not None and SetBootPartition(partition) != 0:
-    VerbosePrint('Unable to set boot partition\n')
-    return HNVRAM_ERR
-
-  return 0
+  return ret
 
 
 def BroadcomDeviceIsSecure():
diff --git a/ginstall/ginstall_test.py b/ginstall/ginstall_test.py
index 07cb294..5b335ea 100755
--- a/ginstall/ginstall_test.py
+++ b/ginstall/ginstall_test.py
@@ -40,13 +40,16 @@
 
   def setUp(self):
     self.tmpdir = tempfile.mkdtemp()
+    self.hnvram_dir = self.tmpdir + '/hnvram'
     self.script_out = self.tmpdir + '/out'
     self.old_path = os.environ['PATH']
     self.old_bufsize = ginstall.BUFSIZE
     self.old_files = ginstall.F
+    os.environ['GINSTALL_HNVRAM_DIR'] = self.hnvram_dir
     os.environ['GINSTALL_OUT_FILE'] = self.script_out
     os.environ['GINSTALL_TEST_FAIL'] = ''
     os.environ['PATH'] = 'testdata/bin:' + self.old_path
+    os.makedirs(self.hnvram_dir)
     os.makedirs(self.tmpdir + '/dev')
     ginstall.F['ETCPLATFORM'] = 'testdata/etc/platform'
     ginstall.F['DEV'] = self.tmpdir + '/dev'
@@ -69,6 +72,9 @@
     ginstall.MMC_RO_LOCK['MMCBLK0BOOT1'] = (
         self.tmpdir + '/mmcblk0boot1/force_ro')
 
+    # default OS to 'fiberos'
+    self.WriteOsFile('fiberos')
+
   def tearDown(self):
     os.environ['PATH'] = self.old_path
     shutil.rmtree(self.tmpdir, ignore_errors=True)
@@ -80,6 +86,23 @@
     open(filename, 'w').write(version)
     ginstall.F['ETCVERSION'] = filename
 
+  def WriteOsFile(self, os_name):
+    """Create a fake /etc/os file in /tmp."""
+    filename = self.tmpdir + '/os'
+    open(filename, 'w').write(os_name)
+    ginstall.F['ETCOS'] = filename
+
+  def WriteHnvramAttr(self, attr, val):
+    filename = self.hnvram_dir + '/%s' % attr
+    open(filename, 'w').write(val)
+
+  def ReadHnvramAttr(self, attr):
+    filename = self.hnvram_dir + '/%s' % attr
+    try:
+      return open(filename).read()
+    except IOError:
+      return None
+
   def testVerify(self):
     self.assertTrue(ginstall.Verify(
         open('testdata/img/loader.bin'),
@@ -158,11 +181,44 @@
                       origfile, 'mtd0.tmp')
 
   def testSetBootPartition(self):
-    ginstall.SetBootPartition(0)
-    ginstall.SetBootPartition(1)
+    self.WriteOsFile('fiberos')
+    ginstall.SetBootPartition('fiberos', 0)
+    self.assertEqual('kernel0', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+    ginstall.SetBootPartition('fiberos', 1)
+    self.assertEqual('kernel1', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+    ginstall.SetBootPartition('android', 0)
+    self.assertEqual('a', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+    self.assertEqual('android', self.ReadHnvramAttr('BOOT_TARGET'))
+    ginstall.SetBootPartition('android', 1)
+    self.assertEqual('b', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+    self.assertEqual('android', self.ReadHnvramAttr('BOOT_TARGET'))
+
+    self.WriteOsFile('android')
+    ginstall.SetBootPartition('fiberos', 0)
+    self.assertEqual('kernel0', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+    self.assertEqual('fiberos', self.ReadHnvramAttr('BOOT_TARGET'))
+    ginstall.SetBootPartition('fiberos', 1)
+    self.assertEqual('kernel1', self.ReadHnvramAttr('ACTIVATED_KERNEL_NAME'))
+    self.assertEqual('fiberos', self.ReadHnvramAttr('BOOT_TARGET'))
+    ginstall.SetBootPartition('android', 0)
+    self.assertEqual('a', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+    ginstall.SetBootPartition('android', 1)
+    self.assertEqual('b', self.ReadHnvramAttr('ANDROID_ACTIVE_PARTITION'))
+
+    # also verify the hnvram command history for good measures
     out = open(self.script_out).read().splitlines()
     self.assertEqual(out[0], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0')
     self.assertEqual(out[1], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel1')
+    self.assertEqual(out[2], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=a')
+    self.assertEqual(out[3], 'hnvram -q -w BOOT_TARGET=android')
+    self.assertEqual(out[4], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=b')
+    self.assertEqual(out[5], 'hnvram -q -w BOOT_TARGET=android')
+    self.assertEqual(out[6], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0')
+    self.assertEqual(out[7], 'hnvram -q -w BOOT_TARGET=fiberos')
+    self.assertEqual(out[8], 'hnvram -q -w ACTIVATED_KERNEL_NAME=kernel1')
+    self.assertEqual(out[9], 'hnvram -q -w BOOT_TARGET=fiberos')
+    self.assertEqual(out[10], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=a')
+    self.assertEqual(out[11], 'hnvram -q -w ANDROID_ACTIVE_PARTITION=b')
 
   def testParseManifest(self):
     l = ('installer_version: 99\nimage_type: fake\n'
@@ -189,6 +245,34 @@
     manifest = ginstall.ParseManifest(in_f)
     self.assertTrue(ginstall.CheckPlatform(manifest))
 
+  def testGetOs(self):
+    self.WriteOsFile('fiberos')
+    self.assertEqual('fiberos', ginstall.GetOs())
+    self.WriteOsFile('android')
+    self.assertEqual('android', ginstall.GetOs())
+    # in case file doesn't exist, default is 'fiberos'
+    os.remove(self.tmpdir + '/os')
+    self.assertEqual('fiberos', ginstall.GetOs())
+
+  def testGetMtdPrefix(self):
+    self.WriteOsFile('fiberos')
+    self.assertEqual(ginstall.F['MTD_PREFIX'], ginstall.GetMtdPrefix())
+    self.WriteOsFile('android')
+    self.assertEqual(ginstall.F['MTD_PREFIX-ANDROID'], ginstall.GetMtdPrefix())
+    # unknown OS returns 'fiberos'
+    self.WriteOsFile('windows')
+    self.assertEqual(ginstall.F['MTD_PREFIX'], ginstall.GetMtdPrefix())
+
+  def testGetMmcblk0Prefix(self):
+    self.WriteOsFile('fiberos')
+    self.assertEqual(ginstall.F['MMCBLK0'], ginstall.GetMmcblk0Prefix())
+    self.WriteOsFile('android')
+    self.assertEqual(ginstall.F['MMCBLK0-ANDROID'],
+                     ginstall.GetMmcblk0Prefix())
+    # unknown OS returns 'fiberos'
+    self.WriteOsFile('windows')
+    self.assertEqual(ginstall.F['MMCBLK0'], ginstall.GetMmcblk0Prefix())
+
   def testGetInternalHarddisk(self):
     self.assertEqual(ginstall.GetInternalHarddisk(), None)
 
@@ -268,20 +352,141 @@
       manifest = {'version': v}
       self.assertRaises(ginstall.Fatal, ginstall.CheckMisc, manifest)
 
-  def testGetBootedFromCmdLine(self):
-    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline1'
+  def MakeManifestWithFilenameSha1s(self, filename):
+    m = ('installer_version: 4\n'
+         'image_type: unlocked\n'
+         'version: gftv254-48-pre2-1100-g25ff8d0-ck\n'
+         'platforms: [ GFHD254 ]\n')
+    if filename is not None:
+      m += '%s-sha1: 9b5236c282b8c11b38a630361b6c690d6aaa50cb\n' % filename
+
+    in_f = StringIO.StringIO(m)
+    return ginstall.ParseManifest(in_f)
+
+  def testGetOsFromManifest(self):
+    # android specific image names return 'android'
+    for img in ginstall.ANDROID_IMAGES:
+      manifest = self.MakeManifestWithFilenameSha1s(img)
+      self.assertEqual('android', ginstall.GetOsFromManifest(manifest))
+
+    # fiberos image names or anything non-android returns 'fiberos'
+    for img in ['rootfs.img', 'kernel.img', 'whatever.img']:
+      manifest = self.MakeManifestWithFilenameSha1s(img)
+      self.assertEqual('fiberos', ginstall.GetOsFromManifest(manifest))
+
+    # no sha1 entry in the manifest returns 'fiberos'
+    manifest = self.MakeManifestWithFilenameSha1s(None)
+    self.assertEqual('fiberos', ginstall.GetOsFromManifest(manifest))
+
+  def testGetBootedPartition(self):
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.none'
+    self.assertEqual(None, ginstall.GetBootedPartition())
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.0'
+    self.assertEqual(0, ginstall.GetBootedPartition())
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.1'
+    self.assertEqual(1, ginstall.GetBootedPartition())
+
+    # Android
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.none'
+    self.assertEqual(None, ginstall.GetBootedPartition())
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.0'
+    self.assertEqual(0, ginstall.GetBootedPartition())
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.1'
+    self.assertEqual(1, ginstall.GetBootedPartition())
+
+    # Prowl
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.none'
     self.assertEqual(ginstall.GetBootedPartition(), None)
-    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline2'
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.0'
     self.assertEqual(ginstall.GetBootedPartition(), 0)
-    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline3'
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.1'
     self.assertEqual(ginstall.GetBootedPartition(), 1)
 
+  def testGetActivePartitionFromHNVRAM(self):
+    # FiberOS looks at ACTIVATED_KERNEL_NAME, not ANDROID_ACTIVE_PARTITION
+    # 0
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+    self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '0')
+    self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '1')
+    self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+    # 1
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '1')
+    self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '0')
+    self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('fiberos'))
+
+    # Android looks at ANDROID_ACTIVE_PARTITION, not ACTIVATED_KERNEL_NAME
+    # 0
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '0')
+    self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('android'))
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+    self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('android'))
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '1')
+    self.assertEqual(0, ginstall.GetActivePartitionFromHNVRAM('android'))
+    # 1
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', '1')
+    self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('android'))
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+    self.assertEqual(1, ginstall.GetActivePartitionFromHNVRAM('android'))
+
+  def TestGetPartition(self):
+    self.assertEqual(0, ginstall.GetPartition('primary', 'fiberos'))
+    self.assertEqual(0, ginstall.GetPartition(0, 'fiberos'))
+    self.assertEqual(1, ginstall.GetPartition('secondary', 'fiberos'))
+    self.assertEqual(1, ginstall.GetPartition(1, 'fiberos'))
+    self.assertEqual(0, ginstall.GetPartition('primary', 'android'))
+    self.assertEqual(0, ginstall.GetPartition(0, 'android'))
+    self.assertEqual(1, ginstall.GetPartition('secondary', 'android'))
+    self.assertEqual(1, ginstall.GetPartition(1, 'android'))
+
+    # other: FiberOS->FiberOS
+    self.WriteOsFile('fiberos')
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.none'
+    self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.0'
+    self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.1'
+    self.assertEqual(0, ginstall.GetPartition('other', 'fiberos'))
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.none'
+    self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.0'
+    self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.1'
+    self.assertEqual(0, ginstall.GetPartition('other', 'fiberos'))
+
+    # other: FiberOS->Android
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', 'a')
+    self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', 'b')
+    self.assertEqual(0, ginstall.GetPartition('other', 'android'))
+    self.WriteHnvramAttr('ANDROID_ACTIVE_PARTITION', 'bla')
+    self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+
+    # other: Android->FiberOS
+    self.WriteOsFile('android')
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '0')
+    self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', '1')
+    self.assertEqual(0, ginstall.GetPartition('other', 'fiberos'))
+    self.WriteHnvramAttr('ACTIVATED_KERNEL_NAME', 'bla')
+    self.assertEqual(1, ginstall.GetPartition('other', 'fiberos'))
+
+    # other: Android->Android
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.none'
+    self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.0'
+    self.assertEqual(1, ginstall.GetPartition('other', 'android'))
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.android.1'
+    self.assertEqual(0, ginstall.GetPartition('other', 'android'))
+
     # Test prowl and gfactive
-    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline4'
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.none'
     self.assertEqual(ginstall.GetBootedPartition(), None)
-    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline5'
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.0'
     self.assertEqual(ginstall.GetBootedPartition(), 0)
-    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline6'
+    ginstall.F['PROC_CMDLINE'] = 'testdata/proc/cmdline.prowl.1'
     self.assertEqual(ginstall.GetBootedPartition(), 1)
 
   def testUloaderSigned(self):
diff --git a/ginstall/install_test.sh b/ginstall/install_test.sh
index ec372c5..910971d 100755
--- a/ginstall/install_test.sh
+++ b/ginstall/install_test.sh
@@ -4,20 +4,29 @@
 
 tmpdir="$(mktemp -d)"
 export PATH="$tmpdir/bin:${PATH}"
+export GINSTALL_HNVRAM_DIR="$tmpdir/hnvram"
 export GINSTALL_OUT_FILE="$tmpdir/out"
+export GINSTALL_PLATFORM_FILE="$tmpdir/etc/platform"
 psiz=$(stat --format=%s testdata/img/loader.gflt110.bin)
 lsiz=$(stat --format=%s testdata/img/loader.img)
 ksiz=$(stat --format=%s testdata/img/kernel.img)
 rsiz=$(stat --format=%s testdata/img/rootfs.img)
 usiz=$(stat --format=%s testdata/img/uloader.img)
+bsiz=$(stat --format=%s testdata/img/boot.img)
+ssiz=$(stat --format=%s testdata/img/system.img.raw)
+asiz=$(stat --format=%s testdata/img/android_bsu.elf)
 testdata/bin/http_server "$tmpdir/http_ctrl" &
 
 setup_fakeroot() {
   platform="$1"
+  running_os="fiberos"
+  [ $# -gt 1 ] && running_os="$2"
   rm -f "$GINSTALL_OUT_FILE"
   rm -rf "$tmpdir/*"
   mkdir -p "$tmpdir/bin" "$tmpdir/dev" "$tmpdir/etc"
+  mkdir -p "$tmpdir/dev/block" "$tmpdir/dev/mtd"
   mkdir -p "$tmpdir/sys/block/sda"
+  mkdir -p "$GINSTALL_HNVRAM_DIR"
   cp -r testdata/bin "$tmpdir"
   cp -r testdata/proc "$tmpdir"
   cp -r testdata/img "$tmpdir"
@@ -30,10 +39,18 @@
   echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/mtd3"
   echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/mtd4"
 
+  # write a pre-existing android_bsu
+  echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/mmcblk0p2"
+  echo 0123456789abcdef0123456789abcdef >"$tmpdir/dev/block/mmcblk0p2"
+
   for i in {5..31}; do touch "$tmpdir/dev/mtd$i"; done
 
+  # duplicate /dev/mtd* to /dev/mtd/mtd* (used by Android)
+  cp ${tmpdir}/dev/mtd[0-9]* ${tmpdir}/dev/mtd/
+
   cp "testdata/proc/mtd.$platform" "$tmpdir/proc/mtd"
   echo "$platform" >"$tmpdir/etc/platform"
+  echo "$running_os" >"$tmpdir/etc/os"
   echo 0123456789abcdef0123456789abcdef >"$tmpdir/etc/gfiber_public.der"
 }
 
@@ -150,6 +167,74 @@
 
 
 
+# kernel in NAND, raw no bbt
+# rootfs on eMMC
+# (GFHD254)
+echo; echo; echo "GFHD254 (fiberos->fiberos)"
+setup_fakeroot GFHD254 fiberos
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd0 0 0
+hnvram -q -w ACTIVATED_KERNEL_NAME=kernel1"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_v4.gi --partition=secondary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$ksiz" "${tmpdir}/dev/mmcblk0p14" testdata/img/kernel.img
+WVPASS cmp --bytes="$rsiz" "${tmpdir}/dev/mmcblk0p15" testdata/img/rootfs.img
+
+# FiberOS->Android (GFHD254)
+echo; echo; echo "GFHD254 (fiberos->android)"
+setup_fakeroot GFHD254 fiberos
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd0 0 0
+hnvram -q -w ANDROID_ACTIVE_PARTITION=b
+hnvram -q -w BOOT_TARGET=android"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_android_v4.gi --partition=secondary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$asiz" "${tmpdir}/dev/mmcblk0p2" testdata/img/android_bsu.elf
+WVPASS cmp --bytes="$bsiz" "${tmpdir}/dev/mmcblk0p6" testdata/img/boot.img
+WVPASS cmp --bytes="$ssiz" "${tmpdir}/dev/mmcblk0p10" testdata/img/system.img.raw
+
+# Android->Android (GFHD254)
+echo; echo; echo "GFHD254 (android->android)"
+setup_fakeroot GFHD254 android
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd/mtd0 0 0
+hnvram -q -w ANDROID_ACTIVE_PARTITION=a"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_android_v4.gi --partition=primary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$asiz" "${tmpdir}/dev/block/mmcblk0p2" testdata/img/android_bsu.elf
+WVPASS cmp --bytes="$bsiz" "${tmpdir}/dev/block/mmcblk0p5" testdata/img/boot.img
+WVPASS cmp --bytes="$ssiz" "${tmpdir}/dev/block/mmcblk0p9" testdata/img/system.img.raw
+
+# Android->FiberOS (GFHD254)
+echo; echo; echo "GFHD254 (android->fiberos)"
+setup_fakeroot GFHD254 android
+expected="\
+psback
+logos ginstall
+flash_erase --quiet ${tmpdir}/dev/mtd/mtd0 0 0
+hnvram -q -w ACTIVATED_KERNEL_NAME=kernel0
+hnvram -q -w BOOT_TARGET=fiberos"
+
+WVPASS ./ginstall.py --basepath="$tmpdir" --tar=./testdata/img/image_v4.gi --partition=primary --skiploadersig
+WVPASSEQ "$expected" "$(cat $GINSTALL_OUT_FILE)"
+WVPASS cmp --bytes="$lsiz" "${tmpdir}/dev/mtd/mtd0" testdata/img/loader.img
+WVPASS cmp --bytes="$ksiz" "${tmpdir}/dev/block/mmcblk0p12" testdata/img/kernel.img
+WVPASS cmp --bytes="$rsiz" "${tmpdir}/dev/block/mmcblk0p13" testdata/img/rootfs.img
+
+
+
 # kernel in NOR, raw
 # rootfs in NOR, raw
 # loader in NOR, raw
diff --git a/ginstall/testdata/bin/hnvram b/ginstall/testdata/bin/hnvram
deleted file mode 120000
index 3c2bde7..0000000
--- a/ginstall/testdata/bin/hnvram
+++ /dev/null
@@ -1 +0,0 @@
-write_args_to_file
\ No newline at end of file
diff --git a/ginstall/testdata/bin/hnvram b/ginstall/testdata/bin/hnvram
new file mode 100755
index 0000000..6faa2b6
--- /dev/null
+++ b/ginstall/testdata/bin/hnvram
@@ -0,0 +1,38 @@
+#!/bin/sh
+
+# log into out file
+exe=$(basename "$0")
+echo "$exe" $* >> "$GINSTALL_OUT_FILE"
+
+# simple cmdline parser
+for i in "$@"; do
+  if [ "$i" == "-q" ]; then
+    continue
+  elif [ "$i" == "-r" ]; then
+    read=1
+  elif [ "$i" == "-w" ]; then
+    write=1
+  else
+    attr_val="$i"
+  fi
+done
+
+IFS='=' read attr val <<< "$attr_val"
+
+GINSTALL_ATTR_FILE="${GINSTALL_HNVRAM_DIR}/${attr}"
+
+if [ -n "$write" ]; then
+  echo -n "$val" > "$GINSTALL_ATTR_FILE"
+elif [ -n "$read" ]; then
+  if [ ! -r "$GINSTALL_ATTR_FILE" ]; then
+    exit 1
+  else
+    cat "$GINSTALL_ATTR_FILE"
+  fi
+fi
+
+if [ ! -z "$GINSTALL_TEST_FAIL" ]; then
+  exit 1
+fi
+
+exit 0
diff --git a/ginstall/testdata/bin/sgdisk b/ginstall/testdata/bin/sgdisk
index 69c640f..7e9392a 100755
--- a/ginstall/testdata/bin/sgdisk
+++ b/ginstall/testdata/bin/sgdisk
@@ -1,6 +1,6 @@
 #!/bin/sh
 
-echo "Disk /dev/mmcblk0: 7634944 sectors, 3.6 GiB
+default="Disk /dev/mmcblk0: 7634944 sectors, 3.6 GiB
 Logical sector size: 512 bytes
 Disk identifier (GUID): FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF
 Partition table holds up to 128 entries
@@ -15,3 +15,37 @@
   19          954368         1740799   384.0 MiB   FFFF  rootfs1
   20         1740800         1871871   64.0 MiB    FFFF  emergency
   21         1871872         7114751   2.5 GiB     8300  data+ext4"
+
+gfhd254="Disk /dev/mmcblk0: 61071360 sectors, 29.1 GiB
+Logical sector size: 512 bytes
+Disk identifier (GUID): BE51F469-09A6-4A05-9A3B-D09855D3E5A1
+Partition table holds up to 128 entries
+First usable sector is 34, last usable sector is 61071326
+Partitions will be aligned on 2-sector boundaries
+Total free space is 0 sectors (0 bytes)
+
+Number  Start (sector)    End (sector)  Size       Code  Name
+   1              34             161   64.0 KiB    8300  nvram
+   2             162             673   256.0 KiB   8300  bsu
+   3             674            2721   1024.0 KiB  8300  misc
+   4            2722            4769   1024.0 KiB  8300  hwcfg
+   5            4770           70305   32.0 MiB    8300  boot_a
+   6           70306          135841   32.0 MiB    8300  boot_b
+   7          135842          152225   8.0 MiB     8300  metadata
+   8          152226          156321   2.0 MiB     8300  eio
+   9          156322         4350625   2.0 GiB     8300  system_a
+  10         4350626         8544929   2.0 GiB     8300  system_b
+  11         8544930         8547873   1.4 MiB     8300  hnvram
+  12         8547874         8613409   32.0 MiB    8300  kernel0
+  13         8613410         9399841   384.0 MiB   8300  rootfs0
+  14         9399842         9465377   32.0 MiB    8300  kernel1
+  15         9465378        10251809   384.0 MiB   8300  rootfs1
+  16        10251810        10382881   64.0 MiB    8300  emergency
+  17        10382882        61071326   24.2 GiB    8300  userdata"
+
+platform="$(cat $GINSTALL_PLATFORM_FILE)"
+if [ "$platform"  == "GFHD254" ]; then
+  echo "$gfhd254"
+else
+  echo "$default"
+fi
diff --git a/ginstall/testdata/img/MANIFEST b/ginstall/testdata/img/MANIFEST
index de8c93b..5e839f4 100644
--- a/ginstall/testdata/img/MANIFEST
+++ b/ginstall/testdata/img/MANIFEST
@@ -1,7 +1,7 @@
 installer_version: 4
 image_type: unittest
 version: gftv200-40.1
-platforms: [ GFUNITTEST, GFSC100, GFMN100, GFHD200, GFHD100, GFMS100, GFRG210, GFRG200 ]
+platforms: [ GFUNITTEST, GFSC100, GFMN100, GFHD200, GFHD254, GFHD100, GFMS100, GFRG210, GFRG200 ]
 loader.img-sha1: 228d83f86ba967704a94afce92e1e4cbba3b24a6
 uloader.img-sha1: 171e9a2e524c1f3a64f43ef7d254b85e764f1096
 kernel.img-sha1: fbd0ad4af43303c6b3001920689db2eb4d5212d0
diff --git a/ginstall/testdata/img/android_bsu.elf b/ginstall/testdata/img/android_bsu.elf
new file mode 100644
index 0000000..44687e7
--- /dev/null
+++ b/ginstall/testdata/img/android_bsu.elf
@@ -0,0 +1 @@
+android_bsu.elf
\ No newline at end of file
diff --git a/ginstall/testdata/img/boot.img b/ginstall/testdata/img/boot.img
new file mode 100644
index 0000000..7afe48a
--- /dev/null
+++ b/ginstall/testdata/img/boot.img
@@ -0,0 +1 @@
+boot.img
\ No newline at end of file
diff --git a/ginstall/testdata/img/image_android_v4.gi b/ginstall/testdata/img/image_android_v4.gi
new file mode 100644
index 0000000..601138a
--- /dev/null
+++ b/ginstall/testdata/img/image_android_v4.gi
Binary files differ
diff --git a/ginstall/testdata/img/image_v4.gi b/ginstall/testdata/img/image_v4.gi
index 11aac14..7d66211 100644
--- a/ginstall/testdata/img/image_v4.gi
+++ b/ginstall/testdata/img/image_v4.gi
Binary files differ
diff --git a/ginstall/testdata/img/system.img.raw b/ginstall/testdata/img/system.img.raw
new file mode 100644
index 0000000..3fcfe41
--- /dev/null
+++ b/ginstall/testdata/img/system.img.raw
@@ -0,0 +1 @@
+system.img.raw
\ No newline at end of file
diff --git a/ginstall/testdata/proc/cmdline2 b/ginstall/testdata/proc/cmdline.0
similarity index 100%
rename from ginstall/testdata/proc/cmdline2
rename to ginstall/testdata/proc/cmdline.0
diff --git a/ginstall/testdata/proc/cmdline3 b/ginstall/testdata/proc/cmdline.1
similarity index 100%
rename from ginstall/testdata/proc/cmdline3
rename to ginstall/testdata/proc/cmdline.1
diff --git a/ginstall/testdata/proc/cmdline.android.0 b/ginstall/testdata/proc/cmdline.android.0
new file mode 100644
index 0000000..8fb7943
--- /dev/null
+++ b/ginstall/testdata/proc/cmdline.android.0
@@ -0,0 +1 @@
+mem=1024m@0m mem=1024m@2048m bmem=336m@672m bmem=256m@2048m brcm_cma=768m@2304m ramoops.mem_address=0x3F800000 ramoops.mem_size=0x800000 ramoops.console_size=0x400000 pmem=8m@1016m androidboot.selinux=permissive androidboot.hardware=gfhd254 androidboot.gfiber_system_img=system_a bootreason=main_chip_input
diff --git a/ginstall/testdata/proc/cmdline.android.1 b/ginstall/testdata/proc/cmdline.android.1
new file mode 100644
index 0000000..4da7328
--- /dev/null
+++ b/ginstall/testdata/proc/cmdline.android.1
@@ -0,0 +1 @@
+mem=1024m@0m mem=1024m@2048m bmem=336m@672m bmem=256m@2048m brcm_cma=768m@2304m ramoops.mem_address=0x3F800000 ramoops.mem_size=0x800000 ramoops.console_size=0x400000 pmem=8m@1016m androidboot.selinux=permissive androidboot.hardware=gfhd254 bootreason=main_chip_input androidboot.gfiber_system_img=system_b
diff --git a/ginstall/testdata/proc/cmdline.android.none b/ginstall/testdata/proc/cmdline.android.none
new file mode 100644
index 0000000..9fb507c
--- /dev/null
+++ b/ginstall/testdata/proc/cmdline.android.none
@@ -0,0 +1 @@
+mem=1024m@0m mem=1024m@2048m bmem=336m@672m bmem=256m@2048m brcm_cma=768m@2304m ramoops.mem_address=0x3F800000 ramoops.mem_size=0x800000 ramoops.console_size=0x400000 pmem=8m@1016m androidboot.selinux=permissive androidboot.hardware=gfhd254 bootreason=main_chip_input
diff --git a/ginstall/testdata/proc/cmdline1 b/ginstall/testdata/proc/cmdline.none
similarity index 100%
rename from ginstall/testdata/proc/cmdline1
rename to ginstall/testdata/proc/cmdline.none
diff --git a/ginstall/testdata/proc/cmdline5 b/ginstall/testdata/proc/cmdline.prowl.0
similarity index 100%
rename from ginstall/testdata/proc/cmdline5
rename to ginstall/testdata/proc/cmdline.prowl.0
diff --git a/ginstall/testdata/proc/cmdline6 b/ginstall/testdata/proc/cmdline.prowl.1
similarity index 100%
rename from ginstall/testdata/proc/cmdline6
rename to ginstall/testdata/proc/cmdline.prowl.1
diff --git a/ginstall/testdata/proc/cmdline4 b/ginstall/testdata/proc/cmdline.prowl.none
similarity index 100%
rename from ginstall/testdata/proc/cmdline4
rename to ginstall/testdata/proc/cmdline.prowl.none
diff --git a/ginstall/testdata/proc/mtd.GFHD254 b/ginstall/testdata/proc/mtd.GFHD254
new file mode 100644
index 0000000..21616d1
--- /dev/null
+++ b/ginstall/testdata/proc/mtd.GFHD254
@@ -0,0 +1,3 @@
+dev:    size   erasesize  name
+mtd0: 00400000 00001000 "flash0.bolt"
+mtd1: 00400000 00001000 "flash0"
diff --git a/rcu_audio/ti-rcu-audio.cc b/rcu_audio/ti-rcu-audio.cc
index e174477..803ce3c 100644
--- a/rcu_audio/ti-rcu-audio.cc
+++ b/rcu_audio/ti-rcu-audio.cc
@@ -96,11 +96,19 @@
        *
        * The first three bytes of payload are some kind of RAS header.
        * RAS_Decode() says the data pointer must include the three bytes
-       * of header, but the length must not include those 3 bytes.
-       * So the length is decremented by 6+1+1+3 for a total of 11.
+       * of header, but that the length must not include those 3 bytes.
+       *
+       * HOWEVER, the documentation is a filthy liar. RAS_Decode decrements
+       * its inputLength by three to account for the header length.
+       * If we obey the instructions and omit those 3 bytes from the
+       * length, the audio quality is noticeably worse because it starts
+       * throwing away three bytes of audio data.
+       *
+       * So both the pointer and the length passed to RAS_Decode *include*
+       * the header bytes.
        */
       data = &ibuf[6 + 1 + 1];
-      data_len = ilen - (6 + 1 + 1 + 3);
+      data_len = ilen - (6 + 1 + 1);
       if (RAS_Decode(RAS_DECODE_TI_TYPE1, data, data_len, obuf, &olen) == 0) {
         rcaudio::AudioSamples samples;
         char bdaddr[18];
diff --git a/taxonomy/dhcp.py b/taxonomy/dhcp.py
index 26ecb82..e415126 100644
--- a/taxonomy/dhcp.py
+++ b/taxonomy/dhcp.py
@@ -31,6 +31,7 @@
     '1,33,3,6,15,28,51,58,59': ['android'],
     '1,3,6,28,33,51,58,59,121': ['android'],
     '1,121,33,3,6,15,28,51,58,59,119': ['android'],
+    '1,3,6,15,26,28,51,58,59,43': ['android'],
 
     '1,3,6,15,112,113,78,79,95,252': ['appletv1'],
 
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index 85a4522..295652d 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -53,12 +53,18 @@
 
     '58:67:1a': ['barnes&noble'],
 
+    '2c:b0:5d': ['dish'],
+
     '30:8c:fb': ['dropcam'],
 
     '00:1a:11': ['google'],
+    '3c:5a:b4': ['google'],
     '54:60:09': ['google'],
+    '94:95:a0': ['google'],
     '94:eb:2c': ['google'],
     'a4:77:33': ['google'],
+    'f4:03:04': ['google'],
+    'f4:f5:d8': ['google'],
     'f4:f5:e8': ['google'],
     'f8:8f:ca': ['google'],
 
@@ -192,12 +198,14 @@
     '10:30:47': ['samsung'],
     '14:32:d1': ['samsung'],
     '14:49:e0': ['samsung'],
+    '18:21:95': ['samsung'],
     '18:22:7e': ['samsung'],
     '20:55:31': ['samsung'],
     '20:6e:9c': ['samsung'],
     '24:4b:81': ['samsung'],
     '28:27:bf': ['samsung'],
     '28:ba:b5': ['samsung'],
+    '2c:0e:3d': ['samsung'],
     '2c:ae:2b': ['samsung'],
     '30:19:66': ['samsung'],
     '34:23:ba': ['samsung'],
@@ -236,10 +244,12 @@
     '94:35:0a': ['samsung'],
     '94:b1:0a': ['samsung'],
     'a0:0b:ba': ['samsung'],
+    'a4:08:ea': ['samsung'],
     'a8:06:00': ['samsung'],
     'ac:36:13': ['samsung'],
     'ac:5f:3e': ['samsung'],
     'b0:47:bf': ['samsung'],
+    'b0:c5:59': ['samsung'],
     'b0:df:3a': ['samsung'],
     'b0:ec:71': ['samsung'],
     'b4:07:f9': ['samsung'],
@@ -273,6 +283,7 @@
     'f0:25:b7': ['samsung'],
     'f4:09:d8': ['samsung'],
     'f8:04:2e': ['samsung'],
+    'fc:19:10': ['samsung'],
     'fc:f1:36': ['samsung'],
 
     '00:d9:d1': ['sony'],
@@ -281,9 +292,11 @@
     '40:b8:37': ['sony'],
     '58:48:22': ['sony'],
     'b4:52:7e': ['sony'],
+    'f8:d0:ac': ['sony'],
 
     '10:08:c1': ['toshiba'],
 
+    '00:6b:9e': ['vizio'],
     'a4:8d:3b': ['vizio'],
 
     'b4:79:a7': ['wink'],
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index 3e2ea1f..116b4d1 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -51,6 +51,8 @@
   ('', './testdata/pcaps/Samsung Infuse 5GHz.pcap'),
   ('', './testdata/pcaps/Samsung Vibrant 2.4GHz.pcap'),
   ('', './testdata/pcaps/Sony Ericsson Xperia X10 2.4GHz.pcap'),
+  ('', './testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap'),
+  ('', './testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap'),
 
   # Names where the identified species doesn't exactly match the filename,
   # usually because multiple devices are too similar to distinguish. We name
@@ -65,16 +67,15 @@
   ('Amazon Kindle', './testdata/pcaps/Amazon Kindle Voyage 2.4GHz B054.pcap'),
   ('iPad 1st or 2nd gen', './testdata/pcaps/iPad 1st gen 5GHz.pcap'),
   ('iPad 1st or 2nd gen', './testdata/pcaps/iPad 2nd gen 5GHz.pcap'),
-  ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad (4th gen) 5GHz.pcap'),
-  ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad (4th gen) 2.4GHz.pcap'),
-  ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad Air 5GHz.pcap'),
-  ('iPad 4th gen or Air 1st gen', './testdata/pcaps/iPad Air 2.4GHz.pcap'),
-  ('iPhone 6/6+', './testdata/pcaps/iPhone 6 5GHz.pcap'),
-  ('iPhone 6/6+', './testdata/pcaps/iPhone 6+ 5GHz.pcap'),
+  ('iPhone 6/6+', './testdata/pcaps/iPhone 6 5GHz iOS 9.pcap'),
+  ('iPhone 6/6+', './testdata/pcaps/iPhone 6+ 5GHz iOS 9.pcap'),
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 2.4GHz.pcap'),
+  ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 5GHz.pcap'),
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz.pcap'),
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap'),
-  ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 5GHz.pcap'),
+  ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap'),
+  ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap'),
+  ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap'),
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 5GHz.pcap'),
   ('iPhone 6s/6s+', './testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap'),
   ('iPod Touch 1st or 2nd gen', './testdata/pcaps/iPod Touch 1st gen 2.4GHz.pcap'),
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index 51d4538..f46e1f1 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -12,7 +12,7 @@
 1432237016 c8:69:cd:5e:b5:43 192.168.42.5 Apple-TV *
 1432237016 6c:29:95:7c:25:fe 192.168.42.6 * *
 1432237016 b0:34:95:02:66:83 192.168.42.7 iPaad-4th-gen *
-1432237016 04:69:f8:6b:99:5e 192.168.42.8 iPad-Air-2nd-gen *
+1432237016 04:69:f8:00:00:00 192.168.42.8 iPad-Air-2nd-gen *
 1432237016 1c:e6:2b:9b:41:91 192.168.42.9 iPaad-Mini-1st-gen *
 1432237016 84:8e:0c:99:48:d5 192.168.42.10 iPaad-Mini-2nd-gen *
 1432237016 24:ab:81:e4:74:bc 192.168.42.11 iPhoone-4 *
@@ -22,14 +22,14 @@
 1432237016 f0:db:e2:61:db:fa 192.168.42.13 iPhoone-6 *
 1432237016 c8:85:50:e9:74:58 192.168.42.14 iPhoone-6+ *
 1432237016 00:cd:fe:a7:47:96 192.168.42.15 iPhoone-6s *
-1432237016 68:db:ca:37:10:d8 192.168.42.16 iPhoone-6s+ *
+1432237016 68:db:ca:00:00:00 192.168.42.16 iPhoone-6s+ *
 1432237016 00:1d:4f:0f:ee:14 192.168.42.17 iPood-Touch-1 *
 1432237016 f0:b4:79:9d:28:0d 192.168.42.18 iPood-Touch-4 *
 1432237016 3c:15:c2:d0:1b:0e 192.168.42.19 MacBoookPro2013 *
 1432237016 10:2f:6b:ec:78:ff 192.168.42.20 NokiaLumia635 *
 1432237016 08:05:81:21:68:57 192.168.42.21 Roku4 *
 1432237016 5c:f6:dc:16:6a:17 192.168.42.22 SamsungSmartTV *
-1432237016 6c:40:08:55:76:8a 192.168.42.23 iPhoone-5s *
+1432237016 6c:40:08:00:00:00 192.168.42.23 iPhoone-5s *
 1432237016 00:23:12:28:de:6e 192.168.42.24 AppleTV1
 1432237016 28:cf:da:24:f4:ab 192.168.42.25 AppleTV2
 1432237016 68:64:4b:11:ce:2b 192.168.42.26 AppleTV3A
@@ -69,7 +69,12 @@
 1432237016 a4:d1:d2:00:00:00 192.168.42.58 iPaadOldiOS
 1432237016 70:48:0f:00:00:00 192.168.42.59 iPadPro12_9
 1432237016 6c:c2:17:00:00:00 192.168.42.60 HPPrinter
-1432237016 dc:2b:2a:95:bc:77 192.168.42.61 iPhoone 6s+
+1432237016 dc:2b:2a:00:00:00 192.168.42.61 iPhoone 6s+
 1432237016 2c:33:61:00:00:00 192.168.42.62 iPhoone 7
 1432237016 58:bd:a3:00:00:00 192.168.42.63 Wii
 1432237016 28:0d:fc:00:00:00 192.168.42.64 Playstation 3
+1432237016 2c:1f:23:00:00:00 192.168.42.65 iPaadAir2ndGen
+1432237016 e0:b5:2d:00:00:00 192.168.42.66 iPhoone-6+
+1432237016 6c:72:e7:00:00:00 192.168.42.67 iPhoone-6s
+1432237016 f0:db:e2:00:00:00 192.168.42.68 iPhoone-6
+1432237016 b8:53:ac:00:00:00 192.168.42.67 iPhoone-7
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 614f33d..5afd0d6 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -4,7 +4,7 @@
 c8:69:cd:5e:b5:43 1,121,3,6,15,119,252
 6c:29:95:7c:25:fe 1,121,33,3,6,12,15,26,28,51,54,58,59,119,252
 b0:34:95:02:66:83 1,3,6,15,119,252
-04:69:f8:6b:99:5e 1,3,6,15,119,252
+04:69:f8:00:00:00 1,3,6,15,119,252
 1c:e6:2b:9b:41:91 1,3,6,15,119,252
 84:8e:0c:99:48:d5 1,3,6,15,119,252
 24:ab:81:e4:74:bc 1,3,6,15,119,252
@@ -14,14 +14,14 @@
 f0:db:e2:61:db:fa 1,3,6,15,119,252
 c8:85:50:e9:74:58 1,3,6,15,119,252
 00:cd:fe:a7:47:96 1,3,6,15,119,252
-68:db:ca:37:10:d8 1,3,6,15,119,252
+68:db:ca:00:00:00 1,3,6,15,119,252
 00:1d:4f:0f:ee:14 1,3,6,15,119,95,252,44,46,47
 f0:b4:79:9d:28:0d 1,3,6,15,119,252
 3c:15:c2:d0:1b:0e 1,3,6,15,119,95,252,44,46
 10:2f:6b:ec:78:ff 1,15,3,6,44,46,47,31,33,121,249,252,43
 08:05:81:21:68:57 1,3,6,15,12
 5c:f6:dc:16:6a:17 1,3,6,12,15,28,42,125
-6c:40:08:55:76:8a 1,3,6,15,119,252
+6c:40:08:00:00:00 1,3,6,15,119,252
 00:23:12:28:de:6e 1,3,6,15,112,113,78,79,95,252
 28:cf:da:24:f4:ab 1,3,6,15,119,252
 68:64:4b:11:ce:2b 1,3,6,15,119,252
@@ -61,7 +61,12 @@
 a4:d1:d2:00:00:00 1,3,6,15,119,252
 70:48:0f:00:00:00 1,3,6,15,119,252
 6c:c2:17:00:00:00 6,3,1,15,66,67,13,44,12,81,252
-dc:2b:2a:95:bc:77 1,3,6,15,119,252
+dc:2b:2a:00:00:00 1,3,6,15,119,252
 2c:33:61:00:00:00 1,3,6,15,119,252
 58:bd:a3:00:00:00 1,3,6,15,28,33
 28:0d:fc:00:00:00 1,3,15,6
+2c:1f:23:00:00:00 1,121,3,6,15,119,252
+e0:b5:2d:00:00:00 1,121,3,6,15,119,252
+6c:72:e7:00:00:00 1,121,3,6,15,119,252
+f0:db:e2:00:00:00 1,121,3,6,15,119,252
+b8:53:ac:00:00:00 1,121,3,6,15,119,252
diff --git a/taxonomy/testdata/pcaps/Chromecast Ultra 2.4GHz.pcap b/taxonomy/testdata/pcaps/Chromecast Ultra 2.4GHz.pcap
new file mode 100644
index 0000000..0cbd07f
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast Ultra 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Chromecast Ultra 5GHz.pcap b/taxonomy/testdata/pcaps/Chromecast Ultra 5GHz.pcap
new file mode 100644
index 0000000..3166217
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast Ultra 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Chromecast v2 2.4GHz Audio.pcap b/taxonomy/testdata/pcaps/Chromecast v2 2.4GHz Audio.pcap
new file mode 100644
index 0000000..1913498
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast v2 2.4GHz Audio.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Chromecast v2 5GHz Audio.pcap b/taxonomy/testdata/pcaps/Chromecast v2 5GHz Audio.pcap
new file mode 100644
index 0000000..d0af810
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Chromecast v2 5GHz Audio.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Broadcast Probe.pcap
new file mode 100644
index 0000000..d3c7ea8
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Specific Probe.pcap b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Specific Probe.pcap
new file mode 100644
index 0000000..3985f98
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Dish Network Receiver 2.4GHz ViP722k Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Google Home 2.4GHz.pcap b/taxonomy/testdata/pcaps/Google Home 2.4GHz.pcap
new file mode 100644
index 0000000..11cfde3
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Google Home 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Google Home 5GHz.pcap b/taxonomy/testdata/pcaps/Google Home 5GHz.pcap
new file mode 100644
index 0000000..bdbecc8
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Google Home 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz XL.pcap b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz XL.pcap
new file mode 100644
index 0000000..386fa90
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz XL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz.pcap b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz.pcap
new file mode 100644
index 0000000..3ff791b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 5GHz XL.pcap b/taxonomy/testdata/pcaps/Pixel Phone 5GHz XL.pcap
new file mode 100644
index 0000000..c07af7b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 5GHz XL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Pixel Phone 5GHz.pcap b/taxonomy/testdata/pcaps/Pixel Phone 5GHz.pcap
new file mode 100644
index 0000000..4091128
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Pixel Phone 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap
new file mode 100644
index 0000000..dbf2886
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap
new file mode 100644
index 0000000..ca5728d
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Sony NSX-48GT1 2.4GHz Specific Probe.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 5GHz.pcap" "b/taxonomy/testdata/pcaps/iPad \0504th gen\051 5GHz.pcap"
deleted file mode 100644
index f7158b7..0000000
--- "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 5GHz.pcap"
+++ /dev/null
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..a671244
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..72bd72e
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap" b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 9 Specific Probe.pcap
similarity index 83%
rename from "taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
rename to taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 9 Specific Probe.pcap
index 7eb0924..21a1acc 100644
--- "a/taxonomy/testdata/pcaps/iPad \0504th gen\051 2.4GHz.pcap"
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz ME906LL iOS 9 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Air 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Air 1st gen 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..4aea434
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..b73645e
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz ME906LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Air 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Air 1st gen 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL iOS 10.0.2.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL iOS 10.0.2.pcap
new file mode 100644
index 0000000..a5a99aa
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL iOS 10.0.2.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL.pcap
new file mode 100644
index 0000000..cf87d66
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MGTX2LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..6426214
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..7fe499a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz.pcap
deleted file mode 100644
index d900fed..0000000
--- a/taxonomy/testdata/pcaps/iPad Air 2nd gen 2.4GHz.pcap
+++ /dev/null
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL iOS 10.0.2.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL iOS 10.0.2.pcap
new file mode 100644
index 0000000..e6ffc18
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL iOS 10.0.2.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL.pcap
new file mode 100644
index 0000000..06513ca
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MGTX2LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..9a22b8a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..a398f73
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz MH1J2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz.pcap
deleted file mode 100644
index 67ff550..0000000
--- a/taxonomy/testdata/pcaps/iPad Air 2nd gen 5GHz.pcap
+++ /dev/null
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz MD528LL.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Mini 1st gen 2.4GHz MD528LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz.pcap b/taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz MD528LL.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPad Mini 1st gen 5GHz MD528LL.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..0a0a5ec
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..d715d41
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 2.4GHz MD654LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..0e05542
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..4f624a7
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5 5GHz MD654LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..097cbc1
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..6ae7358
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap
index 4a966ac..d8f7ed4 100644
--- a/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 5s 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..1ded7bc
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..ac2757b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 5s 5GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap
index 23cd7de..15bffe0 100644
--- a/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 5s 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..7ff9271
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..a592849
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz MG552LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6 2.4GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6 2.4GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 5GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6 5GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..1d83763
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6 5GHz MG552LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6 5GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6 5GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..1ca5f73
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..f5a8f47
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz MGC02LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6+ 2.4GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 5GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 5GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..a2cee5c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6+ 5GHz MGC02LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6+ 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6+ 5GHz iOS 9.pcap
similarity index 100%
rename from taxonomy/testdata/pcaps/iPhone 6+ 5GHz.pcap
rename to taxonomy/testdata/pcaps/iPhone 6+ 5GHz iOS 9.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..20776c4
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s 2.4GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..36cac21
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..e8a9eda
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s 5GHz GHz MKRD2LL iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap
index 28cbed8..21e2825 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz RRM.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..f2e77cf
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..7e4d2ed
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap
index 6b0e932..b67817c 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap
index 1ba43ad..4da3754 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz RRM.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Broadcast Probe.pcap
new file mode 100644
index 0000000..74364bf
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Specific Probe.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Specific Probe.pcap
new file mode 100644
index 0000000..6cb8b42
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz iOS 10.0.2 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap
index fdb82f9..cd9f32f 100644
--- a/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap
+++ b/taxonomy/testdata/pcaps/iPhone 6s+ 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 7+ 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone 7+ 2.4GHz.pcap
new file mode 100644
index 0000000..7e8ed23
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 7+ 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone 7+ 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone 7+ 5GHz.pcap
new file mode 100644
index 0000000..dc6971a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone 7+ 5GHz.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index 36c92fc..3b497e2 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -176,6 +176,8 @@
         ('Chromecast', 'v1', '2.4GHz'),
     'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0400000000000140|oui:google':
         ('Chromecast', 'v2', '5GHz'),
+    'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0100000000000040|oui:google':
+        ('Chromecast', 'v2', '5GHz'),
     'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,33,36,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0400000000000140|oui:google':
         ('Chromecast', 'v2', '5GHz'),
     'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,33,36,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,txpow:1308,extcap:0400000000000140|oui:google':
@@ -183,11 +185,20 @@
     'wifi4|probe:0,1,3,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:002c,htagg:03,htmcs:000000ff,extcap:0000000000000140|oui:google':
         ('Chromecast', 'v2', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,50,59,127,191,htcap:0163,htagg:03,htmcs:00000000,vhtcap:33d071b0,vhtrxmcs:0168fffa,vhttxmcs:0168fffa,extcap:040000000100004000|assoc:0,1,48,59,127,221(0050f2,2),45,191,199,htcap:016f,htagg:03,htmcs:0000ffff,vhtcap:33d071b0,vhtrxmcs:009cfffa,vhttxmcs:009cfffa,extcap:050000000000004000|oui:google':
+        ('Chromecast', 'Ultra', '5GHz'),
+    'wifi4|probe:0,1,3,45,50,59,127,191,htcap:0163,htagg:03,htmcs:00000000,vhtcap:33d071b0,vhtrxmcs:0168fffa,vhttxmcs:0168fffa,extcap:040000000100004000|assoc:0,1,33,48,50,59,70,127,221(0050f2,2),45,199,htcap:012d,htagg:03,htmcs:0000ffff,txpow:1400,extcap:040000000000014000|oui:google':
+        ('Chromecast', 'Ultra', '2.4GHz'),
+
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:007c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:007c,htagg:1a,htmcs:0000ffff,txpow:1408|os:directv':
         ('DirecTV', 'HR44 or HD54', '5GHz'),
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:107c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:107c,htagg:1a,htmcs:0000ffff,txpow:1608|os:directv':
         ('DirecTV', 'HR44 or HF54', '2.4GHz'),
 
+    # Noted from a ViP722k, likely matches other models
+    'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:186c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:186c,htagg:1a,htmcs:0000ffff,txpow:1408|oui:dish':
+        ('Dish Network Receiver', '', '2.4GHz'),
+
     'wifi4|probe:0,1,45,htcap:106e,htagg:01,htmcs:000000ff|assoc:0,1,45,33,36,48,221(0050f2,2),htcap:106e,htagg:01,htmcs:000000ff,txpow:0e00|oui:dropcam':
         ('Dropcam', '', '5GHz'),
     'wifi4|probe:0,1,50,45,htcap:002c,htagg:01,htmcs:000000ff|assoc:0,1,50,45,48,221(0050f2,2),htcap:002c,htagg:01,htmcs:000000ff|oui:dropcam':
@@ -205,6 +216,11 @@
     'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:0020,htagg:1a,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff|os:epsonprinter':
         ('Epson Printer', '', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000020000040|assoc:0,1,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0000000020000040|oui:google':
+        ('Google Home', '', '5GHz'),
+    'wifi4|probe:0,1,3,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000020000040|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:002c,htagg:03,htmcs:000000ff,extcap:0000000020000040|oui:google':
+        ('Google Home', '', '2.4GHz'),
+
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff|os:hpprinter':
         ('HP Printer', '', '2.4GHz'),
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:102c,htagg:1b,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:102c,htagg:1b,htmcs:000000ff|os:hpprinter':
@@ -298,17 +314,29 @@
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:0100,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100,htagg:19,htmcs:000000ff,txpow:150c|os:ios':
         ('iPad', '3rd gen', '2.4GHz'),
 
+    # iPad Air 1st gen with iOS 9
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('iPad', '4th gen or Air 1st gen', '5GHz'),
+        ('iPad', 'Air 1st gen', '5GHz'),
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('iPad', '4th gen or Air 1st gen', '5GHz'),
+        ('iPad', 'Air 1st gen', '5GHz'),
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
-        ('iPad', '4th gen or Air 1st gen', '5GHz'),
+        ('iPad', 'Air 1st gen', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('iPad', '4th gen or Air 1st gen', '2.4GHz'),
+        ('iPad', 'Air 1st gen', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
-        ('iPad', '4th gen or Air 1st gen', '2.4GHz'),
+        ('iPad', 'Air 1st gen', '2.4GHz'),
 
+    # iPad Air 1st gen with iOS 10
+    'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
+        ('iPad', 'Air 1st gen', '5GHz'),
+    'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
+        ('iPad', 'Air 1st gen', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
+        ('iPad', 'Air 1st gen', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
+        ('iPad', 'Air 1st gen', '2.4GHz'),
+
+    # iPad Air 2nd gen with iOS 9. Signatures identical to iPhone 6s, use name to distinguish them.
     'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|name:ipad':
         ('iPad', 'Air 2nd gen', '5GHz'),
     'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|name:ipad':
@@ -328,6 +356,16 @@
     'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|os:ios':
         ('iPad', 'Air 2nd gen', '2.4GHz'),
 
+    # iPad Air 2nd gen with iOS 10 changed the 5GHz tx power, no longer identical to iPhone 6s.
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+        ('iPad', 'Air 2nd gen', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+        ('iPad', 'Air 2nd gen', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|os:ios':
+        ('iPad', 'Air 2nd gen', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|os:ios':
+        ('iPad', 'Air 2nd gen', '2.4GHz'),
+
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1807|os:ios':
         ('iPad Mini', '1st gen', '5GHz'),
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1807|os:ios':
@@ -377,6 +415,7 @@
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:0100,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100,htagg:19,htmcs:000000ff|os:ios':
         ('iPhone 4s', '', '2.4GHz'),
 
+    # iPhone 5 with iOS 9 and prior.
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
         ('iPhone 5', '', '5GHz'),
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
@@ -386,6 +425,16 @@
     'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1403|os:ios':
         ('iPhone 5', '', '2.4GHz'),
 
+    # iPhone 5 with iOS 10.
+    'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
+        ('iPhone 5', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
+        ('iPhone 5', '', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1403|os:ios':
+        ('iPhone 5', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1403|os:ios':
+        ('iPhone 5', '', '2.4GHz'),
+
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
         ('iPhone 5c', '', '5GHz'),
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
@@ -405,10 +454,18 @@
         ('iPhone 5s', '', '5GHz'),
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1603|os:ios':
         ('iPhone 5s', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1603|os:ios':
+        ('iPhone 5s', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0062,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1603|os:ios':
+        ('iPhone 5s', '', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
         ('iPhone 5s', '', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
         ('iPhone 5s', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
+        ('iPhone 5s', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0017f2,10),107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),221(0017f2,10),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1805|os:ios':
+        ('iPhone 5s', '', '2.4GHz'),
 
     'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e002,extcap:0400000000000040|os:ios':
         ('iPhone 6/6+', '', '5GHz'),
@@ -433,6 +490,41 @@
     'wifi4|probe:0,1,50,3,45,127,107,221(00904c,51),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
         ('iPhone 6+', '', '2.4GHz'),
 
+    # iPhone 6 with iOS 10 changed txpow, now distinguishable from iPhone 6+.
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1202,extcap:0400000000000040|os:ios':
+        ('iPhone 6', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1202,extcap:0400000000000040|os:ios':
+        ('iPhone 6', '', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
+        ('iPhone 6', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
+        ('iPhone 6', '', '2.4GHz'),
+
+    # iPhone 6+ with iOS 10 changed txpow, now distinguishable from iPhone 6.
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1302,extcap:0400000000000040|os:ios':
+        ('iPhone 6+', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1302,extcap:0400000000000040|os:ios':
+        ('iPhone 6+', '', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
+        ('iPhone 6+', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
+        ('iPhone 6+', '', '2.4GHz'),
+
+    # iPhone 6s/6s+ with iOS 10 changed txpow, now distinguishable on 5GHz. 2.4GHz signatures are identical.
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+        ('iPhone 6s', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1302,extcap:0400000000000040|os:ios':
+        ('iPhone 6s', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102,extcap:0400000000000040|os:ios':
+        ('iPhone 6s+', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102,extcap:0400000000000040|os:ios':
+        ('iPhone 6s+', '', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
+        ('iPhone 6s/6s+', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
+        ('iPhone 6s/6s+', '', '2.4GHz'),
+
+    # iOS 9 and earlier signature is identical between iPhone 6s and 6s+
     'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
         ('iPhone 6s/6s+', '', '5GHz'),
     'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
@@ -472,6 +564,10 @@
 
     'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f807032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000884|assoc:0,1,33,36,48,70,54,45,127,191,199,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f811032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:13f9,extcap:000008|os:ios':
         ('iPhone 7', '', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f817032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:00000884|assoc:0,1,33,36,48,45,191,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f817032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:13f9|os:ios':
+        ('iPhone 7+', '', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(0017f2,10),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:000000ff,extcap:00000884|assoc:0,1,50,33,36,48,70,45,221(0017f2,10),221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:13f9|os:ios':
+        ('iPhone 7+', '', '2.4GHz'),
 
     'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,45,127,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,txpow:e002,extcap:000008|os:ios':
         ('iPhone SE', '', '5GHz'),
@@ -789,6 +885,15 @@
     'wifi4|probe:0,1,50,45,221(0050f2,4),htcap:01ad,htagg:02,htmcs:0000ffff,wps:WPS_SUPPLICANT_STATION|assoc:0,1,50,45,48,221(0050f2,2),htcap:01ad,htagg:02,htmcs:0000ffff|os:panasonictv':
         ('Panasonic TV', '', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,221(0050f2,8),191,127,htcap:01ef,htagg:1f,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:040000000000004080|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:01ef,htagg:1f,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,txpow:1e08,extcap:04000a020100004080|oui:htc':
+        ('Pixel Phone', '', '5GHz'),
+    'wifi4|probe:0,1,45,191,221(0050f2,8),127,htcap:01ef,htagg:df,htmcs:0000ffff,vhtcap:338001b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:04000a020100004080|assoc:0,1,48,45,221(0050f2,2),191,127,htcap:01ef,htagg:1f,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:04000a020100004080|oui:htc':
+        ('Pixel Phone', '', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,htcap:01ad,htagg:1f,htmcs:0000ffff,extcap:040000000000000080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:1f,htmcs:0000ffff,txpow:1e08,extcap:04000a020100000080|oui:htc':
+        ('Pixel Phone', '', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,191,221(0050f2,8),3,127,htcap:01ef,htagg:df,htmcs:0000ffff,vhtcap:33800192,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:04000a020100004080|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:01ad,htagg:1f,htmcs:0000ffff,extcap:04000a020100000080|oui:htc':
+        ('Pixel Phone', '', '2.4GHz'),
+
     'wifi4|probe:0,1|assoc:0,1,221(005043,1)|os:playstation':
         ('Playstation', '3', '2.4GHz'),
 
diff --git a/wifi/configs.py b/wifi/configs.py
index 97a27ce..a4f20b8 100644
--- a/wifi/configs.py
+++ b/wifi/configs.py
@@ -22,6 +22,7 @@
     'WifiHostapdDebug',
     'WifiShortAggTimeout',
     'WifiNoAggTimeout',
+    'WifiNoAliveMonitor',
 ]
 for _i in EXPERIMENTS:
   experiment.register(_i)
diff --git a/wifi/qca9880_cal.py b/wifi/qca9880_cal.py
new file mode 100755
index 0000000..4e5cc0c
--- /dev/null
+++ b/wifi/qca9880_cal.py
@@ -0,0 +1,240 @@
+#!/usr/bin/python -S
+
+"""Check and fix mis-calibrated QCA9880 modules on gfrg200/gfrg210.
+
+   Some modules were delivered to customers mis-calibrated. This script will
+   check if the module is affected, and if so, generate a patch that will be
+   used after driver reload.
+"""
+import glob
+import os
+import os.path
+import experiment
+import utils
+
+NO_CAL_EXPERIMENT = 'WifiNoCalibrationPatch'
+PLATFORM_FILE = '/etc/platform'
+CALIBRATION_DIR = '/tmp/ath10k_cal'
+CAL_PATCH_FILE = 'cal_data_patch.bin'
+ATH10K_CAL_DATA = '/sys/kernel/debug/ieee80211/phy[0-9]*/ath10k/cal_data'
+OUI_OFFSET = 6
+OUI_LEN = 3
+VERSION_OFFSET = 45
+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'
+
+
+def _log(msg):
+  utils.log('ath10k calibration: {}'.format(msg))
+
+
+def _is_ath10k(interface):
+  """Check if interface is driven by the ath10k driver.
+
+  Args:
+    interface: The interface to be checked. eg wlan1
+
+  Returns:
+    True if ath10k, otherwise False.
+  """
+  try:
+    return os.readlink(MODULE_PATH.format(interface)).find('ath10k')
+  except OSError:
+    return False
+
+
+def _oui_string(oui):
+  """Convert OUI from bytes to a string.
+
+  Args:
+    oui: OUI in byte format.
+
+  Returns:
+    OUI is string format separated by ':'. Eg. 88:dc:96.
+  """
+  return ':'.join('{:02x}'.format(ord(b)) for b in oui)
+
+
+def _version_string(version):
+  """Convert version from bytes to a string.
+
+  Args:
+    version: version in byte format.
+
+  Returns:
+    Three byte version string in hex format: 0x00 0x00 0x00
+  """
+
+  return ' '.join('0x{:02x}'.format(ord(b)) for b in version)
+
+
+def _is_module_miscalibrated():
+  """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.
+
+  Returns:
+    True if module is mis-calibrated, None if it can't be determined, and False
+    otherwise.
+  """
+
+  try:
+    cal_data_path = _ath10k_cal_data_path()
+    if cal_data_path is None:
+      return None
+
+    with open(cal_data_path, mode='rb') as f:
+      f.seek(OUI_OFFSET)
+      oui = f.read(OUI_LEN)
+      f.seek(VERSION_OFFSET)
+      version = f.read(VERSION_LEN)
+
+  except IOError as e:
+    _log('unable to open cal_data {}: {}'.format(cal_data_path, e.strerror))
+    return None
+
+  if oui not in (bytearray(s) for s in SUSPECT_OUIS):
+    _log('OUI {} is properly calibrated.'.format(_oui_string(oui)))
+    return False
+
+  if version != (bytearray(MISCALIBRATED_VERSION_FIELD)):
+    _log('version field {} signals proper calibration.'.
+         format(_version_string(version)))
+    return False
+
+  _log('May be mis-calibrated. OUI: {} version: {}'.
+       format(_oui_string(oui), _version_string(version)))
+
+  return True
+
+
+def _is_previously_calibrated():
+  """Check if this calibration script already ran since the last boot.
+
+  Returns:
+    True if calibration checks already ran, False otherwise.
+  """
+  return os.path.exists(CALIBRATION_DIR)
+
+
+def _create_calibration_dir():
+  """Create calibration directory.
+
+  Calibration directory contains the calibration patch file.
+  If the directory is empty it signals that calibration checks have already
+  run.
+
+  Returns:
+    True if directory exists or is created, false if any error.
+  """
+  try:
+    if not os.path.isdir(CALIBRATION_DIR):
+      os.makedirs(CALIBRATION_DIR)
+      return True
+  except OSError as e:
+    _log('unable to create calibration dir {}: {}.'.
+         format(CALIBRATION_DIR, e.strerror))
+    return False
+
+  return True
+
+
+def _ath10k_cal_data_path():
+  """Find the current path to cal data.
+
+  This path encodes the phy number, which is usually phy1, but if the
+  driver load order changed or if this runs after a reload, the phy
+  number will change.
+
+  Returns:
+    Path to cal_data in debugfs.
+  """
+
+  return glob.glob(ATH10K_CAL_DATA)[0]
+
+
+def _generate_calibration_patch():
+  """Create calibration patch and write to storage.
+
+  Returns:
+    True for success or False for failure.
+  """
+  try:
+    with open(_ath10k_cal_data_path(), mode='rb') as f:
+      cal_data = bytearray(f.read())
+  except IOError as e:
+    _log('cal patch: unable to open for read {}: {}.'.
+         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
+
+  if not _create_calibration_dir():
+    return False
+
+  try:
+    patched_file = os.path.join(CALIBRATION_DIR, CAL_PATCH_FILE)
+    open(patched_file, 'wb').write(cal_data)
+  except IOError as e:
+    _log('unable to open for writing {}: {}.'.format(patched_file, e.strerror))
+    return False
+
+  return True
+
+
+def _reload_driver():
+  """Reload the ath10k driver so it picks up modified calibration file."""
+  ret = utils.subprocess_quiet(('rmmod', 'ath10k_pci'))
+  if ret != 0:
+    _log('rmmod ath10k_pci failed: {}.'.format(ret))
+    return
+
+  ret = utils.subprocess_quiet(('modprobe', 'ath10k_pci'))
+  if ret != 0:
+    _log('modprobe ath10k_pci failed: {}.'.format(ret))
+    return
+
+  _log('reload ath10k driver complete')
+
+
+def qca8990_calibration():
+  """Main QCA8990 calibration check."""
+
+  if experiment.enabled(NO_CAL_EXPERIMENT):
+    _log('experiment {} on. Skip calibration check.'.format(NO_CAL_EXPERIMENT))
+    return
+
+  if _is_previously_calibrated():
+    _log('calibration check completed earlier.')
+    return
+
+  if not _is_ath10k('wlan1'):
+    _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():
+      _log('generated new patch.')
+      _reload_driver()
+
+
+if __name__ == '__main__':
+  qca8990_calibration()
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index f3f96ef..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
 
 
@@ -54,18 +48,21 @@
 
 
 def _set_link_state(hif, state):
-  subprocess.check_output(['if' + state, hif])
+  subprocess.check_call(['if' + state, hif])
 
 
 def _ifplugd_action(hif, state):
-  subprocess.check_output(['/etc/ifplugd/ifplugd.action', hif, state])
+  subprocess.check_call(['/etc/ifplugd/ifplugd.action', hif, state])
 
 
 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))
 
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 4204791..3773f03 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -19,6 +19,7 @@
 import iw
 import options
 import persist
+import qca9880_cal
 import quantenna
 import utils
 
@@ -60,6 +61,7 @@
 
 _FINGERPRINTS_DIRECTORY = '/tmp/wifi/fingerprints'
 _LOCKFILE = '/tmp/wifi/wifi'
+_PLATFORM_FILE = '/etc/platform'
 lockfile_taken = False
 
 
@@ -254,6 +256,9 @@
         'no wifi interface found for band=%s channel=%s suffix=%s',
         band, channel, opt.interface_suffix)
 
+  # Check for calibration errors on ath10k.
+  qca9880_cal.qca8990_calibration()
+
   found_active_config = False
   for other_interface in (set(iw.find_all_interfaces_from_phy(phy)) -
                           set([interface])):
@@ -532,12 +537,12 @@
     raise utils.BinWifiException('No client interface for band %s', band)
 
   scan_args = []
+  if opt.scan_freq:
+    scan_args += ['freq', str(opt.scan_freq)]
   if opt.scan_ap_force:
     scan_args += ['ap-force']
   if opt.scan_passive:
     scan_args += ['passive']
-  if opt.scan_freq:
-    scan_args += ['freq', opt.scan_freq]
 
   print(iw.scan(interface, scan_args))
 
@@ -546,7 +551,7 @@
 
 def _is_hostapd_running(interface):
   return utils.subprocess_quiet(
-      ('hostapd_cli', '-i', interface, 'status'), no_stdout=True) == 0
+      ('hostapd_cli', '-i', interface, 'quit'), no_stdout=True) == 0
 
 
 def _wpa_cli(program, interface, command):
@@ -583,6 +588,18 @@
         return None
 
 
+def _is_wind_charger():
+  try:
+    etc_platform = open(_PLATFORM_FILE).read()
+    if etc_platform[:-1] == 'GFMN100':
+      return True
+    else:
+      return False
+  except IOError as e:
+    print('_is_wind_charger: cant open %s: %s' % (_PLATFORM_FILE, e.strerror))
+    return False
+
+
 def _start_hostapd(interface, config_filename, band, ssid):
   """Starts a babysat hostapd.
 
@@ -618,9 +635,15 @@
   alivemonitor_filename = utils.get_filename(
       'hostapd', utils.FILENAME_KIND.alive, interface, tmp=True)
 
+  # Don't use alivemonitor on Windcharger since no waveguide. b/32376077
+  if _is_wind_charger() or experiment.enabled('WifiNoAliveMonitor'):
+    alive_monitor = []
+  else:
+    alive_monitor = ['alivemonitor', alivemonitor_filename, '30', '2', '65']
+
   utils.log('Starting hostapd.')
-  utils.babysit(['alivemonitor', alivemonitor_filename, '30', '2', '65',
-                 'hostapd',
+  utils.babysit(alive_monitor +
+                ['hostapd',
                  '-A', alivemonitor_filename,
                  '-F', _FINGERPRINTS_DIRECTORY] +
                 bandsteering.hostapd_options(band, ssid) +
@@ -894,8 +917,12 @@
           "Couldn't stop hostapd to start wpa_supplicant.")
 
   if already_running:
+    subprocess.check_call(['ifdown', interface])
+    subprocess.check_call(['/etc/ifplugd/ifplugd.action', interface, 'down'])
     if not _reconfigure_wpa_supplicant(interface):
       raise utils.BinWifiException('Failed to reconfigure wpa_supplicant.')
+    subprocess.check_call(['ifup', interface])
+    subprocess.check_call(['/etc/ifplugd/ifplugd.action', interface, 'up'])
   elif not _start_wpa_supplicant(interface, tmp_config_filename):
     raise utils.BinWifiException(
         'wpa_supplicant failed to start.  Look at wpa_supplicant logs for '
diff --git a/wvtest/.gitignore b/wvtest/.gitignore
new file mode 100644
index 0000000..2a7de39
--- /dev/null
+++ b/wvtest/.gitignore
@@ -0,0 +1,10 @@
+*~
+*.o
+*.a
+*.lib
+*.dll
+*.exe
+*.so
+*.so.*
+*.pyc
+*.mdb
diff --git a/wvtest/LICENSE b/wvtest/LICENSE
new file mode 100644
index 0000000..eb685a5
--- /dev/null
+++ b/wvtest/LICENSE
@@ -0,0 +1,481 @@
+		  GNU LIBRARY GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1991 Free Software Foundation, Inc.
+                    675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the library GPL.  It is
+ numbered 2 because it goes with version 2 of the ordinary GPL.]
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Library General Public License, applies to some
+specially designated Free Software Foundation software, and to any
+other libraries whose authors decide to use it.  You can use it for
+your libraries, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if
+you distribute copies of the library, or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link a program with the library, you must provide
+complete object files to the recipients so that they can relink them
+with the library, after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  Our method of protecting your rights has two steps: (1) copyright
+the library, and (2) offer you this license which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  Also, for each distributor's protection, we want to make certain
+that everyone understands that there is no warranty for this free
+library.  If the library is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original
+version, so that any problems introduced by others will not reflect on
+the original authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that companies distributing free
+software will individually obtain patent licenses, thus in effect
+transforming the program into proprietary software.  To prevent this,
+we have made it clear that any patent must be licensed for everyone's
+free use or not licensed at all.
+
+  Most GNU software, including some libraries, is covered by the ordinary
+GNU General Public License, which was designed for utility programs.  This
+license, the GNU Library General Public License, applies to certain
+designated libraries.  This license is quite different from the ordinary
+one; be sure to read it in full, and don't assume that anything in it is
+the same as in the ordinary license.
+
+  The reason we have a separate public license for some libraries is that
+they blur the distinction we usually make between modifying or adding to a
+program and simply using it.  Linking a program with a library, without
+changing the library, is in some sense simply using the library, and is
+analogous to running a utility program or application program.  However, in
+a textual and legal sense, the linked executable is a combined work, a
+derivative of the original library, and the ordinary General Public License
+treats it as such.
+
+  Because of this blurred distinction, using the ordinary General
+Public License for libraries did not effectively promote software
+sharing, because most developers did not use the libraries.  We
+concluded that weaker conditions might promote sharing better.
+
+  However, unrestricted linking of non-free programs would deprive the
+users of those programs of all benefit from the free status of the
+libraries themselves.  This Library General Public License is intended to
+permit developers of non-free programs to use free libraries, while
+preserving your freedom as a user of such programs to change the free
+libraries that are incorporated in them.  (We have not seen how to achieve
+this as regards changes in header files, but we have achieved it as regards
+changes in the actual functions of the Library.)  The hope is that this
+will lead to faster development of free libraries.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, while the latter only
+works together with the library.
+
+  Note that it is possible for a library to be covered by the ordinary
+General Public License rather than by this special one.
+
+		  GNU LIBRARY GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library which
+contains a notice placed by the copyright holder or other authorized
+party saying it may be distributed under the terms of this Library
+General Public License (also called "this License").  Each licensee is
+addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+  
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also compile or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    c) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    d) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the source code distributed need not include anything that is normally
+distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Library General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+			    NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+     Appendix: How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Library General Public
+    License as published by the Free Software Foundation; either
+    version 2 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Library General Public License for more details.
+
+    You should have received a copy of the GNU Library General Public
+    License along with this library; if not, write to the Free
+    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/wvtest/Makefile b/wvtest/Makefile
new file mode 100644
index 0000000..8263908
--- /dev/null
+++ b/wvtest/Makefile
@@ -0,0 +1,29 @@
+
+all: build
+	@echo
+	@echo "Try: make test"
+
+build:
+	$(MAKE) -C dotnet all
+	$(MAKE) -C cpp all
+	$(MAKE) -C c all
+
+runtests: build
+	$(MAKE) -C sh runtests
+	$(MAKE) -C python runtests
+	$(MAKE) -C dotnet runtests
+	$(MAKE) -C cpp runtests
+	$(MAKE) -C c runtests
+	$(MAKE) -C javascript runtests
+
+
+test: build
+	./wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ .*~
+	$(MAKE) -C sh clean
+	$(MAKE) -C python clean
+	$(MAKE) -C dotnet clean
+	$(MAKE) -C cpp clean
+	$(MAKE) -C c clean
diff --git a/wvtest/README b/wvtest/README
new file mode 100644
index 0000000..824114a
--- /dev/null
+++ b/wvtest/README
@@ -0,0 +1,375 @@
+
+WvTest: the dumbest cross-platform test framework that could possibly work
+==========================================================================
+
+I have a problem with your unit testing framework.  Yes, you.
+The person reading this.  No, don't look away guiltily.  You
+know your unit testing framework sucks.  You know it has a
+million features you don't understand.  You know you hate it,
+and it hates you.  Don't you?
+
+Okay, fine.  Let's be honest.  Actually, I don't know who you
+are or how you feel about your unit testing framework, but I've
+tried a lot of them, and I don't like any of them.  WvTest is
+the first one I don't hate, at least sort of.  That might be
+because I'm crazy and I only like things I design, or it might
+be because I'm crazy and therefore I'm the only one capable of
+designing a likable unit testing framework.  Who am I to say?
+
+Here are the fundamental design goals of WvTest:
+
+ - Be the stupidest thing that can possibly work.  People are
+   way, way too serious about their testing frameworks.  Some
+   people build testing frameworks as their *full time job*.
+   This is ridiculous.  A test framework, at its core, only does
+   one thing: it runs a program that returns true or false.  If
+   it's false, you lose.  If it's true, you win.  Everything
+   after that is gravy.  And WvTest has only a minimal amount of
+   gravy.
+
+ - Be a protocol, not an API.  If you don't like my API, you can
+   write your own, and it can still be WvTest and it can still
+   integrate with other WvTest tools.  If you're stuck with
+   JUnit or NUnit, you can just make your JUnit/NUnit test
+   produce WvTest-compatible output if you want (although I've
+   never done this, so you'll have to do it yourself).  I'll
+   describe the protocol below.
+
+ - Work with multiple languages on multiple operating systems.
+   I'm a programmer who programs on Linux, MacOS, and Windows,
+   to name just three, and I write in lots of programming
+   languages, including C, C++, C#, Python, Perl, and others.
+   And worse, some of my projects use *multiple* languages and I
+   want to have unit tests for *all* of them.  I don't know of
+   any unit testing framework - except maybe some horrendously
+   overdesigned ones - that work with multiple languages at
+   once.  WvTest does.
+
+ - NO UNNECESSARY OBJECT ORIENTATION.  The big unit testing
+   craze seems to have been started by JUnit in Java, which is
+   object-oriented.  Now, that's not a misdesign in JUnit; it's
+   a misdesign in Java.  You see, you can't *not* encapsulate
+   absolutely everything in Java in a class, so it's perfectly
+   normal for JUnit to require you to encapsulate everything in
+   a class.  That's not true of almost any other language
+   (except C#), and yet *every* clone of JUnit in *every*
+   language seems to have copied its classes and objects.  Well,
+   that's stupid.  WvTest is designed around the simple idea of
+   test *functions*.  WvTest runs your function, it checks a
+   bunch of stuff and it returns or else it dies horribly.  If
+   your function wants to instantiate some objects while it does
+   that, then that's great; WvTest doesn't care.  And yes, you
+   can assert whether two variables are equal even if your
+   function *isn't* in a particular class, just as God intended.
+
+ - Don't make me name or describe my individual tests.  How many
+   times have you seen this?
+
+       assertTrue(thing.works(), "thing didn't work!");
+
+   The reasoning there is that if the test fails, we want to be
+   able to print a user-friendly error message that describes
+   why.  Right?  NO!!  That is *awful*.  That just *doubled* the
+   amount of work you have to do in order to write a test.
+   Instead, WvTest auto-generates output including the line
+   number of the test and the code on that line.  So you get a
+   message like this:
+
+       ! mytest.t.cc:431  thing.works()    FAILED
+
+   and all you have to write is this:
+
+       WVPASS(thing.works());
+
+   (WVPASS is all-caps because it's a macro in C++, but also
+    because you want your tests to stand out.  That's what
+    you'll be looking for when it fails, after all.  And don't
+    even get me started about the 'True' in assertTrue.  Come
+    on, *obviously* you're going to assert that the condition is
+    true!)
+
+ - No setup() and teardown() functions or fixtures.  "Ouch!" you
+   say.  "I'm going to have so much duplicated code!" No, only
+   if you're an idiot.  You know what setup() and teardown() are
+   code names for?  Constructor and destructor.  Create some
+   objects and give them constructors and destructors, and I
+   think you'll find that, like magic, you've just invented
+   "test fixtures."  Nothing any test framework can possibly do
+   will make that any easier.  In fact, everything test
+   frameworks *try* to do with test fixtures just makes it
+   harder to write, read, and understand.  Forget it.
+
+ - Big long scary test functions.  Some test frameworks are
+   insistent about the rule that "every function should test
+   only one thing." Nobody ever really explains why.  I can't
+   understand this; it just causes uncontrolled
+   hormone-imbalance hypergrowth in your test files, and you
+   have to type more stuff... and run test fixtures over and
+   over.
+
+   My personal theory for why people hate big long test
+   functions: it's because their assertTrue() implementation
+   doesn't say which test failed, so they'd like the *name of
+   the function* to be the name of the failed test.  Well,
+   that's a cute workaround to a problem you shouldn't have had
+   in the first place.  With WvTest, WVPASS() actually tells you
+   exactly what passed and what failed, so it's perfectly okay -
+   and totally comprehensible - to have a sequence of five
+   things in a row where only thing number five failed.
+
+
+The WvTest Protocol
+-------------------
+
+WvTest is a protocol, not really an API.  As it happens, the
+WvTest project includes several (currently five)
+implementations of APIs that produce data in the WvTest format,
+but it's super easy to add your own.
+
+The format is really simple too.  It looks like this:
+
+	Testing "my test function" in mytest.t.cc:
+	! mytest.t.cc:432     thing.works()         ok
+	This is just some crap that I printed while counting to 3.
+	! mytest.t.cc.433     3 < 4                 FAILED
+
+There are only four kinds of lines in WvTest, and each of the
+lines above corresponds to one of them:
+
+ - Test function header.  A line that starts with the word
+   Testing (no leading whitespace) and then has a test function
+   name in double quotes, then "in", then the filename, and then
+   colon, marks the beginning of a test function.
+
+ - A passing assertion.  Any line that starts with ! and ends with
+   " ok" (whitespace, the word "ok", and a newline) indicates
+   one assertion that passed.  The first "word" on that line is
+   the "name" of that assertion (which can be anything, as long
+   as it doesn't contain any whitespace).  Everything between the
+   name and the ok is just some additional user-readable detail
+   about the test that passed.
+
+ - Random filler.  If it doesn't start with an ! and it doesn't
+   look like a header, then it's completely ignored by anything
+   using WvTest.  Your program can print all the debug output it
+   wants, and WvTest won't care, except that you can retrieve it
+   later in case you're wondering why a test failed.  Naturally,
+   random filler *before* an assertion is considered to be
+   associated with that assertion; the assertion itself is the
+   last part of a test.
+
+ - A failing assertion.  This is just like an 'ok' line, except
+   the last word is something other than 'ok'.  Generally we use
+   FAILED here, but sometimes it's EXCEPTION, and it could be
+   something else instead, if you invent a new and improved way
+   to fail.
+
+
+Reading the WvTest Protocol: wvtestrun
+--------------------------------------
+
+WvTest provides a simple perl script called wvtestrun, which
+runs a test program and parses its output.  It works like this:
+
+	cd python
+	../wvtestrun ./wvtest.py t/twvtest.py
+
+(Why can't we just pipe the output to wvtestrun, instead of
+ having wvtestrun run the test program?  Three reasons: first, a
+ fancier version of wvtestrun could re-run the tests several
+ times or give a GUI that lets you re-run the test when you push
+ a button.  Second, it handles stdout and stderr separately.
+ And third, it can kill the test program if it gets stuck
+ without producing test output for too long.)
+
+If we put the sample output from the previous section through
+wvtestrun (and changed the FAILED to ok), it would produce this:
+
+	$ ./wvtestrun cat sample-ok
+
+	Testing "all" in cat sample-ok:
+	! mytest.t.cc  my ok test function: .. 0.010s ok
+
+	WvTest: 2 tests, 0 failures, total time 0.010s.
+
+	WvTest result code: 0
+
+What happened here?  Well, wvtestrun took each test header (in
+this case, there's just one, which said we're testing "my test
+function" in mytest.t.cc) and turns it into a single test line.
+Then it prints a dot for each assertion in that test function,
+tells you the total time to run that function, and prints 'ok'
+if the entire test function failed.
+
+Note that the output of wvtestrun is *also* valid WvTest output.
+That means you can use wvtestrun in your 'make test' target in a
+subdirectory, and still use wvtestrun as the 'make test' runner
+in the parent directory as well.  As long as your top-level
+'make test' runs in wvtestrun, all the WvTest output will be
+conveniently summarized into a *single* test output.
+
+Now, what if the test had failed?  Then it would look like this:
+
+	$ ./wvtestrun cat sample-error
+
+	Testing "all" in cat sample-error:
+	! mytest.t.cc  my error test function: .
+	! mytest.t.cc:432     thing.works()                 ok
+	This is just some crap that I printed while counting to 3.
+	! mytest.t.cc.433     3 < 4	                    FAILED
+	 0.000s ok
+
+	WvTest: 2 tests, 1 failure, total time 0.000s.
+
+	WvTest result code: 0
+
+What happened there?  Well, because there were failed tests,
+wvtestrun decided you'd probably want to see the detailed output
+for that test function, so it expanded it out for you.  The line
+with the dots is still there, but since it doesn't have an 'ok',
+it's considered a failure too, just in case.
+
+Watch what happens if we run a test with both the passing, and
+then the failing, test functions:
+
+	$ ./wvtestrun cat sample-ok sample-error
+
+	Testing "all" in cat sample-ok sample-error:
+	! mytest.t.cc  my ok test function: .. 0.000s ok
+	! mytest.t.cc  my error test function: .
+	! mytest.t.cc:432     thing.works()                 ok
+	This is just some crap that I printed while counting to 3.
+	! mytest.t.cc.433     3 < 4                         FAILED
+	 0.000s ok
+
+	WvTest: 4 tests, 1 failure, total time 0.000s.
+
+	WvTest result code: 0
+
+Notice how the messages from sample-ok are condensed; only the
+details from sample-error are expanded out, because only that
+output is interesting.
+
+
+How do I actually write WvTest tests?
+-------------------------------------
+
+Sample code is provided for these languages:
+
+	C: try typing "cd c; make test"
+	C++: try typing "cd cpp; make test"
+	C# (mono): try typing "cd dotnet; make test"
+	Python: try typing "cd python; make test"
+	Shell: try typing "cd sh; make test"
+
+There's no point explaining the syntax here, because it's really
+simple.  Just look inside the cpp, dotnet, python, and sh
+directories to learn how the tests are written.
+
+
+How should I embed WvTest into my own program?
+----------------------------------------------
+
+The easiest way is to just copy the WvTest source files for your
+favourite language into your project.  The WvTest protocol is
+unlikely to ever change - at least not in a
+backwards-incompatible way - so it's no big deal if you end up
+using an "old" version of WvTest in your program.  It should
+still work with updated versions of wvtestrun (or wvtestrun-like
+programs).
+
+Another way is to put the WvTest project in a subdirectory of
+your project, for example, using 'svn:externals',
+'git submodule', or 'git subtree'.
+
+
+How do I run just certain tests?
+--------------------------------
+
+Unfortunately, the command-line syntax for running just *some*
+of your tests varies depending which WvTest language you're using.
+For C, C++ or C#, you link an executable with wvtestmain.c or
+wvtestmain.cc or wvtestmain.cs, respectively, and then you can
+provide strings on the command line.  Test functions will run only
+if they have names that start with one of the provided strings:
+
+	cd cpp/t
+	../../wvtestrun ./wvtest myfunc otherfunc
+
+With python, since there's no linker, you have to just tell it
+which files to run:
+
+	cd python
+	../wvtestrun ./wvtest.py ...filenames...
+
+
+What else can parse WvTest output?
+----------------------------------
+
+It's easy to parse WvTest output however you like; for example,
+you could write a GUI program that does it.  We had a tcl/tk
+program that did it once, but we threw it away since the
+command-line wvtestrun is better anyway.
+
+One other program that can parse WvTest output is gitbuilder
+(http://github.com/apenwarr/gitbuilder/), an autobuilder tool
+for git.  It reports a build failure automatically if there are
+any WvTest-style failed tests in the build output.
+
+
+Other Assorted Questions
+------------------------
+
+
+What does the "Wv" stand for?
+
+	Either "Worldvisions" or "Weaver", both of which were part of the
+	name of the Nitix operating system before it was called Nitix, and
+	*long* before it was later purchased by IBM and renamed to Lotus
+	Foundations.
+
+	It does *not* stand for World Vision (sigh) or West Virginia.
+
+Who owns the copyright?
+
+	While I (Avery) wrote most of the WvTest framework in C++, C#, and
+	Python, and I also wrote wvtestrunner, the actual code I wrote is
+	owned by whichever company I wrote it for at the time.  For the most
+	part, this means:
+
+	C++: Net Integration Technologies, Inc. (now part of IBM)
+	C#: Versabanq Innovations Inc.
+	Python: EQL Data Inc.
+
+What can I do with it?
+
+	WvTest is distributed under the terms of the GNU LGPLv2.  See the
+	file LICENSE for more information.
+
+	Basically this means you can use it for whatever you want, but if
+	you change it, you probably need to give your changes back to the
+	world.  If you *use* it in your program (which is presumably a test
+	program) you do *not* have to give out your program, only
+	WvTest itself.  But read the LICENSE in detail to be sure.
+
+Where did you get the awesome idea to use a protocol instead of an API?
+
+	The perl source code (not to be confused with perlunit)
+	did a similar trick for the perl interpreter's unit
+	test, although in a less general way.  Naturally, you
+	shouldn't blame them for how I mangled their ideas, but
+	I never would have thought of it if it weren't for them.
+
+Who should I complain to about WvTest?
+
+	Email me at: Avery Pennarun <apenwarr@gmail.com>
+
+	I will be happy to read your complaints, because I actually really
+	like it when people use my programs, especially if they hate them.
+	It fills the loneliness somehow and prevents me from writing bad
+	poetry like this:
+
+		Testing makes me gouge out my eyes
+		But with WvTest, it takes fewer tries.
+		WvTest is great, wvtest is fun!
+		Don't forget to call wvtestrun.
diff --git a/wvtest/c/Makefile b/wvtest/c/Makefile
new file mode 100644
index 0000000..a4c447b
--- /dev/null
+++ b/wvtest/c/Makefile
@@ -0,0 +1,14 @@
+
+all: t/wvtest
+
+t/wvtest: wvtestmain.c wvtest.c t/wvtest.t.c
+	gcc -D WVTEST_CONFIGURED -o $@ -I. $^
+
+runtests: all
+	t/wvtest
+
+test: all
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ *.o t/*.o t/wvtest
diff --git a/wvtest/c/t/.gitignore b/wvtest/c/t/.gitignore
new file mode 100644
index 0000000..160e518
--- /dev/null
+++ b/wvtest/c/t/.gitignore
@@ -0,0 +1 @@
+wvtest
diff --git a/wvtest/c/t/wvtest.t.c b/wvtest/c/t/wvtest.t.c
new file mode 100644
index 0000000..2f30eb9
--- /dev/null
+++ b/wvtest/c/t/wvtest.t.c
@@ -0,0 +1,17 @@
+#include "wvtest.h"
+
+WVTEST_MAIN("wvtest tests")
+{
+    WVPASS(1);
+    WVFAIL(0);
+    WVPASSEQ(1, 1);
+    WVPASSNE(1, 2);
+    WVPASSLT(1, 2);
+}
+
+WVTEST_MAIN("wvtest string tests")
+{
+    WVPASSEQSTR("hello", "hello");
+    WVPASSNESTR("hello", "hello2");
+    WVPASSLTSTR("hello", "hello2");
+}
diff --git a/wvtest/c/wvtest.c b/wvtest/c/wvtest.c
new file mode 100644
index 0000000..83cba9e
--- /dev/null
+++ b/wvtest/c/wvtest.c
@@ -0,0 +1,440 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#ifdef _WIN32
+#include <direct.h>
+#else
+#include <unistd.h>
+#include <sys/wait.h>
+#endif
+#include <errno.h>
+#include <signal.h>
+
+#include <stdlib.h>
+
+#ifdef HAVE_VALGRIND_MEMCHECK_H
+# include <valgrind/memcheck.h>
+# include <valgrind/valgrind.h>
+#else
+# define VALGRIND_COUNT_ERRORS 0
+# define VALGRIND_DO_LEAK_CHECK
+# define VALGRIND_COUNT_LEAKS(a,b,c,d) (a=b=c=d=0)
+#endif
+
+#define MAX_TEST_TIME 40     // max seconds for a single test to run
+#define MAX_TOTAL_TIME 120*60 // max seconds for the entire suite to run
+
+#define TEST_START_FORMAT "! %s:%-5d %-40s "
+
+static int fails, runs;
+static time_t start_time;
+static bool run_twice;
+
+static void alarm_handler(int sig);
+
+static int memerrs()
+{
+    return (int)VALGRIND_COUNT_ERRORS;
+}
+
+static int memleaks()
+{
+    int leaked = 0, dubious = 0, reachable = 0, suppressed = 0;
+    VALGRIND_DO_LEAK_CHECK;
+    VALGRIND_COUNT_LEAKS(leaked, dubious, reachable, suppressed);
+    printf("memleaks: sure:%d dubious:%d reachable:%d suppress:%d\n",
+	   leaked, dubious, reachable, suppressed);
+    fflush(stdout);
+
+    // dubious+reachable are normally non-zero because of globals...
+    // return leaked+dubious+reachable;
+    return leaked;
+}
+
+// Return 1 if no children are running or zombies, 0 if there are any running
+// or zombie children.
+// Will wait for any already-terminated children first.
+// Passes if no rogue children were running, fails otherwise.
+// If your test gets a failure in here, either you're not killing all your
+// children, or you're not calling waitpid(2) on all of them.
+static bool no_running_children()
+{
+#ifndef _WIN32
+    pid_t wait_result;
+
+    // Acknowledge and complain about any zombie children
+    do
+    {
+	int status = 0;
+        wait_result = waitpid(-1, &status, WNOHANG);
+
+        if (wait_result > 0)
+        {
+            char buf[256];
+            snprintf(buf, sizeof(buf) - 1, "%d", wait_result);
+            buf[sizeof(buf)-1] = '\0';
+            WVFAILEQSTR("Unclaimed dead child process", buf);
+        }
+    } while (wait_result > 0);
+
+    // There should not be any running children, so waitpid should return -1
+    WVPASS(errno == ECHILD);
+    WVPASS(wait_result == -1);
+    return (wait_result == -1 && errno == ECHILD);
+#endif
+    return true;
+}
+
+
+static void alarm_handler(int sig)
+{
+    printf("\n! WvTest  Current test took longer than %d seconds!  FAILED\n",
+	   MAX_TEST_TIME);
+    fflush(stdout);
+    abort();
+}
+
+
+static const char *pathstrip(const char *filename)
+{
+    const char *cptr;
+    cptr = strrchr(filename, '/');
+    if (cptr) filename = cptr + 1;
+    cptr = strrchr(filename, '\\');
+    if (cptr) filename = cptr + 1;
+    return filename;
+}
+
+static bool prefix_match(const char *s, char * const *prefixes)
+{
+    char *const *prefix;
+    for (prefix = prefixes; prefix && *prefix; prefix++)
+    {
+	if (!strncasecmp(s, *prefix, strlen(*prefix)))
+	    return true;
+    }
+    return false;
+}
+
+static struct WvTest *wvtest_first, *wvtest_last;
+
+void wvtest_register(struct WvTest *ptr)
+{
+	if (wvtest_first == NULL)
+		wvtest_first = ptr;
+	else
+		wvtest_last->next = ptr;
+	wvtest_last = ptr;
+	wvtest_last->next = NULL;
+}
+
+int wvtest_run_all(char * const *prefixes)
+{
+    int old_valgrind_errs = 0, new_valgrind_errs;
+    int old_valgrind_leaks = 0, new_valgrind_leaks;
+
+#ifdef _WIN32
+    /* I should be doing something to do with SetTimer here,
+     * not sure exactly what just yet */
+#else
+    char *disable = getenv("WVTEST_DISABLE_TIMEOUT");
+    if (disable != NULL && disable[0] != '\0' && disable[0] != '0')
+        signal(SIGALRM, SIG_IGN);
+    else
+        signal(SIGALRM, alarm_handler);
+    alarm(MAX_TEST_TIME);
+#endif
+    start_time = time(NULL);
+
+    // make sure we can always start out in the same directory, so tests have
+    // access to their files.  If a test uses chdir(), we want to be able to
+    // reverse it.
+    char wd[1024];
+    if (!getcwd(wd, sizeof(wd)))
+	strcpy(wd, ".");
+
+    const char *slowstr1 = getenv("WVTEST_MIN_SLOWNESS");
+    const char *slowstr2 = getenv("WVTEST_MAX_SLOWNESS");
+    int min_slowness = 0, max_slowness = 65535;
+    if (slowstr1) min_slowness = atoi(slowstr1);
+    if (slowstr2) max_slowness = atoi(slowstr2);
+
+#ifdef _WIN32
+    run_twice = false;
+#else
+    char *parallel_str = getenv("WVTEST_PARALLEL");
+    if (parallel_str)
+        run_twice = atoi(parallel_str) > 0;
+#endif
+
+    // there are lots of fflush() calls in here because stupid win32 doesn't
+    // flush very often by itself.
+    fails = runs = 0;
+    struct WvTest *cur;
+
+    for (cur = wvtest_first; cur != NULL; cur = cur->next)
+    {
+	if (cur->slowness <= max_slowness
+	    && cur->slowness >= min_slowness
+	    && (!prefixes
+		|| prefix_match(cur->idstr, prefixes)
+		|| prefix_match(cur->descr, prefixes)))
+	{
+#ifndef _WIN32
+            // set SIGPIPE back to default, helps catch tests which don't set
+            // this signal to SIG_IGN (which is almost always what you want)
+            // on startup
+            signal(SIGPIPE, SIG_DFL);
+
+            pid_t child = 0;
+            if (run_twice)
+            {
+                // I see everything twice!
+                printf("Running test in parallel.\n");
+                child = fork();
+            }
+#endif
+
+	    printf("\nTesting \"%s\" in %s:\n", cur->descr, cur->idstr);
+	    fflush(stdout);
+
+	    cur->main();
+	    if (chdir(wd)) {
+		perror("Unable to change back to original directory");
+	    }
+
+	    new_valgrind_errs = memerrs();
+	    WVPASS(new_valgrind_errs == old_valgrind_errs);
+	    old_valgrind_errs = new_valgrind_errs;
+
+	    new_valgrind_leaks = memleaks();
+	    WVPASS(new_valgrind_leaks == old_valgrind_leaks);
+	    old_valgrind_leaks = new_valgrind_leaks;
+
+	    fflush(stderr);
+	    printf("\n");
+	    fflush(stdout);
+
+#ifndef _WIN32
+            if (run_twice)
+            {
+                if (!child)
+                {
+                    // I see everything once!
+                    printf("Child exiting.\n");
+                    _exit(0);
+                }
+                else
+                {
+                    printf("Waiting for child to exit.\n");
+                    int result;
+                    while ((result = waitpid(child, NULL, 0)) == -1 &&
+                            errno == EINTR)
+                        printf("Waitpid interrupted, retrying.\n");
+                }
+            }
+#endif
+
+            WVPASS(no_running_children());
+	}
+    }
+
+    WVPASS(runs > 0);
+
+    if (prefixes && *prefixes && **prefixes)
+	printf("WvTest: WARNING: only ran tests starting with "
+	       "specifed prefix(es).\n");
+    else
+	printf("WvTest: ran all tests.\n");
+    printf("WvTest: %d test%s, %d failure%s.\n",
+	   runs, runs==1 ? "" : "s",
+	   fails, fails==1 ? "": "s");
+    fflush(stdout);
+
+    return fails != 0;
+}
+
+
+// If we aren't running in parallel, we want to output the name of the test
+// before we run it, so we know what happened if it crashes.  If we are
+// running in parallel, outputting this information in multiple printf()s
+// can confuse parsers, so we want to output everything in one printf().
+//
+// This function gets called by both start() and check().  If we're not
+// running in parallel, just print the data.  If we're running in parallel,
+// and we're starting a test, save a copy of the file/line/description until
+// the test is done and we can output it all at once.
+//
+// Yes, this is probably the worst API of all time.
+static void print_result_str(bool start, const char *_file, int _line,
+			     const char *_condstr, const char *result)
+{
+    static char *file;
+    static char *condstr;
+    static int line;
+    char *cptr;
+
+    if (start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = strdup(pathstrip(_file));
+        condstr = strdup(_condstr);
+        line = _line;
+
+        for (cptr = condstr; *cptr; cptr++)
+        {
+            if (!isprint((unsigned char)*cptr))
+                *cptr = '!';
+        }
+    }
+
+    if (run_twice)
+    {
+        if (!start)
+            printf(TEST_START_FORMAT "%s\n", file, line, condstr, result);
+    }
+    else
+    {
+        if (start)
+            printf(TEST_START_FORMAT, file, line, condstr);
+        else
+            printf("%s\n", result);
+    }
+    fflush(stdout);
+
+    if (!start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = condstr = NULL;
+    }
+}
+
+static inline void
+print_result(bool start, const char *file, int line,
+	     const char *condstr, bool result)
+{
+	print_result_str(start, file, line, condstr, result ? "ok" : "FAILED");
+}
+
+void wvtest_start(const char *file, int line, const char *condstr)
+{
+    // Either print the file, line, and condstr, or save them for later.
+    print_result(true, file, line, condstr, false);
+}
+
+
+void wvtest_check(bool cond, const char *reason)
+{
+#ifndef _WIN32
+    alarm(MAX_TEST_TIME); // restart per-test timeout
+#endif
+    if (!start_time) start_time = time(NULL);
+
+    if (time(NULL) - start_time > MAX_TOTAL_TIME)
+    {
+	printf("\n! WvTest   Total run time exceeded %d seconds!  FAILED\n",
+	       MAX_TOTAL_TIME);
+	fflush(stdout);
+	abort();
+    }
+
+    runs++;
+
+    print_result_str(false, NULL, 0, NULL, cond ? "ok" : (reason ? reason : "FAILED"));
+
+    if (!cond)
+    {
+	fails++;
+
+	if (getenv("WVTEST_DIE_FAST"))
+	    abort();
+    }
+}
+
+
+bool wvtest_start_check_eq(const char *file, int line,
+			   int a, int b, bool expect_pass)
+{
+    size_t len = 11 + 11 + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "%d %s %d", a, expect_pass ? "==" : "!=", b);
+
+    wvtest_start(file, line, str);
+    free(str);
+
+    bool cond = (a == b);
+    if (!expect_pass)
+        cond = !cond;
+
+    wvtest_check(cond, NULL);
+    return cond;
+}
+
+
+bool wvtest_start_check_lt(const char *file, int line,
+			   int a, int b)
+{
+    size_t len = 11 + 11 + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "%d < %d", a, b);
+
+    wvtest_start(file, line, str);
+    free(str);
+
+    bool cond = (a < b);
+    wvtest_check(cond, NULL);
+    return cond;
+}
+bool wvtest_start_check_eq_str(const char *file, int line,
+			       const char *a, const char *b, bool expect_pass)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "[%s] %s [%s]", a, expect_pass ? "==" : "!=", b);
+
+    wvtest_start(file, line, str);
+
+    bool cond = !strcmp(a, b);
+    if (!expect_pass)
+        cond = !cond;
+
+    wvtest_check(cond, NULL);
+    return cond;
+}
+
+
+bool wvtest_start_check_lt_str(const char *file, int line,
+			       const char *a, const char *b)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = malloc(len);
+    sprintf(str, "[%s] < [%s]", a, b);
+
+    wvtest_start(file, line, str);
+    free(str);
+
+    bool cond = strcmp(a, b) < 0;
+    wvtest_check(cond, NULL);
+    return cond;
+}
diff --git a/wvtest/c/wvtest.h b/wvtest/c/wvtest.h
new file mode 100644
index 0000000..0952c00
--- /dev/null
+++ b/wvtest/c/wvtest.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++ -*-
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#ifndef __WVTEST_H
+#define __WVTEST_H
+
+#ifndef WVTEST_CONFIGURED
+# error "Missing settings: HAVE_VALGRIND_MEMCHECK_H HAVE_WVCRASH WVTEST_CONFIGURED"
+#endif
+
+#include <time.h>
+#include <string.h>
+#include <stdbool.h>
+
+typedef void wvtest_mainfunc();
+
+struct WvTest {
+	const char *descr, *idstr;
+	wvtest_mainfunc *main;
+	int slowness;
+	struct WvTest *next;
+};
+
+void wvtest_register(struct WvTest *ptr);
+int wvtest_run_all(char * const *prefixes);
+void wvtest_start(const char *file, int line, const char *condstr);
+void wvtest_check(bool cond, const char *reason);
+static inline bool wvtest_start_check(const char *file, int line,
+				      const char *condstr, bool cond)
+{ wvtest_start(file, line, condstr); wvtest_check(cond, NULL); return cond; }
+bool wvtest_start_check_eq(const char *file, int line,
+			   int a, int b, bool expect_pass);
+bool wvtest_start_check_lt(const char *file, int line,
+			   int a, int b);
+bool wvtest_start_check_eq_str(const char *file, int line,
+			       const char *a, const char *b, bool expect_pass);
+bool wvtest_start_check_lt_str(const char *file, int line,
+			       const char *a, const char *b);
+
+
+#define WVPASS(cond) \
+    wvtest_start_check(__FILE__, __LINE__, #cond, (cond))
+#define WVPASSEQ(a, b) \
+    wvtest_start_check_eq(__FILE__, __LINE__, (a), (b), true)
+#define WVPASSLT(a, b) \
+    wvtest_start_check_lt(__FILE__, __LINE__, (a), (b))
+#define WVPASSEQSTR(a, b) \
+    wvtest_start_check_eq_str(__FILE__, __LINE__, (a), (b), true)
+#define WVPASSLTSTR(a, b) \
+    wvtest_start_check_lt_str(__FILE__, __LINE__, (a), (b))
+#define WVFAIL(cond) \
+    wvtest_start_check(__FILE__, __LINE__, "NOT(" #cond ")", !(cond))
+#define WVFAILEQ(a, b) \
+    wvtest_start_check_eq(__FILE__, __LINE__, (a), (b), false)
+#define WVFAILEQSTR(a, b) \
+    wvtest_start_check_eq_str(__FILE__, __LINE__, (a), (b), false)
+#define WVPASSNE(a, b) WVFAILEQ(a, b)
+#define WVPASSNESTR(a, b) WVFAILEQSTR(a, b)
+#define WVFAILNE(a, b) WVPASSEQ(a, b)
+#define WVFAILNESTR(a, b) WVPASSEQSTR(a, b)
+
+#define WVTEST_MAIN3(_descr, ff, ll, _slowness)				\
+	static void _wvtest_main_##ll();				\
+	struct WvTest _wvtest_##ll = \
+	{ .descr = _descr, .idstr = ff ":" #ll, .main = _wvtest_main_##ll, .slowness = _slowness }; \
+	static void _wvtest_register_##ll() __attribute__ ((constructor)); \
+	static void _wvtest_register_##ll() { wvtest_register(&_wvtest_##ll); } \
+	static void _wvtest_main_##ll()
+#define WVTEST_MAIN2(descr, ff, ll, slowness)	\
+	WVTEST_MAIN3(descr, ff, ll, slowness)
+#define WVTEST_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 0)
+#define WVTEST_SLOW_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 1)
+
+
+#endif // __WVTEST_H
diff --git a/wvtest/c/wvtestmain.c b/wvtest/c/wvtestmain.c
new file mode 100644
index 0000000..46f91a3
--- /dev/null
+++ b/wvtest/c/wvtestmain.c
@@ -0,0 +1,102 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#ifdef HAVE_WVCRASH
+# include "wvcrash.h"
+#endif
+#include <stdlib.h>
+#include <stdio.h>
+#ifdef _WIN32
+#include <io.h>
+#include <windows.h>
+#else
+#include <unistd.h>
+#include <fcntl.h>
+#endif
+
+static bool fd_is_valid(int fd)
+{
+#ifdef _WIN32
+    if ((HANDLE)_get_osfhandle(fd) != INVALID_HANDLE_VALUE) return true;
+#endif
+    int nfd = dup(fd);
+    if (nfd >= 0)
+    {
+	close(nfd);
+	return true;
+    }
+    return false;
+
+}
+
+
+static int fd_count(const char *when)
+{
+    int count = 0;
+    int fd;
+    printf("fds open at %s:", when);
+
+    for (fd = 0; fd < 1024; fd++)
+    {
+	if (fd_is_valid(fd))
+	{
+	    count++;
+	    printf(" %d", fd);
+	    fflush(stdout);
+	}
+    }
+    printf("\n");
+
+    return count;
+}
+
+
+int main(int argc, char **argv)
+{
+    char buf[200];
+#if defined(_WIN32) && defined(HAVE_WVCRASH)
+    setup_console_crash();
+#endif
+
+    // test wvtest itself.  Not very thorough, but you have to draw the
+    // line somewhere :)
+    WVPASS(true);
+    WVPASS(1);
+    WVFAIL(false);
+    WVFAIL(0);
+    int startfd, endfd;
+    char * const *prefixes = NULL;
+
+    if (argc > 1)
+	prefixes = argv + 1;
+
+    startfd = fd_count("start");
+    int ret = wvtest_run_all(prefixes);
+
+    if (ret == 0) // don't pollute the strace output if we failed anyway
+    {
+	endfd = fd_count("end");
+
+	WVPASS(startfd == endfd);
+#ifndef _WIN32
+	if (startfd != endfd)
+	{
+	    sprintf(buf, "ls -l /proc/%d/fd", getpid());
+	    if (system(buf) == -1) {
+		fprintf(stderr, "Unable to list open fds\n");
+	    }
+	}
+#endif
+    }
+
+    // keep 'make' from aborting if this environment variable is set
+    if (getenv("WVTEST_NO_FAIL"))
+	return 0;
+    else
+	return ret;
+}
diff --git a/wvtest/cpp/Makefile b/wvtest/cpp/Makefile
new file mode 100644
index 0000000..e9f2b40
--- /dev/null
+++ b/wvtest/cpp/Makefile
@@ -0,0 +1,14 @@
+
+all: t/wvtest
+
+t/wvtest: wvtestmain.cc wvtest.cc t/wvtest.t.cc
+	g++ -D WVTEST_CONFIGURED -o $@ -I. $^
+
+runtests: all
+	t/wvtest
+
+test: all
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ *.o t/*.o t/wvtest
diff --git a/wvtest/cpp/t/.gitignore b/wvtest/cpp/t/.gitignore
new file mode 100644
index 0000000..160e518
--- /dev/null
+++ b/wvtest/cpp/t/.gitignore
@@ -0,0 +1 @@
+wvtest
diff --git a/wvtest/cpp/t/wvtest.t.cc b/wvtest/cpp/t/wvtest.t.cc
new file mode 100644
index 0000000..aa028d6
--- /dev/null
+++ b/wvtest/cpp/t/wvtest.t.cc
@@ -0,0 +1,16 @@
+#include "wvtest.h"
+
+WVTEST_MAIN("wvtest tests")
+{
+    WVPASS(1);
+    WVPASSEQ(1, 1);
+    WVPASSNE(1, 2);
+    WVPASSEQ(1, 1);
+    WVPASSLT(1, 2);
+
+    WVPASSEQ("hello", "hello");
+    WVPASSNE("hello", "hello2");
+
+    WVPASSEQ(std::string("hello"), std::string("hello"));
+    WVPASSNE(std::string("hello"), std::string("hello2"));
+}
diff --git a/wvtest/cpp/wvtest.cc b/wvtest/cpp/wvtest.cc
new file mode 100644
index 0000000..2fac9d1
--- /dev/null
+++ b/wvtest/cpp/wvtest.cc
@@ -0,0 +1,446 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#ifdef _WIN32
+#include <direct.h>
+#else
+#include <unistd.h>
+#include <sys/wait.h>
+#endif
+#include <errno.h>
+#include <signal.h>
+
+#include <cstdlib>
+
+#ifdef HAVE_VALGRIND_MEMCHECK_H
+# include <valgrind/memcheck.h>
+# include <valgrind/valgrind.h>
+#else
+# define VALGRIND_COUNT_ERRORS 0
+# define VALGRIND_DO_LEAK_CHECK
+# define VALGRIND_COUNT_LEAKS(a,b,c,d) (a=b=c=d=0)
+#endif
+
+#define MAX_TEST_TIME 40     // max seconds for a single test to run
+#define MAX_TOTAL_TIME 120*60 // max seconds for the entire suite to run
+
+#define TEST_START_FORMAT "! %s:%-5d %-40s "
+
+static int memerrs()
+{
+    return (int)VALGRIND_COUNT_ERRORS;
+}
+
+static int memleaks()
+{
+    int leaked = 0, dubious = 0, reachable = 0, suppressed = 0;
+    VALGRIND_DO_LEAK_CHECK;
+    VALGRIND_COUNT_LEAKS(leaked, dubious, reachable, suppressed);
+    printf("memleaks: sure:%d dubious:%d reachable:%d suppress:%d\n",
+	   leaked, dubious, reachable, suppressed);
+    fflush(stdout);
+
+    // dubious+reachable are normally non-zero because of globals...
+    // return leaked+dubious+reachable;
+    return leaked;
+}
+
+// Return 1 if no children are running or zombies, 0 if there are any running
+// or zombie children.
+// Will wait for any already-terminated children first.
+// Passes if no rogue children were running, fails otherwise.
+// If your test gets a failure in here, either you're not killing all your
+// children, or you're not calling waitpid(2) on all of them.
+static bool no_running_children()
+{
+#ifndef _WIN32
+    pid_t wait_result;
+
+    // Acknowledge and complain about any zombie children
+    do
+    {
+	int status = 0;
+        wait_result = waitpid(-1, &status, WNOHANG);
+
+        if (wait_result > 0)
+        {
+            char buf[256];
+            snprintf(buf, sizeof(buf) - 1, "%d", wait_result);
+            buf[sizeof(buf)-1] = '\0';
+            WVFAILEQ("Unclaimed dead child process", buf);
+        }
+    } while (wait_result > 0);
+
+    // There should not be any running children, so waitpid should return -1
+    WVPASSEQ(errno, ECHILD);
+    WVPASSEQ(wait_result, -1);
+    return (wait_result == -1 && errno == ECHILD);
+#endif
+    return true;
+}
+
+
+WvTest *WvTest::first, *WvTest::last;
+int WvTest::fails, WvTest::runs;
+time_t WvTest::start_time;
+bool WvTest::run_twice = false;
+
+void WvTest::alarm_handler(int)
+{
+    printf("\n! WvTest  Current test took longer than %d seconds!  FAILED\n",
+	   MAX_TEST_TIME);
+    fflush(stdout);
+    abort();
+}
+
+
+static const char *pathstrip(const char *filename)
+{
+    const char *cptr;
+    cptr = strrchr(filename, '/');
+    if (cptr) filename = cptr + 1;
+    cptr = strrchr(filename, '\\');
+    if (cptr) filename = cptr + 1;
+    return filename;
+}
+
+
+WvTest::WvTest(const char *_descr, const char *_idstr, MainFunc *_main,
+	       int _slowness) :
+    descr(_descr),
+    idstr(pathstrip(_idstr)),
+    main(_main),
+    slowness(_slowness),
+    next(NULL)
+{
+    if (first)
+	last->next = this;
+    else
+	first = this;
+    last = this;
+}
+
+
+static bool prefix_match(const char *s, const char * const *prefixes)
+{
+    for (const char * const *prefix = prefixes; prefix && *prefix; prefix++)
+    {
+	if (!strncasecmp(s, *prefix, strlen(*prefix)))
+	    return true;
+    }
+    return false;
+}
+
+
+int WvTest::run_all(const char * const *prefixes)
+{
+    int old_valgrind_errs = 0, new_valgrind_errs;
+    int old_valgrind_leaks = 0, new_valgrind_leaks;
+
+#ifdef _WIN32
+    /* I should be doing something to do with SetTimer here,
+     * not sure exactly what just yet */
+#else
+    char *disable(getenv("WVTEST_DISABLE_TIMEOUT"));
+    if (disable != NULL && disable[0] != '\0' && disable[0] != '0')
+        signal(SIGALRM, SIG_IGN);
+    else
+        signal(SIGALRM, alarm_handler);
+    alarm(MAX_TEST_TIME);
+#endif
+    start_time = time(NULL);
+
+    // make sure we can always start out in the same directory, so tests have
+    // access to their files.  If a test uses chdir(), we want to be able to
+    // reverse it.
+    char wd[1024];
+    if (!getcwd(wd, sizeof(wd)))
+	strcpy(wd, ".");
+
+    const char *slowstr1 = getenv("WVTEST_MIN_SLOWNESS");
+    const char *slowstr2 = getenv("WVTEST_MAX_SLOWNESS");
+    int min_slowness = 0, max_slowness = 65535;
+    if (slowstr1) min_slowness = atoi(slowstr1);
+    if (slowstr2) max_slowness = atoi(slowstr2);
+
+#ifdef _WIN32
+    run_twice = false;
+#else
+    char *parallel_str = getenv("WVTEST_PARALLEL");
+    if (parallel_str)
+        run_twice = atoi(parallel_str) > 0;
+#endif
+
+    // there are lots of fflush() calls in here because stupid win32 doesn't
+    // flush very often by itself.
+    fails = runs = 0;
+    for (WvTest *cur = first; cur; cur = cur->next)
+    {
+	if (cur->slowness <= max_slowness
+	    && cur->slowness >= min_slowness
+	    && (!prefixes
+		|| prefix_match(cur->idstr, prefixes)
+		|| prefix_match(cur->descr, prefixes)))
+	{
+#ifndef _WIN32
+            // set SIGPIPE back to default, helps catch tests which don't set
+            // this signal to SIG_IGN (which is almost always what you want)
+            // on startup
+            signal(SIGPIPE, SIG_DFL);
+
+            pid_t child = 0;
+            if (run_twice)
+            {
+                // I see everything twice!
+                printf("Running test in parallel.\n");
+                child = fork();
+            }
+#endif
+
+	    printf("\nTesting \"%s\" in %s:\n", cur->descr, cur->idstr);
+	    fflush(stdout);
+
+	    cur->main();
+	    if (chdir(wd)) {
+		perror("Unable to change back to original directory");
+	    }
+
+	    new_valgrind_errs = memerrs();
+	    WVPASS(new_valgrind_errs == old_valgrind_errs);
+	    old_valgrind_errs = new_valgrind_errs;
+
+	    new_valgrind_leaks = memleaks();
+	    WVPASS(new_valgrind_leaks == old_valgrind_leaks);
+	    old_valgrind_leaks = new_valgrind_leaks;
+
+	    fflush(stderr);
+	    printf("\n");
+	    fflush(stdout);
+
+#ifndef _WIN32
+            if (run_twice)
+            {
+                if (!child)
+                {
+                    // I see everything once!
+                    printf("Child exiting.\n");
+                    _exit(0);
+                }
+                else
+                {
+                    printf("Waiting for child to exit.\n");
+                    int result;
+                    while ((result = waitpid(child, NULL, 0)) == -1 &&
+                            errno == EINTR)
+                        printf("Waitpid interrupted, retrying.\n");
+                }
+            }
+#endif
+
+            WVPASS(no_running_children());
+	}
+    }
+
+    WVPASS(runs > 0);
+
+    if (prefixes && *prefixes && **prefixes)
+	printf("WvTest: WARNING: only ran tests starting with "
+	       "specifed prefix(es).\n");
+    else
+	printf("WvTest: ran all tests.\n");
+    printf("WvTest: %d test%s, %d failure%s.\n",
+	   runs, runs==1 ? "" : "s",
+	   fails, fails==1 ? "": "s");
+    fflush(stdout);
+
+    return fails != 0;
+}
+
+
+// If we aren't running in parallel, we want to output the name of the test
+// before we run it, so we know what happened if it crashes.  If we are
+// running in parallel, outputting this information in multiple printf()s
+// can confuse parsers, so we want to output everything in one printf().
+//
+// This function gets called by both start() and check().  If we're not
+// running in parallel, just print the data.  If we're running in parallel,
+// and we're starting a test, save a copy of the file/line/description until
+// the test is done and we can output it all at once.
+//
+// Yes, this is probably the worst API of all time.
+void WvTest::print_result(bool start, const char *_file, int _line,
+        const char *_condstr, bool result)
+{
+    static char *file;
+    static char *condstr;
+    static int line;
+
+    if (start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = strdup(pathstrip(_file));
+        condstr = strdup(_condstr);
+        line = _line;
+
+        for (char *cptr = condstr; *cptr; cptr++)
+        {
+            if (!isprint((unsigned char)*cptr))
+                *cptr = '!';
+        }
+    }
+
+    const char *result_str = result ? "ok\n" : "FAILED\n";
+    if (run_twice)
+    {
+        if (!start)
+            printf(TEST_START_FORMAT "%s", file, line, condstr, result_str);
+    }
+    else
+    {
+        if (start)
+            printf(TEST_START_FORMAT, file, line, condstr);
+        else
+            printf("%s", result_str);
+    }
+    fflush(stdout);
+
+    if (!start)
+    {
+        if (file)
+            free(file);
+        if (condstr)
+            free(condstr);
+        file = condstr = NULL;
+    }
+}
+
+
+void WvTest::start(const char *file, int line, const char *condstr)
+{
+    // Either print the file, line, and condstr, or save them for later.
+    print_result(true, file, line, condstr, 0);
+}
+
+
+void WvTest::check(bool cond)
+{
+#ifndef _WIN32
+    alarm(MAX_TEST_TIME); // restart per-test timeout
+#endif
+    if (!start_time) start_time = time(NULL);
+
+    if (time(NULL) - start_time > MAX_TOTAL_TIME)
+    {
+	printf("\n! WvTest   Total run time exceeded %d seconds!  FAILED\n",
+	       MAX_TOTAL_TIME);
+	fflush(stdout);
+	abort();
+    }
+
+    runs++;
+
+    print_result(false, NULL, 0, NULL, cond);
+
+    if (!cond)
+    {
+	fails++;
+
+	if (getenv("WVTEST_DIE_FAST"))
+	    abort();
+    }
+}
+
+
+bool WvTest::start_check_eq(const char *file, int line,
+			    const char *a, const char *b, bool expect_pass)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "[%s] %s [%s]", a, expect_pass ? "==" : "!=", b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = !strcmp(a, b);
+    if (!expect_pass)
+        cond = !cond;
+
+    check(cond);
+    return cond;
+}
+
+
+bool WvTest::start_check_eq(const char *file, int line,
+			    const std::string &a, const std::string &b,
+                            bool expect_pass)
+{
+    return start_check_eq(file, line, a.c_str(), b.c_str(), expect_pass);
+}
+
+
+bool WvTest::start_check_eq(const char *file, int line,
+                            int a, int b, bool expect_pass)
+{
+    size_t len = 128 + 128 + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "%d %s %d", a, expect_pass ? "==" : "!=", b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = (a == b);
+    if (!expect_pass)
+        cond = !cond;
+
+    check(cond);
+    return cond;
+}
+
+
+bool WvTest::start_check_lt(const char *file, int line,
+			    const char *a, const char *b)
+{
+    if (!a) a = "";
+    if (!b) b = "";
+
+    size_t len = strlen(a) + strlen(b) + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "[%s] < [%s]", a, b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = strcmp(a, b) < 0;
+    check(cond);
+    return cond;
+}
+
+
+bool WvTest::start_check_lt(const char *file, int line, int a, int b)
+{
+    size_t len = 128 + 128 + 8 + 1;
+    char *str = new char[len];
+    sprintf(str, "%d < %d", a, b);
+
+    start(file, line, str);
+    delete[] str;
+
+    bool cond = a < b;
+    check(cond);
+    return cond;
+}
diff --git a/wvtest/cpp/wvtest.h b/wvtest/cpp/wvtest.h
new file mode 100644
index 0000000..de4975b
--- /dev/null
+++ b/wvtest/cpp/wvtest.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++ -*-
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#ifndef __WVTEST_H
+#define __WVTEST_H
+
+#ifndef WVTEST_CONFIGURED
+# error "Missing settings: HAVE_VALGRIND_MEMCHECK_H HAVE_WVCRASH WVTEST_CONFIGURED"
+#endif
+
+#include <time.h>
+#include <string>
+
+class WvTest
+{
+    typedef void MainFunc();
+    const char *descr, *idstr;
+    MainFunc *main;
+    int slowness;
+    WvTest *next;
+    static WvTest *first, *last;
+    static int fails, runs;
+    static time_t start_time;
+    static bool run_twice;
+
+    static void alarm_handler(int sig);
+
+    static void print_result(bool start, const char *file, int line,
+            const char *condstr, bool result);
+public:
+    WvTest(const char *_descr, const char *_idstr, MainFunc *_main, int _slow);
+    static int run_all(const char * const *prefixes = NULL);
+    static void start(const char *file, int line, const char *condstr);
+    static void check(bool cond);
+    static inline bool start_check(const char *file, int line,
+				   const char *condstr, bool cond)
+        { start(file, line, condstr); check(cond); return cond; }
+    static bool start_check_eq(const char *file, int line,
+			       const char *a, const char *b, bool expect_pass);
+    static bool start_check_eq(const char *file, int line,
+			       const std::string &a, const std::string &b,
+                               bool expect_pass);
+    static bool start_check_eq(const char *file, int line, int a, int b,
+                               bool expect_pass);
+    static bool start_check_lt(const char *file, int line,
+                               const char *a, const char *b);
+    static bool start_check_lt(const char *file, int line, int a, int b);
+};
+
+
+#define WVPASS(cond) \
+    WvTest::start_check(__FILE__, __LINE__, #cond, (cond))
+#define WVPASSEQ(a, b) \
+    WvTest::start_check_eq(__FILE__, __LINE__, (a), (b), true)
+#define WVPASSLT(a, b) \
+    WvTest::start_check_lt(__FILE__, __LINE__, (a), (b))
+#define WVFAIL(cond) \
+    WvTest::start_check(__FILE__, __LINE__, "NOT(" #cond ")", !(cond))
+#define WVFAILEQ(a, b) \
+    WvTest::start_check_eq(__FILE__, __LINE__, (a), (b), false)
+#define WVPASSNE(a, b) WVFAILEQ(a, b)
+#define WVFAILNE(a, b) WVPASSEQ(a, b)
+
+#define WVTEST_MAIN3(descr, ff, ll, slowness) \
+    static void _wvtest_main_##ll(); \
+    static WvTest _wvtest_##ll(descr, ff, _wvtest_main_##ll, slowness); \
+    static void _wvtest_main_##ll()
+#define WVTEST_MAIN2(descr, ff, ll, slowness) \
+    WVTEST_MAIN3(descr, ff, ll, slowness)
+#define WVTEST_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 0)
+#define WVTEST_SLOW_MAIN(descr) WVTEST_MAIN2(descr, __FILE__, __LINE__, 1)
+
+
+#endif // __WVTEST_H
diff --git a/wvtest/cpp/wvtestmain.cc b/wvtest/cpp/wvtestmain.cc
new file mode 100644
index 0000000..f6298d5
--- /dev/null
+++ b/wvtest/cpp/wvtestmain.cc
@@ -0,0 +1,102 @@
+/*
+ * WvTest:
+ *   Copyright (C)1997-2012 Net Integration Technologies and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+#include "wvtest.h"
+#ifdef HAVE_WVCRASH
+# include "wvcrash.h"
+#endif
+#include <stdlib.h>
+#include <stdio.h>
+#ifdef _WIN32
+#include <io.h>
+#include <windows.h>
+#else
+#include <unistd.h>
+#include <fcntl.h>
+#endif
+
+static bool fd_is_valid(int fd)
+{
+#ifdef _WIN32
+    if ((HANDLE)_get_osfhandle(fd) != INVALID_HANDLE_VALUE) return true;
+#endif
+    int nfd = dup(fd);
+    if (nfd >= 0)
+    {
+	close(nfd);
+	return true;
+    }
+    return false;
+
+}
+
+
+static int fd_count(const char *when)
+{
+    int count = 0;
+
+    printf("fds open at %s:", when);
+
+    for (int fd = 0; fd < 1024; fd++)
+    {
+	if (fd_is_valid(fd))
+	{
+	    count++;
+	    printf(" %d", fd);
+	    fflush(stdout);
+	}
+    }
+    printf("\n");
+
+    return count;
+}
+
+
+int main(int argc, char **argv)
+{
+    char buf[200];
+#if defined(_WIN32) && defined(HAVE_WVCRASH)
+    setup_console_crash();
+#endif
+
+    // test wvtest itself.  Not very thorough, but you have to draw the
+    // line somewhere :)
+    WVPASS(true);
+    WVPASS(1);
+    WVFAIL(false);
+    WVFAIL(0);
+    int startfd, endfd;
+    char * const *prefixes = NULL;
+
+    if (argc > 1)
+	prefixes = argv + 1;
+
+    startfd = fd_count("start");
+    int ret = WvTest::run_all(prefixes);
+
+    if (ret == 0) // don't pollute the strace output if we failed anyway
+    {
+	endfd = fd_count("end");
+
+	WVPASS(startfd == endfd);
+#ifndef _WIN32
+	if (startfd != endfd)
+	{
+	    sprintf(buf, "ls -l /proc/%d/fd", getpid());
+	    if (system(buf) == -1) {
+		fprintf(stderr, "Unable to list open fds\n");
+	    }
+	}
+#endif
+    }
+
+    // keep 'make' from aborting if this environment variable is set
+    if (getenv("WVTEST_NO_FAIL"))
+	return 0;
+    else
+	return ret;
+}
diff --git a/wvtest/dotnet/.gitignore b/wvtest/dotnet/.gitignore
new file mode 100644
index 0000000..cd79073
--- /dev/null
+++ b/wvtest/dotnet/.gitignore
@@ -0,0 +1 @@
+*.cs.E
diff --git a/wvtest/dotnet/Makefile b/wvtest/dotnet/Makefile
new file mode 100644
index 0000000..7ed3414
--- /dev/null
+++ b/wvtest/dotnet/Makefile
@@ -0,0 +1,19 @@
+
+all: t/test.exe
+
+include wvtestrules.mk
+
+CPPFLAGS=-I.
+
+t/test.exe: wvtest.cs wvtestmain.cs t/wvtest.t.cs.E
+	gmcs /out:$@ /debug $^
+
+runtests: t/test.exe
+	cd t && mono --debug test.exe
+
+test:
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ .*~ *.E t/*.E *.d t/*.d t/*.exe t/*.mdb
+
diff --git a/wvtest/dotnet/t/wvtest.t.cs b/wvtest/dotnet/t/wvtest.t.cs
new file mode 100644
index 0000000..25461a1
--- /dev/null
+++ b/wvtest/dotnet/t/wvtest.t.cs
@@ -0,0 +1,96 @@
+/*
+ * Versaplex:
+ *   Copyright (C)2007-2008 Versabanq Innovations Inc. and contributors.
+ *       See the included file named LICENSE for license information.
+ */
+#include "wvtest.cs.h"
+using System;
+using Wv.Test;
+
+[TestFixture]
+public class WvTestTest
+{
+ 	[Test] public void test_wvtest()
+	{
+	    WVPASS(1);
+	    WVPASS("hello");
+	    WVPASS(new Object());
+	    WVPASS(0 != 1);
+
+	    WVFAIL(0);
+	    WVFAIL("");
+	    WVFAIL(null);
+
+	    WVPASSEQ(7, 7);
+	    WVPASSEQ("foo", "foo");
+	    WVPASSEQ("", "");
+	    Object obj = new Object();
+	    WVPASSEQ(obj, obj);
+	    WVPASSEQ(null, null);
+
+	    WVPASSNE(7, 8);
+	    WVPASSNE("foo", "blue");
+	    WVPASSNE("", "notempty");
+	    WVPASSNE(null, "");
+	    WVPASSNE(obj, null);
+	    WVPASSNE(obj, new Object());
+	    WVPASSNE(new Object(), new Object());
+	}
+
+	// these are only public to get rid of the "not assigned to" warnings.
+	// we don't assign to them because that's the whole point of the test.
+	public DateTime null_date;
+	public TimeSpan null_span;
+
+	[Test] public void test_dates_and_spans()
+	{
+	    WVPASS(null_date == DateTime.MinValue);
+	    WVPASSEQ(null_date, DateTime.MinValue);
+	    WVPASS(null_span == TimeSpan.Zero);
+	    WVPASSEQ(null_span, TimeSpan.Zero);
+
+	    TimeSpan t = TimeSpan.FromMinutes(60*24*7);
+	    WVPASSEQ(t.ToString(), "7.00:00:00");
+	    WVPASSEQ(t.Ticks, 7*24*60*60*10000000L);
+	    WVPASS(t.TotalMinutes == 7*24*60);
+	    WVPASSEQ(t.TotalMinutes, 7*24*60);
+	    WVPASSEQ(t.TotalSeconds, 7*24*60*60);
+	    WVPASSEQ(t.Minutes, 0);
+	}
+
+	void throw_exception()
+	{
+	    throw new System.Exception("Exception thrown");
+	}
+	void no_throw_exception()
+	{
+	    return;
+	}
+
+	[Test] public void test_exceptions()
+	{
+	    bool caught = false;
+
+	    try {
+		WVEXCEPT(throw_exception());
+	    } catch (Wv.Test.WvAssertionFailure e) {
+		throw e;
+	    } catch (System.Exception) {
+		caught = true;
+	    }
+
+	    WVPASS(caught);
+
+	    caught = false;
+
+	    System.Console.WriteLine("Ignore next failure: it is expected");
+	    WvTest.expect_next_failure();
+	    try {
+		WVEXCEPT(no_throw_exception());
+	    } catch (Wv.Test.WvAssertionFailure) {
+		caught = true;
+	    }
+
+	    WVPASS(caught);
+	}
+}
diff --git a/wvtest/dotnet/wvtest.cs b/wvtest/dotnet/wvtest.cs
new file mode 100644
index 0000000..78fae3a
--- /dev/null
+++ b/wvtest/dotnet/wvtest.cs
@@ -0,0 +1,377 @@
+/*
+ * WvTest:
+ *   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.Serialization;
+using Wv;
+
+// We put this in wvtest.cs since wvtest.cs should be able to compile all
+// by itself, without relying on any other parts of wvdotnet.  On the other
+// hand, it's perfectly fine for wvdotnet to have wvtest.cs in it.
+namespace Wv
+{
+    public static class WvReflection
+    {
+	public static IEnumerable<Type> find_types(Type attrtype)
+	{
+	    foreach (Assembly a in AppDomain.CurrentDomain.GetAssemblies())
+	    {
+		foreach (Type t in a.GetTypes())
+		{
+		    if (!t.IsDefined(attrtype, false))
+			continue;
+
+		    yield return t;
+		}
+	    }
+	}
+
+	public static IEnumerable<MethodInfo> find_methods(this Type t,
+							   Type attrtype)
+	{
+	    foreach (MethodInfo m in t.GetMethods())
+	    {
+		if (!m.IsDefined(attrtype, false))
+		    continue;
+
+		yield return m;
+	    }
+	}
+    }
+}
+
+namespace Wv.Test
+{
+    public class WvTest
+    {
+	struct TestInfo
+	{
+	    public string name;
+	    public Action cb;
+
+	    public TestInfo(string name, Action cb)
+	        { this.name = name; this.cb = cb; }
+	}
+        List<TestInfo> tests = new List<TestInfo>();
+
+        public int failures { get; private set; }
+
+	public WvTest()
+	{
+	    foreach (Type t in
+		     WvReflection.find_types(typeof(TestFixtureAttribute)))
+	    {
+		foreach (MethodInfo m in
+			 t.find_methods(typeof(TestAttribute)))
+		{
+		    // The new t2, m2 are needed so that each delegate gets
+		    // its own copy of the variable.
+		    Type t2 = t;
+		    MethodInfo m2 = m;
+		    RegisterTest(String.Format("{0}/{1}",
+					       t.Name, m.Name),
+				 delegate() {
+				     try {
+					 m2.Invoke(Activator.CreateInstance(t2),
+						   null);
+				     } catch (TargetInvocationException e) {
+					 throw e.InnerException;
+				     }
+				 });
+		}
+	    }
+	}
+
+        public void RegisterTest(string name, Action tc)
+        {
+            tests.Add(new TestInfo(name, tc));
+        }
+
+	public static void DoMain()
+	{
+	    // Enough to run an entire test
+	    Environment.Exit(new WvTest().Run());
+	}
+
+        public int Run()
+        {
+	    string[] args = Environment.GetCommandLineArgs();
+
+	    if (args.Length <= 1)
+		Console.WriteLine("WvTest: Running all tests");
+	    else
+		Console.WriteLine("WvTest: Running only selected tests");
+
+            foreach (TestInfo test in tests)
+	    {
+		string[] parts = test.name.Split(new char[] { '/' }, 2);
+
+		bool runthis = (args.Length <= 1);
+		foreach (string arg in args)
+		    if (parts[0].StartsWith(arg) || parts[1].StartsWith(arg))
+			runthis = true;
+
+		if (!runthis) continue;
+
+                Console.WriteLine("\nTesting \"{0}\" in {1}:",
+				  parts[1], parts[0]);
+
+                try {
+		    test.cb();
+                } catch (WvAssertionFailure) {
+                    failures++;
+                } catch (Exception e) {
+                    Console.WriteLine(e.ToString());
+                    Console.WriteLine("! WvTest Exception received   FAILED");
+                    failures++;
+                }
+            }
+
+	    Console.Out.WriteLine("Result: {0} failures.", failures);
+
+	    // Return a safe unix exit code
+	    return failures > 0 ? 1 : 0;
+        }
+
+	public static bool booleanize(bool x)
+	{
+	    return x;
+	}
+
+	public static bool booleanize(long x)
+	{
+	    return x != 0;
+	}
+
+	public static bool booleanize(ulong x)
+	{
+	    return x != 0;
+	}
+
+	public static bool booleanize(string s)
+	{
+	    return s != null && s != "";
+	}
+
+	public static bool booleanize(object o)
+	{
+	    return o != null;
+	}
+
+	static bool expect_fail = false;
+	public static void expect_next_failure()
+	{
+	    expect_fail = true;
+	}
+
+	public static bool test(bool ok, string file, int line, string s)
+	{
+	    s = s.Replace("\n", "!");
+	    s = s.Replace("\r", "!");
+	    string suffix = "";
+	    if (expect_fail)
+	    {
+		if (!ok)
+		    suffix = " (expected) ok";
+		else
+		    suffix = " (expected fail!) FAILED";
+	    }
+	    Console.WriteLine("! {0}:{1,-5} {2,-40} {3}{4}",
+			      file, line, s,
+			      ok ? "ok" : "FAILED",
+			      suffix);
+	    Console.Out.Flush();
+	    expect_fail = false;
+
+            if (!ok)
+	        throw new WvAssertionFailure(String.Format("{0}:{1} {2}", file, line, s));
+
+	    return ok;
+	}
+
+	public static void test_exception(string file, int line, string s)
+	{
+	    Console.WriteLine("! {0}:{1,-5} {2,-40} {3}",
+					 file, line, s, "EXCEPTION");
+            Console.Out.Flush();
+	}
+
+	public static bool test_eq(bool cond1, bool cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(long cond1, long cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(ulong cond1, ulong cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(double cond1, double cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(decimal cond1, decimal cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_eq(string cond1, string cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}] ({{{2}}} == {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	// some objects can compare themselves to 'null', which is helpful.
+	// for example, DateTime.MinValue == null, but only through
+	// IComparable, not through IObject.
+	public static bool test_eq(IComparable cond1, IComparable cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1.CompareTo(cond2) == 0, file, line,
+			String.Format("[{0}] == [{1}]", s1, s2));
+	}
+
+	public static bool test_eq(object cond1, object cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 == cond2, file, line,
+		String.Format("[{0}] == [{1}]", s1, s2));
+	}
+
+	public static bool test_ne(bool cond1, bool cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(long cond1, long cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(ulong cond1, ulong cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(double cond1, double cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(decimal cond1, decimal cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	public static bool test_ne(string cond1, string cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}] ({{{2}}} != {{{3}}})",
+			      cond1, cond2, s1, s2));
+	}
+
+	// See notes for test_eq(IComparable,IComparable)
+	public static bool test_ne(IComparable cond1, IComparable cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1.CompareTo(cond2) != 0, file, line,
+			String.Format("[{0}] != [{1}]", s1, s2));
+	}
+
+	public static bool test_ne(object cond1, object cond2,
+				   string file, int line,
+				   string s1, string s2)
+	{
+	    return test(cond1 != cond2, file, line,
+		String.Format("[{0}] != [{1}]", s1, s2));
+	}
+    }
+
+    public class WvAssertionFailure : Exception
+    {
+        public WvAssertionFailure()
+            : base()
+        {
+        }
+
+        public WvAssertionFailure(string msg)
+            : base(msg)
+        {
+        }
+    }
+
+    // Placeholders for NUnit compatibility
+    public class TestFixtureAttribute : Attribute
+    {
+    }
+    public class TestAttribute : Attribute
+    {
+    }
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple=true)]
+    public class CategoryAttribute : Attribute
+    {
+        public CategoryAttribute(string x)
+        {
+        }
+    }
+}
diff --git a/wvtest/dotnet/wvtest.cs.h b/wvtest/dotnet/wvtest.cs.h
new file mode 100644
index 0000000..30ce532
--- /dev/null
+++ b/wvtest/dotnet/wvtest.cs.h
@@ -0,0 +1,9 @@
+#ifndef __WVTEST_CS_H // Blank lines in this file mess up line numbering!
+#define __WVTEST_CS_H
+#define WVASSERT(x) try { WvTest.test(WvTest.booleanize(x), __FILE__, __LINE__, #x); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, #x); throw; }
+#define WVPASS(x) WVASSERT(x)
+#define WVFAIL(x) try { WvTest.test(!WvTest.booleanize(x), __FILE__, __LINE__, "NOT(" + #x + ")"); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, "NOT(" + #x + ")"); throw; }
+#define WVEXCEPT(x) { System.Exception _wvex = null; try { x; } catch (System.Exception _wvasserte) { _wvex = _wvasserte; } WvTest.test(_wvex != null, __FILE__, __LINE__, "EXCEPT(" + #x + ")"); if (_wvex != null) throw _wvex; }
+#define WVPASSEQ(x, y) try { WvTest.test_eq((x), (y), __FILE__, __LINE__, #x, #y); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, string.Format("[{0}] == [{1}]", #x, #y)); throw; }
+#define WVPASSNE(x, y) try { WvTest.test_ne((x), (y), __FILE__, __LINE__, #x, #y); } catch (Wv.Test.WvAssertionFailure) { throw; } catch (System.Exception) { WvTest.test_exception(__FILE__, __LINE__, string.Format("[{0}] != [{1}]", #x, #y)); throw; }
+#endif // __WVTEST_CS_H
diff --git a/wvtest/dotnet/wvtestmain.cs b/wvtest/dotnet/wvtestmain.cs
new file mode 100644
index 0000000..9eae04d
--- /dev/null
+++ b/wvtest/dotnet/wvtestmain.cs
@@ -0,0 +1,17 @@
+/*
+ * WvTest:
+ *   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+ *       Licensed under the GNU Library General Public License, version 2.
+ *       See the included file named LICENSE for license information.
+ *       You can get wvtest from: http://github.com/apenwarr/wvtest
+ */
+using System;
+using Wv.Test;
+
+public class WvTestMain
+{
+    public static void Main()
+    {
+	WvTest.DoMain();
+    }
+}
diff --git a/wvtest/dotnet/wvtestrules.mk b/wvtest/dotnet/wvtestrules.mk
new file mode 100644
index 0000000..638f43c
--- /dev/null
+++ b/wvtest/dotnet/wvtestrules.mk
@@ -0,0 +1,29 @@
+default: all
+
+SHELL=/bin/bash
+
+# cc -E tries to guess by extension what to do with the file.
+# And it does other weird things. cpp seems to Just Work(tm), so use that for
+# our C# (.cs) files
+CSCPP=cpp
+
+# Rules for generating autodependencies on header files
+$(patsubst %.cs.E,%.d,$(filter %.cs.E,$(FILES))): %.d: %.cs
+	@echo Generating dependency file $@ for $<
+	@set -e; set -o pipefail; rm -f $@; (\
+	    ($(CSCPP) -M -MM -MQ '$@' $(CPPFLAGS) $< && echo Makefile) \
+		| paste -s -d ' ' - && \
+	    $(CSCPP) -M -MM -MQ '$<'.E $(CPPFLAGS) $< \
+	) > $@ \
+	|| (rm -f $@ && echo "Error generating dependency file." && exit 1)
+
+include $(patsubst %.cs.E,%.d,$(filter %.cs.E,$(FILES)))
+
+# Rule for actually preprocessing source files with headers
+%.cs.E: %.cs
+	@rm -f $@
+	set -o pipefail; $(CSCPP) $(CPPFLAGS) -C -dI $< \
+		| expand -8 \
+		| sed -e 's,^#include,//#include,' \
+		| grep -v '^# [0-9]' \
+		>$@ || (rm -f $@ && exit 1)
diff --git a/wvtest/javascript/.gitignore b/wvtest/javascript/.gitignore
new file mode 100644
index 0000000..2340107
--- /dev/null
+++ b/wvtest/javascript/.gitignore
@@ -0,0 +1 @@
+jsshell
diff --git a/wvtest/javascript/Makefile b/wvtest/javascript/Makefile
new file mode 100644
index 0000000..97d4e2f
--- /dev/null
+++ b/wvtest/javascript/Makefile
@@ -0,0 +1,22 @@
+default: all
+
+all: jsshell
+
+MACOS_JS_PATH=/System/Library/Frameworks/JavaScriptCore.framework/Versions/Current/Resources/jsc
+jsshell: v8shell.cc
+	rm -f $@
+	[ -e "${MACOS_JS_PATH}" ] && \
+	ln -s "${MACOS_JS_PATH}" jsshell || \
+	g++ -g -o $@ v8shell.cc -lv8
+
+runtests: $(patsubst %.js,%.js.run,$(wildcard t/t*.js))
+
+%.js.run: %.js jsshell
+	./jsshell wvtest.js $*.js
+
+test: jsshell
+	../wvtestrun $(MAKE) runtests
+
+clean:
+	rm -f *~ */*~ v8shell jsshell
+	find . -name '*~' -exec rm -f {} \;
diff --git a/wvtest/javascript/t/empty b/wvtest/javascript/t/empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wvtest/javascript/t/empty
diff --git a/wvtest/javascript/t/hello b/wvtest/javascript/t/hello
new file mode 100644
index 0000000..95d09f2
--- /dev/null
+++ b/wvtest/javascript/t/hello
@@ -0,0 +1 @@
+hello world
\ No newline at end of file
diff --git a/wvtest/javascript/t/ttest.js b/wvtest/javascript/t/ttest.js
new file mode 100644
index 0000000..910d0e4
--- /dev/null
+++ b/wvtest/javascript/t/ttest.js
@@ -0,0 +1,52 @@
+
+function print_trace() {
+    print(trace())
+}
+
+
+function x() {
+    print("x()");
+    y(1);
+}
+
+
+function y(a) {
+    print("y(", a, ")");
+    z(a+5, a+6);
+}
+
+
+function z(a,b) {
+    print("z(", a, b, ")");
+    print_trace();
+}
+
+
+print("Hello world");
+x();
+
+
+wvtest('selftests', function() {
+    WVPASS('yes');
+    WVFAIL(false);
+    WVFAIL(1 == 2);
+    WVPASS();
+    WVFAIL(false);
+    WVEXCEPT(ReferenceError, function() { does_not_exist });
+    WVEXCEPT(TypeError, null);
+    WVPASSEQ('5', 5);
+    WVPASSNE('5', 6);
+    WVPASSNE(0.3, 1/3);
+    WVPASSEQ(0.3, 1/3, 0.04);
+    WVPASSNE(0.3, 1/3, 0.03);
+    WVPASSLT('5', 6);
+    WVPASSGT('6', '5');
+    WVPASSLE('5', '6');
+    WVPASSGE('5', 4);
+
+    WVPASSEQ(read('t/empty'), '');
+    WVPASSEQ(read('t/hello'), 'hello world');
+    WVEXCEPT(Error, function() { read('.') });
+    WVEXCEPT(Error, function() { load('missing') });
+    WVPASSEQ(load('t/empty'), undefined);
+});
diff --git a/wvtest/javascript/v8shell.cc b/wvtest/javascript/v8shell.cc
new file mode 100644
index 0000000..a6b42a5
--- /dev/null
+++ b/wvtest/javascript/v8shell.cc
@@ -0,0 +1,349 @@
+// Copyright 2011 the V8 project authors. All rights reserved.
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//     * Redistributions of source code must retain the above copyright
+//       notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+//       copyright notice, this list of conditions and the following
+//       disclaimer in the documentation and/or other materials provided
+//       with the distribution.
+//     * Neither the name of Google Inc. nor the names of its
+//       contributors may be used to endorse or promote products derived
+//       from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <v8.h>
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#ifdef COMPRESS_STARTUP_DATA_BZ2
+#error Using compressed startup data is not supported for this sample
+#endif
+
+
+void RunShell(v8::Handle<v8::Context> context);
+int RunMain(int argc, char* argv[]);
+bool ExecuteString(v8::Handle<v8::String> source,
+                   v8::Handle<v8::Value> name,
+                   bool print_result,
+                   bool report_exceptions);
+v8::Handle<v8::Value> Print(const v8::Arguments& args);
+v8::Handle<v8::Value> Read(const v8::Arguments& args);
+v8::Handle<v8::Value> Load(const v8::Arguments& args);
+v8::Handle<v8::Value> Quit(const v8::Arguments& args);
+v8::Handle<v8::Value> Version(const v8::Arguments& args);
+v8::Handle<v8::Value> ReadFile(const char* name);
+void ReportException(v8::TryCatch* handler);
+
+
+static bool run_shell;
+
+
+int main(int argc, char* argv[]) {
+  int result = 1;
+  v8::V8::SetFlagsFromCommandLine(&argc, argv, true);
+  run_shell = (argc == 1);
+  {
+    v8::HandleScope handle_scope;
+
+    v8::Handle<v8::ObjectTemplate> global = v8::ObjectTemplate::New();
+    global->Set(v8::String::New("print"), v8::FunctionTemplate::New(Print));
+    global->Set(v8::String::New("read"), v8::FunctionTemplate::New(Read));
+    global->Set(v8::String::New("load"), v8::FunctionTemplate::New(Load));
+    global->Set(v8::String::New("quit"), v8::FunctionTemplate::New(Quit));
+    global->Set(v8::String::New("version"), v8::FunctionTemplate::New(Version));
+    v8::Handle<v8::Context> context = v8::Context::New(NULL, global);
+    if (context.IsEmpty()) {
+      fprintf(stderr, "Error creating context\n");
+      return 1;
+    }
+    v8::Context::Scope context_scope(context);
+    result = RunMain(argc, argv);
+    if (run_shell) RunShell(context);
+  }
+  v8::V8::Dispose();
+  return result;
+}
+
+
+// Extracts a C string from a V8 Utf8Value.
+const char* ToCString(const v8::String::Utf8Value& value) {
+  return *value ? *value : "<string conversion failed>";
+}
+
+
+// Returns an exception object corresponding to the given string.
+v8::Handle<v8::Value> StringException(const char *s) {
+  return v8::ThrowException(v8::Exception::Error(v8::String::New(s)));
+}
+
+
+// Returns an exception object corresponding to the Unix errno.
+v8::Handle<v8::Value> ErrnoException(const char *prefix, int errnum) {
+  char errbuf[128];
+  snprintf(errbuf, sizeof(errbuf), "%s: %s", prefix, strerror(errnum));
+  return StringException(errbuf);
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'print'
+// function is called.  Prints its arguments on stdout separated by
+// spaces and ending with a newline.
+v8::Handle<v8::Value> Print(const v8::Arguments& args) {
+  bool first = true;
+  for (int i = 0; i < args.Length(); i++) {
+    v8::HandleScope handle_scope;
+    if (first) {
+      first = false;
+    } else {
+      printf(" ");
+    }
+    v8::String::Utf8Value str(args[i]);
+    printf("%s", ToCString(str));
+  }
+  printf("\n");
+  fflush(stdout);
+  return v8::Undefined();
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'read'
+// function is called.  This function loads the content of the file named in
+// the argument into a JavaScript string.
+v8::Handle<v8::Value> Read(const v8::Arguments& args) {
+  if (args.Length() != 1) {
+    return StringException("Usage: read(filename)");
+  }
+  v8::String::Utf8Value file(args[0]);
+  if (*file == NULL) {
+    return StringException("read(): Missing filename");
+  }
+  v8::Handle<v8::Value> source = ReadFile(*file);
+  if (source.IsEmpty() || !source->IsString()) {
+    return StringException("read(): Error loading file");
+  }
+  return source;
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'load'
+// function is called.  Loads, compiles and executes its argument
+// JavaScript file.
+v8::Handle<v8::Value> Load(const v8::Arguments& args) {
+  for (int i = 0; i < args.Length(); i++) {
+    v8::HandleScope handle_scope;
+    v8::String::Utf8Value filename(args[i]);
+    if (*filename == NULL) {
+      return StringException("load(): Missing filename");
+    }
+    v8::Handle<v8::String> source = v8::Handle<v8::String>::Cast(
+        ReadFile(*filename));
+    if (source.IsEmpty() || !source->IsString()) {
+      return StringException("load(): Error loading file");
+    }
+    if (!ExecuteString(source, v8::String::New(*filename), false, false)) {
+      return StringException("load(): Error executing file");
+    }
+  }
+  return v8::Undefined();
+}
+
+
+// The callback that is invoked by v8 whenever the JavaScript 'quit'
+// function is called.  Quits.
+v8::Handle<v8::Value> Quit(const v8::Arguments& args) {
+  // If not arguments are given args[0] will yield undefined which
+  // converts to the integer value 0.
+  int exit_code = args[0]->Int32Value();
+  fflush(stdout);
+  fflush(stderr);
+  exit(exit_code);
+  return v8::Undefined();
+}
+
+
+v8::Handle<v8::Value> Version(const v8::Arguments& args) {
+  return v8::String::New(v8::V8::GetVersion());
+}
+
+
+// Reads a file into a v8 string.
+v8::Handle<v8::Value> ReadFile(const char* name) {
+  int fd = open(name, O_RDONLY);
+  if (fd < 0) return ErrnoException(name, errno);
+
+  struct stat st;
+  if (fstat(fd, &st) < 0) return ErrnoException("fstat", errno);
+  if (!S_ISREG(st.st_mode)) {
+    return ErrnoException("not a regular file", EINVAL);
+  }
+
+  errno = 0;
+  off_t size = lseek(fd, 0, SEEK_END);
+  if (errno) return ErrnoException("lseek(end)", errno);
+  lseek(fd, 0, SEEK_SET);
+  if (errno) return ErrnoException("lseek(0)", errno);
+
+  char* chars = new char[size + 1];
+  int nread = 0;
+  while (nread < size) {
+    int got = read(fd, &chars[nread], size - nread);
+    if (got < 0) {
+      int errnum = errno;
+      delete[] chars;
+      return ErrnoException("read", errnum);
+    } else if (!got) {
+      break;
+    }
+    nread += got;
+  }
+  chars[nread] = 0;
+  close(fd);
+  v8::Handle<v8::String> result = v8::String::New(chars, size);
+  delete[] chars;
+  return result;
+}
+
+
+// Process remaining command line arguments and execute files
+int RunMain(int argc, char* argv[]) {
+  for (int i = 1; i < argc; i++) {
+    const char* str = argv[i];
+    if (strcmp(str, "--shell") == 0) {
+      run_shell = true;
+    } else if (strcmp(str, "-f") == 0) {
+      // Ignore any -f flags for compatibility with the other stand-
+      // alone JavaScript engines.
+      continue;
+    } else if (strncmp(str, "--", 2) == 0) {
+      fprintf(stderr, "Warning: unknown flag %s.\nTry --help for options\n", str);
+    } else if (strcmp(str, "-e") == 0 && i + 1 < argc) {
+      // Execute argument given to -e option directly.
+      v8::Handle<v8::String> file_name = v8::String::New("unnamed");
+      v8::Handle<v8::String> source = v8::String::New(argv[++i]);
+      if (!ExecuteString(source, file_name, false, true)) return 1;
+    } else {
+      // Use all other arguments as names of files to load and run.
+      v8::Handle<v8::String> file_name = v8::String::New(str);
+      v8::Handle<v8::String> source = v8::Handle<v8::String>::Cast(
+          ReadFile(str));
+      if (source.IsEmpty() || !source->IsString()) {
+        fprintf(stderr, "Error reading '%s'\n", str);
+        continue;
+      }
+      if (!ExecuteString(source, file_name, false, true)) return 1;
+    }
+  }
+  return 0;
+}
+
+
+// The read-eval-execute loop of the shell.
+void RunShell(v8::Handle<v8::Context> context) {
+  fprintf(stderr, "V8 version %s [sample shell]\n", v8::V8::GetVersion());
+  static const int kBufferSize = 256;
+  // Enter the execution environment before evaluating any code.
+  v8::Context::Scope context_scope(context);
+  v8::Local<v8::String> name(v8::String::New("(shell)"));
+  while (true) {
+    char buffer[kBufferSize];
+    fprintf(stderr, "> ");
+    char* str = fgets(buffer, kBufferSize, stdin);
+    if (str == NULL) break;
+    v8::HandleScope handle_scope;
+    ExecuteString(v8::String::New(str), name, true, true);
+  }
+  printf("\n");
+}
+
+
+// Executes a string within the current v8 context.
+bool ExecuteString(v8::Handle<v8::String> source,
+                   v8::Handle<v8::Value> name,
+                   bool print_result,
+                   bool report_exceptions) {
+  v8::HandleScope handle_scope;
+  v8::TryCatch try_catch;
+  v8::Handle<v8::Script> script = v8::Script::Compile(source, name);
+  if (script.IsEmpty()) {
+    // Print errors that happened during compilation.
+    if (report_exceptions)
+      ReportException(&try_catch);
+    return false;
+  } else {
+    v8::Handle<v8::Value> result = script->Run();
+    if (result.IsEmpty()) {
+      assert(try_catch.HasCaught());
+      // Print errors that happened during execution.
+      if (report_exceptions)
+        ReportException(&try_catch);
+      return false;
+    } else {
+      assert(!try_catch.HasCaught());
+      if (print_result && !result->IsUndefined()) {
+        // If all went well and the result wasn't undefined then print
+        // the returned value.
+        v8::String::Utf8Value str(result);
+        printf("%s\n", ToCString(str));
+      }
+      return true;
+    }
+  }
+}
+
+
+void ReportException(v8::TryCatch* try_catch) {
+  v8::HandleScope handle_scope;
+  v8::String::Utf8Value exception(try_catch->Exception());
+  const char* exception_string = ToCString(exception);
+  v8::Handle<v8::Message> message = try_catch->Message();
+  if (message.IsEmpty()) {
+    // V8 didn't provide any extra information about this error; just
+    // print the exception.
+    fprintf(stderr, "%s\n", exception_string);
+  } else {
+    // Print (filename):(line number): (message).
+    v8::String::Utf8Value filename(message->GetScriptResourceName());
+    const char* filename_string = ToCString(filename);
+    int linenum = message->GetLineNumber();
+    fprintf(stderr, "%s:%i: %s\n", filename_string, linenum, exception_string);
+    // Print line of source code.
+    v8::String::Utf8Value sourceline(message->GetSourceLine());
+    const char* sourceline_string = ToCString(sourceline);
+    fprintf(stderr, "%s\n", sourceline_string);
+    // Print wavy underline (GetUnderline is deprecated).
+    int start = message->GetStartColumn();
+    for (int i = 0; i < start; i++) {
+      fprintf(stderr, " ");
+    }
+    int end = message->GetEndColumn();
+    for (int i = start; i < end; i++) {
+      fprintf(stderr, "^");
+    }
+    fprintf(stderr, "\n");
+    v8::String::Utf8Value stack_trace(try_catch->StackTrace());
+    if (stack_trace.length() > 0) {
+      const char* stack_trace_string = ToCString(stack_trace);
+      fprintf(stderr, "%s\n", stack_trace_string);
+    }
+  }
+}
diff --git a/wvtest/javascript/wvtest.js b/wvtest/javascript/wvtest.js
new file mode 100644
index 0000000..6d7d1b8
--- /dev/null
+++ b/wvtest/javascript/wvtest.js
@@ -0,0 +1,152 @@
+
+var files = {};
+
+function lookup(filename, line) {
+    var f = files[filename];
+    if (!f) {
+        try {
+            f = files[filename] = read(filename).split('\n');
+        } catch (e) {
+            f = files[filename] = [];
+        }
+    }
+    return f[line-1] || 'BAD_LINE'; // file line numbers are 1-based
+}
+
+
+// TODO(apenwarr): Right now this only really works right on chrome.
+// Maybe take some advice from this article:
+//  http://stackoverflow.com/questions/5358983/javascript-stack-inspection-on-safari-mobile-ipad
+function trace() {
+    var FILELINE_RE = /[\b\s]\(?([^:\s]+):(\d+)/;
+    var out = [];
+    var e = Error().stack;
+    if (!e) {
+        return [['UNKNOWN', 0], ['UNKNOWN', 0]];
+    }
+    var lines = e.split('\n');
+    for (i in lines) {
+	if (i > 2) {
+	    g = lines[i].match(FILELINE_RE);
+	    if (g) {
+		out.push([g[1], parseInt(g[2])]);
+	    } else {
+		out.push(['UNKNOWN', 0]);
+	    }
+	}
+    }
+    return out;
+}
+
+
+function _pad(len, s) {
+    s += '';
+    while (s.length < len) {
+	s += ' ';
+    }
+    return s;
+}
+
+
+function _check(cond, trace, condstr) {
+    print('!', _pad(15, trace[0] + ':' + trace[1]),
+	  _pad(54, condstr),
+	  cond ? 'ok' : 'FAILED');
+}
+
+
+function _content(trace) {
+    var WV_RE = /WV[\w_]+\((.*)\)/;
+    var line = lookup(trace[0], trace[1]);
+    var g = line.match(WV_RE);
+    return g ? g[1] : '...';
+}
+
+
+function WVPASS(cond) {
+    var t = trace()[1];
+    if (arguments.length >= 1) {
+	var condstr = _content(t);
+	return _check(cond, t, condstr);
+    } else {
+	// WVPASS() with no arguments is a pass, although cond would
+	// default to false
+	return _check(true, t, '');
+    }
+}
+
+
+function WVFAIL(cond) {
+    var t = trace()[1];
+    if (arguments.length >= 1) {
+	var condstr = 'NOT(' + _content(t) + ')';
+	return _check(!cond, t, condstr);
+    } else {
+	// WVFAIL() with no arguments is a fail, although cond would
+	// default to false (which is a pass)
+	return _check(false, t, 'NOT()')
+    }
+}
+
+
+function WVEXCEPT(etype, func) {
+    var t = trace()[1];
+    try {
+	func();
+    } catch (e) {
+	return _check(e instanceof etype, t, e);
+    }
+    return _check(false, t, 'no exception: ' + etype);
+}
+
+
+function WVPASSEQ(a, b, precision) {
+    var t = trace()[1];
+    if (a && b && a.join && b.join) {
+        a = a.join('|');
+        b = b.join('|');
+    }
+    var cond = precision ? Math.abs(a-b) < precision : (a == b);
+    return _check(cond, t, '' + a + ' == ' + b);
+}
+
+
+function WVPASSNE(a, b, precision) {
+    var t = trace()[1];
+    if (a.join && b.join) {
+        a = a.join('|');
+        b = b.join('|');
+    }
+    var cond = precision ? Math.abs(a-b) >= precision : (a != b);
+    return _check(a != b, t, '' + a + ' != ' + b);
+}
+
+
+function WVPASSLT(a, b) {
+    var t = trace()[1];
+    return _check(a < b, t, '' + a + ' < ' + b);
+}
+
+
+function WVPASSGT(a, b) {
+    var t = trace()[1];
+    return _check(a > b, t, '' + a + ' > ' + b);
+}
+
+
+function WVPASSLE(a, b) {
+    var t = trace()[1];
+    return _check(a <= b, t, '' + a + ' <= ' + b);
+}
+
+
+function WVPASSGE(a, b) {
+    var t = trace()[1];
+    return _check(a >= b, t, '' + a + ' >= ' + b);
+}
+
+
+function wvtest(name, f) {
+    print('\nTesting "' + name + '" in ' + trace()[1][0] + ':');
+    return f();
+}
diff --git a/wvtest/python/Makefile b/wvtest/python/Makefile
new file mode 100644
index 0000000..5a2d068
--- /dev/null
+++ b/wvtest/python/Makefile
@@ -0,0 +1,17 @@
+
+all:
+	@echo "Try: make test"
+	@false
+
+runtests:
+	./wvtest.py \
+		$(patsubst ./%t,%t/*.py,$(shell find -type d -name t)) \
+		basedir_test.py
+	python t/twvtest.py
+	python basedir_test.py
+
+test:
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~ *.pyc t/*.pyc
\ No newline at end of file
diff --git a/wvtest/python/__init__.py b/wvtest/python/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wvtest/python/__init__.py
diff --git a/wvtest/python/basedir_test.py b/wvtest/python/basedir_test.py
new file mode 100644
index 0000000..932f9d2
--- /dev/null
+++ b/wvtest/python/basedir_test.py
@@ -0,0 +1,12 @@
+from wvtest import *
+
+
+@wvtest
+def basedirtest():
+    # check that wvtest works with test files in the base directory, not
+    # just in subdirs.
+    WVPASS()
+
+
+if __name__ == '__main__':
+    wvtest_main()
diff --git a/wvtest/python/t/__init__.py b/wvtest/python/t/__init__.py
new file mode 100644
index 0000000..84815ba
--- /dev/null
+++ b/wvtest/python/t/__init__.py
@@ -0,0 +1,3 @@
+import os, sys
+parentdir = os.path.join(os.path.abspath(os.path.split(__file__)[0]), '..')
+sys.path.append(parentdir)
diff --git a/wvtest/python/t/testfile.txt b/wvtest/python/t/testfile.txt
new file mode 100644
index 0000000..3b18e51
--- /dev/null
+++ b/wvtest/python/t/testfile.txt
@@ -0,0 +1 @@
+hello world
diff --git a/wvtest/python/t/twvtest.py b/wvtest/python/t/twvtest.py
new file mode 100644
index 0000000..8224794
--- /dev/null
+++ b/wvtest/python/t/twvtest.py
@@ -0,0 +1,71 @@
+import __init__
+from wvtest import *
+import twvtest2  # twvtest2 will also run *before* us since we import it
+
+last=None
+
+def _except(*args):
+    raise IOError(*args)
+
+
+@wvtest
+def moretest():
+    WVPASSEQ(twvtest2.count, 1)
+
+
+@wvtest
+def test1():
+    WVPASSIS(None, None)
+    WVPASSISNOT(None, [])
+    WVPASSISNOT({}, {})
+    d = {}
+    WVPASSIS(d, d)
+    WVPASSEQ(1, 1)
+    WVPASSNE(1, 2)
+    WVPASSLT(1, 2)
+    WVPASSLE(1, 1)
+    WVPASSGT(2, 1)
+    WVPASSGE(2, 2)
+    WVPASSNEAR(1, 1)
+    WVPASSFAR(1, 0)
+    WVPASSNEAR(1, 1.0)
+    WVPASSFAR(0.1, 0.2)
+    WVPASSNEAR(0.000000005, 0.000000006)
+    WVPASSFAR(0.000000005, 0.000000006, places=9)
+    WVPASSNEAR(0.51, 0.53, delta=0.021)
+    WVPASSFAR(0.51, 0.53, delta=0.019)
+    WVEXCEPT(IOError, _except, 'my exception parameter')
+    with WVEXCEPT(IOError):
+      _except('arg')
+
+    # ensure tests run in the order they were declared
+    global last
+    WVPASSEQ(last, None)
+    last='test1'
+
+@wvtest
+def booga2():
+    # ensure tests run in the order they were declared
+    global last
+    WVPASSEQ(last, 'test1')
+    last='booga2'
+
+@wvtest
+def booga1():
+    # ensure tests run in the order they were declared
+    global last
+    WVPASSEQ(last, 'booga2')
+    last='booga1'
+
+
+@wvtest
+def chdir_test():
+    WVPASS(open('testfile.txt')) # will fail if chdir is wrong
+
+
+if __name__ == '__main__':
+    WVPASSEQ(twvtest2.count, 0)
+    wvtest_main()
+    wvtest_main()
+    WVPASSEQ(last, 'booga1')
+    WVPASSEQ(twvtest2.count, 1)
diff --git a/wvtest/python/t/twvtest2.py b/wvtest/python/t/twvtest2.py
new file mode 100644
index 0000000..447cd28
--- /dev/null
+++ b/wvtest/python/t/twvtest2.py
@@ -0,0 +1,11 @@
+from wvtest import *
+
+
+count = 0
+
+
+@wvtest
+def moretest():
+    global count
+    count += 1
+    WVPASS()
diff --git a/wvtest/python/unittest.py b/wvtest/python/unittest.py
new file mode 100644
index 0000000..78a9525
--- /dev/null
+++ b/wvtest/python/unittest.py
@@ -0,0 +1,89 @@
+import sys
+import traceback
+import wvtest
+
+
+class _Meta(type):
+  def __init__(cls, name, bases, attrs):
+    type.__init__(cls, name, bases, attrs)
+    print 'registering class %r' % name
+    for t in dir(cls):
+      if t.startswith('test'):
+        # TODO(apenwarr): inside a class, sort by source code line number
+        print 'registering func %r' % t
+        def DefineGo(t):
+          def Go():
+            o = cls(t)
+            o.setUp()
+            try:
+              getattr(o, t)()
+            except Exception, e:
+              print
+              print traceback.format_exc()
+              tb = sys.exc_info()[2]
+              wvtest._result(repr(e), traceback.extract_tb(tb)[-1],
+                             'EXCEPTION')
+            finally:
+              o.tearDown()
+          return Go
+        wvtest.wvtest(DefineGo(t), getattr(cls, t))
+
+
+class TestCase():
+  __metaclass__ = _Meta
+
+  def __init__(self, testname):
+    pass
+
+  def setUp(self):
+    pass
+
+  def tearDown(self):
+    pass
+
+  def assertTrue(self, a, unused_msg=''):
+    return wvtest.WVPASS(a, xdepth=1)
+
+  def assertFalse(self, a, unused_msg=''):
+    return wvtest.WVFAIL(a, xdepth=1)
+
+  def assertIs(self, a, b):
+    return wvtest.WVPASSIS(a, b, xdepth=1)
+
+  def assertIsNot(self, a, b):
+    return wvtest.WVPASSISNOT(a, b, xdepth=1)
+
+  def assertEqual(self, a, b):
+    return wvtest.WVPASSEQ(a, b, xdepth=1)
+
+  def assertNotEqual(self, a, b):
+    return wvtest.WVPASSNE(a, b, xdepth=1)
+
+  def assertGreaterEqual(self, a, b):
+    return wvtest.WVPASSGE(a, b, xdepth=1)
+
+  def assertGreaterThan(self, a, b):
+    return wvtest.WVPASSGT(a, b, xdepth=1)
+
+  def assertLessEqual(self, a, b):
+    return wvtest.WVPASSLE(a, b, xdepth=1)
+
+  def assertLessThan(self, a, b):
+    return wvtest.WVPASSLT(a, b, xdepth=1)
+
+  def assertAlmostEqual(self, a, b, places=7, delta=None):
+    return wvtest.WVPASSNEAR(a, b, places=places, delta=delta, xdepth=1)
+
+  def assertNotAlmostEqual(self, a, b, places=7, delta=None):
+    return wvtest.WVPASSFAR(a, b, places=places, delta=delta, xdepth=1)
+
+  def assertRaises(self, etype, func=None, *args, **kwargs):
+    return wvtest._WVEXCEPT(etype, 0, func, *args, **kwargs)
+
+  assertEquals = assertEqual
+  assertNowEquals = assertNotEqual
+  assertGreater = assertGreaterThan
+  assertLess = assertLessThan
+
+def main():
+  wvtest.wvtest_main()
diff --git a/wvtest/python/wvtest.py b/wvtest/python/wvtest.py
new file mode 100755
index 0000000..8f2564c
--- /dev/null
+++ b/wvtest/python/wvtest.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python
+#
+# WvTest:
+#   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#       You can get wvtest from: http://github.com/apenwarr/wvtest
+#
+import atexit
+import inspect
+import os
+import re
+import sys
+import traceback
+
+# NOTE
+# Why do we do we need the "!= main" check?  Because if you run
+# wvtest.py as a main program and it imports your test files, then
+# those test files will try to import the wvtest module recursively.
+# That actually *works* fine, because we don't run this main program
+# when we're imported as a module.  But you end up with two separate
+# wvtest modules, the one that gets imported, and the one that's the
+# main program.  Each of them would have duplicated global variables
+# (most importantly, wvtest._registered), and so screwy things could
+# happen.  Thus, we make the main program module *totally* different
+# from the imported module.  Then we import wvtest (the module) into
+# wvtest (the main program) here and make sure to refer to the right
+# versions of global variables.
+#
+# All this is done just so that wvtest.py can be a single file that's
+# easy to import into your own applications.
+if __name__ != '__main__':   # we're imported as a module
+    _registered = []
+    _tests = 0
+    _fails = 0
+
+    def wvtest(func, innerfunc=None):
+        """ Use this decorator (@wvtest) in front of any function you want to
+            run as part of the unit test suite.  Then run:
+                python wvtest.py path/to/yourtest.py [other test.py files...]
+            to run all the @wvtest functions in the given file(s).
+        """
+        _registered.append((func, innerfunc or func))
+        return func
+
+
+    def _result(msg, tb, code):
+        global _tests, _fails
+        _tests += 1
+        if code != 'ok':
+            _fails += 1
+        (filename, line, func, text) = tb
+        filename = os.path.basename(filename)
+        msg = re.sub(r'\s+', ' ', str(msg))
+        sys.stderr.flush()
+        print '! %-70s %s' % ('%s:%-4d %s' % (filename, line, msg),
+                              code)
+        sys.stdout.flush()
+
+
+    def _check(cond, msg, xdepth):
+        tb = traceback.extract_stack()[-3 - xdepth]
+        if cond:
+            _result(msg, tb, 'ok')
+        else:
+            _result(msg, tb, 'FAILED')
+        return cond
+
+
+    def _code(xdepth):
+        (filename, line, func, text) = traceback.extract_stack()[-3 - xdepth]
+        text = re.sub(r'^[\w\.]+\((.*)\)(\s*#.*)?$', r'\1', str(text));
+        return text
+
+
+    def WVPASS(cond = True, xdepth = 0):
+        ''' Counts a test failure unless cond is true. '''
+        return _check(cond, _code(xdepth), xdepth)
+
+    def WVFAIL(cond = True, xdepth = 0):
+        ''' Counts a test failure  unless cond is false. '''
+        return _check(not cond, 'NOT(%s)' % _code(xdepth), xdepth)
+
+    def WVPASSIS(a, b, xdepth = 0):
+        ''' Counts a test failure unless a is b. '''
+        return _check(a is b, '%s is %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSISNOT(a, b, xdepth = 0):
+        ''' Counts a test failure unless a is not b. '''
+        return _check(a is not b, '%s is not %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSEQ(a, b, xdepth = 0):
+        ''' Counts a test failure unless a == b. '''
+        return _check(a == b, '%s == %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSNE(a, b, xdepth = 0):
+        ''' Counts a test failure unless a != b. '''
+        return _check(a != b, '%s != %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSLT(a, b, xdepth = 0):
+        ''' Counts a test failure unless a < b. '''
+        return _check(a < b, '%s < %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSLE(a, b, xdepth = 0):
+        ''' Counts a test failure unless a <= b. '''
+        return _check(a <= b, '%s <= %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSGT(a, b, xdepth = 0):
+        ''' Counts a test failure unless a > b. '''
+        return _check(a > b, '%s > %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSGE(a, b, xdepth = 0):
+        ''' Counts a test failure unless a >= b. '''
+        return _check(a >= b, '%s >= %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSNEAR(a, b, places = 7, delta = None, xdepth = 0):
+        ''' Counts a test failure unless a ~= b. '''
+        if delta:
+            return _check(abs(a - b) <= abs(delta),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+        else:
+            return _check(round(a, places) == round(b, places),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+
+    def WVPASSFAR(a, b, places = 7, delta = None, xdepth = 0):
+        ''' Counts a test failure unless a ~!= b. '''
+        if delta:
+            return _check(abs(a - b) > abs(delta),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+        else:
+            return _check(round(a, places) != round(b, places),
+                          '%s ~= %s' % (repr(a), repr(b)), xdepth)
+
+    def _except_report(cond, code, xdepth):
+        return _check(cond, 'EXCEPT(%s)' % code, xdepth + 1)
+
+    class _ExceptWrapper(object):
+        def __init__(self, etype, xdepth):
+            self.etype = etype
+            self.xdepth = xdepth
+            self.code = None
+
+        def __enter__(self):
+          self.code = _code(self.xdepth)
+
+        def __exit__(self, etype, value, traceback):
+            if etype == self.etype:
+                _except_report(True, self.code, self.xdepth)
+                return 1  # success, got the expected exception
+            elif etype is None:
+                _except_report(False, self.code, self.xdepth)
+                return 0
+            else:
+                _except_report(False, self.code, self.xdepth)
+
+    def _WVEXCEPT(etype, xdepth, func, *args, **kwargs):
+        if func:
+            code = _code(xdepth + 1)
+            try:
+                func(*args, **kwargs)
+            except etype, e:
+                return _except_report(True, code, xdepth + 1)
+            except:
+                _except_report(False, code, xdepth + 1)
+                raise
+            else:
+                return _except_report(False, code, xdepth + 1)
+        else:
+            return _ExceptWrapper(etype, xdepth)
+
+    def WVEXCEPT(etype, func=None, *args, **kwargs):
+        ''' Counts a test failure unless func throws an 'etype' exception.
+            You have to spell out the function name and arguments, rather than
+            calling the function yourself, so that WVEXCEPT can run before
+            your test code throws an exception.
+        '''
+        return _WVEXCEPT(etype, 0, func, *args, **kwargs)
+
+
+    def _check_unfinished():
+        if _registered:
+            for func, innerfunc in _registered:
+                print 'WARNING: not run: %r' % (innerfunc,)
+            WVFAIL('wvtest_main() not called')
+        if _fails:
+            sys.exit(1)
+
+    atexit.register(_check_unfinished)
+
+
+def _run_in_chdir(path, func, *args, **kwargs):
+    oldwd = os.getcwd()
+    oldpath = sys.path
+    try:
+        if path: os.chdir(path)
+        sys.path += [path, os.path.split(path)[0]]
+        return func(*args, **kwargs)
+    finally:
+        os.chdir(oldwd)
+        sys.path = oldpath
+
+
+def _runtest(fname, f, innerfunc):
+    import wvtest as _wvtestmod
+    mod = inspect.getmodule(innerfunc)
+    relpath = os.path.relpath(mod.__file__, os.getcwd()).replace('.pyc', '.py')
+    print
+    print 'Testing "%s" in %s:' % (fname, relpath)
+    sys.stdout.flush()
+    try:
+        _run_in_chdir(os.path.split(mod.__file__)[0], f)
+    except Exception, e:
+        print
+        print traceback.format_exc()
+        tb = sys.exc_info()[2]
+        _wvtestmod._result(repr(e), traceback.extract_tb(tb)[-1], 'EXCEPTION')
+
+
+def _run_registered_tests():
+    import wvtest as _wvtestmod
+    while _wvtestmod._registered:
+        func, innerfunc = _wvtestmod._registered.pop(0)
+        _runtest(innerfunc.func_name, func, innerfunc)
+        print
+
+
+def wvtest_main(extra_testfiles=[]):
+    import wvtest as _wvtestmod
+    _run_registered_tests()
+    for modname in extra_testfiles:
+        if not os.path.exists(modname):
+            print 'Skipping: %s' % modname
+            continue
+        if modname.endswith('.py'):
+            modname = modname[:-3]
+        print 'Importing: %s' % modname
+        path, mod = os.path.split(os.path.abspath(modname))
+        nicename = modname.replace(os.path.sep, '.')
+        while nicename.startswith('.'):
+            nicename = modname[1:]
+        _run_in_chdir(path, __import__, nicename, None, None, [])
+        _run_registered_tests()
+    print
+    print 'WvTest: %d tests, %d failures.' % (_wvtestmod._tests,
+                                              _wvtestmod._fails)
+
+
+if __name__ == '__main__':
+    import wvtest as _wvtestmod
+    sys.modules['wvtest'] = _wvtestmod
+    sys.modules['wvtest.wvtest'] = _wvtestmod
+    wvtest_main(sys.argv[1:])
diff --git a/wvtest/sample-error b/wvtest/sample-error
new file mode 100644
index 0000000..3890d6d
--- /dev/null
+++ b/wvtest/sample-error
@@ -0,0 +1,5 @@
+Testing "my error test function" in mytest.t.cc:
+! mytest.t.cc:432     thing.works()         ok
+This is just some crap that I printed while counting to 3.
+! mytest.t.cc.433     3 < 4                 FAILED
+
diff --git a/wvtest/sample-ok b/wvtest/sample-ok
new file mode 100644
index 0000000..82047d7
--- /dev/null
+++ b/wvtest/sample-ok
@@ -0,0 +1,5 @@
+Testing "my ok test function" in mytest.t.cc:
+! mytest.t.cc:432     thing.works()         ok
+This is just some crap that I printed while counting to 3.
+! mytest.t.cc.433     3 < 4                 ok
+
diff --git a/wvtest/sh/Makefile b/wvtest/sh/Makefile
new file mode 100644
index 0000000..1f31192
--- /dev/null
+++ b/wvtest/sh/Makefile
@@ -0,0 +1,13 @@
+
+all:
+	@echo "Try: make test"
+	@false
+
+runtests:
+	t/twvtest.sh
+
+test:
+	../wvtestrun $(MAKE) runtests
+
+clean::
+	rm -f *~ t/*~
diff --git a/wvtest/sh/t/twvtest.sh b/wvtest/sh/t/twvtest.sh
new file mode 100755
index 0000000..c02871a
--- /dev/null
+++ b/wvtest/sh/t/twvtest.sh
@@ -0,0 +1,16 @@
+#!/bin/sh
+. ./wvtest.sh
+
+WVSTART "main test"
+WVPASS true
+WVPASS true
+WVPASS true
+WVFAIL false
+WVPASSEQ "$(ls | sort)" "$(ls)"
+WVPASSNE "5" "5 "
+WVPASSEQ "" ""
+(echo nested test; true); WVPASSRC $?
+(echo nested fail; false); WVFAILRC $?
+
+WVSTART another test
+WVPASS true
diff --git a/wvtest/sh/wvtest.sh b/wvtest/sh/wvtest.sh
new file mode 100644
index 0000000..47b4366
--- /dev/null
+++ b/wvtest/sh/wvtest.sh
@@ -0,0 +1,140 @@
+#
+# WvTest:
+#   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#       You can get wvtest from: http://github.com/apenwarr/wvtest
+#
+# Include this file in your shell script by using:
+#         #!/bin/sh
+#         . ./wvtest.sh
+#
+
+# we don't quote $TEXT in case it contains newlines; newlines
+# aren't allowed in test output.  However, we set -f so that
+# at least shell glob characters aren't processed.
+_wvtextclean()
+{
+	( set -f; echo $* )
+}
+
+
+if [ -n "$BASH_VERSION" ]; then
+	_wvfind_caller()
+	{
+		LVL=$1
+		WVCALLER_FILE=${BASH_SOURCE[2]}
+		WVCALLER_LINE=${BASH_LINENO[1]}
+	}
+else
+	_wvfind_caller()
+	{
+		LVL=$1
+		WVCALLER_FILE="unknown"
+		WVCALLER_LINE=0
+	}
+fi
+
+
+_wvcheck()
+{
+	CODE="$1"
+	TEXT=$(_wvtextclean "$2")
+	OK=ok
+	if [ "$CODE" -ne 0 ]; then
+		OK=FAILED
+	fi
+	echo "! $WVCALLER_FILE:$WVCALLER_LINE  $TEXT  $OK" >&2
+	if [ "$CODE" -ne 0 ]; then
+		exit $CODE
+	else
+		return 0
+	fi
+}
+
+
+WVPASS()
+{
+	TEXT="$*"
+
+	_wvfind_caller
+	if "$@"; then
+		_wvcheck 0 "$TEXT"
+		return 0
+	else
+		_wvcheck 1 "$TEXT"
+		# NOTREACHED
+		return 1
+	fi
+}
+
+
+WVFAIL()
+{
+	TEXT="$*"
+
+	_wvfind_caller
+	if "$@"; then
+		_wvcheck 1 "NOT($TEXT)"
+		# NOTREACHED
+		return 1
+	else
+		_wvcheck 0 "NOT($TEXT)"
+		return 0
+	fi
+}
+
+
+_wvgetrv()
+{
+	( "$@" >&2 )
+	echo -n $?
+}
+
+
+WVPASSEQ()
+{
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ "$#" -eq 2 ]) "exactly 2 arguments"
+	echo "Comparing:" >&2
+	echo "$1" >&2
+	echo "--" >&2
+	echo "$2" >&2
+	_wvcheck $(_wvgetrv [ "$1" = "$2" ]) "'$1' = '$2'"
+}
+
+
+WVPASSNE()
+{
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ "$#" -eq 2 ]) "exactly 2 arguments"
+	echo "Comparing:" >&2
+	echo "$1" >&2
+	echo "--" >&2
+	echo "$2" >&2
+	_wvcheck $(_wvgetrv [ "$1" != "$2" ]) "'$1' != '$2'"
+}
+
+
+WVPASSRC()
+{
+	RC=$?
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ $RC -eq 0 ]) "return code($RC) == 0"
+}
+
+
+WVFAILRC()
+{
+	RC=$?
+	_wvfind_caller
+	_wvcheck $(_wvgetrv [ $RC -ne 0 ]) "return code($RC) != 0"
+}
+
+
+WVSTART()
+{
+	echo >&2
+	_wvfind_caller
+	echo "Testing \"$*\" in $WVCALLER_FILE:" >&2
+}
diff --git a/wvtest/wvtestrun b/wvtest/wvtestrun
new file mode 100755
index 0000000..897b95f
--- /dev/null
+++ b/wvtest/wvtestrun
@@ -0,0 +1,187 @@
+#!/usr/bin/perl -w
+#
+# WvTest:
+#   Copyright (C)2007-2012 Versabanq Innovations Inc. and contributors.
+#       Licensed under the GNU Library General Public License, version 2.
+#       See the included file named LICENSE for license information.
+#       You can get wvtest from: http://github.com/apenwarr/wvtest
+#
+use strict;
+use Time::HiRes qw(time);
+
+# always flush
+$| = 1;
+
+if (@ARGV < 1) {
+    print STDERR "Usage: $0 <command line...>\n";
+    exit 127;
+}
+
+print STDERR "Testing \"all\" in @ARGV:\n";
+
+my $pid = open(my $fh, "-|");
+if (!$pid) {
+    # child
+    setpgrp();
+    open STDERR, '>&STDOUT' or die("Can't dup stdout: $!\n");
+    exec(@ARGV);
+    exit 126; # just in case
+}
+
+my $istty = -t STDOUT && $ENV{'TERM'} ne "dumb";
+my @log = ();
+my ($gpasses, $gfails) = (0,0);
+
+sub bigkill($)
+{
+    my $pid = shift;
+
+    if (@log) {
+	print "\n" . join("\n", @log) . "\n";
+    }
+
+    print STDERR "\n! Killed by signal    FAILED\n";
+
+    ($pid > 0) || die("pid is '$pid'?!\n");
+
+    local $SIG{CHLD} = sub { }; # this will wake us from sleep() faster
+    kill 15, $pid;
+    sleep(2);
+
+    if ($pid > 1) {
+	kill 9, -$pid;
+    }
+    kill 9, $pid;
+
+    exit(125);
+}
+
+# parent
+local $SIG{INT} = sub { bigkill($pid); };
+local $SIG{TERM} = sub { bigkill($pid); };
+local $SIG{ALRM} = sub {
+    print STDERR "Alarm timed out!  No test results for too long.\n";
+    bigkill($pid);
+};
+
+sub colourize($)
+{
+    my $result = shift;
+    my $pass = ($result eq "ok");
+
+    if ($istty) {
+	my $colour = $pass ? "\e[32;1m" : "\e[31;1m";
+	return "$colour$result\e[0m";
+    } else {
+	return $result;
+    }
+}
+
+sub mstime($$$)
+{
+    my ($floatsec, $warntime, $badtime) = @_;
+    my $ms = int($floatsec * 1000);
+    my $str = sprintf("%d.%03ds", $ms/1000, $ms % 1000);
+
+    if ($istty && $ms > $badtime) {
+        return "\e[31;1m$str\e[0m";
+    } elsif ($istty && $ms > $warntime) {
+        return "\e[33;1m$str\e[0m";
+    } else {
+        return "$str";
+    }
+}
+
+sub resultline($$)
+{
+    my ($name, $result) = @_;
+    return sprintf("! %-65s %s", $name, colourize($result));
+}
+
+my $allstart = time();
+my ($start, $stop);
+
+sub endsect()
+{
+    $stop = time();
+    if ($start) {
+	printf " %s %s\n", mstime($stop - $start, 500, 1000), colourize("ok");
+    }
+}
+
+while (<$fh>)
+{
+    chomp;
+    s/\r//g;
+
+    if (/^\s*Testing "(.*)" in (.*):\s*$/)
+    {
+        alarm(120);
+	my ($sect, $file) = ($1, $2);
+
+	endsect();
+
+	printf("! %s  %s: ", $file, $sect);
+	@log = ();
+	$start = $stop;
+    }
+    elsif (/^!\s*(.*?)\s+(\S+)\s*$/)
+    {
+        alarm(120);
+
+	my ($name, $result) = ($1, $2);
+	my $pass = ($result eq "ok");
+
+	if (!$start) {
+	    printf("\n! Startup: ");
+	    $start = time();
+	}
+
+	push @log, resultline($name, $result);
+
+	if (!$pass) {
+	    $gfails++;
+	    if (@log) {
+		print "\n" . join("\n", @log) . "\n";
+		@log = ();
+	    }
+	} else {
+	    $gpasses++;
+	    print ".";
+	}
+    }
+    else
+    {
+	push @log, $_;
+    }
+}
+
+endsect();
+
+my $newpid = waitpid($pid, 0);
+if ($newpid != $pid) {
+    die("waitpid returned '$newpid', expected '$pid'\n");
+}
+
+my $code = $?;
+my $ret = ($code >> 8);
+
+# return death-from-signal exits as >128.  This is what bash does if you ran
+# the program directly.
+if ($code && !$ret) { $ret = $code | 128; }
+
+if ($ret && @log) {
+    print "\n" . join("\n", @log) . "\n";
+}
+
+if ($code != 0) {
+    print resultline("Program returned non-zero exit code ($ret)", "FAILED");
+}
+
+my $gtotal = $gpasses+$gfails;
+printf("\nWvTest: %d test%s, %d failure%s, total time %s.\n",
+    $gtotal, $gtotal==1 ? "" : "s",
+    $gfails, $gfails==1 ? "" : "s",
+    mstime(time() - $allstart, 2000, 5000));
+print STDERR "\nWvTest result code: $ret\n";
+exit( $ret ? $ret : ($gfails ? 125 : 0) );