Merge "Update gpio-mailbox LED driver to support pwm brightness control."
diff --git a/cmds/.gitignore b/cmds/.gitignore
index 358adfb..5189e0e 100644
--- a/cmds/.gitignore
+++ b/cmds/.gitignore
@@ -17,14 +17,17 @@
 gsetsid
 gstatic
 host-*
+http_bouncer
 ionice
 isoping
 isostream
 logos
 mcastreceive
 memwatcher
+mmap
 multicast_join
 netusage
+randint
 randomdata
 readubootver
 realtime
diff --git a/cmds/Makefile b/cmds/Makefile
index a9800fb..134ca85 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -19,12 +19,12 @@
 TARGETS=\
 	$(PORTABLE_TARGETS) \
 	alivemonitor \
-	asus_hosts \
 	bsa2bluez \
 	burnin-flash \
 	buttonmon \
 	chg_mod_own \
 	cpulog \
+	dhcpvendortax \
 	dhcp-rogue \
 	dir-monitor \
 	diskbench \
@@ -49,15 +49,20 @@
 LIB_TARGETS=\
 	stdoutline.so
 HOST_TEST_TARGETS=\
-	host-asus_hosts_test \
 	host-netusage_test \
 	host-utils_test
 SCRIPT_TARGETS=\
 	is-secure-boot
 ARCH_TARGETS=\
 
+ifeq ($(BUILD_ASUS),y)
+TARGETS += asustax
+HOST_TEST_TARGETS += host-asustax_test
+endif
+
 ifeq ($(BUILD_SSDP),y)
-TARGETS += ssdp_poll
+TARGETS += ssdptax
+HOST_TEST_TARGETS += host-test-ssdptax.sh
 endif
 
 ifeq ($(BUILD_DNSSD),y)
@@ -86,10 +91,12 @@
 HOST_CXX ?= g++
 HOST_LD ?= cc
 HOST_PROTOC ?= $(HOSTDIR)/usr/bin/protoc
+GPERF ?= gperf
 CFLAGS += -Wall -Wextra -Wswitch-enum -Werror -Wno-unused-parameter \
 	-g -O -std=c99 -D_GNU_SOURCE $(EXTRACFLAGS)
 CXXFLAGS += -Wall -Wextra -Wswitch-enum -Werror -Wno-unused-parameter \
 	-g -O -std=gnu++0x -D_GNU_SOURCE $(EXTRACXXFLAGS)
+LDFLAGS += $(EXTRALDFLAGS)
 HOST_INCS=-I$(HOSTDIR)/usr/include
 HOST_LIBS=-L$(HOSTDIR)/usr/lib -Wl,-rpath=$(HOSTDIR)/usr/lib
 INCS=-I../libstacktrace
@@ -188,7 +195,16 @@
 http_bouncer: LIBS+=-lcurl $(RT)
 http_bouncer: http_bouncer.o
 host-utils_test: host-utils_test.o host-utils.o
-ssdp_poll: ssdp_poll.o
+asustax: asustax.o l2utils.o
+asustax: LIBS += -lnl-3 -lstdc++ -lm
+host-asustax: host-asustax.o host-l2utils.o
+host-asustax: LIBS += $(HOST_LIBS) -lnl-3 -lstdc++ -lm
+host-asustax_test: host-asustax_test.o
+host-asustax_test: LIBS += $(HOST_LIBS) -lstdc++ -lm
+ssdptax: ssdptax.o l2utils.o
+ssdptax: LIBS += -lcurl -lnl-3 -lstdc++ -lm
+host-ssdptax: host-ssdptax.o host-l2utils.o
+host-ssdptax: LIBS += $(HOST_LIBS) -lcurl -lnl-3 -lstdc++ -lm
 statpitcher.o: device_stats.pb.o
 statpitcher: LIBS+=-L$(DESTDIR)$(PREFIX)/usr/lib -lprotobuf-lite -lpthread -lstdc++
 statpitcher: device_stats.pb.o statpitcher.o
@@ -212,6 +228,13 @@
 wifi_files: LIBS += -lnl-3 -lnl-genl-3 -lglib-2.0
 host-wifi_files_test: host-wifi_files_test.o
 host-wifi_files_test: LIBS += -lnl-3 -lnl-genl-3 -lglib-2.0
+dhcpvendortax: dhcpvendortax.o dhcpvendorlookup.o
+dhcpvendorlookup.c: dhcpvendorlookup.gperf
+	$(GPERF) -G -C -t -L ANSI-C -N exact_match -K vendor_class \
+		--includes --output-file=dhcpvendorlookup.c dhcpvendorlookup.gperf
+dhcpvendorlookup.o: CFLAGS += -Wno-missing-field-initializers
+host-dhcpvendorlookup.o: CFLAGS += -Wno-missing-field-initializers
+host-dhcpvendortax: host-dhcpvendortax.o host-dhcpvendorlookup.o
 
 TESTS = $(wildcard test-*.sh) $(wildcard test-*.py) $(wildcard *_test.py) $(TEST_TARGETS)
 ifeq ($(RUN_HOST_TESTS),y)
diff --git a/cmds/asus_hosts.c b/cmds/asustax.cc
similarity index 88%
rename from cmds/asus_hosts.c
rename to cmds/asustax.cc
index 96ba81f..edbcd4f 100644
--- a/cmds/asus_hosts.c
+++ b/cmds/asustax.cc
@@ -24,6 +24,8 @@
 #include <sys/socket.h>
 #include <unistd.h>
 
+#include "l2utils.h"
+
 #define ASUS_DISCOVERY_PORT 9999
 #define PACKET_LENGTH       512
 
@@ -129,11 +131,17 @@
   return dst;
 }
 
-int receive_response(int s, char *response, int responselen)
+int receive_response(int s, L2Map *l2map, char *response, int responselen)
 {
   struct timeval tv;
   fd_set rfds;
 
+  if (l2map == NULL || response == NULL) {
+    fprintf(stderr, "%s: l2map=%p response=%p\n", __FUNCTION__,
+        l2map, response);
+    exit(1);
+  }
+
   memset(&tv, 0, sizeof(tv));
   tv.tv_sec = 1;
   tv.tv_usec = 0;
@@ -146,7 +154,8 @@
   }
   if (FD_ISSET(s, &rfds)) {
     uint8_t buf[PACKET_LENGTH + 64];
-    char addrbuf[16], namebuf[80];
+    char addrbuf[INET_ADDRSTRLEN], namebuf[80];
+    const char *mac;
     struct sockaddr_in from;
     socklen_t fromlen = sizeof(from);
     asus_discovery_packet_t *discovery = (asus_discovery_packet_t *)buf;
@@ -174,7 +183,13 @@
     id_len = strnlen((char *)discovery->product_id,
                      sizeof(discovery->product_id));
     replace_newlines(discovery->product_id, id_len, namebuf, sizeof(namebuf));
-    snprintf(response, responselen, "%s|%s", addrbuf, namebuf);
+    L2Map::iterator ii = l2map->find(std::string(addrbuf));
+    if (ii != l2map->end()) {
+      mac = ii->second.c_str();
+    } else {
+      mac = "00:00:00:00:00:00";
+    }
+    snprintf(response, responselen, "asus %s %s", mac, namebuf);
 
     return 0;
   } else {
@@ -193,7 +208,7 @@
 int main(int argc, char **argv)
 {
   int s, opt, i;
-  char *ifname = "br0";
+  const char *ifname = "br0";
 
   while ((opt = getopt(argc, argv, "i:")) != -1) {
     switch (opt) {
@@ -213,7 +228,9 @@
   send_discovery(s);
   for (i = 0; i < 128; i++) {
     char response[128];
-    int rc = receive_response(s, response, sizeof(response));
+    L2Map l2map;
+    get_l2_map(&l2map);
+    int rc = receive_response(s, &l2map, response, sizeof(response));
     if (rc < 0) {
       break;
     } else if (rc == 0) {
diff --git a/cmds/asus_hosts_test.c b/cmds/asustax_test.cc
similarity index 96%
rename from cmds/asus_hosts_test.c
rename to cmds/asustax_test.cc
index 2fe98a7..eab37bf 100644
--- a/cmds/asus_hosts_test.c
+++ b/cmds/asustax_test.cc
@@ -18,7 +18,7 @@
 #include <sys/socket.h>
 
 #define UNIT_TESTS
-#include "asus_hosts.c"
+#include "asustax.cc"
 
 /* Taken from a packet capture from an ASUS RT-68U */
 static const unsigned char asus_pkt_normal[] = {
@@ -233,8 +233,9 @@
 {
   int sv[2];
   char response[256];
-  char *expected;
+  const char *expected;
   ssize_t len;
+  L2Map l2map;
 
   if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv)) {
     perror("socketpair");
@@ -249,7 +250,7 @@
   }
 
   response[0] = '\0';
-  if (receive_response(sv[1], response, sizeof(response)) != 0) {
+  if (receive_response(sv[1], &l2map, response, sizeof(response)) != 0) {
     fprintf(stderr, "receive_response could not parse packet\n");
     exit(1);
   }
@@ -268,7 +269,7 @@
   }
 
   response[0] = '\0';
-  if (receive_response(sv[1], response, sizeof(response)) == 0) {
+  if (receive_response(sv[1], &l2map, response, sizeof(response)) == 0) {
     fprintf(stderr, "receive_response should not parse packet\n");
     exit(1);
   }
@@ -286,7 +287,7 @@
   }
 
   response[0] = '\0';
-  if (receive_response(sv[1], response, sizeof(response)) == 0) {
+  if (receive_response(sv[1], &l2map, response, sizeof(response)) == 0) {
     fprintf(stderr, "receive_response should not parse packet\n");
     exit(1);
   }
@@ -304,7 +305,7 @@
   }
 
   response[0] = '\0';
-  if (receive_response(sv[1], response, sizeof(response)) != 0) {
+  if (receive_response(sv[1], &l2map, response, sizeof(response)) != 0) {
     fprintf(stderr, "receive_response could not parse packet\n");
     exit(1);
   }
@@ -315,6 +316,5 @@
     exit(1);
   }
 
-
   exit(0);
 }
diff --git a/cmds/dhcp-rogue.c b/cmds/dhcp-rogue.c
index 8d590bf..2441015 100644
--- a/cmds/dhcp-rogue.c
+++ b/cmds/dhcp-rogue.c
@@ -418,9 +418,8 @@
 
 void usage(const char *progname)
 {
-  fprintf(stderr, "usage: %s [-i br0] [-l]\n", progname);
+  fprintf(stderr, "usage: %s [-i br0]\n", progname);
   fprintf(stderr, "\t-i: name of the interface to probe for DHCP servers.\n");
-  fprintf(stderr, "\t-l: show a response from localhost\n");
   exit(1);
 }
 
diff --git a/cmds/dhcpvendorlookup.gperf b/cmds/dhcpvendorlookup.gperf
new file mode 100644
index 0000000..b4b0ee1
--- /dev/null
+++ b/cmds/dhcpvendorlookup.gperf
@@ -0,0 +1,67 @@
+%{
+/*
+ * 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.
+ */
+
+%}
+struct string_match {
+  char *vendor_class;
+  char *species;
+};
+%%
+6328-2Re, "InnoMedia VoIP adapter"
+AEROHIVE, "Aerohive Wifi AP"
+AirStation Series BUFFALO INC., "Buffalo Wifi AP"
+ArubaAP, "Aruba Wifi AP"
+ArubaInstantAP, "Aruba Wifi AP"
+ccp.avaya.com, "Avaya IP Phone"
+Cisco 802.11n AP Bridge, "Cisco Wifi AP"
+Dell Network Printer, "Dell Printer"
+DUNEHD, "Dune media player"
+ecobee1, "ecobee thermostat"
+HD409N, "ZaapTV"
+Hewlett-Packard JetDirect, "HP Printer"
+Hewlett-Packard LaserJet, "HP LaserJet"
+Hewlett-Packard OfficeJet, "HP OfficeJet"
+iDRAC, "Dell Remote Access Controller"
+ipphone.mitel.com, "Mitel IP Phone"
+IP2061, "Icon IP Phone"
+IWATSUIP, "Icon IP Phone"
+MC361, "Oki Printer"
+MC362, "Oki Printer"
+MERAKI, "Meraki Wifi AP"
+MicroChip Network Stack, "Microchip board"
+Motorola_AP, "Motorola Wifi AP"
+OptiIpPhone, "Siemens IP Phone"
+PS3, "Sony Playstation 3"
+PS4, "Sony Playstation 4"
+PS Vita, "Sony Playstation Vita"
+PS Vita TV, "Sony Playstation Vita"
+Ruckus CPE, "Ruckus Wifi AP"
+SAMSUNG Network Printer, "Samsung Printer"
+SEC_ITP, "Samsung IP Phone"
+ShoreTel IP Phone, "ShoreTel IP Phone"
+SIP-T38G, "Yealink IP Phone"
+SSG5-Serial-WLAN, "Juniper Gateway"
+TOSHIBA IPedge, "Toshiba VoIP adapter"
+ubnt, "Ubiquiti AP"
+VIZIO VIA, "Vizio TV"
+Withings00, "Withings Scale"
+XBOX 1.0, "Xbox"
+Xbox 360, "Xbox 360"
+Xerox Phaser, "Xerox Printer"
+XEROX Network Printer, "Xerox Printer"
+yealink, "Yealink IP Phone"
+%%
diff --git a/cmds/dhcpvendortax.c b/cmds/dhcpvendortax.c
new file mode 100644
index 0000000..a3d4b9d
--- /dev/null
+++ b/cmds/dhcpvendortax.c
@@ -0,0 +1,415 @@
+/*
+ * 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 <getopt.h>
+#include <inttypes.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+struct string_match {
+  char *vendor_class;
+  char *species;
+};
+
+/* Function generated by gperf for the exact_match lookup table. */
+extern const struct string_match *exact_match (const char *str,
+    unsigned int len);
+
+
+struct string_match substring_matches[] = {
+  /*
+   * Examples:
+   *   AastraIPPhone55i
+   *   AastraIPPhone57iCT
+   *   AastraIPPhone6737i
+   */
+  {"AastraIPPhone", "Aastra IP Phone"},
+
+  /* Examples:
+   * AXIS,Network Camera,M3006,5.40.13
+   * AXIS,Network Camera,P3346,5.20.1
+   * AXIS,Thermal Network Camera,Q1931-E,5.55.4.1
+   */
+  {"AXIS,Network Camera", "AXIS Network Camera"},
+  {"AXIS,Thermal Network Camera", "AXIS Network Camera"},
+
+  /* Examples:
+   * Canon MF620C Series
+   */
+  {"Canon MF", "Canon Printer"},
+
+  /* Examples:
+   * Cisco AP c1200
+   * Cisco AP c1240
+   */
+  {"Cisco AP", "Cisco Wifi AP"},
+
+  /* Examples:
+   *   Cisco Systems, Inc. IP Phone CP-7961G
+   *   Cisco Systems, Inc. IP Phone CP-8861
+   */
+  {"Cisco Systems, Inc. IP Phone", "Cisco IP Phone"},
+
+  /* Examples:
+   *   Cisco SPA504G
+   *   Cisco SPA525G2
+   *   CISCO SPA112
+   *   ATA186-H6.0|V3.2.0|B041111A
+   */
+  {"Cisco SPA", "Cisco IP Phone"},
+  {"CISCO SPA", "Cisco IP Phone"},
+  {"ATA186", "Cisco IP Phone"},
+
+  /* Examples:
+   * CPQRIB3
+   */
+  {"CPQRIB", "Compaq Remote Insight"},
+
+  /* Examples:
+   * Dell Color MFP E525w
+   */
+  {"Dell Color MFP", "Dell Printer"},
+
+  /* Examples:
+   * digium_D40_1_4_2_0_63880
+   */
+  {"digium", "Digium IP Phone"},
+
+  /* Examples:
+   * FortiAP-FP321C-AC-Discovery
+   * FortiAP-FP221B-AC-Discovery
+   * FortiAP-FP321C
+   * FortiWiFi-60D-POE
+   */
+  {"FortiAP", "Fortinet Wifi AP"},
+  {"FortiWiFi", "Fortinet Wifi AP"},
+
+  /* Examples:
+   * Grandstream GXP1405 dslforum.org
+   * Grandstream GXP2124 dslforum.org
+   * Grandstream GXV3275 dslforum.org
+   * Grandstream HT702 dslforum.org
+   */
+  {"Grandstream GXP", "Grandstream IP Phone"},
+  {"Grandstream GXV", "Grandstream IP Phone"},
+  {"Grandstream HT", "Grandstream VoIP adapter"},
+
+  /* Examples:
+   * iPECS IP Edge 5000i-24G
+   */
+  {"iPECS IP Edge", "iPECS IP PHONE"},
+
+  /* Examples:
+   * Juniper-ex2200-c-12p-2g
+   */
+  {"Juniper-ex", "Juniper router"},
+
+  /* Examples:
+   *   LINKSYS SPA-922
+   *   LINKSYS SPA-942
+   */
+  {"LINKSYS SPA", "Linksys IP Phone"},
+
+  /* Examples:
+   * MotorolaAP.AP7131
+   */
+  {"MotorolaAP", "Motorola Wifi AP"},
+
+  /* Examples:
+   * NECDT700
+   */
+  {"NECDT", "NEC IP Phone"},
+
+  /* Examples:
+   *   6=qPolycomSoundPointIP-SPIP_1234567-12345-001
+   *   6=tPolycomSoundStationIP-SSIP_12345678-12345-001
+   */
+  {"PolycomSoundPointIP", "Polycom IP Phone"},
+
+  /* Examples:
+   *   Polycom-SPIP335
+   *   Polycom-SPIP550
+   *   Polycom-SSIP7000
+   *   Polycom-VVX310
+   *   Polycom-VVX500
+   *   Polycom-VVX600
+   */
+  {"Polycom-SPIP", "Polycom IP Phone"},
+  {"Polycom-SSIP", "Polycom IP Phone"},
+  {"Polycom-VVX", "Polycom IP Phone"},
+
+  /* Examples:
+   * Rabbit2000-TCPIP:Z-World:Testfoo:1.1.3
+   * Rabbit-TCPIP:Z-World:DHCP-Test:1.2.0
+   */
+  {"Rabbit-TCPIP", "Rabbit Microcontroller"},
+  {"Rabbit2000-TCPIP", "Rabbit Microcontroller"},
+
+  /* Examples:
+   * ReadyNet_WRT500
+   */
+  {"ReadyNet_WRT", "ReadyNet Wifi AP"},
+
+  /* Examples:
+   * SAMSUNG SCX-6x45
+   */
+  {"SAMSUNG SCX", "Samsung Network MFP"},
+
+  /* Examples:
+   * SF200-24P
+   * SG 200-08
+   * SG 200-26
+   * SG 300-10
+   * SG 300-20
+   * SG200-26
+   * SG200-50P
+   * SG300-10
+   */
+  {"SF200", "Cisco Managed Switch"},
+  {"SG 200", "Cisco Managed Switch"},
+  {"SG200", "Cisco Managed Switch"},
+  {"SG 300", "Cisco Managed Switch"},
+  {"SG300", "Cisco Managed Switch"},
+
+  /* Examples:
+   * snom-m3-SIP/02.11//18-Aug-10 15:36
+   * snom320
+   * snom710
+   */
+  {"snom", "Snom IP Phone"},
+
+  /* Examples:
+   * telsey-stb-f8
+   */
+  {"telsey-stb", "Telsey Media Player"},
+
+  {NULL, NULL}
+};
+
+
+/* Copy a string with no funny schtuff allowed; only alphanumerics + space. */
+static void no_mischief_strncpy(char *dst, const char *src, size_t n)
+{
+  size_t i;
+  for (i = 0; i < n; ++i) {
+    unsigned char s = src[i];
+    int is_lower = (s >= 'a' && s <= 'z');
+    int is_upper = (s >= 'A' && s <= 'Z');
+    int is_digit = (s >= '0' && s <= '9');
+    if (s == '\0') {
+      dst[i] = '\0';
+      break;
+    } else if (is_lower || is_upper || is_digit) {
+      dst[i] = s;
+    } else if (s == ' ' || s == '\t') {
+      dst[i] = ' ';
+    } else {
+      dst[i] = '_';
+    }
+  }
+
+  dst[n - 1] = '\0';
+}
+
+/*
+ * Check for vendor options pattern populated by a number of
+ * printer manufacturers:
+ *
+ *   Mfg=DELL;Typ=Printer;Mod=Dell 2330dn Laser Printer;Ser=0123AB5;
+ *   Mfg=FujiXerox;Typ=AIO;Mod=WorkCentre 6027;Ser=P1A234567
+ *   Mfg=Hewlett Packard;Typ=Printer;Mod=HP LaserJet 400 M401n;Ser=ABCDE01234;
+ *   mfg=Xerox;typ=MFP;mod=WorkCentre 3220;ser=ABC012345;loc=
+ */
+int check_for_printer(const char *vendor_class, char *species,
+    size_t species_len)
+{
+  regex_t r_vendor, r_type, r_model;
+  regmatch_t match[2];
+  char *vendor = NULL, *type = NULL, *model = NULL;
+  int rc = 1;
+
+  if (regcomp(&r_vendor, "mfg=([^;]+)", REG_EXTENDED | REG_ICASE) ||
+      regcomp(&r_type, "typ=([^;]+)", REG_EXTENDED | REG_ICASE) ||
+      regcomp(&r_model, "mod=([^;]+)", REG_EXTENDED | REG_ICASE)) {
+    fprintf(stderr, "%s: regcomp failed!\n", __FUNCTION__);
+    exit(1);
+  }
+
+  if (regexec(&r_vendor, vendor_class, 2, match, 0) == 0) {
+    int len = match[1].rm_eo - match[1].rm_so;
+    vendor = strndup(vendor_class + match[1].rm_so, len);
+  }
+
+  if (regexec(&r_type, vendor_class, 2, match, 0) == 0) {
+    int len = match[1].rm_eo - match[1].rm_so;
+    type = strndup(vendor_class + match[1].rm_so, len);
+  }
+
+  if (regexec(&r_model, vendor_class, 2, match, 0) == 0) {
+    int len = match[1].rm_eo - match[1].rm_so;
+    model = strndup(vendor_class + match[1].rm_so, len);
+  }
+
+  if (vendor && type) {
+    char buf[128];
+    snprintf(buf, sizeof(buf), "%s %s", vendor, type);
+    no_mischief_strncpy(species, buf, species_len);
+    rc = 0;
+  } else if (model) {
+    no_mischief_strncpy(species, model, species_len);
+    rc = 0;
+  }
+
+  if (vendor) free(vendor);
+  if (type) free(type);
+  if (model) free(model);
+
+  return(rc);
+}
+
+/*
+ * Check a few patterns from common vendors with lots of model
+ * numbers.
+ */
+int check_specials(const char *vendor_class, char *species,
+    size_t species_len)
+{
+  regex_t r_dellprinter, r_grandstream;
+
+  /*
+   * Dell printers. Examples:
+   * Dell C1760nw Color Printer
+   * Dell C2660dn Color Laser
+   * Dell 2155cn Color MFP
+   */
+  if (regcomp(&r_dellprinter, "^Dell \\S+ Color (Printer|Laser|MFP)",
+        REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
+    fprintf(stderr, "%s: regcomp failed!\n", __FUNCTION__);
+    exit(1);
+  }
+  if (regexec(&r_dellprinter, vendor_class, 0, NULL, 0) == 0) {
+    snprintf(species, species_len, "Dell Printer");
+    return(0);
+  }
+
+  /*
+   * Grandstream Voice over IP adapters. Examples:
+   * HT500 dslforum.org
+   * HT7XX dslforum.org
+   */
+  if (regcomp(&r_grandstream, "^HT.* dslforum.org",
+        REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
+    fprintf(stderr, "%s: regcomp failed!\n", __FUNCTION__);
+    exit(1);
+  }
+  if (regexec(&r_grandstream, vendor_class, 0, NULL, 0) == 0) {
+    snprintf(species, species_len, "Grandstream VoIP adapter");
+    return(0);
+  }
+
+  /*
+   * Grandstream IP phones. Examples:
+   * DP7XX dslforum.org
+   */
+  if (regcomp(&r_grandstream, "^DP.* dslforum.org",
+        REG_EXTENDED | REG_ICASE | REG_NOSUB)) {
+    fprintf(stderr, "%s: regcomp failed!\n", __FUNCTION__);
+    exit(1);
+  }
+  if (regexec(&r_grandstream, vendor_class, 0, NULL, 0) == 0) {
+    snprintf(species, species_len, "Grandstream IP phone");
+    return(0);
+  }
+
+  return(1);
+}
+
+
+int lookup_vc(const char *vendor_class, char *species, size_t species_len)
+{
+  const struct string_match *p;
+  int slen = strlen(vendor_class);
+
+  if ((p = exact_match(vendor_class, slen)) != NULL) {
+    no_mischief_strncpy(species, p->species, species_len);
+    return(0);
+  }
+
+  p = &substring_matches[0];
+  while (p->vendor_class != NULL) {
+    if (strstr(vendor_class, p->vendor_class) != NULL) {
+      no_mischief_strncpy(species, p->species, species_len);
+      return(0);
+    }
+    p++;
+  }
+
+  if (check_for_printer(vendor_class, species, species_len) == 0) {
+    return(0);
+  }
+
+  if (check_specials(vendor_class, species, species_len) == 0) {
+    return(0);
+  }
+
+  return(1);
+}
+
+
+void usage(const char *progname)
+{
+  fprintf(stderr, "usage: %s -v vendor_string -l label\n", progname);
+  exit(1);
+}
+
+
+int main(int argc, char **argv)
+{
+  struct option long_options[] = {
+    {"label",   required_argument, 0, 'l'},
+    {"vendor",  required_argument, 0, 'v'},
+    {0,         0,                 0, 0},
+  };
+  int c;
+  const char *label = NULL;
+  const char *vendor = NULL;
+  char species[80];
+
+  while ((c = getopt_long(argc, argv, "l:v:", long_options, NULL)) != -1) {
+    switch (c) {
+    case 'l':
+      label = optarg;
+      break;
+    case 'v':
+      vendor = optarg;
+      break;
+    default:
+      usage(argv[0]);
+    }
+  }
+
+  if (optind < argc || vendor == NULL || label == NULL)
+    usage(argv[0]);
+
+  memset(species, 0, sizeof(species));
+  if (lookup_vc(vendor, species, sizeof(species)) == 0) {
+    printf("dhcpv %s %s\n", label, species);
+  }
+  exit(0);
+}
diff --git a/cmds/host-test-ssdptax.sh b/cmds/host-test-ssdptax.sh
new file mode 100755
index 0000000..d6796dd
--- /dev/null
+++ b/cmds/host-test-ssdptax.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+#
+# Copyright 2016 Google Inc. All Rights Reserved.
+
+. ./wvtest/wvtest.sh
+
+SSDP=./host-ssdptax
+
+WVSTART "ssdptax test"
+WVPASSEQ "$($SSDP -t)" "ssdp 00:01:02:03:04:05 Test Device"
diff --git a/cmds/isostream.c b/cmds/isostream.c
index 8225e11..9407319 100644
--- a/cmds/isostream.c
+++ b/cmds/isostream.c
@@ -107,6 +107,7 @@
           "\n"
           "Server specific:\n"
           "      -P <number>     limit to this many parallel connections\n"
+          "      -C <algo>       override TCP congestion control algorithm\n"
           "Client specific:\n"
           "      -b <Mbits/sec>  Mbits per second\n"
           "      -I <interface>  set source interface to specified interface\n"
@@ -148,6 +149,24 @@
 }
 
 
+int set_cong_ctl(int sock, const char *cong_ctl) {
+#ifdef TCP_CONGESTION
+  if (setsockopt(sock, IPPROTO_TCP, TCP_CONGESTION,
+                 cong_ctl, strlen(cong_ctl)) != 0) {
+    char buf[128];
+    int e = errno;
+    snprintf(buf, sizeof(buf), "tcp_congestion('%s')", cong_ctl);
+    errno = e;
+    perror(buf);
+    return -1;
+  } else {
+    fprintf(stderr, "tcp_congestion set to '%s'.\n", cong_ctl);
+  }
+#endif
+  return 0;
+}
+
+
 static int do_select(int sock, long long usec_timeout) {
   fd_set rfds;
   FD_ZERO(&rfds);
@@ -499,10 +518,11 @@
   double sufficient = 0;
   int timeout = 0;
   int max_children = MAX_CHILDREN;
+  const char *cong_ctl = NULL;
 
   int c;
   char *ifr_name = NULL;
-  while ((c = getopt(argc, argv, "b:I:P:s:t:h?")) >= 0) {
+  while ((c = getopt(argc, argv, "b:I:P:C:s:t:h?")) >= 0) {
     switch (c) {
     case 'b':
       megabits_per_sec = atoi(optarg);
@@ -523,6 +543,14 @@
         return 99;
       }
       break;
+    case 'C':
+      cong_ctl = optarg;
+#ifndef TCP_CONGESTION
+      fprintf(stderr, "%s: no support for congestion control overrides.\n",
+              argv[0]);
+      return 99;
+#endif
+      break;
     case 's':
       sufficient = atof(optarg);
       if (sufficient < 1) {
@@ -582,6 +610,9 @@
       perror("getsockname");
       return 1;
     }
+    if (cong_ctl && set_cong_ctl(sock, cong_ctl) != 0) {
+      return 1;
+    }
     if (listen(sock, 1)) {
       perror("listen");
       return 1;
@@ -612,6 +643,9 @@
           perror("accept");
           continue;
         }
+        if (cong_ctl && set_cong_ctl(conn, cong_ctl) != 0) {
+          return 1;
+        }
         pid_t pid = fork();
         if (pid < 0) {
           perror("fork");
@@ -635,6 +669,11 @@
     }
   } else if (argc - optind == 1) {
     fprintf(stderr, "client mode.\n");
+    if (cong_ctl) {
+      fprintf(stderr, "%s: can't set congestion control in client mode.\n",
+              argv[0]);
+      usage_and_die(argv[0]);
+    }
 
     if (!megabits_per_sec) {
       fprintf(stderr, "%s: must specify -b in client mode\n", argv[0]);
diff --git a/cmds/l2utils.cc b/cmds/l2utils.cc
new file mode 100644
index 0000000..d0549cb
--- /dev/null
+++ b/cmds/l2utils.cc
@@ -0,0 +1,121 @@
+/*
+ * 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 <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <netlink/msg.h>
+#include <netlink/netlink.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "l2utils.h"
+
+void get_l2_map(L2Map *l2map)
+{
+  int s;
+  struct {
+    struct nlmsghdr hdr;
+    struct ndmsg msg;
+  } nlreq;
+  struct sockaddr_nl addr;
+  struct msghdr msg;
+  static uint8_t l2buf[256 * 1024];
+  struct iovec iov = {.iov_base = l2buf, .iov_len = sizeof(l2buf)};
+  struct nlmsghdr *nh;
+  struct ndmsg *ndm;
+  struct nlattr *tb[NDA_MAX+1];
+  int len;
+  int af[] = {AF_INET, AF_INET6};
+  unsigned int i;
+
+  for (i = 0; i < (sizeof(af) / sizeof(af[0])); ++i) {
+    if ((s = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
+      perror("socket AF_NETLINK");
+      exit(1);
+    }
+
+    memset(&addr, 0, sizeof(addr));
+    addr.nl_family = AF_NETLINK;
+    addr.nl_pid = getpid();
+    addr.nl_groups = 0;
+
+    if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
+      perror("bind AF_NETLINK");
+      exit(1);
+    }
+
+    memset(&nlreq, 0, sizeof(nlreq));
+    nlreq.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(nlreq.msg));
+    nlreq.hdr.nlmsg_type = RTM_GETNEIGH;
+    nlreq.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+    nlreq.msg.ndm_family = af[i];
+
+    if (send(s, &nlreq, nlreq.hdr.nlmsg_len, 0) < 0) {
+      perror("send AF_NETLINK");
+      exit(1);
+    }
+
+    memset(&msg, 0, sizeof(msg));
+    msg.msg_iov = &iov;
+    msg.msg_iovlen = 1;
+    msg.msg_controllen = 0;
+    msg.msg_control = NULL;
+    msg.msg_flags = 0;
+
+    if ((len = recvmsg(s, &msg, 0)) <= 0) {
+      perror("recvmsg AL_NETLINK");
+      exit(1);
+    }
+
+    if (msg.msg_flags & MSG_TRUNC) {
+      fprintf(stderr, "recvmsg AL_NETLINK MSG_TRUNC\n");
+      exit(1);
+    }
+
+    memset(tb, 0, sizeof(tb));
+    nh = (struct nlmsghdr *)l2buf;
+    while (nlmsg_ok(nh, len)) {
+      ndm = (struct ndmsg *)nlmsg_data(nh);
+      if (nlmsg_parse(nh, sizeof(*ndm), tb, NDA_MAX, NULL)) {
+        fprintf(stderr, "nlmsg_parse failed\n");
+        exit(1);
+      }
+
+      if (tb[NDA_DST] && tb[NDA_LLADDR] &&
+          !(ndm->ndm_state & (NUD_INCOMPLETE | NUD_FAILED)) &&
+          (ndm->ndm_family == AF_INET || ndm->ndm_family == AF_INET6)) {
+        char mac[18];
+        char ipaddr[INET6_ADDRSTRLEN];
+        uint8_t *p;
+
+        p = (uint8_t *)nla_data(tb[NDA_LLADDR]);
+        snprintf(mac, sizeof(mac), "%02x:%02x:%02x:%02x:%02x:%02x",
+            p[0], p[1], p[2], p[3], p[4], p[5]);
+
+        p = (uint8_t *)nla_data(tb[NDA_DST]);
+        inet_ntop(ndm->ndm_family, p, ipaddr, sizeof(ipaddr));
+
+        (*l2map)[std::string(ipaddr)] = std::string(mac);
+      }
+
+      nh = nlmsg_next(nh, &len);
+    }
+
+    close(s);
+  }
+}
diff --git a/cmds/l2utils.h b/cmds/l2utils.h
new file mode 100644
index 0000000..6b2385a
--- /dev/null
+++ b/cmds/l2utils.h
@@ -0,0 +1,10 @@
+#include <string>
+#include <tr1/unordered_map>
+
+#ifndef L2UTILS_H
+#define L2UTILS_H
+
+typedef std::tr1::unordered_map<std::string, std::string> L2Map;
+extern void get_l2_map(L2Map *l2map);
+
+#endif  // L2UTILS_H
diff --git a/cmds/ssdp_poll.c b/cmds/ssdp_poll.c
deleted file mode 100644
index ef24b94..0000000
--- a/cmds/ssdp_poll.c
+++ /dev/null
@@ -1,127 +0,0 @@
-/* ssdp_poll
- *
- * A client implementing the API described in
- * http://miniupnp.free.fr/minissdpd.html
- *
- * Requests the list of all known SSDP nodes and the
- * services they export, and prints it to stdout in
- * a format which is simple to parse.
- */
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/socket.h>
-#include <sys/time.h>
-#include <sys/types.h>
-#include <sys/un.h>
-#include <unistd.h>
-
-/* Encode length by using 7bit per Byte :
- * Most significant bit of each byte specifies that the
- * following byte is part of the code */
-#define DECODELENGTH(n, p) { \
-  n = 0; \
-  do { n = (n << 7) | (*p & 0x7f); } \
-  while (*(p++)&0x80); \
-}
-
-#define CODELENGTH(n, p) { \
-  if(n>=0x10000000) *(p++) = (n >> 28) | 0x80; \
-  if(n>=0x200000) *(p++) = (n >> 21) | 0x80; \
-  if(n>=0x4000) *(p++) = (n >> 14) | 0x80; \
-  if(n>=0x80) *(p++) = (n >> 7) | 0x80; \
-  *(p++) = n & 0x7f; \
-}
-
-#define SOCK_PATH "/var/run/minissdpd.sock"
-
-int connect_to_ssdpd()
-{
-  struct sockaddr_un addr;
-  int s;
-
-  s = socket(AF_UNIX, SOCK_STREAM, 0);
-  if(s < 0) {
-    perror("socket AF_UNIX failed");
-    exit(1);
-  }
-  memset(&addr, 0, sizeof(addr));
-  addr.sun_family = AF_UNIX;
-  strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path));
-  if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
-    perror("connect to minisspd failed");
-    exit(1);
-  }
-
-  return s;
-}
-
-int main()
-{
-  unsigned char *buffer;
-  unsigned char *p;
-  const char *device = "ssdp:all";
-  int device_len = (int)strlen(device);
-  int socket = connect_to_ssdpd();
-  size_t siz = 65536;
-  ssize_t len;
-  fd_set readfds;
-  struct timeval tv;
-
-  if ((buffer = (unsigned char *)malloc(siz)) == NULL) {
-    fprintf(stderr, "malloc(%zu) failed\n", siz);
-    exit(1);
-  }
-  memset(buffer, 0, siz);
-
-  buffer[0] = 5; /* request type : request all device server IDs */
-  p = buffer + 1;
-  CODELENGTH(device_len, p);
-  memcpy(p, device, device_len);
-  p += device_len;
-  if (write(socket, buffer, p - buffer) < 0) {
-    perror("write to minissdpd failed");
-    exit(1);
-  }
-
-  FD_ZERO(&readfds);
-  FD_SET(socket, &readfds);
-  memset(&tv, 0, sizeof(tv));
-  tv.tv_sec = 2;
-
-  if (select(socket + 1, &readfds, NULL, NULL, &tv) < 1) {
-    fprintf(stderr, "select failed\n");
-    exit(1);
-  }
-
-  if ((len = read(socket, buffer, siz)) < 0) {
-    perror("read from minissdpd failed");
-    exit(1);
-  }
-
-  int num = buffer[0];
-  p = buffer + 1;
-  while (num-- > 0) {
-    size_t copylen, slen;
-    char url[256];
-    char server[512];
-
-    DECODELENGTH(slen, p);
-    copylen = (slen >= sizeof(url)) ? sizeof(url) - 1 : slen;
-    memcpy(url, p, copylen);
-    url[copylen] = '\0';
-    p += slen;
-
-    DECODELENGTH(slen, p);
-    copylen = (slen >= sizeof(server)) ? sizeof(server) - 1 : slen;
-    memcpy(server, p, copylen);
-    server[copylen] = '\0';
-    p += slen;
-
-    printf("%s|%s\n", url, server);
-  }
-
-  free(buffer);
-  exit(0);
-}
diff --git a/cmds/ssdptax.cc b/cmds/ssdptax.cc
new file mode 100644
index 0000000..d218c0a
--- /dev/null
+++ b/cmds/ssdptax.cc
@@ -0,0 +1,593 @@
+/*
+ * 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.
+ */
+
+/*
+ * ssdptax (SSDP Taxonomy)
+ *
+ * A client implementing the API described in
+ * http://miniupnp.free.fr/minissdpd.html
+ *
+ * Requests the list of all known SSDP nodes, requests
+ * device info from them, and tries to figure out what
+ * they are.
+ */
+
+#include <arpa/inet.h>
+#include <asm/types.h>
+#include <ctype.h>
+#include <curl/curl.h>
+#include <getopt.h>
+#include <netinet/in.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "l2utils.h"
+
+/* Encode length by using 7bit per Byte :
+ * Most significant bit of each byte specifies that the
+ * following byte is part of the code */
+#define DECODELENGTH(n, p) { \
+  n = 0; \
+  do { n = (n << 7) | (*p & 0x7f); } \
+  while (*(p++)&0x80); \
+}
+
+#define CODELENGTH(n, p) { \
+  if(n>=0x10000000) *(p++) = (n >> 28) | 0x80; \
+  if(n>=0x200000) *(p++) = (n >> 21) | 0x80; \
+  if(n>=0x4000) *(p++) = (n >> 14) | 0x80; \
+  if(n>=0x80) *(p++) = (n >> 7) | 0x80; \
+  *(p++) = n & 0x7f; \
+}
+
+#define SOCK_PATH "/var/run/minissdpd.sock"
+
+
+typedef struct {
+  char server[512];
+  char url[512];
+  char friendlyName[64];
+  int failed;
+} ssdp_info_t;
+
+
+/* Unit test support */
+char *get_test_ssdp_data();
+void get_test_l2_map(L2Map *l2map);
+
+static void memcpy_printable(char *dst, const char *src, size_t n)
+{
+  size_t i;
+  for (i = 0; i < n; ++i) {
+    unsigned char s = src[i];
+    if (isspace(s)) {
+      dst[i] = ' ';  // deliberately convert newline to space
+    } else if (isprint(s)) {
+      dst[i] = s;
+    } else {
+      dst[i] = '_';
+    }
+  }
+}
+
+
+/*
+ * Send a request to minissdpd. Returns a pointer to a buffer
+ * allocated using malloc(). Caller must free() the buffer when done.
+ */
+char *request_from_ssdpd(int reqtype, const char *device)
+{
+  int s = socket(AF_UNIX, SOCK_STREAM, 0);
+  struct sockaddr_un addr;
+  size_t siz = 256 * 1024;
+  char *buffer, *p;
+  ssize_t len;
+  int device_len = (int)strlen(device);
+  fd_set readfds;
+  struct timeval tv;
+
+  if (s < 0) {
+    perror("socket AF_UNIX failed");
+    exit(1);
+  }
+  memset(&addr, 0, sizeof(addr));
+  addr.sun_family = AF_UNIX;
+  strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path));
+  if(connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)) < 0) {
+    perror("connect to minisspd failed");
+    exit(1);
+  }
+
+  if ((buffer = (char *)malloc(siz)) == NULL) {
+    fprintf(stderr, "malloc(%zu) failed\n", siz);
+    exit(1);
+  }
+  memset(buffer, 0, siz);
+
+  buffer[0] = reqtype;
+  p = buffer + 1;
+  CODELENGTH(device_len, p);
+  memcpy(p, device, device_len);
+  p += device_len;
+  if (write(s, buffer, p - buffer) < 0) {
+    perror("write to minissdpd failed");
+    exit(1);
+  }
+
+  FD_ZERO(&readfds);
+  FD_SET(s, &readfds);
+  memset(&tv, 0, sizeof(tv));
+  tv.tv_sec = 2;
+
+  if (select(s + 1, &readfds, NULL, NULL, &tv) < 1) {
+    fprintf(stderr, "select failed\n");
+    exit(1);
+  }
+
+  if ((len = read(s, buffer, siz)) < 0) {
+    perror("read from minissdpd failed");
+    exit(1);
+  }
+
+  close(s);
+  return(buffer);
+}
+
+
+static void print_responses(const std::string &ipaddr,
+    const ssdp_info_t *info, L2Map *l2map)
+{
+  const char *mac;
+
+  if (info->failed) {
+    /*
+     * We could not fetch information from this client. That often means that
+     * the device was powered off recently. minissdpd still remembers that
+     * it is there, but we cannot contact it.
+     *
+     * Don't print anything for these, as we'd end up calling them "Unknown"
+     * and that is misleading. We only report information about devices which
+     * are active right now.
+     */
+    return;
+  }
+
+  L2Map::const_iterator ii = l2map->find(ipaddr);
+  if (ii != l2map->end()) {
+    mac = ii->second.c_str();
+  } else {
+    mac = "00:00:00:00:00:00";
+  }
+
+  /* taxonomy information to stdout */
+  if (strlen(info->friendlyName)) {
+    printf("ssdp %s %s\n", mac, info->friendlyName);
+  } else {
+    printf("ssdp %s Unknown;%s\n", mac, info->server);
+  }
+}
+
+
+const char *parse_minissdpd_response(const char *response,
+    char *key, size_t key_len,
+    char *url, size_t url_len,
+    char *value, size_t value_len)
+{
+  const char *p = response;
+  size_t copylen, slen;
+  int prefix = 0;
+  struct in6_addr in6;
+  struct in_addr in;
+  char ip[INET6_ADDRSTRLEN];
+
+  key[0] = url[0] = value[0] = '\0';
+
+  DECODELENGTH(slen, p);
+  copylen = (slen >= url_len) ? url_len - 1 : slen;
+  memcpy_printable(url, p, copylen);
+  url[copylen] = '\0';
+  p += slen;
+
+  DECODELENGTH(slen, p);
+  copylen = (slen >= value_len) ? value_len - 1 : slen;
+  memcpy_printable(value, p, copylen);
+  value[copylen] = '\0';
+  p += slen;
+
+  if (strncasecmp(url, "https://[", 9) == 0) prefix = 9;
+  if (strncasecmp(url, "http://[", 8) == 0) prefix = 8;
+  if (strncasecmp(url, "https://", 8) == 0) prefix = 8;
+  if (strncasecmp(url, "http://", 7) == 0) prefix = 7;
+  strncpy(ip, url + prefix, sizeof(ip));
+  strtok(ip, ":/@");
+
+  if (inet_pton(AF_INET6, ip, &in6)) {
+    inet_ntop(AF_INET6, &in6, key, key_len);
+  }
+  if (inet_pton(AF_INET, ip, &in)) {
+    inet_ntop(AF_INET, &in, key, key_len);
+  }
+
+  return p;
+}
+
+
+ssdp_info_t *dupinfo(ssdp_info_t *info)
+{
+  ssdp_info_t *i = (ssdp_info_t *)malloc(sizeof(*info));
+
+  if (i == NULL) {
+    perror("malloc");
+    exit(1);
+  }
+
+  memcpy(i, info, sizeof(*i));
+  return i;
+}
+
+
+const char *findXmlField(const char *ptr, const char *label, ssize_t *len)
+{
+  char openlabel[64], closelabel[64];
+  const char *start, *end;
+
+  snprintf(openlabel, sizeof(openlabel), "<%s>", label);
+  snprintf(closelabel, sizeof(closelabel), "</%s>", label);
+
+  start = strcasestr(ptr, openlabel) + strlen(openlabel);
+  end = strcasestr(ptr, closelabel);
+
+  if ((end - start) > 0) {
+    *len = end - start;
+    return start;
+  }
+
+  return NULL;
+}
+
+
+/*
+ * libcurl calls this function back with the result of the HTTP GET.
+ *
+ * Expected value is an XML blob of
+ * http://upnp.org/specs/basic/UPnP-basic-Basic-v1-Device.pdf
+ *
+ * Like this (a Samsung TV):
+ *  <?xml version="1.0"?>
+ *  <root xmlns='urn:schemas-upnp-org:device-1-0' ...
+ *   <device>
+ *    <deviceType>urn:dial-multiscreen-org:device:dialreceiver:1</deviceType>
+ *    <friendlyName>[TV]Samsung LED60</friendlyName>
+ *    <manufacturer>Samsung Electronics</manufacturer>
+ *    <manufacturerURL>http://www.samsung.com/sec</manufacturerURL>
+ *    <modelDescription>Samsung TV NS</modelDescription>
+ *    <modelName>UN60F6300</modelName>
+ *    <modelNumber>1.0</modelNumber>
+ *    <modelURL>http://www.samsung.com/sec</modelURL>
+ * ... etc, etc ...
+ */
+size_t callback(const char *ptr, size_t size, size_t nmemb, void *userdata)
+{
+  ssdp_info_t *info = (ssdp_info_t *)userdata;
+  const char *p;
+  ssize_t len;
+  ssize_t max = (ssize_t)sizeof(info->friendlyName);
+
+  if ((p = findXmlField(ptr, "friendlyName", &len)) == NULL) {
+    p = findXmlField(ptr, "modelDescription", &len);
+  }
+
+  if (p && (len > 0) && (len < max)) {
+    /* the len < max check ensures there will be a NUL byte at the end */
+    memcpy(info->friendlyName, p, len);
+  }
+
+  return size * nmemb;
+}
+
+
+/*
+ * SSDP returned an endpoint URL, use curl to GET its contents.
+ */
+void fetch_device_info(const char *url, ssdp_info_t *ssdp)
+{
+  CURL *curl = curl_easy_init();
+  int rc;
+
+  if (!curl) {
+    fprintf(stderr, "curl_easy_init failed\n");
+    return;
+  }
+  curl_easy_setopt(curl, CURLOPT_URL, url);
+  curl_easy_setopt(curl, CURLOPT_PATH_AS_IS, 1L);
+  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &callback);
+  curl_easy_setopt(curl, CURLOPT_WRITEDATA, ssdp);
+  curl_easy_setopt(curl, CURLOPT_USERAGENT, "ssdptax/1.0");
+  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1L);
+  if ((rc = curl_easy_perform(curl)) != CURLE_OK) {
+    ssdp->failed = 1;
+  }
+  curl_easy_cleanup(curl);
+}
+
+
+void usage(char *progname) {
+  printf("usage: %s [-t]\n", progname);
+  printf("\t-t\ttest mode, run a test with fake SSDP data.\n");
+  exit(1);
+}
+
+
+int main(int argc, char **argv)
+{
+  char *buffer;
+  const char *p;
+  typedef std::tr1::unordered_map<std::string, ssdp_info_t*> ResponsesMap;
+  ResponsesMap responses;
+  L2Map l2map;
+  int c, num;
+  int testmode = 0;
+
+  setlinebuf(stdout);
+  if (curl_global_init(CURL_GLOBAL_NOTHING)) {
+    fprintf(stderr, "curl_global_init failed\n");
+    exit(1);
+  }
+
+  while ((c = getopt(argc, argv, "t")) != -1) {
+    switch(c) {
+      case 't': testmode = 1; break;
+      default: usage(argv[0]); break;
+    }
+  }
+
+  if (!testmode) {
+    /* 5 == request all device server IDs */
+    buffer = request_from_ssdpd(5, "ssdp:all");
+  } else {
+    buffer = get_test_ssdp_data();
+  }
+
+  num = buffer[0];
+  p = buffer + 1;
+  while ((num-- > 0) && (p < (buffer + sizeof(buffer)))) {
+    char key[INET6_ADDRSTRLEN];
+    ssdp_info_t info;
+
+    memset(&info, 0, sizeof(info));
+    p = parse_minissdpd_response(p, key, sizeof(key),
+        info.url, sizeof(info.url), info.server, sizeof(info.server));
+    if (strlen(key) && responses.find(std::string(key)) == responses.end()) {
+      if (!testmode) {
+        fetch_device_info(info.url, &info);
+      } else {
+        snprintf(info.friendlyName, sizeof(info.friendlyName), "Test Device");
+      }
+      responses.insert(std::make_pair<std::string,
+          ssdp_info_t*>(std::string(key), dupinfo(&info)));
+    }
+  }
+  free(buffer);
+
+  if (!testmode) {
+    get_l2_map(&l2map);
+  } else {
+    get_test_l2_map(&l2map);
+  }
+
+  for(ResponsesMap::const_iterator ii = responses.begin();
+      ii != responses.end(); ++ii) {
+    print_responses(ii->first, ii->second, &l2map);
+  }
+
+  curl_global_cleanup();
+  exit(0);
+}
+
+
+/*
+ * data for a unit test, response from a single SSDP
+ * client.
+ */
+uint8_t test_ssdp_data[] = {
+  0x12, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,
+  0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38,
+  0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a,
+  0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70,
+  0x5f, 0x32, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50,
+  0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31,
+  0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73,
+  0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50,
+  0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30,
+  0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f,
+  0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e,
+  0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37,
+  0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f,
+  0x32, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e,
+  0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75,
+  0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20,
+  0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22,
+  0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31,
+  0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34,
+  0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36,
+  0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32,
+  0x39, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30,
+  0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e,
+  0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53,
+  0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68,
+  0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39,
+  0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32,
+  0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37,
+  0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32, 0x39,
+  0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c,
+  0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44,
+  0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74,
+  0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32,
+  0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e,
+  0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36,
+  0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31, 0x39, 0x5f,
+  0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50,
+  0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20,
+  0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b,
+  0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74, 0x74,
+  0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e,
+  0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31,
+  0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f,
+  0x73, 0x6d, 0x70, 0x5f, 0x31, 0x39, 0x5f, 0x23,
+  0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53,
+  0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f,
+  0x31, 0x2e, 0x30, 0x22, 0x68, 0x74, 0x74, 0x70,
+  0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31,
+  0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32,
+  0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f, 0x73,
+  0x6d, 0x70, 0x5f, 0x31, 0x39, 0x5f, 0x23, 0x53,
+  0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50,
+  0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61,
+  0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55, 0x50,
+  0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31,
+  0x2e, 0x30, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a,
+  0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36,
+  0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35,
+  0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d,
+  0x70, 0x5f, 0x31, 0x39, 0x5f, 0x23, 0x53, 0x48,
+  0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f,
+  0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d,
+  0x73, 0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e,
+  0x30, 0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,
+  0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38,
+  0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a,
+  0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70,
+  0x5f, 0x31, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50,
+  0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31,
+  0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73,
+  0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50,
+  0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30,
+  0x22, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f,
+  0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e,
+  0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37,
+  0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f,
+  0x31, 0x39, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e,
+  0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75,
+  0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20,
+  0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22,
+  0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31,
+  0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34,
+  0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36,
+  0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31,
+  0x31, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30,
+  0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e,
+  0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53,
+  0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68,
+  0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39,
+  0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32,
+  0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37,
+  0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31, 0x31,
+  0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c,
+  0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44,
+  0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74,
+  0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32,
+  0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e,
+  0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36,
+  0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x31, 0x31, 0x5f,
+  0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50,
+  0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20,
+  0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20,
+  0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b,
+  0x2f, 0x31, 0x2e, 0x30, 0x22, 0x68, 0x74, 0x74,
+  0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e,
+  0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31,
+  0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f,
+  0x73, 0x6d, 0x70, 0x5f, 0x31, 0x31, 0x5f, 0x23,
+  0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53,
+  0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f,
+  0x31, 0x2e, 0x30, 0x21, 0x68, 0x74, 0x74, 0x70,
+  0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31,
+  0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32,
+  0x35, 0x3a, 0x37, 0x36, 0x37, 0x36, 0x2f, 0x73,
+  0x6d, 0x70, 0x5f, 0x32, 0x5f, 0x23, 0x53, 0x48,
+  0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f,
+  0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d,
+  0x73, 0x75, 0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e,
+  0x30, 0x21, 0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f,
+  0x2f, 0x31, 0x39, 0x32, 0x2e, 0x31, 0x36, 0x38,
+  0x2e, 0x34, 0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a,
+  0x37, 0x36, 0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70,
+  0x5f, 0x32, 0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e,
+  0x30, 0x2c, 0x20, 0x53, 0x61, 0x6d, 0x73, 0x75,
+  0x6e, 0x67, 0x20, 0x55, 0x50, 0x6e, 0x50, 0x20,
+  0x53, 0x44, 0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x21,
+  0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31,
+  0x39, 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34,
+  0x32, 0x2e, 0x31, 0x32, 0x35, 0x3a, 0x37, 0x36,
+  0x37, 0x36, 0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32,
+  0x5f, 0x23, 0x53, 0x48, 0x50, 0x2c, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c,
+  0x20, 0x53, 0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67,
+  0x20, 0x55, 0x50, 0x6e, 0x50, 0x20, 0x53, 0x44,
+  0x4b, 0x2f, 0x31, 0x2e, 0x30, 0x21, 0x68, 0x74,
+  0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x39, 0x32,
+  0x2e, 0x31, 0x36, 0x38, 0x2e, 0x34, 0x32, 0x2e,
+  0x31, 0x32, 0x35, 0x3a, 0x37, 0x36, 0x37, 0x36,
+  0x2f, 0x73, 0x6d, 0x70, 0x5f, 0x32, 0x5f, 0x23,
+  0x53, 0x48, 0x50, 0x2c, 0x20, 0x55, 0x50, 0x6e,
+  0x50, 0x2f, 0x31, 0x2e, 0x30, 0x2c, 0x20, 0x53,
+  0x61, 0x6d, 0x73, 0x75, 0x6e, 0x67, 0x20, 0x55,
+  0x50, 0x6e, 0x50, 0x20, 0x53, 0x44, 0x4b, 0x2f,
+  0x31, 0x2e, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00
+};
+
+
+char *get_test_ssdp_data()
+{
+  size_t len = sizeof(test_ssdp_data);
+  char *buffer = (char *)malloc(len);
+
+  if (buffer == NULL) {
+    perror("malloc failed");
+    exit(1);
+  }
+
+  memcpy(buffer, test_ssdp_data, len);
+  return buffer;
+}
+
+
+void get_test_l2_map(L2Map *l2map)
+{
+  (*l2map)[std::string("192.168.42.125")] = std::string("00:01:02:03:04:05");
+}
diff --git a/cmds/test-dhcpvendortax.sh b/cmds/test-dhcpvendortax.sh
new file mode 100755
index 0000000..8f8d474
--- /dev/null
+++ b/cmds/test-dhcpvendortax.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+. ./wvtest/wvtest.sh
+
+pid=$$
+TAX=./host-dhcpvendortax
+
+WVSTART "dhcpvendortax test"
+
+# Check regex matches
+WVPASS $TAX -l label -v "AastraIPPhone55i" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Aastra IP Phone"
+WVPASS $TAX -l label -v "6=qPolycomSoundPointIP-SPIP_1234567-12345-001" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Polycom IP Phone"
+WVPASS $TAX -l label -v "Polycom-VVX310" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Polycom IP Phone"
+
+# Check exact matches
+WVPASS $TAX -l label -v "Dell Network Printer" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Dell Printer"
+WVPASS $TAX -l label -v "Xbox 360" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Xbox 360"
+
+# Check model/type/manufacturer handling
+WVPASS $TAX -l label -v "Mfg=DELL;Typ=Printer;Mod=Dell 2330dn Laser Printer;Ser=0123AB5;" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label DELL Printer"
+WVPASS $TAX -l label -v "Mfg=DELL;Mod=Dell 2330dn Laser Printer;Ser=0123AB5;" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Dell 2330dn Laser Printer"
+
+# Check case sensitivity
+WVPASS $TAX -l label -v "mFG=DELL;tYP=Printer;mOD=Dell 2330dn Laser Printer;Ser=0123AB5;" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label DELL Printer"
+
+# Check some other printer vendor formats
+WVPASS $TAX -l label -v "Mfg=FujiXerox;Typ=AIO;Mod=WorkCentre 6027;Ser=P1A234567" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label FujiXerox AIO"
+WVPASS $TAX -l label -v "Mfg=Hewlett Packard;Typ=Printer;Mod=HP LaserJet 400 M401n;Ser=ABCDE01234;" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Hewlett Packard Printer"
+WVPASS $TAX -l label -v "mfg=Xerox;typ=MFP;mod=WorkCentre 3220;ser=ABC012345;loc=" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Xerox MFP"
+
+# Check specials
+WVPASS $TAX -l label -v "Dell 2155cn Color MFP" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Dell Printer"
+WVPASS $TAX -l label -v "Dell C1760nw Color Printer" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Dell Printer"
+WVPASS $TAX -l label -v "Dell C2660dn Color Laser" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Dell Printer"
+
+WVPASS $TAX -l label -v "HT500 dslforum.org" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Grandstream VoIP adapter"
+WVPASS $TAX -l label -v "HT7XX dslforum.org" >test1.$pid.tmp
+WVPASSEQ "$(cat test1.$pid.tmp)" "dhcpv label Grandstream VoIP adapter"
+
+# check invalid or missing arguments. -l and -v are required.
+WVFAIL $TAX
+WVFAIL $TAX -l label
+WVFAIL $TAX -v vendor
+
+rm -f *.$pid.tmp
diff --git a/craftui/craftui.py b/craftui/craftui.py
index 0f21e11..d25b40b 100755
--- a/craftui/craftui.py
+++ b/craftui/craftui.py
@@ -124,7 +124,21 @@
   """Validate as PA power level."""
 
   def __init__(self):
-    super(VPower, self).__init__(0, 2000000)       # TODO(edjames)
+    super(VPower, self).__init__(0, 2000)
+
+
+class VGain(VRange):
+  """Validate as gain level."""
+
+  def __init__(self):
+    super(VGain, self).__init__(0, 63)
+
+
+class VGainIndex(VRange):
+  """Validate as gain index."""
+
+  def __init__(self):
+    super(VGainIndex, self).__init__(0, 5)
 
 
 class VDict(Validator):
@@ -206,7 +220,6 @@
     """Handle a JSON request to glaukusd."""
     url = 'http://localhost:8080' + self.api
     payload = self.fmt % self.validator.config
-    # TODO(edjames)
     print 'Glaukus: ', url, payload
     try:
       fd = urllib2.urlopen(url, payload)
@@ -239,33 +252,41 @@
       'peer_ipaddr': PtpConfig(VSlash, 'peer_ipaddr'),
 
       'vlan_inband': PtpConfig(VVlan, 'vlan_inband'),
+      'vlan_ooband': PtpConfig(VVlan, 'vlan_ooband'),
       'vlan_peer': PtpConfig(VVlan, 'vlan_peer'),
 
       'craft_ipaddr_activate': PtpActivate(VTrueFalse, 'craft_ipaddr'),
       'link_ipaddr_activate': PtpActivate(VTrueFalse, 'local_ipaddr'),
       'peer_ipaddr_activate': PtpActivate(VTrueFalse, 'peer_ipaddr'),
+
       'vlan_inband_activate': PtpActivate(VTrueFalse, 'vlan_inband'),
+      'vlan_ooband_activate': PtpActivate(VTrueFalse, 'vlan_ooband'),
       'vlan_peer_activate': PtpActivate(VTrueFalse, 'vlan_peer'),
 
       'freq_hi': Glaukus(VFreqHi, '/api/radio/frequency', '{"hiFrequency":%s}'),
       'freq_lo': Glaukus(VFreqLo, '/api/radio/frequency', '{"loFrequency":%s}'),
       'mode_hi': Glaukus(VTx, '/api/radio/hiTransceiver/mode', '%s'),
       'tx_powerlevel': Glaukus(VPower, '/api/radio/tx/paPowerSet', '%s'),
-      'tx_on': Glaukus(VTrueFalse, '/api/radio/paLnaPowerEnabled', '%s'),
+      'tx_gain': Glaukus(VGain, '/api/radio/tx/vgaGain', '%s'),
+      'rx_gainindex': Glaukus(VGainIndex, '/api/radio/rx/agcDigitalGainIndex',
+                              '%s'),
+      'palna_on': Glaukus(VTrueFalse, '/api/radio/paLnaPowerEnabled', '%s'),
+      'transceivers_on': Glaukus(VTrueFalse,
+                                 '/api/radio/transceiversPowerEnabled', '%s'),
 
       'reboot': Reboot(VTrueFalse)
   }
   ifmap = {
       'craft0': 'craft',
       'br0': 'bridge',
-      'eth1.outofband': 'outofband',
-      'eth1.inband': 'inband',
-      'eth1.peer': 'link',
+      'sw0.ooband': 'ooband',
+      'sw0.inband': 'inband',
+      'sw0.peer': 'link',
   }
   ifvlan = [
-      'eth1.outofband',
-      'eth1.inband',
-      'eth1.peer'
+      'sw0.ooband',
+      'sw0.inband',
+      'sw0.peer'
   ]
   stats = [
       'multicast',
@@ -409,7 +430,7 @@
     data['link_ipaddr'] = self.ReadFile(sim + cs + 'local_ipaddr')
     data['peer_ipaddr'] = self.ReadFile(sim + cs + 'peer_ipaddr')
     data['vlan_inband'] = self.ReadFile(sim + cs + 'vlan_inband')
-    data['vlan_outofband'] = self.ReadFile(sim + cs + 'vlan_outofband')
+    data['vlan_ooband'] = self.ReadFile(sim + cs + 'vlan_ooband')
     data['vlan_link'] = self.ReadFile(sim + cs + 'vlan_peer')
     self.AddIpAddr(data)
     self.AddInterfaceStats(data)
diff --git a/craftui/sim.tgz b/craftui/sim.tgz
index c0293f9..136253f 100644
--- a/craftui/sim.tgz
+++ b/craftui/sim.tgz
Binary files differ
diff --git a/craftui/www/config.thtml b/craftui/www/config.thtml
index ccfc835..ddca423 100644
--- a/craftui/www/config.thtml
+++ b/craftui/www/config.thtml
@@ -27,7 +27,7 @@
       <input type="radio" id="tab-1" name="tab-group-1" checked>
       <label for="tab-1">Site Configuration</label>
       <div class="content">
-	<b>Platfrom Parameters:</b>
+	<b>Platform Parameters:</b>
 	<table>
 	  <tr>
 	    <td align=center><b>Parameter
@@ -86,15 +86,15 @@
 
 	  <tr>
 	    <td><b>Out-of-band Management VLAN
-	    <td align=right><span id="platform/active_outofband_vlan">...</span>
+	    <td align=right><span id="platform/active_ooband_vlan">...</span>
 	    <td align=right>
-	      <span id="platform/vlan_outofband">...</span>
-	      <input type=submit value="Apply Now" onclick="CraftUI.config('vlan_outofband', 1)">
+	      <span id="platform/vlan_ooband">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('vlan_ooband', 1)">
 	    <td>
-	      <input id=vlan_outofband type=text value="">
-	      <input type=submit value=Configure onclick="CraftUI.config('vlan_outofband')">
+	      <input id=vlan_ooband type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('vlan_ooband')">
 	    <td>
-	      <span id=vlan_outofband_result>...</span>
+	      <span id=vlan_ooband_result>...</span>
 
 	  <tr>
 	    <td><b>Link VLAN (to peer)
@@ -145,7 +145,7 @@
 	      <span id=mode_hi_result>...</span>
 
 	  <tr>
-	    <td><b>Power Level
+	    <td><b>Transmit Power (dB x 100)
 	    <td align=right><span id="radio/tx/paPowerSet">...</span>
 	    <td>
 	      <input id=tx_powerlevel type=text value="">
@@ -154,13 +154,40 @@
 	      <span id=tx_powerlevel_result>...</span>
 
 	  <tr>
-	    <td><b>Power Enabled
+	    <td><b>Transmit VGA Gain
+	    <td align=right><span id="radio/tx/vgaGain">...</span>
+	    <td>
+	      <input id=tx_gain type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('tx_gain')">
+	    <td>
+	      <span id=tx_gain_result>...</span>
+
+	  <tr>
+	    <td><b>Receiver AGC Digital Gain Index
+	    <td align=right><span id="radio/rx/agcDigitalGainIndex">...</span>
+	    <td>
+	      <input id=rx_gainindex type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('rx_gainindex')">
+	    <td>
+	      <span id=rx_gainindex_result>...</span>
+
+	  <tr>
+	    <td><b>Receiver PA/LNA Power Enabled
 	    <td align=right><span id="radio/paLnaPowerEnabled">...</span>
 	    <td>
-	      <input id=tx_on type=text value="">
-	      <input type=submit value=Configure onclick="CraftUI.config('tx_on')">
+	      <input id=palna_on type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('palna_on')">
 	    <td>
-	      <span id=tx_on_result>...</span>
+	      <span id=palna_on_result>...</span>
+
+	  <tr>
+	    <td><b>Transceivers Power Enabled
+	    <td align=right><span id="radio/transceiversPowerEnabled">...</span>
+	    <td>
+	      <input id=transceivers_on type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('transceivers_on')">
+	    <td>
+	      <span id=transceivers_on_result>...</span>
 
 	</table>
       </div>
diff --git a/craftui/www/index.thtml b/craftui/www/index.thtml
index e53ddd7..20bba69 100644
--- a/craftui/www/index.thtml
+++ b/craftui/www/index.thtml
@@ -61,10 +61,10 @@
 	    <td align=right><span id="platform/active_bridge_inet6">...</span></td></tr>
 	  <tr>
             <td><b>Out-of-Band (PoE)</b></td>
-	    <td align=right><span id="platform/outofband_mac">...</span></td>
-	    <td align=right><span id="platform/active_outofband_vlan">...</span></td>
-	    <td align=right><span id="platform/active_outofband_inet">...</span></td>
-	    <td align=right><span id="platform/active_outofband_inet6">...</span></td></tr>
+	    <td align=right><span id="platform/ooband_mac">...</span></td>
+	    <td align=right><span id="platform/active_ooband_vlan">...</span></td>
+	    <td align=right><span id="platform/active_ooband_inet">...</span></td>
+	    <td align=right><span id="platform/active_ooband_inet6">...</span></td></tr>
 	  <tr>
             <td><b>Link (to peer)</b></td>
 	    <td align=right><span id="platform/link_mac">...</span></td>
@@ -177,27 +177,27 @@
 
 	  <tr>
             <td><b>Out-of-Band (PoE)<b></td>
-            <td align=right><span id="platform/outofband_rx_bytes">...</span></td>
-            <td align=right><span id="platform/outofband_rx_packets">...</span></td>
-            <td align=right><span id="platform/outofband_multicast">...</span></td>
-            <td align=right><span id="platform/outofband_broadcast">...</span></td>
-            <td align=right><span id="platform/outofband_unicast">...</span></td>
+            <td align=right><span id="platform/ooband_rx_bytes">...</span></td>
+            <td align=right><span id="platform/ooband_rx_packets">...</span></td>
+            <td align=right><span id="platform/ooband_multicast">...</span></td>
+            <td align=right><span id="platform/ooband_broadcast">...</span></td>
+            <td align=right><span id="platform/ooband_unicast">...</span></td>
 
-            <td align=right><span id="platform/outofband_tx_bytes">...</span></td>
-            <td align=right><span id="platform/outofband_tx_packets">...</span></td>
+            <td align=right><span id="platform/ooband_tx_bytes">...</span></td>
+            <td align=right><span id="platform/ooband_tx_packets">...</span></td>
             <td align=right>-</td>
             <td align=right>-</td>
             <td align=right>-</td>
 
-            <td align=right><span id="platform/outofband_rx_errors">...</span></td>
-            <td align=right><span id="platform/outofband_rx_dropped">...</span></td>
+            <td align=right><span id="platform/ooband_rx_errors">...</span></td>
+            <td align=right><span id="platform/ooband_rx_dropped">...</span></td>
             <td align=right>-</td>
             <td align=right>-</td>
-            <td align=right><span id="platform/outofband_tx_errors">...</span></td>
-            <td align=right><span id="platform/outofband_tx_dropped">...</span></td>
+            <td align=right><span id="platform/ooband_tx_errors">...</span></td>
+            <td align=right><span id="platform/ooband_tx_dropped">...</span></td>
             <td align=right>-</td>
             <td align=right>-</td>
-            <td align=right><span id="platform/outofband_collisions">...</span></td>
+            <td align=right><span id="platform/ooband_collisions">...</span></td>
 
 	  <tr>
             <td><b>Link (to peer)<b></td>
diff --git a/craftui/www/static/craft.js b/craftui/www/static/craft.js
index 04e0f2c..81501aa 100644
--- a/craftui/www/static/craft.js
+++ b/craftui/www/static/craft.js
@@ -81,11 +81,13 @@
   var el = document.getElementById(key);
   var value = el.value;
   var xhr = new XMLHttpRequest();
+  var action = "Configured";
   xhr.open('post', 'content.json');
   xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
   var data;
   if (activate) {
     data = { config: [ { [key + "_activate"]: "true" } ] };
+    action = "Applied";
   } else {
     data = { config: [ { [key]: value } ] };
   }
@@ -95,7 +97,7 @@
   xhr.onload = function(e) {
     var json = JSON.parse(xhr.responseText);
     if (json.error == 0) {
-      el.innerHTML = "Success!";
+      el.innerHTML = action + " successfully.";
     } else {
       el.innerHTML = "Error: " + json.errorstring;
     }
@@ -104,6 +106,7 @@
   xhr.onerror = function(e) {
     el.innerHTML = xhr.statusText + xhr.responseText;
   }
+  el.innerHTML = "sending...";
   xhr.send(txt);
 };
 
diff --git a/presterastats/Makefile b/presterastats/Makefile
index 3a5f19b..af578cd 100644
--- a/presterastats/Makefile
+++ b/presterastats/Makefile
@@ -9,6 +9,7 @@
 install:
 	mkdir -p $(BINDIR)
 	cp presterastats.py $(BINDIR)/presterastats
+	cp prestera_periodic.py $(BINDIR)/prestera_periodic
 
 install-libs:
 	@echo "No libs to install."
diff --git a/presterastats/prestera_periodic.py b/presterastats/prestera_periodic.py
new file mode 100755
index 0000000..91f146e
--- /dev/null
+++ b/presterastats/prestera_periodic.py
@@ -0,0 +1,121 @@
+#!/usr/bin/python
+# 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.
+
+"""Periodically call presterastats and save results to filesystem."""
+
+__author__ = 'poist@google.com (Gregory Poist)'
+
+import errno
+import os
+import subprocess
+import sys
+import tempfile
+import time
+
+import options
+
+
+optspec = """
+presterastats [options]
+--
+startup_delay=    wait this many seconds before first query [60]
+interval=         interval to read statistics [15]
+"""
+
+
+class PresteraPeriodic(object):
+  """Class wrapping a cpss command to request stats."""
+
+  OUTPUT_DIR = '/tmp/prestera'
+
+  def __init__(self, interval):
+    self.interval = interval
+    self.ports_output_file = os.path.join(self.OUTPUT_DIR, 'ports.json')
+
+  def WriteToStderr(self, msg):
+    """Write a message to stderr."""
+
+    sys.stderr.write(msg)
+    sys.stderr.flush()
+
+  def RunPresteraStats(self):
+    """Run presterastats, return command output."""
+    return subprocess.check_output(['presterastats'])
+
+  def AcquireStats(self):
+    """Call the child process and get stats."""
+
+    # Output goes to a temporary file, which is renamed to the destination
+    tmpfile = ''
+    ports_stats = ''
+    try:
+      ports_stats = self.RunPresteraStats()
+    except OSError as ex:
+      self.WriteToStderr('Failed to run presterastats: %s\n' % ex)
+    except subprocess.CalledProcessError as ex:
+      self.WriteToStderr('presterastats exited non-zero: %s\n' % ex)
+
+    if not ports_stats:
+      self.WriteToStderr('Failed to get data from presterastats\n')
+      return
+
+    try:
+      with tempfile.NamedTemporaryFile(delete=False) as fd:
+        if not self.CreateDirs(os.path.dirname(self.ports_output_file)):
+          self.WriteToStderr('Failed to create output directory: %s\n' %
+                             os.path.dirname(self.ports_output_file))
+          return
+        tmpfile = fd.name
+        fd.write(ports_stats)
+        fd.flush()
+        os.fsync(fd.fileno())
+        try:
+          os.rename(tmpfile, self.ports_output_file)
+        except OSError as ex:
+          self.WriteToStderr('Failed to move %s to %s: %s\n' % (
+              tmpfile, self.ports_output_file, ex))
+          return
+    finally:
+      if tmpfile and os.path.exists(tmpfile):
+        os.unlink(tmpfile)
+
+  def CreateDirs(self, dir_to_create):
+    """Recursively creates directories."""
+    try:
+      os.makedirs(dir_to_create)
+    except os.error as ex:
+      if ex.errno == errno.EEXIST:
+        return True
+      self.WriteToStderr('Failed to create directory: %s' % ex)
+      return False
+    return True
+
+  def RunForever(self):
+    while True:
+      self.AcquireStats()
+      time.sleep(self.interval)
+
+
+def main():
+  o = options.Options(optspec)
+  (opt, unused_flags, unused_extra) = o.parse(sys.argv[1:])
+  if opt.startup_delay:
+    time.sleep(opt.startup_delay)
+  prestera = PresteraPeriodic(opt.interval)
+  prestera.RunForever()
+
+
+if __name__ == '__main__':
+  main()
diff --git a/presterastats/prestera_periodic_test.py b/presterastats/prestera_periodic_test.py
new file mode 100644
index 0000000..10be8e4
--- /dev/null
+++ b/presterastats/prestera_periodic_test.py
@@ -0,0 +1,129 @@
+#!/usr/bin/python
+# 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.
+
+"""Tests for prestera_periodic."""
+
+__author__ = 'poist@google.com (Gregory Poist)'
+
+import errno
+import os
+import subprocess
+import tempfile
+import unittest
+import prestera_periodic
+
+STATS_JSON = """
+{
+  "port-interface-statistics": {
+    "0/0": {
+      "broadcast_packets_received": 8739,
+      "broadcast_packets_sent": 3,
+      "bytes_received": 32061162,
+      "bytes_sent": 10145704,
+      "multicast_packets_received": 35484,
+      "multicast_packets_sent": 20471,
+      "unicast_packets_received": 22875,
+      "unicast_packets_sent": 20737
+    }
+  }
+}
+"""
+
+
+class FakePresteraPeriodic(prestera_periodic.PresteraPeriodic):
+  """Mock PresteraPeriodic."""
+
+  def WriteToStderr(self, msg):
+    self.error_count += 1
+
+  def RunPresteraStats(self):
+    self.get_stats_called = True
+    if self.raise_os_error:
+      raise OSError(errno.ENOENT, 'raise an exception')
+    if self.raise_subprocess:
+      raise subprocess.CalledProcessError(cmd='durp', returncode=1)
+    return self.stats_response
+
+
+class PresteraPeriodicTest(unittest.TestCase):
+
+  def CreateTempFile(self):
+    # Create a temp file and have that be the target output file.
+    fd, self.output_file = tempfile.mkstemp()
+    os.close(fd)
+
+  def DeleteTempFile(self):
+    if os.path.exists(self.output_file):
+      os.unlink(self.output_file)
+
+  def setUp(self):
+    self.CreateTempFile()
+    self.periodic = FakePresteraPeriodic(1000)
+    self.periodic.raise_os_error = False
+    self.periodic.raise_subprocess = False
+    self.periodic.stats_response = STATS_JSON
+    self.periodic.ports_output_file = self.output_file
+    self.periodic.error_count = 0
+
+  def tearDown(self):
+    self.DeleteTempFile()
+
+  def testAcquireStats(self):
+    self.periodic.AcquireStats()
+
+    self.assertEquals(True, self.periodic.get_stats_called)
+    with open(self.output_file, 'r') as f:
+      output = ''.join(line for line in f)
+      self.assertEqual(self.periodic.stats_response, output)
+
+  def testAcquireStatsFailureToCreateOutputDir(self):
+    self.periodic.ports_output_file = '/root/nope/cant/write/this'
+
+    self.periodic.AcquireStats()
+    self.assertTrue(self.periodic.error_count > 0)
+
+  def testSubsequentEmptyDataNoOverwrite(self):
+    self.periodic.AcquireStats()
+
+    self.periodic.stats_response = ''
+    self.periodic.AcquireStats()
+
+    with open(self.output_file, 'r') as f:
+      output = ''.join(line for line in f)
+      self.assertEqual(STATS_JSON, output)
+
+  def testSubsequentExecError(self):
+    self.periodic.AcquireStats()
+
+    self.periodic.raise_os_error = True
+    self.periodic.AcquireStats()
+
+    self.assertTrue(self.periodic.error_count > 0)
+    with open(self.output_file, 'r') as f:
+      output = ''.join(line for line in f)
+      self.assertEqual(STATS_JSON, output)
+
+  def testExecError(self):
+    self.periodic.raise_subprocess = True
+    self.periodic.AcquireStats()
+
+    self.assertTrue(self.periodic.error_count > 0)
+    with open(self.output_file, 'r') as f:
+      output = ''.join(line for line in f)
+      self.assertEqual('', output)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/speedtest/.gitignore b/speedtest/.gitignore
new file mode 100644
index 0000000..17febb8
--- /dev/null
+++ b/speedtest/.gitignore
@@ -0,0 +1 @@
+speedtest
diff --git a/speedtest/Makefile b/speedtest/Makefile
index af5ea27..f926c9a 100644
--- a/speedtest/Makefile
+++ b/speedtest/Makefile
@@ -13,53 +13,116 @@
 TFLAGS=$(DEBUG) -isystem ${GTEST_DIR}/include -isystem $(GMOCK_DIR)/include -pthread -std=c++11
 
 LIBS=-lcurl -lpthread -ljsoncpp
-TOBJS=curl_env.o url.o errors.o request.o utils.o
+TOBJS=curl_env.o url.o errors.o request.o status.o utils.o
 OBJS=config.o \
      curl_env.o \
-     download_task.o \
+     download.o \
      errors.o \
-     http_task.o \
+     find_nearest.o \
+     init.o \
      options.o \
-     ping_task.o \
+     ping.o \
+     region.o \
      request.o \
+     result.o \
      speedtest.o \
-     task.o \
-     timed_runner.o \
+     status.o \
      transfer_runner.o \
-     transfer_task.o \
-     upload_task.o \
+     upload.o \
      url.o \
      utils.o
 
 all: speedtest
 
-config.o: config.cc config.h url.h
+config.o: config.cc \
+          config.h \
+          errors.h \
+          region.h \
+          request.h \
+          status.h \
+          url.h \
+          utils.h
+curl_env.o: curl_env.cc curl_env.h errors.h request.h utils.h
+download.o: download.cc \
+            download.h \
+            request.h \
+            status.h \
+            utils.h
 errors.o: errors.cc errors.h
-curl_env.o: curl_env.cc curl_env.h errors.h request.h
-download_task.o: download_task.cc download_task.h transfer_task.h utils.h
-http_task.o: http_task.cc http_task.h
-options.o: options.cc options.h url.h
-ping_task.o: ping_task.cc ping_task.h http_task.h request.h url.h utils.h
-request.o: request.cc request.h url.h
+find_nearest.o: find_nearest.cc \
+                find_nearest.h \
+                ping.h \
+                region.h \
+                request.h \
+                status.h \
+                utils.h
+init.o: init.cc \
+        init.h \
+        config.h \
+        find_nearest.h \
+        region.h \
+        request.h \
+        status.h \
+        timed_runner.h \
+        url.h \
+        utils.h
+options.o: options.cc options.h request.h url.h
+ping.o: ping.cc \
+        ping.h \
+        errors.h \
+        region.h \
+        request.h \
+        status.h \
+        url.h \
+        utils.h
+region.o: region.cc \
+          region.h \
+          errors.h \
+          request.h \
+          status.h \
+          region.h \
+          utils.h
+request.o: request.cc request.h url.h utils.h
+result.o: result.cc \
+          result.h \
+          config.h \
+          find_nearest.h \
+          init.h \
+          ping.h \
+          speedtest.h \
+          transfer_runner.h \
+          url.h
 speedtest.o: speedtest.cc \
              speedtest.h \
              config.h \
-             curl_env.h \
-             download_task.h \
+             download.h \
+             errors.h \
+             init.h \
              options.h \
-             ping_task.h \
+             region.h \
              request.h \
-             task.h \
+             result.h \
+             status.h \
              timed_runner.h \
              transfer_runner.h \
-             upload_task.h \
-             url.h
-speedtest_main.o: speedtest_main.cc options.h speedtest.h
-task.o: task.cc task.h utils.h
-timed_runner.o: timed_runner.cc timed_runner.h task.h
-transfer_runner.o: transfer_runner.cc transfer_runner.h transfer_task.h utils.h
-transfer_task.o: transfer_task.cc transfer_task.h http_task.h
-upload_task.o: upload_task.cc upload_task.h transfer_task.h utils.h
+             upload.h \
+             url.h \
+             utils.h
+speedtest_main.o: speedtest_main.cc \
+                  curl_env.h \
+                  options.h \
+                  request.h \
+                  speedtest.h
+status.o: status.cc status.h utils.h
+transfer_runner.o: transfer_runner.cc \
+                   transfer_runner.h \
+                   status.h \
+                   utils.h
+upload.o: upload.cc \
+          upload.h \
+          request.h \
+          status.h \
+          utils.h
 utils.o: utils.cc options.h
 url.o: url.cc url.h utils.h
 
@@ -87,10 +150,10 @@
 	$(CXX) -c $< $(TFLAGS) $(CXXFLAGS)
 
 %_test: %_test.o %.o libgmock.a libspeedtesttest.a
-	$(CXX) -o $@ $(TFLAGS) googlemock/src/gmock_main.cc $< $*.o $(LDFLAGS) $(LIBS) libgmock.a libspeedtesttest.a
+	$(CXX) -o $@ $(TFLAGS) googlemock/src/gmock_main.cc $< $*.o $(LDFLAGS) libgmock.a libspeedtesttest.a $(LIBS)
 	./$@
 
-test: config_test options_test request_test url_test
+test: config_test options_test region_test request_test url_test
 
 install: speedtest
 	$(INSTALL) -m 0755 speedtest $(BINDIR)/
diff --git a/speedtest/config.cc b/speedtest/config.cc
index aede959..41803eb 100644
--- a/speedtest/config.cc
+++ b/speedtest/config.cc
@@ -16,26 +16,63 @@
 
 #include "config.h"
 
+#include <curl/curl.h>
+#include "errors.h"
+#include "request.h"
+#include "utils.h"
+
 // For some reason, the libjsoncpp package installs to /usr/include/jsoncpp/json
 // instead of /usr{,/local}/include/json
 #include <jsoncpp/json/json.h>
 
 namespace speedtest {
 
-bool ParseConfig(const std::string &json, Config *config) {
+ConfigResult LoadConfig(ConfigOptions options) {
+  ConfigResult result;
+  result.start_time = SystemTimeMicros();
+  if (!options.request_factory) {
+    result.status = Status(StatusCode::INVALID_ARGUMENT,
+                           "request factory not set");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  http::Url config_url(options.region_url);
+  config_url.set_path("/config");
+  if (options.verbose) {
+    std::cout << "Loading config from " << config_url.url() << "\n";
+  }
+  http::Request::Ptr request = options.request_factory(config_url);
+  request->set_url(config_url);
+  request->set_timeout_millis(500);
+  std::string json;
+  CURLcode code = request->Get([&](void *data, size_t size){
+    json.assign(static_cast<const char *>(data), size);
+  });
+  if (code != CURLE_OK) {
+    result.status = Status(StatusCode::INTERNAL, http::ErrorString(code));
+  } else {
+    result.status = ParseConfig(json, &result.config);
+  }
+  result.end_time = SystemTimeMicros();
+  return result;
+}
+
+Status ParseConfig(const std::string &json, Config *config) {
   if (!config) {
-    return false;
+    return Status(StatusCode::FAILED_PRECONDITION, "Config is null");
   }
 
   Json::Reader reader;
   Json::Value root;
   if (!reader.parse(json, root, false)) {
-    return false;
+    return Status(StatusCode::INVALID_ARGUMENT, "Failed to parse config JSON");
   }
 
-  config->download_size = root["downloadSize"].asInt();
-  config->upload_size = root["uploadSize"].asInt();
+  config->download_bytes = root["downloadSize"].asInt();
+  config->upload_bytes = root["uploadSize"].asInt();
   config->interval_millis = root["intervalSize"].asInt();
+  config->location_id = root["locationId"].asString();
   config->location_name = root["locationName"].asString();
   config->min_transfer_intervals = root["minTransferIntervals"].asInt();
   config->max_transfer_intervals = root["maxTransferIntervals"].asInt();
@@ -44,32 +81,12 @@
   config->max_transfer_variance = root["maxTransferVariance"].asDouble();
   config->num_uploads = root["numConcurrentUploads"].asInt();
   config->num_downloads = root["numConcurrentDownloads"].asInt();
-  config->ping_runtime = root["pingRunTime"].asInt();
-  config->ping_timeout = root["pingTimeout"].asInt();
+  config->ping_runtime_millis = root["pingRunTime"].asInt();
+  config->ping_timeout_millis = root["pingTimeout"].asInt();
   config->transfer_port_start = root["transferPortStart"].asInt();
   config->transfer_port_end = root["transferPortEnd"].asInt();
-  return true;
-}
-
-bool ParseServers(const std::string &json, std::vector<http::Url> *servers) {
-  if (!servers) {
-    return false;
-  }
-
-  Json::Reader reader;
-  Json::Value root;
-  if (!reader.parse(json, root, false)) {
-    return false;
-  }
-
-  for (const auto &it : root["regionalServers"]) {
-    http::Url url(it.asString());
-    if (!url.ok()) {
-      return false;
-    }
-    servers->emplace_back(url);
-  }
-  return true;
+  config->average_type = root["averageType"].asString();
+  return Status::OK;
 }
 
 void PrintConfig(const Config &config) {
@@ -77,9 +94,10 @@
 }
 
 void PrintConfig(std::ostream &out, const Config &config) {
-  out << "Download size: " << config.download_size << " bytes\n"
-      << "Upload size: " << config.upload_size << " bytes\n"
+  out << "Download size: " << config.download_bytes << " bytes\n"
+      << "Upload size: " << config.upload_bytes << " bytes\n"
       << "Interval size: " << config.interval_millis << " ms\n"
+      << "Location ID: " << config.location_id << "\n"
       << "Location name: " << config.location_name << "\n"
       << "Min transfer intervals: " << config.min_transfer_intervals << "\n"
       << "Max transfer intervals: " << config.max_transfer_intervals << "\n"
@@ -88,10 +106,11 @@
       << "Max transfer variance: " << config.max_transfer_variance << "\n"
       << "Number of downloads: " << config.num_downloads << "\n"
       << "Number of uploads: " << config.num_uploads << "\n"
-      << "Ping runtime: " << config.ping_runtime << " ms\n"
-      << "Ping timeout: " << config.ping_timeout << " ms\n"
+      << "Ping runtime: " << config.ping_runtime_millis << " ms\n"
+      << "Ping timeout: " << config.ping_timeout_millis << " ms\n"
       << "Transfer port start: " << config.transfer_port_start << "\n"
-      << "Transfer port end: " << config.transfer_port_end << "\n";
+      << "Transfer port end: " << config.transfer_port_end << "\n"
+      << "Average type: " << config.average_type << "\n";
 }
 
 }  // namespace
diff --git a/speedtest/config.h b/speedtest/config.h
index 2988484..f03f6cf 100644
--- a/speedtest/config.h
+++ b/speedtest/config.h
@@ -20,41 +20,56 @@
 #include <iostream>
 #include <string>
 #include <vector>
+#include "region.h"
+#include "request.h"
+#include "status.h"
 #include "url.h"
 
 namespace speedtest {
 
 struct Config {
-  int download_size = 0;
-  int upload_size = 0;
-  int interval_millis = 0;
+  long download_bytes = 0;
+  long upload_bytes = 0;
+  long interval_millis = 0;
+  std::string location_id;
   std::string location_name;
   int min_transfer_intervals = 0;
   int max_transfer_intervals = 0;
-  int min_transfer_runtime = 0;
-  int max_transfer_runtime = 0;
+  long min_transfer_runtime = 0;
+  long max_transfer_runtime = 0;
   double max_transfer_variance = 0;
   int num_downloads = 0;
   int num_uploads = 0;
-  int ping_runtime = 0;
-  int ping_timeout = 0;
+  long ping_runtime_millis = 0;
+  long ping_timeout_millis = 0;
   int transfer_port_start = 0;
   int transfer_port_end = 0;
+  std::string average_type;
 };
 
+struct ConfigOptions {
+  bool verbose;
+  http::Request::Factory request_factory;
+  http::Url region_url;
+};
+
+struct ConfigResult {
+  long start_time;
+  long end_time;
+  Status status;
+  Config config;
+};
+
+ConfigResult LoadConfig(ConfigOptions options);
+
 // Parses a JSON document into a config struct.
 // Returns true with the config struct populated on success.
 // Returns false if the JSON is invalid or config is null.
-bool ParseConfig(const std::string &json, Config *config);
-
-// Parses a JSON document into a list of server URLs
-// Returns true with the servers populated in the vector on success.
-// Returns false if the JSON is invalid or servers is null.
-bool ParseServers(const std::string &json, std::vector<http::Url> *servers);
+Status ParseConfig(const std::string &json, Config *config);
 
 void PrintConfig(const Config &config);
 void PrintConfig(std::ostream &out, const Config &config);
 
 }  // namespace speedtest
 
-#endif //SPEEDTEST_CONFIG_H
+#endif // SPEEDTEST_CONFIG_H
diff --git a/speedtest/config_test.cc b/speedtest/config_test.cc
index 5924921..7b04d38 100644
--- a/speedtest/config_test.cc
+++ b/speedtest/config_test.cc
@@ -18,6 +18,12 @@
 
 #include <gtest/gtest.h>
 #include <gmock/gmock.h>
+#include <string>
+#include <vector>
+#include "region.h"
+
+#define EXPECT_OK(statement) EXPECT_EQ(::speedtest::Status::OK, (statement))
+#define EXPECT_ERROR(statement) EXPECT_NE(::speedtest::Status::OK, (statement))
 
 namespace speedtest {
 namespace {
@@ -26,6 +32,7 @@
 {
     "downloadSize": 10000000,
     "intervalSize": 200,
+    "locationId": "mci",
     "locationName": "Kansas City",
     "maxTransferIntervals": 25,
     "maxTransferRunTime": 20000,
@@ -42,91 +49,42 @@
 }
 )CONFIG";
 
-const char *kValidServers = R"SERVERS(
-{
-    "locationName": "Kansas City",
-    "regionalServers": [
-        "http://austin.speed.googlefiber.net/",
-        "http://kansas.speed.googlefiber.net/",
-        "http://provo.speed.googlefiber.net/",
-        "http://stanford.speed.googlefiber.net/"
-    ]
-}
-)SERVERS";
-
-const char *kInvalidServers = R"SERVERS(
-{
-    "locationName": "Kansas City",
-    "regionalServers": [
-        "example.com..",
-    ]
-}
-)SERVERS";
-
 const char *kInvalidJson = "{{}{";
 
 TEST(ParseConfigTest, NullConfig_Invalid) {
-  EXPECT_FALSE(ParseConfig(kValidConfig, nullptr));
+  EXPECT_ERROR(ParseConfig(kValidConfig, nullptr));
 }
 
 TEST(ParseConfigTest, EmptyJson_Invalid) {
   Config config;
-  EXPECT_FALSE(ParseConfig("", &config));
+  EXPECT_ERROR(ParseConfig("", &config));
 }
 
 TEST(ParseConfigTest, InvalidJson_Invalid) {
   Config config;
-  EXPECT_FALSE(ParseConfig(kInvalidJson, &config));
+  EXPECT_ERROR(ParseConfig(kInvalidJson, &config));
 }
 
 TEST(ParseConfigTest, FullConfig_Valid) {
   Config config;
-  EXPECT_TRUE(ParseConfig(kValidConfig, &config));
-  EXPECT_EQ(10000000, config.download_size);
-  EXPECT_EQ(20000000, config.upload_size);
+  EXPECT_OK(ParseConfig(kValidConfig, &config));
+  EXPECT_EQ(10000000, config.download_bytes);
+  EXPECT_EQ(20000000, config.upload_bytes);
   EXPECT_EQ(20, config.num_downloads);
   EXPECT_EQ(15, config.num_uploads);
   EXPECT_EQ(200, config.interval_millis);
+  EXPECT_EQ("mci", config.location_id);
   EXPECT_EQ("Kansas City", config.location_name);
   EXPECT_EQ(10, config.min_transfer_intervals);
   EXPECT_EQ(25, config.max_transfer_intervals);
   EXPECT_EQ(5000, config.min_transfer_runtime);
   EXPECT_EQ(20000, config.max_transfer_runtime);
   EXPECT_EQ(0.08, config.max_transfer_variance);
-  EXPECT_EQ(3000, config.ping_runtime);
-  EXPECT_EQ(300, config.ping_timeout);
+  EXPECT_EQ(3000, config.ping_runtime_millis);
+  EXPECT_EQ(300, config.ping_timeout_millis);
   EXPECT_EQ(3004, config.transfer_port_start);
   EXPECT_EQ(3023, config.transfer_port_end);
 }
 
-TEST(ParseServersTest, NullServers_Invalid) {
-  EXPECT_FALSE(ParseServers(kValidServers, nullptr));
-}
-
-TEST(ParseServersTest, EmptyServers_Invalid) {
-  std::vector<http::Url> servers;
-  EXPECT_FALSE(ParseServers("", &servers));
-}
-
-TEST(ParseServersTest, InvalidJson_Invalid) {
-  std::vector<http::Url> servers;
-  EXPECT_FALSE(ParseServers(kInvalidJson, &servers));
-}
-
-TEST(ParseServersTest, FullServers_Valid) {
-  std::vector<http::Url> servers;
-  EXPECT_TRUE(ParseServers(kValidServers, &servers));
-  EXPECT_THAT(servers, testing::UnorderedElementsAre(
-      http::Url("http://austin.speed.googlefiber.net/"),
-      http::Url("http://kansas.speed.googlefiber.net/"),
-      http::Url("http://provo.speed.googlefiber.net/"),
-      http::Url("http://stanford.speed.googlefiber.net/")));
-}
-
-TEST(ParseServersTest, InvalidServers_Invalid) {
-  std::vector<http::Url> servers;
-  EXPECT_FALSE(ParseServers(kInvalidServers, &servers));
-}
-
 }  // namespace
 }  // namespace speedtest
diff --git a/speedtest/curl_env.cc b/speedtest/curl_env.cc
index eed370f..ee959ee 100644
--- a/speedtest/curl_env.cc
+++ b/speedtest/curl_env.cc
@@ -19,7 +19,6 @@
 #include <cstdlib>
 #include <iostream>
 #include "errors.h"
-#include "request.h"
 
 namespace http {
 namespace {
@@ -71,7 +70,7 @@
   curl_global_cleanup();
 }
 
-std::unique_ptr<Request> CurlEnv::NewRequest(const Url &url) {
+Request::Ptr CurlEnv::NewRequest(const Url &url) {
   // curl_global_init is not threadsafe and calling curl_easy_init may
   // implicitly call it so we need to mutex lock on creating all requests
   // to ensure the global initialization is done in a threadsafe manner.
@@ -101,6 +100,7 @@
   }
 
   curl_easy_setopt(handle.get(), CURLOPT_SHARE, share_);
+  curl_easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1);
   return std::unique_ptr<Request>(new Request(handle, url));
 }
 
diff --git a/speedtest/curl_env.h b/speedtest/curl_env.h
index 6a70f28..7bf0b60 100644
--- a/speedtest/curl_env.h
+++ b/speedtest/curl_env.h
@@ -20,7 +20,9 @@
 #include <curl/curl.h>
 #include <memory>
 #include <mutex>
+#include "request.h"
 #include "url.h"
+#include "utils.h"
 
 namespace http {
 
@@ -37,7 +39,7 @@
   static std::shared_ptr<CurlEnv> NewCurlEnv(const Options &options);
   virtual ~CurlEnv();
 
-  std::unique_ptr<Request> NewRequest(const Url &url);
+  Request::Ptr NewRequest(const Url &url);
 
   void Lock(curl_lock_data lock_type);
   void Unlock(curl_lock_data lock_type);
@@ -54,9 +56,7 @@
   std::mutex dns_mutex_;
   CURLSH *share_;  // owned
 
-  // disable
-  CurlEnv(const CurlEnv &other) = delete;
-  void operator=(const CurlEnv &other) = delete;
+  DISALLOW_COPY_AND_ASSIGN(CurlEnv);
 };
 
 }  // namespace http
diff --git a/speedtest/download.cc b/speedtest/download.cc
new file mode 100644
index 0000000..f1dcc20
--- /dev/null
+++ b/speedtest/download.cc
@@ -0,0 +1,85 @@
+/*
+ * 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 "download.h"
+
+#include <string>
+#include <vector>
+#include <thread>
+
+namespace speedtest {
+
+Download::Download(const Options &options)
+    : options_(options),
+      start_time_(0),
+      end_time_(0),
+      bytes_transferred_(0) {
+}
+
+Download::Result Download::operator()(std::atomic_bool *cancel) {
+  start_time_ = SystemTimeMicros();
+  bytes_transferred_ = 0;
+
+  if (!cancel) {
+    end_time_ = SystemTimeMicros();
+    return GetResult(Status(StatusCode::FAILED_PRECONDITION, "cancel is null"));
+  }
+
+  std::vector<std::thread> threads;
+  for (int i = 0; i < options_.num_transfers; ++i) {
+    threads.emplace_back([=]{
+      http::Request::Ptr download = options_.request_factory(i);
+      while (!*cancel) {
+        long downloaded = 0;
+        download->set_param("i", to_string(i));
+        download->set_param("size", to_string(options_.download_bytes));
+        download->set_param("time", to_string(SystemTimeMicros()));
+        download->set_progress_fn([&](curl_off_t,
+                                      curl_off_t dlnow,
+                                      curl_off_t,
+                                      curl_off_t) -> bool {
+          if (dlnow > downloaded) {
+            bytes_transferred_ += dlnow - downloaded;
+            downloaded = dlnow;
+          }
+          return *cancel;
+        });
+        download->Get();
+        download->Reset();
+      }
+    });
+  }
+
+  for (std::thread &thread : threads) {
+    if (thread.joinable()) {
+      thread.join();
+    }
+  }
+
+  end_time_ = SystemTimeMicros();
+  return GetResult(Status::OK);
+}
+
+Download::Result Download::GetResult(Status status) const {
+  Download::Result result;
+  result.start_time = start_time_;
+  result.end_time = end_time_;
+  result.status = status;
+  result.bytes_transferred = bytes_transferred_;
+  return result;
+}
+
+}  // namespace
diff --git a/speedtest/download.h b/speedtest/download.h
new file mode 100644
index 0000000..65c202e
--- /dev/null
+++ b/speedtest/download.h
@@ -0,0 +1,65 @@
+/*
+ * 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 SPEEDTEST_DOWNLOAD_H
+#define SPEEDTEST_DOWNLOAD_H
+
+#include <atomic>
+#include <functional>
+#include "request.h"
+#include "status.h"
+#include "utils.h"
+
+namespace speedtest {
+
+class Download {
+ public:
+  struct Options {
+    bool verbose;
+    std::function<http::Request::Ptr(int)> request_factory;
+    int num_transfers;
+    long download_bytes;
+  };
+
+  struct Result {
+    long start_time;
+    long end_time;
+    Status status;
+    long bytes_transferred;
+  };
+
+  explicit Download(const Options &options);
+
+  Result operator()(std::atomic_bool *cancel);
+
+  long start_time() const { return start_time_; }
+  long end_time() const { return end_time_; }
+  long bytes_transferred() const { return bytes_transferred_; }
+
+ private:
+  Result GetResult(Status status) const;
+
+  Options options_;
+  std::atomic_long start_time_;
+  std::atomic_long end_time_;
+  std::atomic_long bytes_transferred_;
+
+  DISALLOW_COPY_AND_ASSIGN(Download);
+};
+
+}  // namespace speedtest
+
+#endif // SPEEDTEST_DOWNLOAD_H
diff --git a/speedtest/download_task.cc b/speedtest/download_task.cc
deleted file mode 100644
index a643725..0000000
--- a/speedtest/download_task.cc
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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 "download_task.h"
-
-#include <algorithm>
-#include <cassert>
-#include <iostream>
-#include <thread>
-#include "utils.h"
-
-namespace speedtest {
-
-DownloadTask::DownloadTask(const Options &options)
-    : TransferTask(options_),
-      options_(options) {
-  assert(options_.num_transfers > 0);
-  assert(options_.download_size > 0);
-}
-
-void DownloadTask::RunInternal() {
-  ResetCounters();
-  threads_.clear();
-  if (options_.verbose) {
-    std::cout << "Downloading " << options_.num_transfers
-              << " threads with " << options_.download_size << " bytes\n";
-  }
-  for (int i = 0; i < options_.num_transfers; ++i) {
-    threads_.emplace_back([=]{
-      RunDownload(i);
-    });
-  }
-}
-
-void DownloadTask::StopInternal() {
-  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
-    t.join();
-  });
-}
-
-void DownloadTask::RunDownload(int id) {
-  http::Request::Ptr download = options_.request_factory(id);
-  while (GetStatus() == TaskStatus::RUNNING) {
-    long downloaded = 0;
-    download->set_param("i", to_string(id));
-    download->set_param("size", to_string(options_.download_size));
-    download->set_param("time", to_string(SystemTimeMicros()));
-    download->set_progress_fn([&](curl_off_t,
-                                  curl_off_t dlnow,
-                                  curl_off_t,
-                                  curl_off_t) -> bool {
-      if (dlnow > downloaded) {
-        TransferBytes(dlnow - downloaded);
-        downloaded = dlnow;
-      }
-      return GetStatus() != TaskStatus::RUNNING;
-    });
-    StartRequest();
-    download->Get();
-    EndRequest();
-    download->Reset();
-  }
-}
-
-}  // namespace speedtest
diff --git a/speedtest/download_task.h b/speedtest/download_task.h
deleted file mode 100644
index 2b65478..0000000
--- a/speedtest/download_task.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 SPEEDTEST_DOWNLOAD_TASK_H
-#define SPEEDTEST_DOWNLOAD_TASK_H
-
-#include <thread>
-#include <vector>
-#include "transfer_task.h"
-
-namespace speedtest {
-
-class DownloadTask : public TransferTask {
- public:
-  struct Options : TransferTask::Options {
-    int download_size = 0;
-  };
-
-  explicit DownloadTask(const Options &options);
-
- protected:
-  void RunInternal() override;
-  void StopInternal() override;
-
- private:
-  void RunDownload(int id);
-
-  Options options_;
-  std::vector<std::thread> threads_;
-
-  // disallowed
-  DownloadTask(const DownloadTask &) = delete;
-  void operator=(const DownloadTask &) = delete;
-};
-
-}  // namespace speedtest
-
-#endif  // SPEEDTEST_DOWNLOAD_TASK_H
diff --git a/speedtest/find_nearest.cc b/speedtest/find_nearest.cc
new file mode 100644
index 0000000..f1547d4
--- /dev/null
+++ b/speedtest/find_nearest.cc
@@ -0,0 +1,102 @@
+/*
+ * 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 "find_nearest.h"
+
+#include <iostream>
+#include <limits>
+#include <mutex>
+#include <thread>
+
+namespace speedtest {
+namespace {
+
+const long kDefaultPingTimeoutMillis = 500;
+
+}
+
+FindNearest::FindNearest(const Options &options)
+    : options_(options),
+      start_time_(0),
+      end_time_(0) {
+}
+
+FindNearest::Result FindNearest::operator()(std::atomic_bool *cancel) {
+  FindNearest::Result result;
+  result.start_time = SystemTimeMicros();
+
+  if (!cancel) {
+    result.status = Status(StatusCode::FAILED_PRECONDITION, "cancel is null");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  if (options_.regions.size() == 1) {
+    result.selected_region = options_.regions.front();
+    result.status = Status::OK;
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  std::vector<std::thread> threads;
+  std::mutex mutex;
+  for (const Region &region : options_.regions) {
+    threads.emplace_back([&]{
+      Ping::Options ping_options;
+      ping_options.verbose = options_.verbose;
+      ping_options.request_factory = options_.request_factory;
+      ping_options.timeout_millis = options_.ping_timeout_millis > 0
+                                    ? options_.ping_timeout_millis
+                                    : kDefaultPingTimeoutMillis;
+      ping_options.num_concurrent_pings = 0;
+      ping_options.region = region;
+      Ping ping(ping_options);
+      Ping::Result ping_result = ping(cancel);
+      std::lock_guard<std::mutex> lock(mutex);
+      result.ping_results.push_back(ping_result);
+    });
+  }
+
+  for (std::thread &thread : threads) {
+    if (thread.joinable()) {
+      thread.join();
+    }
+  }
+
+  const Ping::Result *fastest = nullptr;
+  for (const Ping::Result &ping_result : result.ping_results) {
+    if (ping_result.received > 0) {
+      if (!fastest) {
+        fastest = &ping_result;
+      } else if (ping_result.min_ping_micros < fastest->min_ping_micros) {
+        fastest = &ping_result;
+      }
+    }
+  }
+
+  if (!fastest) {
+    result.status = Status(StatusCode::UNAVAILABLE,
+                           "All pings failed for find nearest");
+  } else {
+    result.selected_region = fastest->region;
+    result.min_ping_micros = fastest->min_ping_micros;
+    result.status = Status::OK;
+  }
+  result.end_time = SystemTimeMicros();
+  return result;
+}
+
+}  // namespace speedtest
diff --git a/speedtest/find_nearest.h b/speedtest/find_nearest.h
new file mode 100644
index 0000000..65ae3bf
--- /dev/null
+++ b/speedtest/find_nearest.h
@@ -0,0 +1,64 @@
+/*
+ * 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 SPEEDTEST_FIND_NEAREST_H
+#define SPEEDTEST_FIND_NEAREST_H
+
+#include <vector>
+#include "ping.h"
+#include "region.h"
+#include "request.h"
+#include "status.h"
+#include "utils.h"
+
+namespace speedtest {
+
+class FindNearest {
+ public:
+  struct Options {
+    bool verbose;
+    http::Request::Factory request_factory;
+    std::vector<Region> regions;
+    long ping_timeout_millis;
+  };
+
+  struct Result {
+    long start_time;
+    long end_time;
+    std::vector<Ping::Result> ping_results;
+    Status status;
+    Region selected_region;
+    long min_ping_micros;
+  };
+
+  explicit FindNearest(const Options &options);
+
+  Result operator()(std::atomic_bool *cancel);
+
+  long start_time() const { return start_time_; }
+  long end_time() const { return end_time_; }
+
+ private:
+  Options options_;
+  std::atomic_long start_time_;
+  std::atomic_long end_time_;
+
+  DISALLOW_COPY_AND_ASSIGN(FindNearest);
+};
+
+}  // namespace speedtest
+
+#endif // SPEEDTEST_FIND_NEAREST_H
diff --git a/speedtest/http_task.cc b/speedtest/http_task.cc
deleted file mode 100644
index 1275aa4..0000000
--- a/speedtest/http_task.cc
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * 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 "http_task.h"
-
-namespace speedtest {
-
-HttpTask::HttpTask(const Options &options): Task(options) {
-}
-
-}  // namespace speedtest
diff --git a/speedtest/http_task.h b/speedtest/http_task.h
deleted file mode 100644
index a54e4ba..0000000
--- a/speedtest/http_task.h
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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 SPEEDTEST_HTTP_TASK_H
-#define SPEEDTEST_HTTP_TASK_H
-
-#include "task.h"
-
-#include "request.h"
-
-namespace speedtest {
-
-class HttpTask : public Task {
- public:
-  struct Options : Task::Options {
-    bool verbose = false;
-    std::function<http::Request::Ptr(int)> request_factory;
-  };
-
-  explicit HttpTask(const Options &options);
-
- private:
-  // disallowed
-  HttpTask(const Task &) = delete;
-  void operator=(const HttpTask &) = delete;
-};
-
-}  // namespace speedtest
-
-#endif  // SPEEDTEST_HTTP_TASK_H
diff --git a/speedtest/init.cc b/speedtest/init.cc
new file mode 100644
index 0000000..39b646b
--- /dev/null
+++ b/speedtest/init.cc
@@ -0,0 +1,118 @@
+/*
+ * 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 "init.h"
+
+#include "timed_runner.h"
+
+namespace speedtest {
+
+Init::Init(const Options &options)
+    : options_(options) {
+}
+
+Init::Result Init::operator()(std::atomic_bool *cancel) {
+  Init::Result result;
+  result.start_time = SystemTimeMicros();
+
+  if (!cancel) {
+    result.status = Status(StatusCode::FAILED_PRECONDITION, "cancel is null");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "init aborted");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  RegionOptions region_options;
+  region_options.verbose = options_.verbose;
+  region_options.request_factory = options_.request_factory;
+  region_options.global = options_.global;
+  region_options.global_url = options_.global_url;
+  region_options.regional_urls = options_.regional_urls;
+  result.region_result = LoadRegions(region_options);
+  if (!result.region_result.status.ok()) {
+    result.status = result.region_result.status;
+    result.end_time = SystemTimeMicros();
+    if (options_.verbose) {
+      std::cout << "Load regions failed: " << result.status.ToString() << "\n";
+    }
+    return result;
+  }
+  if (options_.verbose) {
+    std::cout << "Load regions succeeded:\n";
+    for (const Region &region : result.region_result.regions) {
+      std::cout << "  " << DescribeRegion(region) << "\n";
+    }
+  }
+
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "init aborted");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  FindNearest::Options find_options;
+  find_options.verbose = options_.verbose;
+  find_options.request_factory = options_.request_factory;
+  find_options.ping_timeout_millis = options_.ping_timeout_millis;
+  find_options.regions = result.region_result.regions;
+  FindNearest find_nearest(find_options);
+  result.find_nearest_result = RunTimed(std::ref(find_nearest), cancel, 2000);
+  if (!result.find_nearest_result.status.ok()) {
+    result.status = result.find_nearest_result.status;
+    result.end_time = SystemTimeMicros();
+    if (options_.verbose) {
+      std::cout << "Find nearest failed: " << result.status.ToString() << "\n";
+    }
+    return result;
+  }
+  result.selected_region = result.find_nearest_result.selected_region;
+
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "init aborted");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  ConfigOptions config_options;
+  config_options.verbose = options_.verbose;
+  config_options.request_factory = options_.request_factory;
+  config_options.region_url = result.selected_region.urls.front();
+  result.config_result = LoadConfig(config_options);
+  if (!result.config_result.status.ok()) {
+    result.status = result.config_result.status;
+    if (options_.verbose) {
+      std::cout << "Load config failed: " << result.status.ToString() << "\n";
+    }
+  } else {
+    result.status = Status::OK;
+    if (result.selected_region.id.empty()) {
+      result.selected_region.id = result.config_result.config.location_id;
+    }
+    if (result.selected_region.name.empty()) {
+      result.selected_region.name = result.config_result.config.location_name;
+    }
+  }
+
+  result.end_time = SystemTimeMicros();
+  return result;
+}
+
+}  // namespace speedtest
diff --git a/speedtest/init.h b/speedtest/init.h
new file mode 100644
index 0000000..5c69120
--- /dev/null
+++ b/speedtest/init.h
@@ -0,0 +1,65 @@
+/*
+ * 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 SPEEDTEST_INIT_H
+#define SPEEDTEST_INIT_H
+
+#include <atomic>
+#include <vector>
+#include "config.h"
+#include "find_nearest.h"
+#include "region.h"
+#include "request.h"
+#include "status.h"
+#include "url.h"
+#include "utils.h"
+
+namespace speedtest {
+
+class Init {
+ public:
+  struct Options {
+    bool verbose;
+    http::Request::Factory request_factory;
+    bool global;
+    http::Url global_url;
+    std::vector<http::Url> regional_urls;
+    long ping_timeout_millis;
+  };
+
+  struct Result {
+    long start_time;
+    long end_time;
+    Status status;
+    RegionResult region_result;
+    FindNearest::Result find_nearest_result;
+    Region selected_region;
+    ConfigResult config_result;
+  };
+
+  explicit Init(const Options &options);
+
+  Result operator()(std::atomic_bool *cancel);
+
+ private:
+  Options options_;
+
+  DISALLOW_COPY_AND_ASSIGN(Init);
+};
+
+}  // namespace speedtest
+
+#endif // SPEEDTEST_INIT_H
diff --git a/speedtest/options.cc b/speedtest/options.cc
index 133b857..f657b0c 100644
--- a/speedtest/options.cc
+++ b/speedtest/options.cc
@@ -24,9 +24,12 @@
 #include "url.h"
 
 namespace speedtest {
+namespace {
 
 const char* kDefaultHost = "any.speed.gfsvc.com";
 
+}  // namespace
+
 namespace {
 
 bool ParseLong(const char *s, char **endptr, long *number) {
@@ -69,7 +72,11 @@
 
 const int kOptDisableDnsCache = 1000;
 const int kOptMaxConnections = 1001;
-const int kOptExponentialMovingAverage = 1002;
+const int kOptReportResults = 1002;
+const int kOptSkipDownload = 1003;
+const int kOptSkipUpload = 1004;
+const int kOptSkipPing = 1005;
+const int kOptNoReportResults = 1006;
 
 const int kOptMinTransferTime = 1100;
 const int kOptMaxTransferTime = 1101;
@@ -79,19 +86,23 @@
 const int kOptIntervalMillis = 1105;
 const int kOptPingRuntime = 1106;
 const int kOptPingTimeout = 1107;
+const int kOptExponentialMovingAverage = 1108;
 
 const char *kShortOpts = "hvg:a:d:s:t:u:p:";
 
 struct option kLongOpts[] = {
     {"help", no_argument, nullptr, 'h'},
     {"verbose", no_argument, nullptr, 'v'},
-    {"global_host", required_argument, nullptr, 'g'},
+    {"global_url", required_argument, nullptr, 'g'},
     {"user_agent", required_argument, nullptr, 'a'},
     {"disable_dns_cache", no_argument, nullptr, kOptDisableDnsCache},
     {"max_connections", required_argument, nullptr, kOptMaxConnections},
     {"progress_millis", required_argument, nullptr, 'p'},
-    {"exponential_moving_average", no_argument, nullptr,
-        kOptExponentialMovingAverage},
+    {"skip_download", no_argument, nullptr, kOptSkipDownload},
+    {"skip_upload", no_argument, nullptr, kOptSkipUpload},
+    {"skip_ping", no_argument, nullptr, kOptSkipPing},
+    {"report_results", no_argument, nullptr, kOptReportResults},
+    {"noreport_results", no_argument, nullptr, kOptNoReportResults},
 
     {"num_downloads", required_argument, nullptr, 'd'},
     {"download_size", required_argument, nullptr, 's'},
@@ -108,6 +119,8 @@
     {"interval_millis", required_argument, nullptr, kOptIntervalMillis},
     {"ping_runtime", required_argument, nullptr, kOptPingRuntime},
     {"ping_timeout", required_argument, nullptr, kOptPingTimeout},
+    {"exponential_moving_average", no_argument, nullptr,
+        kOptExponentialMovingAverage},
     {nullptr, 0, nullptr, 0},
 };
 const int kMaxNumber = 1000;
@@ -124,12 +137,15 @@
 Usage: speedtest [options] [host ...]
  -h, --help                    This help text
  -v, --verbose                 Verbose output
- -g, --global_host URL         Global host URL
+ -g, --global_url URL         Global host URL
  -a, --user_agent AGENT        User agent string for HTTP requests
  -p, --progress_millis NUM     Delay in milliseconds between updates
  --disable_dns_cache           Disable global DNS cache
  --max_connections NUM         Maximum number of parallel connections
- --exponential_moving_average  Use exponential instead of simple moving average
+ --skip_download               Skip the download test
+ --skip_upload                 Skip the upload test
+ --skip_ping                   Skip the ping test
+ --[no]report_results          Whether to report Speedtest results to server
 
 These options override the speedtest config parameters:
  -d, --num_downloads NUM       Number of simultaneous downloads
@@ -144,6 +160,7 @@
  --interval_millis TIME        Interval size in milliseconds
  --ping_runtime TIME           Ping runtime in milliseconds
  --ping_timeout TIME           Ping timeout in milliseconds
+ --exponential_moving_average  Use exponential instead of simple moving average
 )USAGE";
 
 }  // namespace
@@ -152,30 +169,34 @@
   assert(options != nullptr);
   options->usage = false;
   options->verbose = false;
-  options->global_host = http::Url(kDefaultHost);
+  options->global_url = http::Url(kDefaultHost);
   options->global = false;
   options->user_agent = "";
   options->progress_millis = 0;
   options->disable_dns_cache = false;
   options->max_connections = 0;
   options->exponential_moving_average = false;
+  options->skip_download = false;
+  options->skip_upload = false;
+  options->skip_ping = false;
+  options->report_results = true;
 
   options->num_downloads = 0;
-  options->download_size = 0;
+  options->download_bytes = 0;
   options->num_uploads = 0;
-  options->upload_size = 0;
+  options->upload_bytes = 0;
   options->min_transfer_runtime = 0;
   options->max_transfer_runtime = 0;
   options->min_transfer_intervals = 0;
   options->max_transfer_intervals = 0;
   options->max_transfer_variance = 0.0;
   options->interval_millis = 0;
-  options->ping_runtime = 0;
-  options->ping_timeout = 0;
+  options->ping_runtime_millis = 0;
+  options->ping_timeout_millis = 0;
 
-  options->hosts.clear();
+  options->regional_urls.clear();
 
-  if (!options->global_host.ok()) {
+  if (!options->global_url.ok()) {
     std::cerr << "Invalid global host " << kDefaultHost << "\n";
     return false;
   }
@@ -210,7 +231,7 @@
           std::cerr << "Invalid global host " << optarg << "\n";
           return false;
         }
-        options->global_host = url;
+        options->global_url = url;
         break;
       }
       case 'h':
@@ -228,17 +249,17 @@
                     << ", got '" << optarg << "'\n";
           return false;
         }
-        options->progress_millis = static_cast<int>(progress);
+        options->progress_millis = progress;
         break;
       }
       case 's':
-        if (!ParseSize(optarg, &options->download_size)) {
+        if (!ParseSize(optarg, &options->download_bytes)) {
           std::cerr << "Invalid download size '" << optarg << "'\n";
           return false;
         }
         break;
       case 't':
-        if (!ParseSize(optarg, &options->upload_size)) {
+        if (!ParseSize(optarg, &options->upload_bytes)) {
           std::cerr << "Invalid upload size '" << optarg << "'\n";
           return false;
         }
@@ -279,8 +300,20 @@
         options->max_connections = static_cast<int>(max_connections);
         break;
       }
-      case kOptExponentialMovingAverage:
-        options->exponential_moving_average = true;
+      case kOptReportResults:
+        options->report_results = true;
+        break;
+      case kOptSkipDownload:
+        options->skip_download = true;
+        break;
+      case kOptSkipUpload:
+        options->skip_upload = true;
+        break;
+      case kOptSkipPing:
+        options->skip_ping = true;
+        break;
+      case kOptNoReportResults:
+        options->report_results = false;
         break;
       case kOptMinTransferTime: {
         long transfer_time;
@@ -295,7 +328,7 @@
                     << optarg << "'\n";
           return false;
         }
-        options->min_transfer_runtime = static_cast<int>(transfer_time);
+        options->min_transfer_runtime = transfer_time;
         break;
       }
       case kOptMaxTransferTime: {
@@ -311,7 +344,7 @@
                     << optarg << "'\n";
           return false;
         }
-        options->max_transfer_runtime = static_cast<int>(transfer_time);
+        options->max_transfer_runtime = transfer_time;
         break;
       }
       case kOptMinTransferIntervals: {
@@ -372,7 +405,7 @@
                     << optarg << "'\n";
           return false;
         }
-        options->interval_millis = static_cast<int>(interval_millis);
+        options->interval_millis = interval_millis;
         break;
       }
       case kOptPingRuntime: {
@@ -387,7 +420,7 @@
                     << optarg << "'\n";
           return false;
         }
-        options->ping_runtime = static_cast<int>(ping_runtime);
+        options->ping_runtime_millis = ping_runtime;
         break;
       }
       case kOptPingTimeout: {
@@ -402,9 +435,12 @@
                     << optarg << "'\n";
           return false;
         }
-        options->ping_timeout = static_cast<int>(ping_timeout);
+        options->ping_timeout_millis = ping_timeout;
         break;
       }
+      case kOptExponentialMovingAverage:
+        options->exponential_moving_average = true;
+        break;
       default:
         return false;
     }
@@ -423,10 +459,10 @@
       url.clear_path();
       url.clear_query_string();
       url.clear_fragment();
-      options->hosts.emplace_back(url);
+      options->regional_urls.emplace_back(url);
     }
   }
-  if (options->hosts.empty()) {
+  if (options->regional_urls.empty()) {
     options->global = true;
   }
   return true;
@@ -439,29 +475,37 @@
 void PrintOptions(std::ostream &out, const Options &options) {
   out << "Usage: " << (options.usage ? "true" : "false") << "\n"
       << "Verbose: " << (options.verbose ? "true" : "false") << "\n"
-      << "Global host: " << options.global_host.url() << "\n"
+      << "Global host: " << options.global_url.url() << "\n"
       << "Global: " << (options.global ? "true" : "false") << "\n"
       << "User agent: " << options.user_agent << "\n"
       << "Progress interval: " << options.progress_millis << " ms\n"
       << "Disable DNS cache: "
       << (options.disable_dns_cache ? "true" : "false") << "\n"
       << "Max connections: " << options.max_connections << "\n"
-      << "Exponential moving average: "
-      << (options.exponential_moving_average ? "true" : "false") << "\n"
+      << "Skip download: "
+      << (options.skip_download ? "true" : "false") << "\n"
+      << "Skip upload: "
+      << (options.skip_upload ? "true" : "false") << "\n"
+      << "Skip ping: "
+      << (options.skip_ping ? "true" : "false") << "\n"
+      << "Report results: "
+      << (options.report_results ? "true" : "false") << "\n"
       << "Number of downloads: " << options.num_downloads << "\n"
-      << "Download size: " << options.download_size << " bytes\n"
+      << "Download size: " << options.download_bytes << " bytes\n"
       << "Number of uploads: " << options.num_uploads << "\n"
-      << "Upload size: " << options.upload_size << " bytes\n"
+      << "Upload size: " << options.upload_bytes << " bytes\n"
       << "Min transfer runtime: " << options.min_transfer_runtime << " ms\n"
       << "Max transfer runtime: " << options.max_transfer_runtime << " ms\n"
       << "Min transfer intervals: " << options.min_transfer_intervals << "\n"
       << "Max transfer intervals: " << options.max_transfer_intervals << "\n"
       << "Max transfer variance: " << options.max_transfer_variance << "\n"
       << "Interval size: " << options.interval_millis << " ms\n"
-      << "Ping runtime: " << options.ping_runtime << " ms\n"
-      << "Ping timeout: " << options.ping_timeout << " ms\n"
+      << "Ping runtime: " << options.ping_runtime_millis << " ms\n"
+      << "Ping timeout: " << options.ping_timeout_millis << " ms\n"
+      << "Exponential moving average: "
+      << (options.exponential_moving_average ? "true" : "false") << "\n"
       << "Hosts:\n";
-  for (const http::Url &host : options.hosts) {
+  for (const http::Url &host : options.regional_urls) {
     out << "  " << host.url() << "\n";
   }
 }
diff --git a/speedtest/options.h b/speedtest/options.h
index 9028f70..090c236 100644
--- a/speedtest/options.h
+++ b/speedtest/options.h
@@ -20,38 +20,43 @@
 #include <iostream>
 #include <string>
 #include <vector>
+#include "request.h"
 #include "url.h"
 
 namespace speedtest {
 
-extern const char* kDefaultHost;
-
 struct Options {
+  bool verbose;
+  http::Request::Factory request_factory;
+
   bool usage = false;
-  bool verbose = false;
-  http::Url global_host;
+  http::Url global_url;
   bool global = false;
   std::string user_agent;
   bool disable_dns_cache = false;
   int max_connections = 0;
   int progress_millis = 0;
-  bool exponential_moving_average = false;
+  bool skip_download = false;
+  bool skip_upload = false;
+  bool skip_ping = false;
+  bool report_results = true;
 
   // A value of 0 means use the speedtest config parameters
   int num_downloads = 0;
-  long download_size = 0;
+  long download_bytes = 0;
   int num_uploads = 0;
-  long upload_size = 0;
-  int min_transfer_runtime = 0;
-  int max_transfer_runtime = 0;
+  long upload_bytes = 0;
+  long min_transfer_runtime = 0;
+  long max_transfer_runtime = 0;
   int min_transfer_intervals = 0;
   int max_transfer_intervals = 0;
   double max_transfer_variance = 0.0;
-  int interval_millis = 0;
-  int ping_runtime = 0;
-  int ping_timeout = 0;
+  long interval_millis = 0;
+  long ping_runtime_millis = 0;
+  long ping_timeout_millis = 0;
+  bool exponential_moving_average = false;
 
-  std::vector<http::Url> hosts;
+  std::vector<http::Url> regional_urls;
 };
 
 // Parse command line options putting results into 'options'
diff --git a/speedtest/options_test.cc b/speedtest/options_test.cc
index 601984a..67ef986 100644
--- a/speedtest/options_test.cc
+++ b/speedtest/options_test.cc
@@ -86,25 +86,29 @@
   EXPECT_FALSE(options.usage);
   EXPECT_FALSE(options.verbose);
   EXPECT_TRUE(options.global);
-  EXPECT_EQ(http::Url("any.speed.gfsvc.com"), options.global_host);
+  EXPECT_EQ(http::Url("any.speed.gfsvc.com"), options.global_url);
   EXPECT_FALSE(options.disable_dns_cache);
   EXPECT_EQ(0, options.max_connections);
   EXPECT_EQ(0, options.progress_millis);
-  EXPECT_FALSE(options.exponential_moving_average);
+  EXPECT_FALSE(options.skip_download);
+  EXPECT_FALSE(options.skip_upload);
+  EXPECT_FALSE(options.skip_ping);
+  EXPECT_TRUE(options.report_results);
 
   EXPECT_EQ(0, options.num_downloads);
-  EXPECT_EQ(0, options.download_size);
+  EXPECT_EQ(0, options.download_bytes);
   EXPECT_EQ(0, options.num_uploads);
-  EXPECT_EQ(0, options.upload_size);
+  EXPECT_EQ(0, options.upload_bytes);
   EXPECT_EQ(0, options.min_transfer_runtime);
   EXPECT_EQ(0, options.max_transfer_runtime);
   EXPECT_EQ(0, options.min_transfer_intervals);
   EXPECT_EQ(0, options.max_transfer_intervals);
   EXPECT_EQ(0, options.max_transfer_variance);
   EXPECT_EQ(0, options.interval_millis);
-  EXPECT_EQ(0, options.ping_runtime);
-  EXPECT_EQ(0, options.ping_timeout);
-  EXPECT_THAT(options.hosts, testing::IsEmpty());
+  EXPECT_EQ(0, options.ping_runtime_millis);
+  EXPECT_EQ(0, options.ping_timeout_millis);
+  EXPECT_THAT(options.regional_urls, testing::IsEmpty());
+  EXPECT_FALSE(options.exponential_moving_average);
 }
 
 TEST(OptionsTest, Usage_Valid) {
@@ -123,7 +127,7 @@
 TEST(OptionsTest, OneHost_Valid) {
   Options options;
   TestValidOptions({"efgh"}, &options);
-  EXPECT_THAT(options.hosts, testing::ElementsAre(http::Url("efgh")));
+  EXPECT_THAT(options.regional_urls, testing::ElementsAre(http::Url("efgh")));
 }
 
 TEST(OptionsTest, ShortOptions_Valid) {
@@ -141,12 +145,12 @@
                     &options);
   EXPECT_TRUE(options.verbose);
   EXPECT_EQ(20, options.num_downloads);
-  EXPECT_EQ(5122, options.download_size);
+  EXPECT_EQ(5122, options.download_bytes);
   EXPECT_EQ(15, options.num_uploads);
-  EXPECT_EQ(7653, options.upload_size);
+  EXPECT_EQ(7653, options.upload_bytes);
   EXPECT_EQ(500, options.progress_millis);
   EXPECT_FALSE(options.global);
-  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
+  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_url);
   EXPECT_EQ("CrOS", options.user_agent);
 
   EXPECT_EQ(0, options.max_connections);
@@ -158,10 +162,10 @@
   EXPECT_EQ(0, options.max_transfer_intervals);
   EXPECT_EQ(0, options.max_transfer_variance);
   EXPECT_EQ(0, options.interval_millis);
-  EXPECT_EQ(0, options.ping_runtime);
-  EXPECT_EQ(0, options.ping_timeout);
+  EXPECT_EQ(0, options.ping_runtime_millis);
+  EXPECT_EQ(0, options.ping_timeout_millis);
 
-  EXPECT_THAT(options.hosts, testing::UnorderedElementsAre(
+  EXPECT_THAT(options.regional_urls, testing::UnorderedElementsAre(
       http::Url("foo.speed.googlefiber.net"),
       http::Url("bar.speed.googlefiber.net")));
 }
@@ -169,12 +173,15 @@
 TEST(OptionsTest, LongOptions_Valid) {
   Options options;
   TestValidOptions({"--verbose",
-                    "--global_host", "speed.gfsvc.com",
+                    "--global_url", "speed.gfsvc.com",
                     "--user_agent", "CrOS",
                     "--progress_millis", "1000",
                     "--disable_dns_cache",
                     "--max_connections", "23",
-                    "--exponential_moving_average",
+                    "--noreport_results",
+                    "--skip_download",
+                    "--skip_upload",
+                    "--skip_ping",
                     "--num_downloads", "16",
                     "--download_size", "5122",
                     "--num_uploads", "12",
@@ -187,21 +194,25 @@
                     "--interval_millis", "250",
                     "--ping_runtime", "2500",
                     "--ping_timeout", "300",
+                    "--exponential_moving_average",
                     "foo.speed.googlefiber.net",
                     "bar.speed.googlefiber.net"},
                     &options);
   EXPECT_TRUE(options.verbose);
   EXPECT_FALSE(options.global);
-  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
+  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_url);
   EXPECT_EQ("CrOS", options.user_agent);
   EXPECT_EQ(1000, options.progress_millis);
   EXPECT_TRUE(options.disable_dns_cache);
   EXPECT_EQ(23, options.max_connections);
-  EXPECT_TRUE(options.exponential_moving_average);
+  EXPECT_TRUE(options.skip_download);
+  EXPECT_TRUE(options.skip_upload);
+  EXPECT_TRUE(options.skip_ping);
+  EXPECT_FALSE(options.report_results);
   EXPECT_EQ(16, options.num_downloads);
-  EXPECT_EQ(5122, options.download_size);
+  EXPECT_EQ(5122, options.download_bytes);
   EXPECT_EQ(12, options.num_uploads);
-  EXPECT_EQ(7653, options.upload_size);
+  EXPECT_EQ(7653, options.upload_bytes);
   EXPECT_EQ("CrOS", options.user_agent);
   EXPECT_EQ(7500, options.min_transfer_runtime);
   EXPECT_EQ(13500, options.max_transfer_runtime);
@@ -209,9 +220,10 @@
   EXPECT_EQ(22, options.max_transfer_intervals);
   EXPECT_EQ(0.12, options.max_transfer_variance);
   EXPECT_EQ(250, options.interval_millis);
-  EXPECT_EQ(2500, options.ping_runtime);
-  EXPECT_EQ(300, options.ping_timeout);
-  EXPECT_THAT(options.hosts, testing::UnorderedElementsAre(
+  EXPECT_EQ(2500, options.ping_runtime_millis);
+  EXPECT_EQ(300, options.ping_timeout_millis);
+  EXPECT_TRUE(options.exponential_moving_average);
+  EXPECT_THAT(options.regional_urls, testing::UnorderedElementsAre(
       http::Url("foo.speed.googlefiber.net"),
       http::Url("bar.speed.googlefiber.net")));
 }
diff --git a/speedtest/ping.cc b/speedtest/ping.cc
new file mode 100644
index 0000000..0a61661
--- /dev/null
+++ b/speedtest/ping.cc
@@ -0,0 +1,120 @@
+/*
+ * 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 "ping.h"
+
+#include <curl/curl.h>
+#include <iostream>
+#include <limits>
+#include <mutex>
+#include <thread>
+#include <vector>
+#include "errors.h"
+#include "url.h"
+
+namespace speedtest {
+
+Ping::Ping(const Options &options)
+    : options_(options),
+      start_time_(0),
+      end_time_(0),
+      pings_received_(0),
+      min_ping_micros_(std::numeric_limits<long>::max()) {
+}
+
+Ping::Result Ping::operator()(std::atomic_bool *cancel) {
+  start_time_ = SystemTimeMicros();
+
+  if (!cancel) {
+    end_time_ = SystemTimeMicros();
+    return GetResult(Status(StatusCode::FAILED_PRECONDITION, "cancel is null"));
+  }
+
+  if (!options_.request_factory) {
+    end_time_ = SystemTimeMicros();
+    return GetResult(Status(StatusCode::INVALID_ARGUMENT,
+                            "request factory not set"));
+  }
+
+  if (options_.region.urls.empty()) {
+    end_time_ = SystemTimeMicros();
+    return GetResult(Status(StatusCode::INVALID_ARGUMENT, "region URLs empty"));
+  }
+
+  std::vector<std::thread> threads;
+  min_ping_micros_ = std::numeric_limits<long>::max();
+  pings_received_ = 0;
+  int num_pings = options_.num_concurrent_pings > 0
+                  ? options_.num_concurrent_pings
+                  : options_.region.urls.size();
+  for (int index = 0; index < num_pings; ++index) {
+    threads.emplace_back([&]{
+      size_t url_index = index % options_.region.urls.size();
+      http::Url url(options_.region.urls[url_index]);
+      url.set_path("/ping");
+      http::Request::Ptr ping = options_.request_factory(url);
+      while (!*cancel) {
+        ping->add_param("i", to_string(index + 1));
+        ping->add_param("time", to_string(SystemTimeMicros()));
+        ping->UpdateUrl();
+        if (options_.timeout_millis > 0) {
+          ping->set_timeout_millis(options_.timeout_millis);
+        }
+        long req_start = SystemTimeMicros();
+        CURLcode curl_code = ping->Get();
+        if (curl_code == CURLE_OK) {
+          long req_end = SystemTimeMicros();
+          long ping_time = req_end - req_start;
+          pings_received_++;
+          std::lock_guard<std::mutex> lock(mutex_);
+          min_ping_micros_ = std::min(min_ping_micros_, ping_time);
+        } else if (options_.verbose) {
+          std::cout << "Ping " << ping->url().url() << " failed: "
+                    << http::ErrorString(curl_code) << "\n";
+        }
+        ping->Reset();
+        std::this_thread::sleep_for(std::chrono::milliseconds(100));
+      }
+    });
+  }
+
+  for (std::thread &thread : threads) {
+    if (thread.joinable()) {
+      thread.join();
+    }
+  }
+
+  end_time_ = SystemTimeMicros();
+  return GetResult(Status::OK);
+}
+
+long Ping::min_ping_micros() const {
+  std::lock_guard<std::mutex> lock(mutex_);
+  return min_ping_micros_;
+}
+
+Ping::Result Ping::GetResult(Status status) const {
+  Ping::Result result;
+  result.start_time = start_time_;
+  result.end_time = end_time_;
+  result.status = status;
+  result.region = options_.region;
+  result.min_ping_micros = min_ping_micros();
+  result.received = pings_received_;
+  return result;
+}
+
+}  // namespace speedtest
diff --git a/speedtest/ping.h b/speedtest/ping.h
new file mode 100644
index 0000000..f97e184
--- /dev/null
+++ b/speedtest/ping.h
@@ -0,0 +1,73 @@
+/*
+ * 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 SPEEDTEST_PING_H
+#define SPEEDTEST_PING_H
+
+#include <atomic>
+#include <mutex>
+#include <string>
+#include "region.h"
+#include "request.h"
+#include "status.h"
+#include "utils.h"
+
+namespace speedtest {
+
+class Ping {
+ public:
+  struct Options {
+    bool verbose;
+    http::Request::Factory request_factory;
+    long timeout_millis;
+    long num_concurrent_pings;
+    Region region;
+  };
+
+  struct Result {
+    long start_time;
+    long end_time;
+    Status status;
+    Region region;
+    long min_ping_micros;
+    int received;
+  };
+
+  explicit Ping(const Options &options);
+
+  Result operator()(std::atomic_bool *cancel);
+
+  long start_time() const { return start_time_; }
+  long end_time() const { return end_time_; }
+  long min_ping_micros() const;
+
+ private:
+  Result GetResult(Status status) const;
+
+  Options options_;
+  std::atomic_long start_time_;
+  std::atomic_long end_time_;
+  std::atomic_int pings_received_;
+
+  mutable std::mutex mutex_;
+  long min_ping_micros_;
+
+  DISALLOW_COPY_AND_ASSIGN(Ping);
+};
+
+}  // namespace speedtest
+
+#endif // SPEEDTEST_PING_H
diff --git a/speedtest/ping_task.cc b/speedtest/ping_task.cc
deleted file mode 100644
index 7a1c7be..0000000
--- a/speedtest/ping_task.cc
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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 "ping_task.h"
-
-#include <algorithm>
-#include <cassert>
-#include <iomanip>
-#include <iostream>
-#include "utils.h"
-
-namespace speedtest {
-
-PingTask::PingTask(const Options &options)
-    : HttpTask(options),
-      options_(options) {
-  assert(options_.num_pings > 0);
-}
-
-void PingTask::RunInternal() {
-  ResetCounters();
-  success_ = false;
-  threads_.clear();
-  for (int i = 0; i < options_.num_pings; ++i) {
-    threads_.emplace_back([=]() {
-      RunPing(i);
-    });
-  }
-}
-
-void PingTask::StopInternal() {
-  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
-    t.join();
-  });
-  threads_.clear();
-  if (options_.verbose) {
-    std::cout << "Pinged " << options_.num_pings << " "
-              << (options_.num_pings == 1 ? "host" : "hosts") << ":\n";
-  }
-  const PingStats *min_stats = nullptr;
-  for (const auto &stat : stats_) {
-    if (options_.verbose) {
-      std::cout << "  " << stat.url.url() << ": ";
-      if (stat.pings_received == 0) {
-        std::cout << "no packets received";
-      } else {
-        double mean_micros = ((double) stat.total_micros) / stat.pings_received;
-        std::cout << "min " << round(stat.min_micros / 1000.0d, 2) << " ms"
-                  << " from " << stat.pings_received << " pings"
-                  << " (mean " << round(mean_micros / 1000.0d, 2) << " ms)";
-      }
-      std::cout << "\n";
-    }
-    if (stat.pings_received > 0) {
-      if (!min_stats || stat.min_micros < min_stats->min_micros) {
-        min_stats = &stat;
-      }
-    }
-  }
-
-  std::lock_guard<std::mutex> lock(mutex_);
-  if (!min_stats) {
-    // no servers respondeded
-    success_ = false;
-  } else {
-    fastest_ = *min_stats;
-    success_ = true;
-  }
-}
-
-void PingTask::RunPing(size_t index) {
-  http::Request::Ptr ping = options_.request_factory(index);
-  stats_[index].url = ping->url();
-  while (GetStatus() == TaskStatus::RUNNING) {
-    long req_start = SystemTimeMicros();
-    if (ping->Get() == CURLE_OK) {
-      long req_end = SystemTimeMicros();
-      long ping_time = req_end - req_start;
-      stats_[index].total_micros += ping_time;
-      stats_[index].pings_received++;
-      stats_[index].min_micros = std::min(stats_[index].min_micros, ping_time);
-    }
-    ping->Reset();
-    std::this_thread::sleep_for(std::chrono::milliseconds(100));
-  }
-}
-
-bool PingTask::IsSucceeded() const {
-  return success_;
-}
-
-PingStats PingTask::GetFastest() const {
-  std::lock_guard<std::mutex> lock(mutex_);
-  return fastest_;
-}
-
-void PingTask::ResetCounters() {
-  stats_.clear();
-  stats_.resize(options_.num_pings);
-}
-
-}  // namespace speedtest
diff --git a/speedtest/ping_task.h b/speedtest/ping_task.h
deleted file mode 100644
index b2923a8..0000000
--- a/speedtest/ping_task.h
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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 SPEEDTEST_PING_TASK_H
-#define SPEEDTEST_PING_TASK_H
-
-#include <atomic>
-#include <functional>
-#include <limits>
-#include <memory>
-#include <mutex>
-#include <thread>
-#include <vector>
-#include "http_task.h"
-#include "request.h"
-#include "url.h"
-
-namespace speedtest {
-
-struct PingStats {
-  long total_micros = 0;
-  int pings_received = 0;
-  long min_micros = std::numeric_limits<long>::max();
-  http::Url url;
-};
-
-class PingTask : public HttpTask {
- public:
-  struct Options : HttpTask::Options {
-    int timeout = 0;
-    int num_pings = 0;
-  };
-
-  explicit PingTask(const Options &options);
-
-  bool IsSucceeded() const;
-
-  PingStats GetFastest() const;
-
- protected:
-  void RunInternal() override;
-  void StopInternal() override;
-
- private:
-  void RunPing(size_t index);
-
-  void ResetCounters();
-
-  Options options_;
-  std::vector<PingStats> stats_;
-  std::vector<std::thread> threads_;
-  std::atomic_bool success_;
-
-  mutable std::mutex mutex_;
-  PingStats fastest_;
-
-  // disallowed
-  PingTask(const PingTask &) = delete;
-  void operator=(const PingTask &) = delete;
-};
-
-}  // namespace speedtest
-
-#endif  // SPEEDTEST_PING_TASK_H
diff --git a/speedtest/region.cc b/speedtest/region.cc
new file mode 100644
index 0000000..7e9e05a
--- /dev/null
+++ b/speedtest/region.cc
@@ -0,0 +1,154 @@
+#include "region.h"
+
+#include <curl/curl.h>
+#include <iostream>
+#include <sstream>
+#include <string>
+
+// For some reason, the libjsoncpp package installs to /usr/include/jsoncpp/json
+// instead of /usr{,/local}/include/json
+#include <jsoncpp/json/json.h>
+
+#include "errors.h"
+
+namespace speedtest {
+namespace {
+
+bool AddUrl(const Json::Value &url_json, std::vector<http::Url> *urls) {
+  if (!url_json.isString()) {
+    return false;
+  }
+  http::Url url = http::Url(url_json.asString());
+  if (!url.ok()) {
+    return false;
+  }
+  urls->push_back(url);
+  return true;
+}
+
+}  // namesapce
+
+std::string DescribeRegion(const Region &region) {
+  if (region.id.empty() && region.name.empty()) {
+    return region.urls.front().url();
+  }
+  if (region.id.empty()) {
+    return region.name;
+  }
+  if (region.name.empty()) {
+    return region.id;
+  }
+  std::stringstream ss;
+  ss << region.name << " (" << region.id << ")";
+  return ss.str();
+}
+
+RegionResult LoadRegions(RegionOptions options) {
+  RegionResult result;
+  result.start_time = SystemTimeMicros();
+  if (!options.request_factory) {
+    result.status = Status(StatusCode::INVALID_ARGUMENT,
+                           "request factory not set");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  if (!options.global) {
+    if (options.verbose) {
+      std::cout << "Explicit server list:\n";
+      for (const auto &url : options.regional_urls) {
+        std::cout << "  " << url.url() << "\n";
+      }
+    }
+    for (const http::Url &url : options.regional_urls) {
+      Region region;
+      region.urls.emplace_back(url.url());
+      result.regions.emplace_back(region);
+    }
+    result.status = Status::OK;
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  http::Url config_url(options.global_url);
+  config_url.set_path("/config");
+  if (options.verbose) {
+    std::cout << "Loading regions from " << config_url.url() << "\n";
+  }
+  http::Request::Ptr request = options.request_factory(config_url);
+  request->set_url(config_url);
+  request->set_timeout_millis(500);
+  std::string json;
+  CURLcode code = request->Get([&](void *data, size_t size){
+    json.assign(static_cast<const char *>(data), size);
+  });
+  if (code != CURLE_OK) {
+    result.status = Status(StatusCode::INTERNAL, http::ErrorString(code));
+  } else {
+    result.status = ParseRegions(json, &result.regions);
+  }
+  result.end_time = SystemTimeMicros();
+  return result;
+}
+
+Status ParseRegions(const std::string &json, std::vector<Region> *regions) {
+  if (!regions) {
+    return Status(StatusCode::FAILED_PRECONDITION, "Regions is null");
+  }
+
+  Json::Reader reader;
+  Json::Value root;
+  if (!reader.parse(json, root, false)) {
+    return Status(StatusCode::INVALID_ARGUMENT, "Failed to parse regions JSON");
+  }
+
+  if (!root.isMember("regions") || !root["regions"].isArray()) {
+    return Status(StatusCode::INVALID_ARGUMENT, "no regions element found");
+  }
+  for (const auto &it : root["regions"]) {
+    Region region;
+
+    if (!it.isMember("id")) {
+      return Status(StatusCode::INVALID_ARGUMENT, "Region missing id");
+    }
+    if (!it["id"].isString()) {
+      return Status(StatusCode::INVALID_ARGUMENT, "Region id not a string");
+    }
+    region.id = it["id"].asString();
+
+    if (it.isMember("name")) {
+      if (!it["name"].isString()) {
+        return Status(StatusCode::INVALID_ARGUMENT, "Region name not a string");
+      }
+      region.name = it["name"].asString();
+    }
+
+    if (!it.isMember("url")) {
+      return Status(StatusCode::INVALID_ARGUMENT, "Region URL missing");
+    }
+    if (it["url"].isString()) {
+      if (!AddUrl(it["url"], &region.urls)) {
+        return Status(StatusCode::INVALID_ARGUMENT,
+                      "Failed to parse region URL");
+      }
+    } else if (it["url"].isArray()) {
+      for (const auto &url_it : it["url"]) {
+        if (!AddUrl(url_it, &region.urls)) {
+          return Status(StatusCode::INVALID_ARGUMENT,
+                        "Failed to parse region URL");
+        }
+      }
+      if (region.urls.empty()) {
+        return Status(StatusCode::INVALID_ARGUMENT, "Region missing URLs");
+      }
+    } else {
+      return Status(StatusCode::INVALID_ARGUMENT,
+                    "Region URL not string or array");
+    }
+
+    regions->emplace_back(region);
+  }
+  return Status::OK;
+}
+
+}  // namespace
diff --git a/speedtest/region.h b/speedtest/region.h
new file mode 100644
index 0000000..6b94ab1
--- /dev/null
+++ b/speedtest/region.h
@@ -0,0 +1,60 @@
+/*
+ * 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 SPEEDTEST_REGION_H
+#define SPEEDTEST_REGION_H
+
+#include <string>
+#include <vector>
+#include "request.h"
+#include "status.h"
+#include "url.h"
+
+namespace speedtest {
+
+struct Region {
+  std::string id;
+  std::string name;
+  std::vector<http::Url> urls;
+};
+
+struct RegionOptions {
+  bool verbose;
+  http::Request::Factory request_factory;
+  bool global;
+  http::Url global_url;
+  std::vector<http::Url> regional_urls;
+};
+
+struct RegionResult {
+  long start_time;
+  long end_time;
+  Status status;
+  std::vector<Region> regions;
+};
+
+RegionResult LoadRegions(RegionOptions options);
+
+std::string DescribeRegion(const Region &region);
+
+// Parses a JSON document into a list of regions
+// Returns true with the regions populated in the vector on success.
+// Returns false if the JSON is invalid or regions is null.
+Status ParseRegions(const std::string &json, std::vector<Region> *regions);
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_REGION_H
diff --git a/speedtest/region_test.cc b/speedtest/region_test.cc
new file mode 100644
index 0000000..7ef5f7f
--- /dev/null
+++ b/speedtest/region_test.cc
@@ -0,0 +1,216 @@
+/*
+ * 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 "config.h"
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+#include <string>
+#include <vector>
+#include "region.h"
+
+#define EXPECT_OK(statement) EXPECT_EQ(::speedtest::Status::OK, (statement))
+#define EXPECT_ERROR(statement) EXPECT_NE(::speedtest::Status::OK, (statement))
+
+namespace speedtest {
+namespace {
+
+const char *kValidRegions = R"REGIONS(
+{
+    "regions": [
+        {
+            "id": "aus",
+            "name": "Austin",
+            "url": "http://austin.speed.googlefiber.net/"
+        },
+        {
+            "id": "mci",
+            "name": "Kansas City",
+            "url": [
+                "http://kansas.speed.googlefiber.net/"
+            ]
+        },
+        {
+            "id": "slc",
+            "name": "Provo",
+            "url": [
+                "http://provo.speed.googlefiber.net/"
+            ]
+        },
+        {
+            "id": "sfo",
+            "name": "Stanford",
+            "url": [
+                "http://stanford.speed.googlefiber.net/"
+            ]
+        }
+    ]
+}
+)REGIONS";
+
+const char *kRegionMissingId = R"REGIONS(
+{
+    "regions": [
+        {
+            "name": "Kansas City",
+            "url": [
+                "http://kansas.speed.googlefiber.net/"
+            ]
+        },
+    ]
+}
+)REGIONS";
+
+const char *kRegionMissingName = R"REGIONS(
+{
+    "regions": [
+        {
+            "id": "mci",
+            "url": [
+                "http://kansas.speed.googlefiber.net/"
+            ]
+        },
+    ]
+}
+)REGIONS";
+
+const char *kRegionMissingUrl = R"REGIONS(
+{
+    "regions": [
+        {
+            "id": "mci",
+            "name": "Kansas City",
+        },
+    ]
+}
+)REGIONS";
+
+const char *kRegionEmptyUrl = R"REGIONS(
+{
+    "regions": [
+        {
+            "id": "mci",
+            "name": "Kansas City",
+            "url": [
+            ]
+        },
+    ]
+}
+)REGIONS";
+
+const char *kRegionMultipleUrls = R"REGIONS(
+{
+    "regions": [
+        {
+            "id": "mci",
+            "name": "Kansas City",
+            "url": [
+                "http://kansas1.speed.googlefiber.net/",
+                "http://kansas2.speed.googlefiber.net/"
+            ]
+        }
+    ]
+}
+)REGIONS";
+
+const char *kRegionInvalidUrl = R"REGIONS(
+{
+    "regions": [
+        {
+            "id": "mci",
+            "name": "Kansas City",
+            "url": [
+                "example.com..",
+            ]
+        },
+    ]
+}
+)REGIONS";
+
+const char *kInvalidJson = "{{}{";
+
+std::vector<std::string> RegionList(const std::vector<Region> &regions) {
+  std::vector<std::string> region_list;
+  for (const Region &region : regions) {
+    std::stringstream ss;
+    ss << region.id << ", " << region.name;
+    for (const http::Url &url : region.urls) {
+      ss << ", " << url.url();
+    }
+    region_list.emplace_back(ss.str());
+  }
+  return region_list;
+}
+
+TEST(ParseRegionsTest, NullRegions_Invalid) {
+  EXPECT_ERROR(ParseRegions(kValidRegions, nullptr));
+}
+
+TEST(ParseRegionsTest, EmptyRegions_Invalid) {
+  std::vector<Region> regions;
+  EXPECT_ERROR(ParseRegions("", &regions));
+}
+
+TEST(ParseRegionsTest, InvalidJson_Invalid) {
+  std::vector<Region> regions;
+  EXPECT_ERROR(ParseRegions(kInvalidJson, &regions));
+}
+
+TEST(ParseRegionsTest, FullRegions_Valid) {
+  std::vector<Region> regions;
+  EXPECT_OK(ParseRegions(kValidRegions, &regions));
+  EXPECT_THAT(RegionList(regions), testing::UnorderedElementsAre(
+      "aus, Austin, http://austin.speed.googlefiber.net/",
+      "mci, Kansas City, http://kansas.speed.googlefiber.net/",
+      "slc, Provo, http://provo.speed.googlefiber.net/",
+      "sfo, Stanford, http://stanford.speed.googlefiber.net/"));
+}
+
+TEST(ParseRegionsTest, MissingId_Invalid) {
+  std::vector<Region> regions;
+  EXPECT_ERROR(ParseRegions(kRegionMissingId, &regions));
+}
+
+TEST(ParseRegionsTest, MissingName_Invalid) {
+  std::vector<Region> regions;
+  EXPECT_ERROR(ParseRegions(kRegionMissingName, &regions));
+}
+
+TEST(ParseRegionsTest, MissingUrl_Invalid) {
+  std::vector<Region> regions;
+  EXPECT_ERROR(ParseRegions(kRegionMissingUrl, &regions));
+}
+
+TEST(ParseRegionsTest, EmptyUrl_Invalid) {
+  std::vector<Region> regions;
+  EXPECT_ERROR(ParseRegions(kRegionEmptyUrl, &regions));
+}
+
+TEST(ParseRegionsTest, InvalidRegionUrl_Invalid) {
+  std::vector<Region> regions;
+  EXPECT_ERROR(ParseRegions(kRegionInvalidUrl, &regions));
+}
+
+TEST(ParseRegionsTest, MultipleUrls_Valid) {
+  std::vector<Region> regions;
+  EXPECT_OK(ParseRegions(kRegionMultipleUrls, &regions));
+  EXPECT_THAT(RegionList(regions), testing::UnorderedElementsAre(
+      "mci, Kansas City, http://kansas1.speed.googlefiber.net/, "
+      "http://kansas2.speed.googlefiber.net/"));
+}
+
+}  // namespace
+}  // namespace speedtest
diff --git a/speedtest/request.h b/speedtest/request.h
index 8588e29..6cc830a 100644
--- a/speedtest/request.h
+++ b/speedtest/request.h
@@ -23,6 +23,7 @@
 #include <memory>
 #include <string>
 #include "url.h"
+#include "utils.h"
 
 namespace http {
 
@@ -43,6 +44,7 @@
                                         curl_off_t,
                                         curl_off_t)>;
   using Ptr = std::unique_ptr<Request>;
+  using Factory = std::function<Ptr(const Url &)>;
 
   Request(std::shared_ptr<CURL> handle, const Url &url);
   virtual ~Request();
@@ -100,9 +102,7 @@
   QueryStringParams params_;
   ProgressFn progress_fn_;
 
-  // disable
-  Request(const Request &) = delete;
-  void operator=(const Request &) = delete;
+  DISALLOW_COPY_AND_ASSIGN(Request);
 };
 
 }  // namespace http
diff --git a/speedtest/result.cc b/speedtest/result.cc
new file mode 100644
index 0000000..e27e15a
--- /dev/null
+++ b/speedtest/result.cc
@@ -0,0 +1,135 @@
+/*
+ * 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 "result.h"
+
+#include "url.h"
+
+namespace speedtest {
+namespace {
+
+template <typename T>
+void PopulateDuration(Json::Value &json, const T &t) {
+  json["startMillis"] = static_cast<Json::Value::Int64>(t.start_time);
+  json["endMillis"] = static_cast<Json::Value::Int64>(t.end_time);
+}
+
+}  // namespace
+
+void PopulateParameters(Json::Value &json, const Config &config) {
+  json["downloadSize"] =
+      static_cast<Json::Value::Int64>(config.download_bytes);
+  json["uploadSize"] =
+      static_cast<Json::Value::Int64>(config.upload_bytes);
+  json["intervalSize"] =
+      static_cast<Json::Value::Int64>(config.interval_millis);
+  json["locationId"] = config.location_id;
+  json["locationName"] = config.location_name;
+  json["minTransferIntervals"] = config.min_transfer_intervals;
+  json["maxTransferIntervals"] = config.max_transfer_intervals;
+  json["minTransferRunTime"] =
+      static_cast<Json::Value::Int64>(config.min_transfer_runtime);
+  json["maxTransferRunTime"] =
+      static_cast<Json::Value::Int64>(config.max_transfer_runtime);
+  json["maxTransferVariance"] = config.max_transfer_variance;
+  json["numConcurrentDownloads"] = config.num_downloads;
+  json["numConcurrentUploads"] = config.num_uploads;
+  json["pingRunTime"] =
+      static_cast<Json::Value::Int64>(config.ping_runtime_millis);
+  json["pingTimeout"] =
+      static_cast<Json::Value::Int64>(config.ping_timeout_millis);
+  json["transferPortStart"] = config.transfer_port_start;
+  json["transferPortEnd"] = config.transfer_port_end;
+  json["averageType"] = config.average_type;
+}
+
+void PopulateConfigResult(Json::Value &json,
+                          const ConfigResult &config_result) {
+  PopulateDuration(json, config_result);
+  PopulateParameters(json["parameters"], config_result.config);
+}
+
+void PopulateFindNearest(Json::Value &json,
+                         const FindNearest::Result &find_nearest) {
+  PopulateDuration(json, find_nearest);
+  json["pingResults"] = Json::Value(Json::arrayValue);
+  for (const Ping::Result &ping_result : find_nearest.ping_results) {
+    Json::Value ping;
+    ping["id"] = ping_result.region.id;
+    ping["url"] = ping_result.region.urls.front().url();
+    if (ping_result.received > 0) {
+      ping["minPingMillis"] =
+          static_cast<Json::Value::Int64>(ping_result.min_ping_micros);
+    }
+    json["pingResults"].append(ping);
+  }
+}
+
+void PopulateInitResult(Json::Value &json,
+                        const Init::Result &init_result) {
+  PopulateDuration(json, init_result);
+  PopulateConfigResult(json["configResult"], init_result.config_result);
+  if (!init_result.find_nearest_result.ping_results.empty()) {
+    PopulateFindNearest(json["findNearest"], init_result.find_nearest_result);
+  }
+  json["selectedRegion"] = init_result.selected_region.id;
+}
+
+void PopulateTransfer(Json::Value &json,
+                      const TransferResult &transfer_result) {
+  PopulateDuration(json, transfer_result);
+  json["speedMbps"] = transfer_result.speed_mbps;
+  json["totalBytes"] =
+      static_cast<Json::Value::Int64>(transfer_result.total_bytes);
+  json["buckets"] = Json::Value(Json::arrayValue);
+  for (const Bucket &bucket : transfer_result.buckets) {
+    Json::Value bucket_json;
+    bucket_json["totalBytes"] =
+        static_cast<Json::Value::Int64>(bucket.total_bytes);
+    bucket_json["longSpeedMbps"] = bucket.long_megabits;
+    bucket_json["shortSpeedMbps"] = bucket.short_megabits;
+    bucket_json["offsetMillis"] = bucket.start_time / 1000.0d;
+    json["buckets"].append(bucket_json);
+  }
+}
+
+void PopulatePingResult(Json::Value &json, const Ping::Result &ping_result) {
+  PopulateDuration(json, ping_result);
+  json["id"] = ping_result.region.id;
+  json["url"] = ping_result.region.urls.front().url();
+  if (ping_result.received > 0) {
+    json["minPingMillis"] =
+        static_cast<Json::Value::Int64>(ping_result.min_ping_micros);
+  }
+}
+
+void PopulateSpeedtest(Json::Value &json,
+                       const Speedtest::Result &speedtest_result) {
+  PopulateDuration(json, speedtest_result);
+  PopulateInitResult(json["initResult"], speedtest_result.init_result);
+  if (speedtest_result.download_run) {
+    PopulateTransfer(json["downloadResult"], speedtest_result.download_result);
+  }
+  if (speedtest_result.upload_run) {
+    PopulateTransfer(json["uploadResult"], speedtest_result.upload_result);
+  }
+  if (speedtest_result.ping_run) {
+    PopulatePingResult(json["pingResult"], speedtest_result.ping_result);
+  }
+  json["endState"] = "COMPLETE";
+}
+
+}  // namespace speedtest
diff --git a/speedtest/result.h b/speedtest/result.h
new file mode 100644
index 0000000..4ffa07c
--- /dev/null
+++ b/speedtest/result.h
@@ -0,0 +1,45 @@
+/*
+ * 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 SPEEDTEST_RESULT_H
+#define SPEEDTEST_RESULT_H
+
+#include <jsoncpp/json/json.h>
+#include "config.h"
+#include "find_nearest.h"
+#include "init.h"
+#include "ping.h"
+#include "speedtest.h"
+#include "transfer_runner.h"
+
+namespace speedtest {
+
+void PopulateParameters(Json::Value &json, const Config &config);
+void PopulateConfigResult(Json::Value &json,
+                          const ConfigResult &config_result);
+void PopulateFindNearest(Json::Value &json,
+                         const FindNearest::Result &find_nearest);
+void PopulateInitResult(Json::Value &json,
+                        const Init::Result &init_result);
+void PopulateTransfer(Json::Value &json,
+                      const TransferResult &transfer_result);
+void PopulatePingResult(Json::Value &json, const Ping::Result &ping_result);
+void PopulateSpeedtest(Json::Value &json,
+                       const Speedtest::Result &speedtest_result);
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_RESULT_H
diff --git a/speedtest/speedtest.cc b/speedtest/speedtest.cc
index ed1263d..93a0248 100644
--- a/speedtest/speedtest.cc
+++ b/speedtest/speedtest.cc
@@ -16,408 +16,318 @@
 
 #include "speedtest.h"
 
-#include <chrono>
-#include <cstring>
-#include <limits>
-#include <random>
-#include <thread>
-#include <iomanip>
-#include <fstream>
-#include <streambuf>
-
+#include <curl/curl.h>
+#include <jsoncpp/json/json.h>
+#include <jsoncpp/json/writer.h>
+#include "download.h"
 #include "errors.h"
+#include "result.h"
 #include "timed_runner.h"
-#include "transfer_runner.h"
-#include "utils.h"
+#include "upload.h"
 
 namespace speedtest {
-namespace {
 
-std::shared_ptr<std::string> MakeRandomData(size_t size) {
-  std::random_device rd;
-  std::default_random_engine random_engine(rd());
-  std::uniform_int_distribution<char> uniform_dist(1, 255);
-  auto random_data = std::make_shared<std::string>();
-  random_data->resize(size);
-  for (size_t i = 0; i < size; ++i) {
-    (*random_data)[i] = uniform_dist(random_engine);
-  }
-  return std::move(random_data);
+Speedtest::Speedtest(const Options &options): options_(options) {
 }
 
-const char *kFileSerial = "/etc/serial";
-const char *kFileVersion = "/etc/version";
+Speedtest::Result Speedtest::operator()(std::atomic_bool *cancel) {
+  Speedtest::Result result;
+  result.start_time = SystemTimeMicros();
+  result.download_run = false;
+  result.upload_run = false;
+  result.ping_run = false;
 
-std::string LoadFile(const std::string &file_name) {
-  std::ifstream in(file_name);
-  return std::string(std::istreambuf_iterator<char>(in),
-                     std::istreambuf_iterator<char>());
-}
-
-}  // namespace
-
-Speedtest::Speedtest(const Options &options)
-    : options_(options) {
-  http::CurlEnv::Options curl_options;
-  curl_options.disable_dns_cache = options_.disable_dns_cache;
-  curl_options.max_connections = options_.max_connections;
-  env_ = http::CurlEnv::NewCurlEnv(curl_options);
-}
-
-Speedtest::~Speedtest() {
-}
-
-void Speedtest::Run() {
-  InitUserAgent();
-  LoadServerList();
-  if (servers_.empty()) {
-    std::cerr << "No servers found in global server list\n";
-    std::exit(1);
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "Speedtest aborted");
+    result.end_time = SystemTimeMicros();
+    return result;
   }
-  FindNearestServer();
-  if (!server_url_) {
-    std::cout << "No servers responded. Exiting\n";
-    return;
+
+  Init::Options init_options;
+  init_options.verbose = options_.verbose;
+  init_options.request_factory = options_.request_factory;
+  init_options.global = options_.global;
+  init_options.global_url = options_.global_url;
+  init_options.ping_timeout_millis = options_.ping_timeout_millis;
+  init_options.regional_urls = options_.regional_urls;
+  Init init(init_options);
+  result.init_result = init(cancel);
+  if (!result.init_result.status.ok()) {
+    result.status = result.init_result.status;
+    result.end_time = SystemTimeMicros();
+    return result;
   }
-  std::string json = LoadConfig(*server_url_);
-  if (!ParseConfig(json, &config_)) {
-    std::cout << "Could not parse config\n";
-    return;
-  }
+
+  selected_region_ = result.init_result.selected_region;
   if (options_.verbose) {
-    std::cout << "Server config:\n";
+    std::cout << "Setting selected region to "
+              << DescribeRegion(selected_region_) << "\n";
+  }
+
+  if (result.init_result.config_result.config.location_id.empty()) {
+    result.init_result.config_result.config.location_id = selected_region_.id;
+  }
+  if (result.init_result.config_result.config.location_name.empty()) {
+    result.init_result.config_result.config.location_name = selected_region_.name;
+  }
+
+  OverrideConfigWithOptions(&result.init_result.config_result.config, options_);
+  config_ = result.init_result.config_result.config;
+  if (options_.verbose) {
     PrintConfig(config_);
   }
-  std::cout << "Location: " << config_.location_name << "\n";
-  std::cout << "URL: " << server_url_->url() << "\n";
-  RunDownloadTest();
-  RunUploadTest();
-  RunPingTest();
-}
 
-void Speedtest::InitUserAgent() {
-  if (options_.user_agent.empty()) {
-    std::string serial = LoadFile(kFileSerial);
-    std::string version = LoadFile(kFileVersion);
-    Trim(&serial);
-    Trim(&version);
-    user_agent_ = "CPE";
-    if (!version.empty()) {
-      user_agent_ += "/" + version;
-      if (!serial.empty()) {
-        user_agent_ += "/" + serial;
-      }
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "Speedtest aborted");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  std::cout << "ID: " << result.init_result.selected_region.id << "\n";
+  std::cout << "Location: " << result.init_result.selected_region.name << "\n";
+
+  if (options_.skip_download) {
+    std::cout << "Skipping download test\n";
+  } else {
+    result.download_result = RunDownloadTest(cancel);
+    if (!result.download_result.status.ok()) {
+      result.status = result.download_result.status;
+      result.end_time = SystemTimeMicros();
+      return result;
+    }
+    result.download_run = true;
+    std::cout << "Download speed: "
+              << round(result.download_result.speed_mbps, 2)
+              << " Mbps\n";
+  }
+
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "Speedtest aborted");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  if (options_.skip_upload) {
+    std::cout << "Skipping upload test\n";
+  } else {
+    result.upload_result = RunUploadTest(cancel);
+    if (!result.upload_result.status.ok()) {
+      result.status = result.upload_result.status;
+      result.end_time = SystemTimeMicros();
+      return result;
+    }
+    result.upload_run = true;
+    std::cout << "Upload speed: "
+              << round(result.upload_result.speed_mbps, 2)
+              << " Mbps\n";
+  }
+
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "Speedtest aborted");
+    result.end_time = SystemTimeMicros();
+    return result;
+  }
+
+  if (options_.skip_ping) {
+    std::cout << "Skipping ping test\n";
+  } else {
+    result.ping_result = RunPingTest(cancel);
+    if (!result.ping_result.status.ok()) {
+      result.status = result.ping_result.status;
+      result.end_time = SystemTimeMicros();
+      return result;
+    }
+    result.ping_run = true;
+    std::cout << "Ping time: "
+              << ToMillis(result.ping_result.min_ping_micros)
+              << " ms\n";
+  }
+
+  result.status = Status::OK;
+  result.end_time = SystemTimeMicros();
+
+  if (!options_.report_results) {
+    if (options_.verbose) {
+      std::cout << "Not reporting results to server\n";
     }
   } else {
-    user_agent_ = options_.user_agent;
-    return;
-  }
-  if (options_.verbose) {
-    std::cout << "Setting user agent to " << user_agent_ << "\n";
-  }
-}
+    Json::Value root;
+    PopulateSpeedtest(root, result);
+    Json::FastWriter writer;
+    std::string out = writer.write(root);
 
-void Speedtest::LoadServerList() {
-  servers_.clear();
-  if (!options_.global) {
+    http::Url result_url(selected_region_.urls.front());
+    result_url.set_path("/result");
     if (options_.verbose) {
-      std::cout << "Explicit server list:\n";
-      for (const auto &url : options_.hosts) {
-        std::cout << "  " << url.url() << "\n";
+      std::cout << "Posting results to " << result_url.url() << "\n";
+    }
+    http::Request::Ptr request = options_.request_factory(result_url);
+    request->set_header("Content-Type", "application/json");
+    CURLcode curl_code = request->Post(out.c_str(), out.size());
+    if (curl_code == CURLE_OK) {
+      if (options_.verbose) {
+        std::cout << "Result posted successfully\n";
       }
-    }
-    servers_ = options_.hosts;
-    return;
-  }
-
-  std::string json = LoadConfig(options_.global_host);
-  if (json.empty()) {
-    std::cerr << "Failed to load config JSON\n";
-    std::exit(1);
-  }
-  if (options_.verbose) {
-    std::cout << "Loaded config JSON: " << json << "\n";
-  }
-  if (!ParseServers(json, &servers_)) {
-    std::cerr << "Failed to parse server list: " << json << "\n";
-    std::exit(1);
-  }
-  if (options_.verbose) {
-    std::cout << "Loaded servers:\n";
-    for (const auto &url : servers_) {
-      std::cout << "  " << url.url() << "\n";
+    } else {
+      std::cout << "Failed to report results: "
+                << http::ErrorString(curl_code) << "\n";
     }
   }
+  return result;
 }
 
-void Speedtest::FindNearestServer() {
-  server_url_.reset();
-  if (servers_.size() == 1) {
-    server_url_.reset(new http::Url(servers_[0]));
-    if (options_.verbose) {
-      std::cout << "Only 1 server so using " << server_url_->url() << "\n";
-    }
-    return;
-  }
-
-  PingTask::Options options;
-  options.verbose = options_.verbose;
-  options.timeout = PingTimeout();
-  std::vector<http::Url> hosts;
-  for (const auto &server : servers_) {
-    http::Url url(server);
-    url.set_path("/ping");
-    hosts.emplace_back(url);
-  }
-  options.num_pings = hosts.size();
+TransferResult Speedtest::RunDownloadTest(std::atomic_bool *cancel) {
   if (options_.verbose) {
-    std::cout << "There are " << hosts.size() << " ping URLs:\n";
-    for (const auto &host : hosts) {
-      std::cout << "  " << host.url() << "\n";
-    }
+    std::cout << "Starting download test to "
+              << DescribeRegion(selected_region_) << ")\n";
   }
-  options.request_factory = [&](int id) -> http::Request::Ptr{
-    return MakeRequest(hosts[id]);
-  };
-  PingTask find_nearest(options);
-  if (options_.verbose) {
-    std::cout << "Starting to find nearest server\n";
-  }
-  RunTimed(&find_nearest, 1500);
-  find_nearest.WaitForEnd();
-  if (find_nearest.IsSucceeded()) {
-    PingStats fastest = find_nearest.GetFastest();
-    server_url_.reset(new http::Url(fastest.url));
-    server_url_->clear_path();
-    if (options_.verbose) {
-      double ping_millis = fastest.min_micros / 1000.0d;
-      std::cout << "Found nearest server: " << fastest.url.url()
-                   << " (" << round(ping_millis, 2) << " ms)\n";
-    }
-  }
-}
-
-std::string Speedtest::LoadConfig(const http::Url &url) {
-  http::Url config_url(url);
-  config_url.set_path("/config");
-  if (options_.verbose) {
-    std::cout << "Loading config from " << config_url.url() << "\n";
-  }
-  http::Request::Ptr request = MakeRequest(config_url);
-  request->set_url(config_url);
-  std::string json;
-  request->Get([&](void *data, size_t size){
-    json.assign(static_cast<const char *>(data), size);
-  });
-  return json;
-}
-
-void Speedtest::RunPingTest() {
-  PingTask::Options options;
-  options.verbose = options_.verbose;
-  options.timeout = PingTimeout();
-  options.num_pings = 1;
-  http::Url ping_url(*server_url_);
-  ping_url.set_path("/ping");
-  options.request_factory = [&](int id) -> http::Request::Ptr{
-    return MakeRequest(ping_url);
-  };
-  std::unique_ptr<PingTask> ping(new PingTask(options));
-  RunTimed(ping.get(), PingRunTime());
-  ping->WaitForEnd();
-  PingStats fastest = ping->GetFastest();
-  if (ping->IsSucceeded()) {
-    long micros = fastest.min_micros;
-    std::cout << "Ping time: " << round(micros / 1000.0d, 3) << " ms\n";
-  } else {
-    std::cout << "Failed to get ping response from "
-              << config_.location_name << " (" << fastest.url << ")\n";
-  }
-}
-
-void Speedtest::RunDownloadTest() {
-  if (options_.verbose) {
-    std::cout << "Starting download test to " << config_.location_name
-              << " (" << server_url_->url() << ")\n";
-  }
-  DownloadTask::Options download_options;
+  Download::Options download_options;
   download_options.verbose = options_.verbose;
-  download_options.num_transfers = NumDownloads();
-  download_options.download_size = DownloadSize();
+  download_options.num_transfers = config_.num_downloads;
+  download_options.download_bytes = config_.download_bytes;
   download_options.request_factory = [this](int id) -> http::Request::Ptr{
     return MakeTransferRequest(id, "/download");
   };
-  std::unique_ptr<DownloadTask> download(new DownloadTask(download_options));
-  TransferRunner::Options runner_options;
-  runner_options.verbose = options_.verbose;
-  runner_options.task = download.get();
-  runner_options.min_runtime = MinTransferRuntime();
-  runner_options.max_runtime = MaxTransferRuntime();
-  runner_options.min_intervals = MinTransferIntervals();
-  runner_options.max_intervals = MaxTransferIntervals();
-  runner_options.max_variance = MaxTransferVariance();
-  runner_options.interval_millis = IntervalMillis();
+  Download download(download_options);
+
+  TransferOptions transfer_options;
+  transfer_options.verbose = options_.verbose;
+  transfer_options.min_runtime_millis = config_.min_transfer_runtime;
+  transfer_options.max_runtime_millis = config_.max_transfer_runtime;
+  transfer_options.min_intervals = config_.min_transfer_intervals;
+  transfer_options.max_intervals = config_.max_transfer_intervals;
+  transfer_options.max_variance = config_.max_transfer_variance;
+  transfer_options.interval_millis = config_.interval_millis;
+  transfer_options.exponential_moving_average =
+      config_.average_type == "EXPONENTIAL";
   if (options_.progress_millis > 0) {
-    runner_options.progress_millis = options_.progress_millis;
-    runner_options.progress_fn = [](Interval interval) {
-      double speed_variance = variance(interval.short_megabits,
-                                       interval.long_megabits);
-      std::cout << "[+" << round(interval.running_time / 1000.0, 0) << " ms] "
-                << "Download speed: " << round(interval.short_megabits, 2)
-                << " - " << round(interval.long_megabits, 2)
-                << " Mbps (" << interval.bytes << " bytes, variance "
+    transfer_options.progress_millis = options_.progress_millis;
+    transfer_options.progress_fn = [](Bucket bucket) {
+      double speed_variance = variance(bucket.short_megabits,
+                                       bucket.long_megabits);
+      std::cout << "[+" << round(bucket.start_time / 1000.0, 0) << " ms] "
+                << "Download speed: " << round(bucket.short_megabits, 2)
+                << " - " << round(bucket.long_megabits, 2)
+                << " Mbps (" << bucket.total_bytes << " bytes, variance "
                 << round(speed_variance, 4) << ")\n";
     };
   }
-  TransferRunner runner(runner_options);
-  runner.Run();
-  runner.WaitForEnd();
-  if (options_.verbose) {
-    long running_time = download->GetRunningTimeMicros();
-    std::cout << "Downloaded " << download->bytes_transferred()
-              << " bytes in " << round(running_time / 1000.0, 0) << " ms\n";
-  }
-  std::cout << "Download speed: "
-            << round(runner.GetSpeedInMegabits(), 2) << " Mbps\n";
+  return RunTransfer(std::ref(download), cancel, transfer_options);
 }
 
-void Speedtest::RunUploadTest() {
+TransferResult Speedtest::RunUploadTest(std::atomic_bool *cancel) {
   if (options_.verbose) {
-    std::cout << "Starting upload test to " << config_.location_name
-              << " (" << server_url_->url() << ")\n";
+    std::cout << "Starting upload test to "
+              << DescribeRegion(selected_region_) << ")\n";
   }
-  UploadTask::Options upload_options;
+  Upload::Options upload_options;
   upload_options.verbose = options_.verbose;
-  upload_options.num_transfers = NumUploads();
-  upload_options.payload = MakeRandomData(UploadSize());
+  upload_options.num_transfers = config_.num_uploads;
+  upload_options.payload = MakeRandomData(config_.upload_bytes);
   upload_options.request_factory = [this](int id) -> http::Request::Ptr{
     return MakeTransferRequest(id, "/upload");
   };
+  Upload upload(upload_options);
 
-  std::unique_ptr<UploadTask> upload(new UploadTask(upload_options));
-  TransferRunner::Options runner_options;
-  runner_options.verbose = options_.verbose;
-  runner_options.task = upload.get();
-  runner_options.min_runtime = MinTransferRuntime();
-  runner_options.max_runtime = MaxTransferRuntime();
-  runner_options.min_intervals = MinTransferIntervals();
-  runner_options.max_intervals = MaxTransferIntervals();
-  runner_options.max_variance = MaxTransferVariance();
-  runner_options.interval_millis = IntervalMillis();
+  TransferOptions transfer_options;
+  transfer_options.verbose = options_.verbose;
+  transfer_options.min_runtime_millis = config_.min_transfer_runtime;
+  transfer_options.max_runtime_millis = config_.max_transfer_runtime;
+  transfer_options.min_intervals = config_.min_transfer_intervals;
+  transfer_options.max_intervals = config_.max_transfer_intervals;
+  transfer_options.max_variance = config_.max_transfer_variance;
+  transfer_options.interval_millis = config_.interval_millis;
+  transfer_options.exponential_moving_average =
+      config_.average_type == "EXPONENTIAL";
   if (options_.progress_millis > 0) {
-    runner_options.progress_millis = options_.progress_millis;
-    runner_options.progress_fn = [](Interval interval) {
-      double speed_variance = variance(interval.short_megabits,
-                                       interval.long_megabits);
-      std::cout << "[+" << round(interval.running_time / 1000.0, 0) << " ms] "
-                << "Upload speed: " << round(interval.short_megabits, 2)
-                << " - " << round(interval.long_megabits, 2)
-                << " Mbps (" << interval.bytes << " bytes, variance "
+    transfer_options.progress_millis = options_.progress_millis;
+    transfer_options.progress_fn = [](Bucket bucket) {
+      double speed_variance = variance(bucket.short_megabits,
+                                       bucket.long_megabits);
+      std::cout << "[+" << round(bucket.start_time / 1000.0, 0) << " ms] "
+                << "Upload speed: " << round(bucket.short_megabits, 2)
+                << " - " << round(bucket.long_megabits, 2)
+                << " Mbps (" << bucket.total_bytes << " bytes, variance "
                 << round(speed_variance, 4) << ")\n";
     };
   }
-  TransferRunner runner(runner_options);
-  runner.Run();
-  runner.WaitForEnd();
-  if (options_.verbose) {
-    long running_time = upload->GetRunningTimeMicros();
-    std::cout << "Uploaded " << upload->bytes_transferred()
-              << " bytes in " << round(running_time / 1000.0, 0) << " ms\n";
+  return RunTransfer(std::ref(upload), cancel, transfer_options);
+}
+
+Ping::Result Speedtest::RunPingTest(std::atomic_bool *cancel) {
+  Ping::Options ping_options;
+  ping_options.verbose = options_.verbose;
+  ping_options.timeout_millis = config_.ping_timeout_millis;
+  ping_options.region = selected_region_;
+  ping_options.num_concurrent_pings = 0;
+  ping_options.request_factory = [&](const http::Url &url){
+    return MakeRequest(url);
+  };
+  Ping ping(ping_options);
+  return RunTimed(std::ref(ping), cancel, config_.ping_runtime_millis);
+}
+
+void Speedtest::OverrideConfigWithOptions(Config *config,
+                                          const Options &options) {
+  if (options_.num_downloads > 0) {
+    config->num_downloads = options_.num_downloads;
   }
-  std::cout << "Upload speed: "
-            << round(runner.GetSpeedInMegabits(), 2) << " Mbps\n";
+  if (options_.download_bytes > 0) {
+    config->download_bytes = options_.download_bytes;
+  }
+  if (options_.num_uploads > 0) {
+    config->num_uploads = options_.num_uploads;
+  }
+  if (options_.upload_bytes > 0) {
+    config->upload_bytes = options_.upload_bytes;
+  }
+  if (options_.ping_runtime_millis > 0) {
+    config->ping_runtime_millis = options_.ping_runtime_millis;
+  }
+  if (options_.ping_timeout_millis > 0) {
+    config->ping_timeout_millis = options_.ping_timeout_millis;
+  }
+  if (options_.min_transfer_runtime > 0) {
+    config->min_transfer_runtime = options_.min_transfer_runtime;
+  }
+  if (options_.max_transfer_runtime > 0) {
+    config->max_transfer_runtime = options_.max_transfer_runtime;
+  }
+  if (options_.min_transfer_intervals > 0) {
+    config->min_transfer_intervals = options_.min_transfer_intervals;
+  }
+  if (options_.max_transfer_intervals > 0) {
+    config->max_transfer_intervals = options_.max_transfer_intervals;
+  }
+  if (options_.max_transfer_variance > 0) {
+    config->max_transfer_variance = options_.max_transfer_variance;
+  }
+  if (options_.interval_millis > 0) {
+    config->interval_millis = options_.interval_millis;
+  }
+  if (options_.exponential_moving_average){
+    config->average_type = "EXPONENTIAL";
+  }
 }
 
-int Speedtest::NumDownloads() const {
-  return options_.num_downloads
-         ? options_.num_downloads
-         : config_.num_downloads;
-}
-
-int Speedtest::DownloadSize() const {
-  return options_.download_size
-         ? options_.download_size
-         : config_.download_size;
-}
-
-int Speedtest::NumUploads() const {
-  return options_.num_uploads
-         ? options_.num_uploads
-         : config_.num_uploads;
-}
-
-int Speedtest::UploadSize() const {
-  return options_.upload_size
-         ? options_.upload_size
-         : config_.upload_size;
-}
-
-int Speedtest::PingRunTime() const {
-  return options_.ping_runtime
-         ? options_.ping_runtime
-         : config_.ping_runtime;
-}
-
-int Speedtest::PingTimeout() const {
-  return options_.ping_timeout
-         ? options_.ping_timeout
-         : config_.ping_timeout;
-}
-
-int Speedtest::MinTransferRuntime() const {
-  return options_.min_transfer_runtime
-         ? options_.min_transfer_runtime
-         : config_.min_transfer_runtime;
-}
-
-int Speedtest::MaxTransferRuntime() const {
-  return options_.max_transfer_runtime
-         ? options_.max_transfer_runtime
-         : config_.max_transfer_runtime;
-}
-
-int Speedtest::MinTransferIntervals() const {
-  return options_.min_transfer_intervals
-         ? options_.min_transfer_intervals
-         : config_.min_transfer_intervals;
-}
-
-int Speedtest::MaxTransferIntervals() const {
-  return options_.max_transfer_intervals
-         ? options_.max_transfer_intervals
-         : config_.max_transfer_intervals;
-}
-
-double Speedtest::MaxTransferVariance() const {
-  return options_.max_transfer_variance
-         ? options_.max_transfer_variance
-         : config_.max_transfer_variance;
-}
-
-int Speedtest::IntervalMillis() const {
-  return options_.interval_millis
-         ? options_.interval_millis
-         : config_.interval_millis;
-}
-
-http::Request::Ptr Speedtest::MakeRequest(const http::Url &url) {
-  http::Request::Ptr request = env_->NewRequest(url);
-  if (!user_agent_.empty()) {
-    request->set_user_agent(user_agent_);
+http::Request::Ptr Speedtest::MakeRequest(const http::Url &url) const {
+  http::Request::Ptr request = options_.request_factory(url);
+  if (!options_.user_agent.empty()) {
+    request->set_user_agent(options_.user_agent);
   }
   return std::move(request);
 }
 
 http::Request::Ptr Speedtest::MakeBaseRequest(
-    int id, const std::string &path) {
-  http::Url url(*server_url_);
+    int id, const std::string &path) const {
+  http::Url url(selected_region_.urls.front());
   url.set_path(path);
   return MakeRequest(url);
 }
 
 http::Request::Ptr Speedtest::MakeTransferRequest(
-    int id, const std::string &path) {
-  http::Url url(*server_url_);
+    int id, const std::string &path) const {
+  http::Url url(selected_region_.urls.front().url());
   int port_start = config_.transfer_port_start;
   int port_end = config_.transfer_port_end;
   int num_ports = port_end - port_start + 1;
diff --git a/speedtest/speedtest.h b/speedtest/speedtest.h
index fb32355..7b58122 100644
--- a/speedtest/speedtest.h
+++ b/speedtest/speedtest.h
@@ -18,64 +18,58 @@
 #define SPEEDTEST_SPEEDTEST_H
 
 #include <atomic>
-#include <memory>
 #include <string>
-
 #include "config.h"
-#include "curl_env.h"
-#include "download_task.h"
+#include "init.h"
 #include "options.h"
-#include "ping_task.h"
-#include "upload_task.h"
-#include "url.h"
+#include "ping.h"
+#include "region.h"
 #include "request.h"
+#include "status.h"
+#include "transfer_runner.h"
+#include "url.h"
+#include "utils.h"
 
 namespace speedtest {
 
 class Speedtest {
  public:
-  explicit Speedtest(const Options &options);
-  virtual ~Speedtest();
+  struct Result {
+    long start_time;
+    long end_time;
+    Status status;
+    Init::Result init_result;
 
-  void Run();
+    bool download_run;
+    TransferResult download_result;
+
+    bool upload_run;
+    TransferResult upload_result;
+
+    bool ping_run;
+    Ping::Result ping_result;
+  };
+
+  explicit Speedtest(const Options &options);
+
+  Result operator()(std::atomic_bool *cancel);
 
  private:
-  void InitUserAgent();
-  void LoadServerList();
-  void FindNearestServer();
-  std::string LoadConfig(const http::Url &url);
-  void RunPingTest();
-  void RunDownloadTest();
-  void RunUploadTest();
+  TransferResult RunDownloadTest(std::atomic_bool *cancel);
+  TransferResult RunUploadTest(std::atomic_bool *cancel);
+  Ping::Result RunPingTest(std::atomic_bool *cancel);
 
-  int NumDownloads() const;
-  int DownloadSize() const;
-  int NumUploads() const;
-  int UploadSize() const;
-  int PingTimeout() const;
-  int PingRunTime() const;
-  int MinTransferRuntime() const;
-  int MaxTransferRuntime() const;
-  int MinTransferIntervals() const;
-  int MaxTransferIntervals() const;
-  double MaxTransferVariance() const;
-  int IntervalMillis() const;
+  void OverrideConfigWithOptions(Config *config, const Options &options);
 
-  http::Request::Ptr MakeRequest(const http::Url &url);
-  http::Request::Ptr MakeBaseRequest(int id, const std::string &path);
-  http::Request::Ptr MakeTransferRequest(int id, const std::string &path);
+  http::Request::Ptr MakeRequest(const http::Url &url) const;
+  http::Request::Ptr MakeBaseRequest(int id, const std::string &path) const;
+  http::Request::Ptr MakeTransferRequest(int id, const std::string &path) const;
 
-  std::shared_ptr <http::CurlEnv> env_;
   Options options_;
   Config config_;
-  std::string user_agent_;
-  std::vector<http::Url> servers_;
-  std::unique_ptr<http::Url> server_url_;
-  std::unique_ptr<std::string> send_data_;
+  Region selected_region_;
 
-  // disable
-  Speedtest(const Speedtest &) = delete;
-  void operator=(const Speedtest &) = delete;
+  DISALLOW_COPY_AND_ASSIGN(Speedtest);
 };
 
 }  // namespace speedtest
diff --git a/speedtest/speedtest_main.cc b/speedtest/speedtest_main.cc
index d756c4b..efe6c68 100644
--- a/speedtest/speedtest_main.cc
+++ b/speedtest/speedtest_main.cc
@@ -14,11 +14,45 @@
  * limitations under the License.
  */
 
+#include <atomic>
 #include <cstdlib>
-#include <iostream>
+#include <fstream>
+#include <iterator>
 #include <memory>
+#include <string>
+#include "curl_env.h"
 #include "options.h"
+#include "request.h"
 #include "speedtest.h"
+#include "utils.h"
+
+namespace {
+
+const char *kFileSerial = "/etc/serial";
+const char *kFileVersion = "/etc/version";
+
+std::string LoadFile(const std::string &file_name) {
+  std::ifstream in(file_name);
+  return std::string(std::istreambuf_iterator<char>(in),
+                     std::istreambuf_iterator<char>());
+}
+
+std::string GetDefaultUserAgent() {
+  std::string serial = LoadFile(kFileSerial);
+  std::string version = LoadFile(kFileVersion);
+  speedtest::Trim(&serial);
+  speedtest::Trim(&version);
+  std::string user_agent_ = "CPE";
+  if (!version.empty()) {
+    user_agent_ += "/" + version;
+    if (!serial.empty()) {
+      user_agent_ += "/" + serial;
+    }
+  }
+  return user_agent_;
+}
+
+}
 
 int main(int argc, char *argv[]) {
   speedtest::Options options;
@@ -26,10 +60,22 @@
     speedtest::PrintUsage(argv[0]);
     std::exit(1);
   }
+  if (options.user_agent.empty()) {
+    options.user_agent = GetDefaultUserAgent();
+  }
   if (options.verbose) {
     speedtest::PrintOptions(options);
   }
+  http::CurlEnv::Options curl_options;
+  curl_options.disable_dns_cache = options.disable_dns_cache;
+  curl_options.max_connections = options.max_connections;
+  std::shared_ptr<http::CurlEnv> curl_env =
+      http::CurlEnv::NewCurlEnv(curl_options);
+  options.request_factory = [&](const http::Url &url) -> http::Request::Ptr {
+    return curl_env->NewRequest(url);
+  };
   speedtest::Speedtest speed(options);
-  speed.Run();
+  std::atomic_bool cancel(false);
+  speed(&cancel);
   return 0;
 }
diff --git a/speedtest/status.cc b/speedtest/status.cc
new file mode 100644
index 0000000..2b541bb
--- /dev/null
+++ b/speedtest/status.cc
@@ -0,0 +1,77 @@
+/*
+ * 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 "status.h"
+
+#include <sstream>
+#include <type_traits>
+#include "utils.h"
+
+namespace speedtest {
+
+std::string ErrorString(StatusCode status_code) {
+  switch (status_code) {
+    case StatusCode::OK: return "OK";
+    case StatusCode::INVALID_ARGUMENT: return "INVALID_ARGUMENT";
+    case StatusCode::ABORTED: return "ABORTED";
+    case StatusCode::INTERNAL: return "INTERNAL";
+    case StatusCode::FAILED_PRECONDITION: return "FAILED_PRECONDITION";
+    case StatusCode::UNAVAILABLE: return "UNAVAILABLE";
+    case StatusCode::UNKNOWN: return "UNKNOWN";
+  }
+  return std::string("Unknown status code ") + to_string(
+      static_cast<std::underlying_type<StatusCode>::type>(status_code));
+}
+
+const Status Status::OK;
+
+Status::Status(): code_(StatusCode::OK) {
+}
+
+Status::Status(StatusCode code): code_(code) {
+}
+
+Status::Status(StatusCode code, std::string message)
+    : code_(code), message_(message) {
+}
+
+bool Status::ok() const {
+  return code_ == StatusCode::OK;
+}
+
+StatusCode Status::code() const {
+  return code_;
+}
+
+const std::string & Status::message() const {
+  return message_;
+}
+
+std::string Status::ToString() const {
+  std::stringstream ss;
+  ss << ErrorString(code_) << ": " << message_;
+  return ss.str();
+}
+
+bool Status::operator==(const Status &status) const {
+  return code_ == status.code_ && message_ == status.message_;
+}
+
+bool Status::operator!=(const Status &status) const {
+  return code_ != status.code_ || message_ != status.message_;
+}
+
+}  // namespace speedtest
diff --git a/speedtest/status.h b/speedtest/status.h
new file mode 100644
index 0000000..4ee8011
--- /dev/null
+++ b/speedtest/status.h
@@ -0,0 +1,59 @@
+/*
+ * 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 SPEEDTEST_STATUS_H
+#define SPEEDTEST_STATUS_H
+
+#include <string>
+
+namespace speedtest {
+
+enum class StatusCode {
+  OK = 0,
+  INVALID_ARGUMENT = 1,
+  ABORTED = 2,
+  INTERNAL = 3,
+  FAILED_PRECONDITION = 4,
+  UNAVAILABLE = 5,
+  UNKNOWN = 6
+};
+
+std::string ErrorString(StatusCode status_code);
+
+class Status {
+ public:
+  Status();
+  explicit Status(StatusCode code);
+  Status(StatusCode code, std::string message);
+
+  bool ok() const;
+  StatusCode code() const;
+  const std::string &message() const;
+  std::string ToString() const;
+
+  bool operator==(const Status &status) const;
+  bool operator!=(const Status &status) const;
+
+  static const Status OK;
+
+ private:
+  StatusCode code_;
+  std::string message_;
+};
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_STATUS_H
diff --git a/speedtest/task.cc b/speedtest/task.cc
deleted file mode 100644
index 84d12c9..0000000
--- a/speedtest/task.cc
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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 "task.h"
-
-#include <algorithm>
-#include <cassert>
-#include <iostream>
-#include <thread>
-#include "utils.h"
-
-namespace speedtest {
-
-const char *AsString(TaskStatus status) {
-  switch (status) {
-    case TaskStatus::NOT_STARTED: return "NOT_STARTED";
-    case TaskStatus::RUNNING: return "RUNNING";
-    case TaskStatus::STOPPING: return "STOPPING";
-    case TaskStatus::STOPPED: return "STOPPED";
-  }
-  std::exit(1);
-}
-
-Task::Task(const Options &options)
-    : status_(TaskStatus::NOT_STARTED) {
-  assert(options.request_factory);
-}
-
-Task::~Task() {
-  Stop();
-  if (runner_.joinable()) {
-    runner_.join();
-  }
-  if (stopper_.joinable()) {
-    stopper_.join();
-  }
-}
-
-void Task::Run() {
-  runner_ = std::thread([=]{
-    {
-      std::lock_guard <std::mutex> lock(mutex_);
-      if (status_ != TaskStatus::NOT_STARTED &&
-          status_ != TaskStatus::STOPPED) {
-        return;
-      }
-      UpdateStatusLocked(TaskStatus::RUNNING);
-      start_time_ = SystemTimeMicros();
-    }
-    RunInternal();
-  });
-  stopper_ = std::thread([=]{
-    WaitFor(TaskStatus::STOPPING);
-    StopInternal();
-    std::lock_guard <std::mutex> lock(mutex_);
-    UpdateStatusLocked(TaskStatus::STOPPED);
-    end_time_ = SystemTimeMicros();
-  });
-}
-
-void Task::Stop() {
-  std::lock_guard <std::mutex> lock(mutex_);
-  if (status_ != TaskStatus::RUNNING) {
-    return;
-  }
-  UpdateStatusLocked(TaskStatus::STOPPING);
-}
-
-TaskStatus Task::GetStatus() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  return status_;
-}
-
-long Task::GetStartTime() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  return start_time_;
-}
-
-long Task::GetEndTime() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  return end_time_;
-}
-
-long Task::GetRunningTimeMicros() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  switch (status_) {
-    case TaskStatus::NOT_STARTED:
-      break;
-    case TaskStatus::RUNNING:
-    case TaskStatus::STOPPING:
-      return SystemTimeMicros() - start_time_;
-    case TaskStatus::STOPPED:
-      return end_time_ - start_time_;
-  }
-  return 0;
-}
-
-void Task::WaitForEnd() {
-  WaitFor(TaskStatus::STOPPED);
-}
-
-void Task::UpdateStatusLocked(TaskStatus status) {
-  status_ = status;
-  status_cond_.notify_all();
-}
-
-void Task::WaitFor(TaskStatus status) {
-  std::unique_lock<std::mutex> lock(mutex_);
-  status_cond_.wait(lock, [=]{
-    return status_ == status;
-  });
-}
-
-}  // namespace speedtest
diff --git a/speedtest/task.h b/speedtest/task.h
deleted file mode 100644
index 429b078..0000000
--- a/speedtest/task.h
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * 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 SPEEDTEST_TASK_H
-#define SPEEDTEST_TASK_H
-
-#include <condition_variable>
-#include <functional>
-#include <memory>
-#include <mutex>
-#include <thread>
-
-namespace speedtest {
-
-enum class TaskStatus {
-  NOT_STARTED,
-  RUNNING,
-  STOPPING,
-  STOPPED
-};
-
-const char *AsString(TaskStatus status);
-
-class Task {
- public:
-  struct Options {
-    bool verbose = false;
-  };
-
-  explicit Task(const Options &options);
-  virtual ~Task();
-
-  void Run();
-  void Stop();
-
-  TaskStatus GetStatus() const;
-  long GetStartTime() const;
-  long GetEndTime() const;
-  long GetRunningTimeMicros() const;
-  void WaitForEnd();
-
- protected:
-  virtual void RunInternal() = 0;
-  virtual void StopInternal() {}
-
- private:
-  // Only call with mutex_
-  void UpdateStatusLocked(TaskStatus status);
-
-  void WaitFor(TaskStatus status);
-
-  mutable std::mutex mutex_;
-  std::thread runner_;
-  std::thread stopper_;
-  std::condition_variable status_cond_;
-  TaskStatus status_;
-  long start_time_;
-  long end_time_;
-
-  // disallowed
-  Task(const Task &) = delete;
-  void operator=(const Task &) = delete;
-};
-
-}  // namespace speedtest
-
-#endif  //SPEEDTEST_TASK_H
diff --git a/speedtest/timed_runner.cc b/speedtest/timed_runner.cc
deleted file mode 100644
index bf7c4cc..0000000
--- a/speedtest/timed_runner.cc
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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 "timed_runner.h"
-
-#include <cassert>
-#include <thread>
-
-namespace speedtest {
-
-void RunTimed(Task *task, long millis) {
-  assert(task);
-  task->Run();
-  std::thread timer([=] {
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(millis));
-    task->Stop();
-  });
-  timer.join();
-}
-
-}  // namespace speedtest
diff --git a/speedtest/timed_runner.h b/speedtest/timed_runner.h
index 02e673f..1a474c1 100644
--- a/speedtest/timed_runner.h
+++ b/speedtest/timed_runner.h
@@ -14,16 +14,34 @@
  * limitations under the License.
  */
 
-#ifndef SPEEDTEST_RUNNER_H
-#define SPEEDTEST_RUNNER_H
+#ifndef SPEEDTEST_TIMED_RUNNER_H
+#define SPEEDTEST_TIMED_RUNNER_H
 
-#include "task.h"
+#include <future>
+#include <thread>
+#include <type_traits>
+#include "utils.h"
 
 namespace speedtest {
 
-// Run a task for a set duration
-void RunTimed(Task *task, long millis);
+template <typename F>
+typename std::result_of<F(std::atomic_bool *)>::type
+RunTimed(F &&fn, std::atomic_bool *cancel, long timeout_millis) {
+  std::atomic_bool local_cancel(false);
+  long start_time = SystemTimeMicros();
+  long end_time = start_time + timeout_millis * 1000;
+  auto fut = ReallyAsync(std::forward<F>(fn), &local_cancel);
+  std::thread timer([&]{
+    while (!*cancel && SystemTimeMicros() <= end_time) {
+      std::this_thread::sleep_for(std::chrono::milliseconds(100));
+    }
+    local_cancel = true;
+  });
+  timer.join();
+  fut.wait();
+  return fut.get();
+}
 
-}  // namespace speedtest
+}  // namespace
 
-#endif  // SPEEDTEST_RUNNER_H
+#endif // SPEEDTEST_TIMED_RUNNER_H
diff --git a/speedtest/transfer_runner.cc b/speedtest/transfer_runner.cc
index d37f087..5d1c241 100644
--- a/speedtest/transfer_runner.cc
+++ b/speedtest/transfer_runner.cc
@@ -17,12 +17,8 @@
 #include "transfer_runner.h"
 
 #include <algorithm>
-#include <cassert>
 #include <chrono>
 #include <iostream>
-#include <thread>
-#include "transfer_task.h"
-#include "utils.h"
 
 namespace speedtest {
 namespace {
@@ -31,141 +27,36 @@
 
 }  // namespace
 
-TransferRunner::TransferRunner(const Options &options)
-    : Task(options),
-      options_(options) {
-  if (options_.interval_millis <= 0) {
-    options_.interval_millis = kDefaultIntervalMillis;
-  }
-}
-
-void TransferRunner::RunInternal() {
-  threads_.clear();
-  intervals_.clear();
-
-  // sentinel value of all zeroes
-  intervals_.emplace_back();
-
-  // If progress updates are created add a thread to send updates
-  if (options_.progress_fn && options_.progress_millis > 0) {
-    if (options_.verbose) {
-      std::cout << "Progress updates every "
-                << options_.progress_millis << " ms\n";
-    }
-    threads_.emplace_back([&] {
-      std::this_thread::sleep_for(
-          std::chrono::milliseconds(options_.progress_millis));
-      while (GetStatus() == TaskStatus::RUNNING) {
-        Interval progress = GetLastInterval();
-        options_.progress_fn(progress);
-        std::this_thread::sleep_for(
-            std::chrono::milliseconds(options_.progress_millis));
-      }
-      Interval progress = GetLastInterval();
-      options_.progress_fn(progress);
-    });
-  } else if (options_.verbose) {
-    std::cout << "No progress updates\n";
-  }
-
-  // Updating thread
-  if (options_.verbose) {
-    std::cout << "Transfer runner updates every "
-              << options_.interval_millis << " ms\n";
-  }
-  threads_.emplace_back([&] {
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(options_.interval_millis));
-    while (GetStatus() == TaskStatus::RUNNING) {
-      const Interval &interval = AddInterval();
-      if (interval.running_time > options_.max_runtime * 1000) {
-        Stop();
-        return;
-      }
-      if (interval.running_time >= options_.min_runtime * 1000 &&
-          interval.long_megabits > 0 &&
-          interval.short_megabits > 0) {
-        double speed_variance = variance(interval.short_megabits,
-                                         interval.long_megabits);
-        if (speed_variance <= options_.max_variance) {
-          Stop();
-          return;
-        }
-      }
-      std::this_thread::sleep_for(
-          std::chrono::milliseconds(options_.interval_millis));
-    }
-  });
-
-  options_.task->Run();
-}
-
-void TransferRunner::StopInternal() {
-  options_.task->Stop();
-  options_.task->WaitForEnd();
-  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
-    t.join();
-  });
-  threads_.clear();
-}
-
-const Interval &TransferRunner::AddInterval() {
-  std::lock_guard <std::mutex> lock(mutex_);
-  intervals_.emplace_back();
-  Interval &interval = intervals_[intervals_.size() - 1];
-  interval.running_time = options_.task->GetRunningTimeMicros();
-  interval.bytes = options_.task->bytes_transferred();
-  if (options_.exponential_moving_average) {
-    interval.short_megabits = GetShortEma(options_.min_intervals);
-    interval.long_megabits = GetLongEma(options_.max_intervals);
-  } else {
-    interval.short_megabits = GetSimpleAverage(options_.min_intervals);
-    interval.long_megabits = GetSimpleAverage(options_.max_intervals);
-  }
-  speed_ = interval.long_megabits;
-  return intervals_.back();
-}
-
-Interval TransferRunner::GetLastInterval() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  return intervals_.back();
-}
-
-double TransferRunner::GetSpeedInMegabits() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  return speed_;
-}
-
-double TransferRunner::GetShortEma(int num_intervals) {
-  if (intervals_.empty() || num_intervals <= 0) {
+double GetShortEma(std::vector<Bucket> *buckets, int num_buckets) {
+  if (buckets->empty() || num_buckets <= 0) {
     return 0.0;
   }
-  Interval last_interval = GetLastInterval();
-  double percent = 2.0d / (num_intervals + 1);
-  return GetSimpleAverage(1) * percent +
-      last_interval.short_megabits * (1 - percent);
+  const Bucket &last_bucket = buckets->back();
+  double percent = 2.0d / (num_buckets + 1);
+  return GetSimpleAverage(buckets, 1) * percent +
+      last_bucket.short_megabits * (1 - percent);
 }
 
-double TransferRunner::GetLongEma(int num_intervals) {
-  if (intervals_.empty() || num_intervals <= 0) {
+double GetLongEma(std::vector<Bucket> *buckets, int num_buckets) {
+  if (buckets->empty() || num_buckets <= 0) {
     return 0.0;
   }
-  Interval last_interval = GetLastInterval();
-  double percent = 2.0d / (num_intervals + 1);
-  return GetSimpleAverage(1) * percent +
-      last_interval.long_megabits * (1 - percent);
+  const Bucket &last_bucket = buckets->back();
+  double percent = 2.0d / (num_buckets + 1);
+  return GetSimpleAverage(buckets, 1) * percent +
+      last_bucket.long_megabits * (1 - percent);
 }
 
-double TransferRunner::GetSimpleAverage(int num_intervals) {
-  if (intervals_.empty() || num_intervals <= 0) {
+double GetSimpleAverage(std::vector<Bucket> *buckets, int num_buckets) {
+  if (buckets->empty() || num_buckets <= 0) {
     return 0.0;
   }
-  int end_index = intervals_.size() - 1;
-  int start_index = std::max(0, end_index - num_intervals);
-  const Interval &end = intervals_[end_index];
-  const Interval &start = intervals_[start_index];
-  return ToMegabits(end.bytes - start.bytes,
-                    end.running_time - start.running_time);
+  int end_index = buckets->size() - 1;
+  int start_index = std::max(0, end_index - num_buckets);
+  const Bucket &end = (*buckets)[end_index];
+  const Bucket &start = (*buckets)[start_index];
+  return ToMegabits(end.total_bytes - start.total_bytes,
+                    end.start_time - start.start_time);
 }
 
 }  // namespace
diff --git a/speedtest/transfer_runner.h b/speedtest/transfer_runner.h
index 793c8ec..4fb0620 100644
--- a/speedtest/transfer_runner.h
+++ b/speedtest/transfer_runner.h
@@ -18,67 +18,164 @@
 #define SPEEDTEST_TRANSFER_RUNNER_H
 
 #include <functional>
+#include <iostream>
+#include <memory>
 #include <mutex>
 #include <thread>
 #include <vector>
-#include "task.h"
-#include "transfer_task.h"
+#include "status.h"
+#include "utils.h"
 
 namespace speedtest {
 
-struct Interval {
-  long bytes = 0;
-  long running_time = 0;
+struct Bucket {
+  long total_bytes = 0;
+  long start_time = 0;
   double short_megabits = 0.0;
   double long_megabits = 0.0;
 };
 
+struct TransferOptions {
+  bool verbose = false;
+  long min_runtime_millis = 0;
+  long max_runtime_millis = 0;
+  long interval_millis = 0;
+  long progress_millis = 0;
+  int min_intervals = 0;
+  int max_intervals = 0;
+  double max_variance = 0.0;
+  bool exponential_moving_average = false;
+  std::function<void(const Bucket)> progress_fn;
+};
+
+struct TransferResult {
+  long start_time;
+  long end_time;
+  Status status;
+  std::vector<Bucket> buckets;
+  double speed_mbps;
+  long total_bytes;
+};
+
+double GetShortEma(std::vector<Bucket> *buckets, int num_buckets);
+double GetLongEma(std::vector<Bucket> *buckets, int num_intervals);
+double GetSimpleAverage(std::vector<Bucket> *buckets, int num_intervals);
+
 // Run a variable length transfer test using two moving averages.
 // The test runs between min_runtime and max_runtime and otherwise
 // ends when the speed is "stable" meaning the two moving averages
 // are relatively close to one another.
-class TransferRunner : public Task {
- public:
-  struct Options : public Task::Options {
-    TransferTask *task = nullptr;
-    int min_runtime = 0;
-    int max_runtime = 0;
-    int interval_millis = 0;
-    int progress_millis = 0;
-    int min_intervals = 0;
-    int max_intervals = 0;
-    double max_variance = 0.0;
-    bool exponential_moving_average = false;
-    std::function<void(Interval)> progress_fn;
-  };
+template <typename F>
+TransferResult
+RunTransfer(F &&fn, std::atomic_bool *cancel, TransferOptions options) {
+  TransferResult result;
+  result.start_time = SystemTimeMicros();
 
-  explicit TransferRunner(const Options &options);
+  // sentinel value of all zeroes
+  result.buckets.emplace_back();
 
-  double GetSpeedInMegabits() const;
-  Interval GetLastInterval() const;
+  // If progress updates are created add a thread to send updates
+  std::thread progress;
+  std::atomic_bool local_cancel(false);
+  if (options.progress_fn && options.progress_millis > 0) {
+    if (options.verbose) {
+      std::cout << "Progress updates every "
+                << options.progress_millis << " ms\n";
+    }
+    progress = std::thread([&] {
+      std::this_thread::sleep_for(
+          std::chrono::milliseconds(options.progress_millis));
+      while (!local_cancel) {
+        options.progress_fn(result.buckets.back());
+        std::this_thread::sleep_for(
+            std::chrono::milliseconds(options.progress_millis));
+      }
+      options.progress_fn(result.buckets.back());
+    });
+  } else if (options.verbose) {
+    std::cout << "No progress updates\n";
+  }
 
- protected:
-  void RunInternal() override;
-  void StopInternal() override;
+  // Updating thread
+  if (options.verbose) {
+    std::cout << "Transfer runner updates every "
+              << options.interval_millis << " ms\n";
+  }
+  long min_runtime_micros = options.min_runtime_millis * 1000;
+  long max_runtime_micros = options.max_runtime_millis * 1000;
+  std::mutex mutex;
+  std::thread updater([&] {
+    std::this_thread::sleep_for(
+        std::chrono::milliseconds(options.interval_millis));
+    while (!local_cancel) {
+      if (*cancel) {
+        local_cancel = true;
+        break;
+      }
 
- private:
-  const Interval &AddInterval();
-  double GetSimpleAverage(int num_intervals);
-  double GetShortEma(int num_intervals);
-  double GetLongEma(int num_intervals);
+      Bucket last_bucket;
+      long running_time = SystemTimeMicros() - result.start_time;
+      {
+        std::lock_guard <std::mutex> lock(mutex);
+        result.buckets.emplace_back();
+        Bucket &bucket = result.buckets.back();
+        bucket.start_time = running_time;
+        bucket.total_bytes = fn.get().bytes_transferred();
+        result.total_bytes = bucket.total_bytes;
+        if (options.exponential_moving_average) {
+          bucket.short_megabits = GetShortEma(&result.buckets,
+                                              options.min_intervals);
+          bucket.long_megabits = GetLongEma(&result.buckets,
+                                            options.max_intervals);
+        } else {
+          bucket.short_megabits = GetSimpleAverage(&result.buckets,
+                                                   options.min_intervals);
+          bucket.long_megabits = GetSimpleAverage(&result.buckets,
+                                                  options.max_intervals);
+        }
+        result.speed_mbps = bucket.long_megabits;
+        last_bucket = result.buckets.back();
+      }
 
-  Options options_;
+      if (running_time > max_runtime_micros) {
+        local_cancel = true;
+        break;
+      }
+      if (running_time > min_runtime_micros &&
+          last_bucket.short_megabits > 0 &&
+          last_bucket.long_megabits > 0) {
+        double speed_variance = variance(last_bucket.short_megabits,
+                                         last_bucket.long_megabits);
+        if (speed_variance <= options.max_variance) {
+          local_cancel = true;
+          break;
+        }
+      }
+      std::this_thread::sleep_for(
+          std::chrono::milliseconds(options.interval_millis));
+    }
+  });
 
-  mutable std::mutex mutex_;
-  std::vector<Interval> intervals_;
-  std::vector<std::thread> threads_;
-  double speed_;
+  // transfer task
+  std::thread task([&]{
+    fn(&local_cancel);
+  });
 
-  // disallowed
-  TransferRunner(const TransferRunner &) = delete;
-  void operator=(const TransferRunner &) = delete;
-};
+  task.join();
+  updater.join();
+  if (progress.joinable()) {
+    progress.join();
+  }
+
+  if (*cancel) {
+    result.status = Status(StatusCode::ABORTED, "transfer runner aborted");
+  } else {
+    result.status = Status::OK;
+  }
+  result.end_time = SystemTimeMicros();
+  return result;
+}
 
 }  // namespace
 
-#endif //SPEEDTEST_TRANSFER_RUNNER_H
+#endif // SPEEDTEST_TRANSFER_RUNNER_H
diff --git a/speedtest/transfer_task.cc b/speedtest/transfer_task.cc
deleted file mode 100644
index d742d87..0000000
--- a/speedtest/transfer_task.cc
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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 "transfer_task.h"
-
-#include <cassert>
-#include <thread>
-#include <vector>
-
-namespace speedtest {
-
-TransferTask::TransferTask(const Options &options)
-    : HttpTask(options),
-      bytes_transferred_(0),
-      requests_started_(0),
-      requests_ended_(0) {
-  assert(options.num_transfers > 0);
-}
-
-void TransferTask::ResetCounters() {
-  bytes_transferred_ = 0;
-  requests_started_ = 0;
-  requests_ended_ = 0;
-}
-
-void TransferTask::StartRequest() {
-  requests_started_++;
-}
-
-void TransferTask::EndRequest() {
-  requests_ended_++;
-}
-
-void TransferTask::TransferBytes(long bytes) {
-  bytes_transferred_ += bytes;
-}
-
-}  // namespace speedtest
diff --git a/speedtest/transfer_task.h b/speedtest/transfer_task.h
deleted file mode 100644
index 83cff9e..0000000
--- a/speedtest/transfer_task.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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 SPEEDTEST_TRANSFER_TEST_H
-#define SPEEDTEST_TRANSFER_TEST_H
-
-#include <atomic>
-#include "http_task.h"
-
-namespace speedtest {
-
-class TransferTask : public HttpTask {
- public:
-  struct Options : HttpTask::Options {
-    int num_transfers = 0;
-  };
-
-  explicit TransferTask(const Options &options);
-
-  long bytes_transferred() const { return bytes_transferred_; }
-  long requests_started() const { return requests_started_; }
-  long requests_ended() const { return requests_ended_; }
-
- protected:
-  void ResetCounters();
-  void StartRequest();
-  void EndRequest();
-  void TransferBytes(long bytes);
-
- private:
-  std::atomic_long bytes_transferred_;
-  std::atomic_int requests_started_;
-  std::atomic_int requests_ended_;
-
-  // disallowed
-  TransferTask(const TransferTask &) = delete;
-  void operator=(const TransferTask &) = delete;
-};
-
-}  // namespace speedtest
-
-#endif  // SPEEDTEST_TRANSFER_TEST_H
diff --git a/speedtest/upload.cc b/speedtest/upload.cc
new file mode 100644
index 0000000..9760078
--- /dev/null
+++ b/speedtest/upload.cc
@@ -0,0 +1,91 @@
+/*
+ * 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 "upload.h"
+
+#include <string>
+#include <thread>
+#include <vector>
+
+namespace speedtest {
+
+Upload::Upload(const Options &options)
+    : options_(options),
+      start_time_(0),
+      end_time_(0),
+      bytes_transferred_(0) {
+}
+
+Upload::Result Upload::operator()(std::atomic_bool *cancel) {
+  start_time_ = SystemTimeMicros();
+  bytes_transferred_ = 0;
+
+  if (!cancel) {
+    end_time_ = SystemTimeMicros();
+    return GetResult(Status(StatusCode::FAILED_PRECONDITION, "cancel is null"));
+  }
+
+  std::vector<std::thread> threads;
+  for (int i = 0; i < options_.num_transfers; ++i) {
+    threads.emplace_back([=]{
+      http::Request::Ptr upload = options_.request_factory(i);
+      while (!*cancel) {
+        long uploaded = 0;
+        upload->set_param("i", to_string(i));
+        upload->set_param("time", to_string(SystemTimeMicros()));
+        upload->set_progress_fn([&](curl_off_t,
+                                    curl_off_t,
+                                    curl_off_t,
+                                    curl_off_t ulnow) -> bool {
+          if (ulnow > uploaded) {
+            bytes_transferred_ += ulnow - uploaded;
+            uploaded = ulnow;
+          }
+          return *cancel;
+        });
+
+        // disable the Expect header as the server isn't expecting it (perhaps
+        // it should?). If the server isn't then libcurl waits for 1 second
+        // before sending the data anyway. So sending this header eliminated
+        // the 1 second delay.
+        upload->set_header("Expect", "");
+
+        upload->Post(options_.payload->c_str(), options_.payload->size());
+        upload->Reset();
+      }
+    });
+  }
+
+  for (std::thread &thread : threads) {
+    if (thread.joinable()) {
+      thread.join();
+    }
+  }
+
+  end_time_ = SystemTimeMicros();
+  return GetResult(Status::OK);
+}
+
+Upload::Result Upload::GetResult(Status status) const {
+  Upload::Result result;
+  result.start_time = start_time_;
+  result.end_time = end_time_;
+  result.status = status;
+  result.bytes_transferred = bytes_transferred_;
+  return result;
+}
+
+}  // namespace
diff --git a/speedtest/upload.h b/speedtest/upload.h
new file mode 100644
index 0000000..4a5dd8a
--- /dev/null
+++ b/speedtest/upload.h
@@ -0,0 +1,66 @@
+/*
+ * 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 SPEEDTEST_UPLOAD_H
+#define SPEEDTEST_UPLOAD_H
+
+#include <atomic>
+#include <functional>
+#include <memory>
+#include "request.h"
+#include "status.h"
+#include "utils.h"
+
+namespace speedtest {
+
+class Upload {
+ public:
+  struct Options {
+    bool verbose;
+    std::function<http::Request::Ptr(int)> request_factory;
+    int num_transfers;
+    std::shared_ptr<std::string> payload;
+  };
+
+  struct Result {
+    long start_time;
+    long end_time;
+    Status status;
+    long bytes_transferred;
+  };
+
+  explicit Upload(const Options &options);
+
+  Result operator()(std::atomic_bool *cancel);
+
+  long start_time() const { return start_time_; }
+  long end_time() const { return end_time_; }
+  long bytes_transferred() const { return bytes_transferred_; }
+
+ private:
+  Result GetResult(Status status) const;
+
+  Options options_;
+  std::atomic_long start_time_;
+  std::atomic_long end_time_;
+  std::atomic_long bytes_transferred_;
+
+  DISALLOW_COPY_AND_ASSIGN(Upload);
+};
+
+}  // namespace speedtest
+
+#endif // SPEEDTEST_UPLOAD_H
diff --git a/speedtest/upload_task.cc b/speedtest/upload_task.cc
deleted file mode 100644
index 251fc41..0000000
--- a/speedtest/upload_task.cc
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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 "upload_task.h"
-
-#include <algorithm>
-#include <cassert>
-#include <iostream>
-#include "utils.h"
-
-namespace speedtest {
-
-UploadTask::UploadTask(const Options &options)
-    : TransferTask(options),
-      options_(options) {
-  assert(options_.payload);
-  assert(options_.payload->size() > 0);
-}
-
-void UploadTask::RunInternal() {
-  ResetCounters();
-  threads_.clear();
-  if (options_.verbose) {
-    std::cout << "Uploading " << options_.num_transfers
-              << " threads with " << options_.payload->size() << " bytes\n";
-  }
-  for (int i = 0; i < options_.num_transfers; ++i) {
-    threads_.emplace_back([=]{
-      RunUpload(i);
-    });
-  }
-}
-
-void UploadTask::StopInternal() {
-  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
-    t.join();
-  });
-}
-
-void UploadTask::RunUpload(int id) {
-  http::Request::Ptr upload = options_.request_factory(id);
-  while (GetStatus() == TaskStatus::RUNNING) {
-    long uploaded = 0;
-    upload->set_param("i", to_string(id));
-    upload->set_param("time", to_string(SystemTimeMicros()));
-    upload->set_progress_fn([&](curl_off_t,
-                                  curl_off_t,
-                                  curl_off_t,
-                                  curl_off_t ulnow) -> bool {
-      if (ulnow > uploaded) {
-        TransferBytes(ulnow - uploaded);
-        uploaded = ulnow;
-      }
-      return GetStatus() != TaskStatus::RUNNING;
-    });
-
-    // disable the Expect header as the server isn't expecting it (perhaps
-    // it should?). If the server isn't then libcurl waits for 1 second
-    // before sending the data anyway. So sending this header eliminated
-    // the 1 second delay.
-    upload->set_header("Expect", "");
-
-    StartRequest();
-    upload->Post(options_.payload->c_str(), options_.payload->size());
-    EndRequest();
-    upload->Reset();
-  }
-}
-
-}  // namespace speedtest
diff --git a/speedtest/upload_task.h b/speedtest/upload_task.h
deleted file mode 100644
index 323f904..0000000
--- a/speedtest/upload_task.h
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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 SPEEDTEST_UPLOAD_TASK_H
-#define SPEEDTEST_UPLOAD_TASK_H
-
-#include <memory>
-#include <string>
-#include <thread>
-#include <vector>
-#include "transfer_task.h"
-
-namespace speedtest {
-
-class UploadTask : public TransferTask {
- public:
-  struct Options : TransferTask::Options {
-    std::shared_ptr<std::string> payload;
-  };
-
-  explicit UploadTask(const Options &options);
-
- protected:
-  void RunInternal() override;
-  void StopInternal() override;
-
- private:
-  void RunUpload(int id);
-
-  Options options_;
-  std::vector<std::thread> threads_;
-
-  // disallowed
-  UploadTask(const UploadTask &) = delete;
-  void operator=(const UploadTask &) = delete;
-};
-
-}  // namespace speedtest
-
-#endif  // SPEEDTEST_UPLOAD_TASK_H
diff --git a/speedtest/utils.cc b/speedtest/utils.cc
index 8144174..d3c00d0 100644
--- a/speedtest/utils.cc
+++ b/speedtest/utils.cc
@@ -62,6 +62,18 @@
   return (8.0d * bytes) / micros;
 }
 
+std::string ToMillis(long micros) {
+  double millis = micros / 1000.0d;
+  if (millis < 1) {
+    return round(millis, 3);
+  } else if (millis < 10) {
+    return round(millis, 2);
+  } else if (millis < 1000) {
+    return round(millis, 1);
+  }
+  return round(millis, 0);
+}
+
 bool ParseInt(const std::string &str, int *result) {
   if (!result) {
     return false;
@@ -95,4 +107,16 @@
   RightTrim(s);
 }
 
+std::shared_ptr<std::string> MakeRandomData(size_t size) {
+  std::random_device rd;
+  std::default_random_engine random_engine(rd());
+  std::uniform_int_distribution<char> uniform_dist(1, 255);
+  auto random_data = std::make_shared<std::string>();
+  random_data->resize(size);
+  for (size_t i = 0; i < size; ++i) {
+    (*random_data)[i] = uniform_dist(random_engine);
+  }
+  return std::move(random_data);
+}
+
 }  // namespace speedtest
diff --git a/speedtest/utils.h b/speedtest/utils.h
index 7e8d251..f7471b7 100644
--- a/speedtest/utils.h
+++ b/speedtest/utils.h
@@ -17,10 +17,23 @@
 #ifndef SPEEDTEST_UTILS_H
 #define SPEEDTEST_UTILS_H
 
+#include <future>
+#include <memory>
 #include <string>
 
+#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
+  TypeName(const TypeName&) = delete;      \
+  TypeName& operator=(const TypeName&) = delete
+
 namespace speedtest {
 
+template <typename F, typename... Ts>
+inline std::future<typename std::result_of<F(Ts...)>::type>
+ReallyAsync(F&& f, Ts&&... params) {
+  return std::async(std::launch::async, std::forward<F>(f),
+                    std::forward<Ts>(params)...);
+}
+
 // Return relative time in microseconds
 // This isn't convertible to an absolute date and time
 long SystemTimeMicros();
@@ -37,6 +50,9 @@
 // Convert bytes and time in micros to speed in megabits
 double ToMegabits(long bytes, long micros);
 
+// Convert to milliseconds, round to at least 3 significant figures.
+std::string ToMillis(long micros);
+
 // Parse an int.
 // If successful, write result to result and return true.
 // If result is null or the int can't be parsed, return false.
@@ -54,6 +70,8 @@
 // Caller retains ownership
 void Trim(std::string *s);
 
+std::shared_ptr<std::string> MakeRandomData(size_t size);
+
 }  // namespace speedtst
 
 #endif  // SPEEDTEST_UTILS_H
diff --git a/taxonomy/.gitignore b/taxonomy/.gitignore
index 796b96d..b1c9bfa 100644
--- a/taxonomy/.gitignore
+++ b/taxonomy/.gitignore
@@ -1 +1,2 @@
 /build
+tax_signature
diff --git a/taxonomy/dhcp.py b/taxonomy/dhcp.py
index 7a503bc..faa9a39 100644
--- a/taxonomy/dhcp.py
+++ b/taxonomy/dhcp.py
@@ -41,6 +41,8 @@
 
     '1,3,6': ['dashbutton'],
 
+    '1,3,6,28': ['ecobee'],
+
     '1,3,6,12,15,17,28,40,41,42': ['epsonprinter'],
 
     '6,3,1,15,66,67,13,44,12': ['hpprinter'],
@@ -52,22 +54,28 @@
 
     '1,3,6,15,119,95,252,44,46,47': ['ipodtouch1'],
 
+    '252,3,42,15,6,1,12': ['lgtv'],
+
     '1,3,6,15,119,95,252,44,46,101': ['macos'],
     '1,3,6,15,119,95,252,44,46': ['macos'],
     '1,121,3,6,15,119,252,95,44,46': ['macos'],
 
+    '58,59,6,15,51,54,1,3': ['panasonictv'],
+
     '1,3,15,6': ['playstation'],
 
     '1,3,6,15,12': ['roku'],
 
     '1,3,6,12,15,28,42,125': ['samsungtv'],
 
+    '1,3,6,12,15,28,42': ['visiotv'],
     '1,3,6,12,15,28,40,41,42': ['visiotv', 'kindle'],
 
     '1,3,6,15,28,33': ['wii'],
     '1,3,6,15': ['wii', 'xbox'],
 
-    '1,15,3,6,44,46,47,31,33,121,249,252,43': ['windows-phone'],
+    '1,15,3,6,44,46,47,31,33,121,249,252,43': ['windows-phone', 'windows'],
+    '1,3,6,15,31,33,43,44,46,47,121,249,252': ['windows'],
 }
 
 
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index 0635073..ca10bee 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -28,12 +28,17 @@
     '10:ae:60': ['amazon'],
     '28:ef:01': ['amazon'],
     '74:75:48': ['amazon'],
+    '74:c2:46': ['amazon'],
     '84:d6:d0': ['amazon'],
     'a0:02:dc': ['amazon'],
     'f0:27:2d': ['amazon'],
     'f0:4f:7c': ['amazon'],
     'f0:a2:25': ['amazon'],
 
+    '08:60:6e': ['asus'],
+    '08:62:66': ['asus'],
+    '1c:87:2c': ['asus'],
+    '2c:56:dc': ['asus'],
     '30:85:a9': ['asus'],
     '5c:ff:35': ['asus'],
     '60:a4:4c': ['asus'],
@@ -41,6 +46,7 @@
     'ac:22:0b': ['asus'],
     'bc:ee:7b': ['asus'],
     'd8:50:e6': ['asus'],
+    'f8:32:e4': ['asus'],
 
     '30:8c:fb': ['dropcam'],
 
@@ -53,17 +59,22 @@
 
     # These are registered to AzureWave, but used for Chromecast v1.
     '6c:ad:f8': ['azurewave', 'google'],
+    '80:d2:1d': ['azurewave', 'google'],
     'b0:ee:45': ['azurewave', 'google'],
     'd0:e7:82': ['azurewave', 'google'],
 
+    '44:61:32': ['ecobee'],
+
     '00:23:76': ['htc'],
     '00:ee:bd': ['htc'],
     '18:87:96': ['htc'],
     '1c:b0:94': ['htc'],
+    '2c:8a:72': ['htc'],
     '38:e7:d8': ['htc'],
     '50:2e:5c': ['htc'],
     '64:a7:69': ['htc'],
     '7c:61:93': ['htc'],
+    '80:01:84': ['htc'],
     '84:7a:88': ['htc'],
     '90:e7:c4': ['htc'],
     'a0:f4:50': ['htc'],
@@ -75,11 +86,13 @@
     '10:68:3f': ['lg'],
     '2c:54:cf': ['lg'],
     '34:fc:ef': ['lg'],
+    '3c:bd:d8': ['lg'],
     '40:b0:fa': ['lg'],
     '58:3f:54': ['lg'],
     '64:89:9a': ['lg'],
     '64:bc:0c': ['lg'],
     '78:f8:82': ['lg'],
+    '88:07:4b': ['lg'],
     '88:c9:d0': ['lg'],
     '8c:3a:e3': ['lg'],
     'a0:39:f7': ['lg'],
@@ -87,14 +100,21 @@
     'bc:f5:ac': ['lg'],
     'c4:43:8f': ['lg'],
     'c4:9a:02': ['lg'],
+    'cc:fa:00': ['lg'],
+    'e8:5b:5b': ['lg'],
     'f8:95:c7': ['lg'],
     'f8:a9:d0': ['lg'],
 
+    '00:1d:d8': ['microsoft'],
     '28:18:78': ['microsoft'],
     '50:1a:c5': ['microsoft'],
+    '58:82:a8': ['microsoft'],
     '60:45:bd': ['microsoft'],
+    '7c:1e:52': ['microsoft'],
     '7c:ed:8d': ['microsoft'],
+    'b4:ae:2b': ['microsoft'],
 
+    '14:1a:a3': ['motorola'],
     '14:30:c6': ['motorola'],
     '1c:56:fe': ['motorola'],
     '24:da:9b': ['motorola'],
@@ -139,45 +159,68 @@
     '00:27:09': ['nintendo'],
     '34:af:2c': ['nintendo'],
 
+    'c0:ee:fb': ['oneplus'],
+
+    '00:15:99': ['samsung'],
     '00:26:37': ['samsung'],
     '08:d4:2b': ['samsung'],
     '08:ec:a9': ['samsung'],
     '14:32:d1': ['samsung'],
     '24:4b:81': ['samsung'],
+    '28:ba:b5': ['samsung'],
+    '2c:ae:2b': ['samsung'],
     '30:19:66': ['samsung'],
     '34:23:ba': ['samsung'],
+    '38:2d:e8': ['samsung'],
     '38:aa:3c': ['samsung'],
+    '38:d4:0b': ['samsung'],
     '3c:8b:fe': ['samsung'],
     '40:0e:85': ['samsung'],
     '48:5a:3f': ['samsung', 'wisol'],
+    '50:cc:f8': ['samsung'],
     '54:88:0e': ['samsung'],
     '5c:0a:5b': ['samsung'],
     '5c:f6:dc': ['samsung'],
     '6c:2f:2c': ['samsung'],
     '6c:83:36': ['samsung'],
+    '78:40:e4': ['samsung'],
     '78:d6:f0': ['samsung'],
+    '78:bd:bc': ['samsung'],
     '80:65:6d': ['samsung'],
     '84:11:9e': ['samsung'],
     '84:25:db': ['samsung'],
+    '84:2e:27': ['samsung'],
     '84:38:38': ['samsung'],
+    '84:55:a5': ['samsung'],
     '88:32:9b': ['samsung'],
     '8c:77:12': ['samsung'],
     '90:18:7c': ['samsung'],
+    '90:f1:aa': ['samsung'],
     '94:35:0a': ['samsung'],
+    '94:b1:0a': ['samsung'],
     'a0:0b:ba': ['samsung'],
     'a8:06:00': ['samsung'],
     'ac:36:13': ['samsung'],
+    'ac:5f:3e': ['samsung'],
     'b0:df:3a': ['samsung'],
     'b0:ec:71': ['samsung'],
     'b4:07:f9': ['samsung'],
+    'b4:79:a7': ['samsung'],
+    'b8:5a:73': ['samsung'],
     'bc:20:a4': ['samsung'],
+    'bc:72:b1': ['samsung'],
+    'bc:8c:cd': ['samsung'],
+    'bc:e6:3f': ['samsung'],
     'c0:bd:d1': ['samsung'],
     'c4:42:02': ['samsung'],
+    'c4:73:1e': ['samsung'],
     'cc:07:ab': ['samsung'],
     'cc:3a:61': ['samsung'],
     'd0:22:be': ['samsung'],
     'e0:99:71': ['samsung'],
+    'e0:db:10': ['samsung'],
     'e4:12:1d': ['samsung'],
+    'e4:92:fb': ['samsung'],
     'e8:3a:12': ['samsung'],
     'e8:50:8b': ['samsung'],
     'ec:1f:72': ['samsung'],
@@ -189,9 +232,12 @@
     '28:0d:fc': ['sony'],
     '30:17:c8': ['sony'],
     '40:b8:37': ['sony'],
+    '58:48:22': ['sony'],
     'b4:52:7e': ['sony'],
 
     '00:24:e4': ['withings'],
+
+    '64:cc:2e': ['xiaomi'],
 }
 
 
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index bb76e15..fd620d8 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -9,25 +9,13 @@
 
 regression = [
   # devices for which we have a pcap but have decided not to add
-  # to the database, generally because the device is not common
-  # enough.
-  ('Unknown', './testdata/pcaps/Amazon Fire Phone 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/Amazon Fire Phone 5GHz Broadcast.pcap'),
-  ('Unknown', './testdata/pcaps/Amazon Fire Phone 5GHz Specific.pcap'),
-  ('Unknown', './testdata/pcaps/Amazon Fire Phone 5GHz.pcap'),
+  # to the database
   ('Unknown', './testdata/pcaps/ASUS Transformer TF300 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Blackberry Bold 9930 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Blackberry Bold 9930 5GHz.pcap'),
-  ('Unknown', './testdata/pcaps/iPhone 2 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/iPhone 3 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/iPhone 3GS 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/iPhone 3GS 2.4GHz M137LL.pcap'),
   ('Unknown', './testdata/pcaps/HTC Evo 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Incredible 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Inspire 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/HTC One V 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/HTC One X 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/HTC One X 5GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Sensation 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Thunderbolt 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/HTC Titan 2.4GHz.pcap'),
@@ -44,8 +32,6 @@
   ('Unknown', './testdata/pcaps/Motorola Droid Razr 5GHz XT910.pcap'),
   ('Unknown', './testdata/pcaps/Motorola Droid Razr Maxx 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Nexus One 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/Nokia Lumia 920 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/Nokia Lumia 920 5GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Charge 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Captivate 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Continuum 2.4GHz.pcap'),
@@ -55,12 +41,11 @@
   ('Unknown', './testdata/pcaps/Samsung Galaxy Tab 2 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Infuse 5GHz.pcap'),
   ('Unknown', './testdata/pcaps/Samsung Vibrant 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/Sony Xperia Z5 2.4GHz.pcap'),
-  ('Unknown', './testdata/pcaps/Sony Xperia Z5 5GHz.pcap'),
 
-  # Names which contain a slash ('/'), which Linux filenames do not
-  # tolerate. Inferring the expected result from the filename doesn't
-  # work for these, instead we add them explicitly.
+  # Names where the identified species doesn't exactly match the filename,
+  # usually because multiple devices are too similar to distinguish. We name
+  # the file for the specific device which was captured, and add an entry
+  # here for the best identification which we can manage.
   ('iPad (1st/2nd gen)', './testdata/pcaps/iPad 1st gen 5GHz.pcap'),
   ('iPad (1st/2nd gen)', './testdata/pcaps/iPad 2nd gen 5GHz.pcap'),
   ('iPad (4th gen or Air)', './testdata/pcaps/iPad (4th gen) 5GHz.pcap'),
@@ -81,6 +66,10 @@
   ('Samsung Galaxy Note or S2+', './testdata/pcaps/Samsung Galaxy Note 5GHz.pcap'),
   ('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap'),
   ('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Infuse 2.4GHz.pcap'),
+  ('Sony Xperia Z4/Z5', './testdata/pcaps/Sony Xperia Z5 5GHz.pcap'),
+  ('Sony Xperia Z4/Z5', './testdata/pcaps/Sony Xperia Z5 2.4GHz.pcap'),
+  ('Sony Xperia Z4/Z5', './testdata/pcaps/Sony Xperia Z4 Tablet 5GHz.pcap'),
+  ('Sony Xperia Z4/Z5', './testdata/pcaps/Sony Xperia Z4 Tablet 2.4GHz.pcap'),
 ]
 
 
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index 64aa1ca..6f13edf 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -46,3 +46,6 @@
 1432237016 8c:2d:aa:9c:ce:0f 192.168.42.36 iPood-5
 1432237016 dc:86:d8:a0:c8:de 192.168.42.37 iPhoone-5c
 1432237016 54:ae:27:32:ef:7f 192.168.42.38 iPaad-Air-1
+1432237016 00:1e:c2:24:7f:10 192.168.42.39 iPhoone-2
+1432237016 00:23:12:99:30:93 192.168.42.39 iPhoone-3
+1432237016 34:c8:03:89:d3:e8 192.168.42.40 Nokia-Lumia-920
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 4831315..06b641b 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -38,3 +38,6 @@
 8c:2d:aa:9c:ce:0f 1,3,6,15,119,252
 dc:86:d8:a0:c8:de 1,3,6,15,119,252
 54:ae:27:32:ef:7f 1,3,6,15,119,252
+00:1e:c2:24:7f:10 1,3,6,15,119,252
+00:23:12:99:30:93 1,3,6,15,119,252
+34:c8:03:89:d3:e8 1,15,3,6,44,46,47,31,33,121,249,252,43
diff --git a/taxonomy/testdata/pcaps/Oneplus X 2.4GHz.pcap b/taxonomy/testdata/pcaps/Oneplus X 2.4GHz.pcap
new file mode 100644
index 0000000..54f6f8f
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Oneplus X 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S7 2.4GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S7 2.4GHz.pcap
new file mode 100644
index 0000000..8bafff8
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S7 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S7 5GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S7 5GHz.pcap
new file mode 100644
index 0000000..647546b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S7 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S7 Edge 2.4GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S7 Edge 2.4GHz.pcap
new file mode 100644
index 0000000..908793a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S7 Edge 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Samsung Galaxy S7 Edge 5GHz.pcap b/taxonomy/testdata/pcaps/Samsung Galaxy S7 Edge 5GHz.pcap
new file mode 100644
index 0000000..03d468b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Samsung Galaxy S7 Edge 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Xiaomi Redmi 3 2.4GHz.pcap b/taxonomy/testdata/pcaps/Xiaomi Redmi 3 2.4GHz.pcap
new file mode 100644
index 0000000..105d560
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Xiaomi Redmi 3 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index 3a12713..109095c 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -26,16 +26,25 @@
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff|os:dashbutton':
         ('BCM43362', 'Amazon Dash Button', '2.4GHz'),
 
-    'wifi4|probe:0,1,50|assoc:0,1,50,48,221(0050f2,2)|os:kindle':
+    'wifi4|probe:0,1,3,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,48,45,221(0050f2,2),191,127,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a0200000040|oui:amazon':
+        ('', 'Amazon Fire Phone', '5GHz'),
+    'wifi4|probe:0,1,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,48,45,221(0050f2,2),191,127,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a0200000040|oui:amazon':
+        ('', 'Amazon Fire Phone', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a0200000000|oui:amazon':
+        ('', 'Amazon Fire Phone', '2.4GHz'),
+
+    'wifi4|probe:0,1,45,htcap:11ee,htagg:02,htmcs:0000ffff|assoc:0,1,33,36,48,221(0050f2,2),45,127,htcap:11ee,htagg:02,htmcs:0000ffff,txpow:0e00,extcap:01|oui:amazon':
+        ('', 'Amazon Kindle', '5GHz'),
+    'wifi4|probe:0,1,50|assoc:0,1,50,48,221(0050f2,2)|oui:amazon':
         ('', 'Amazon Kindle', '2.4GHz'),
-    'wifi|probe:0,1,50,45,htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:01ac,htagg:02,htmcs:0000ffff|os:kindle':
+    'wifi4|probe:0,1,50,45,htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01|oui:amazon':
         ('', 'Amazon Kindle', '2.4GHz'),
 
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:1130,htagg:18,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),htcap:1130,htagg:18,htmcs:000000ff|oui:amazon':
         ('TI_WL1271', 'Amazon Kindle Fire 7" (2011 edition)', '2.4GHz'),
     'wifi|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:KFASWI|assoc:0,1,50,45,127,221(0050f2,2),48,htcap:1172,htagg:03,htmcs:000000ff':
         ('', 'Amazon Kindle Fire 7" (2014 edition)', '2.4GHz'),
-    'wifi|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:KFFOWI|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff':
+    'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:KFFOWI|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
         ('', 'Amazon Kindle Fire 7" (2015 edition)', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:007e,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(001018,2),221(0050f2,2),htcap:007e,htagg:1b,htmcs:0000ffff,txpow:e50d|oui:amazon':
@@ -78,6 +87,12 @@
     '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|name:appletv':
         ('', 'Apple TV (4th gen)', '2.4GHz'),
 
+    'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:BLU_DASH_M|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
+        ('', 'BLU Dash M', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,127,107,221(0050f2,4),221(506f9a,9),221(506f9a,16),extcap:00000080,wps:BLU_STUDIO_5_0_C_HD|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:0100008000c6':
+        ('', 'BLU Studio 5.0.C HD', '2.4GHz'),
+
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:112c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:112c,htagg:19,htmcs:000000ff|os:brotherprinter':
         ('', 'Brother Printer', '2.4GHz'),
 
@@ -116,17 +131,30 @@
     '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':
         ('Marvell_88W8887', 'Chromecast v2', '2.4GHz'),
 
-    'wifi|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':
+    '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':
         ('', 'Dropcam', '2.4GHz'),
 
+    '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:ecobee':
+        ('', 'ecobee thermostat', '2.4GHz'),
+
     'wifi|probe:0,1,3,45,50,htcap:0162,htagg:00,htmcs:000000ff|assoc:0,1,45,48,127,50,221(0050f2,2),htcap:016e,htagg:1b,htmcs:000000ff|os:epsonprinter':
         ('', 'Epson Printer', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:182c,htagg:1b,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:182c,htagg:1b,htmcs:000000ff|os:epsonprinter':
+        ('', 'Epson Printer', '2.4GHz'),
+    'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:epsonprinter':
+        ('', 'Epson Printer', '2.4GHz'),
 
     'wifi|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'),
-    'wifi|probe:0,1,3,45,50,htcap:0160,htagg:03,htmcs:000000ff|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:016c,htagg:03,htmcs:000000ff|os:hpprinter':
+    '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':
         ('', 'HP Printer', '2.4GHz'),
-    'wifi|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),221(506f9a,9),htcap:0020,htagg:1a,htmcs:000000ff|os:hpprinter':
+    'wifi4|probe:0,1,3,45,50,htcap:0160,htagg:03,htmcs:000000ff|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:016c,htagg:03,htmcs:000000ff,extcap:00|os:hpprinter':
+        ('', 'HP Printer', '2.4GHz'),
+    'wifi4|probe:0,1,3,45,50,htcap:0160,htagg:03,htmcs:000000ff|assoc:0,1,45,48,127,50,221(0050f2,2),htcap:016c,htagg:03,htmcs:000000ff,extcap:00000000|os:hpprinter':
+        ('', 'HP Printer', '2.4GHz'),
+    '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),221(506f9a,9),htcap:0020,htagg:1a,htmcs:000000ff|os:hpprinter':
         ('', 'HP Printer', '2.4GHz'),
     'wifi|probe:0,1,3,45,50,htcap:0060,htagg:03,htmcs:000000ff|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:006c,htagg:03,htmcs:000000ff|os:hpprinter':
         ('', 'HP Printer', '2.4GHz'),
@@ -163,6 +191,14 @@
     'wifi4|probe:0,1,50,3,45,127,221(506f9a,16),221(0050f2,8),221(001018,2),htcap:1063,htagg:17,htmcs:000000ff,extcap:0000088001400040|assoc:0,1,50,33,36,45,127,107,221(001018,2),221(0050f2,2),htcap:1063,htagg:17,htmcs:000000ff,txpow:1309,extcap:000008800140|oui:htc':
         ('BCM4356', 'HTC One M9', '2.4GHz'),
 
+    'wifi4|probe:0,1,50,45,3,221(0050f2,4),221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff,wps:_|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff|oui:htc':
+        ('', 'HTC One V', '2.4GHz'),
+
+    'wifi4|probe:0,1,45,htcap:0130,htagg:18,htmcs:000000ff|assoc:0,1,48,45,221(0050f2,2),htcap:013c,htagg:18,htmcs:000000ff|oui:htc':
+        ('', 'HTC One X', '5GHz'),
+    'wifi4|probe:0,1,50,45,htcap:0130,htagg:18,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),htcap:013c,htagg:18,htmcs:000000ff|oui:htc':
+        ('', 'HTC One X', '2.4GHz'),
+
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:080c,htagg:1b,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:080c,htagg:1b,htmcs:000000ff,txpow:1008|os:ios':
         ('BCM4329', 'iPad (1st/2nd gen)', '5GHz'),
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0800,htagg:1b,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0800,htagg:1b,htmcs:000000ff,txpow:1008|os:ios':
@@ -180,7 +216,7 @@
         ('BCM4330', 'iPad (3rd gen)', '5GHz'),
     'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0100,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100,htagg:19,htmcs:000000ff,txpow:180f|os:ios':
         ('BCM4330', 'iPad (3rd gen)', '5GHz'),
-    'wifi|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:0100|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100|os:ios':
+    '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':
         ('BCM4330', 'iPad (3rd gen)', '2.4GHz'),
 
     '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':
@@ -229,6 +265,15 @@
     '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:1603|os:ios':
         ('BCM4324', 'iPad Mini (2nd gen)', '2.4GHz'),
 
+    'wifi4|probe:0,1,3,50|assoc:0,1,48,50|os:ios':
+        ('', 'iPhone 2', '2.4GHz'),
+
+    'wifi4|probe:0,1,3,50|assoc:0,1,48,50,221(0050f2,2)|os:ios':
+        ('', 'iPhone 3', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,3,221(001018,2)|assoc:0,1,48,50,221(001018,2),221(0050f2,2)|os:ios':
+        ('', 'iPhone 3GS', '2.4GHz'),
+
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:1800,htagg:1b,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1800,htagg:1b,htmcs:000000ff|os:ios':
         ('BCM4329', 'iPhone 4', '2.4GHz'),
 
@@ -248,10 +293,14 @@
 
     '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':
         ('BCM4334', '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':
+        ('BCM4334', 'iPhone 5c', '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:00000004|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1704|os:ios':
         ('BCM4334', 'iPhone 5c', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1704|os:ios':
         ('BCM4334', 'iPhone 5c', '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:1704|os:ios':
+        ('BCM4334', 'iPhone 5c', '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:00000804|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1603|os:ios':
         ('BCM4334', 'iPhone 5s', '5GHz'),
@@ -285,6 +334,8 @@
         ('BCM4350', 'iPhone 6s/6s+', '5GHz'),
     'wifi4|probe:0,1,45,127,107,191,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(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
         ('BCM4350', 'iPhone 6s/6s+', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,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(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f815832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e002,extcap:0400000000000040|os:ios':
+        ('BCM4350', '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:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
         ('BCM4350', '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,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':
@@ -299,6 +350,8 @@
         ('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,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(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0000000000000040|os:ios':
         ('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,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(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:000000ff,txpow:1202,extcap:0000000000000040|os:ios':
+        ('BCM4350', 'iPhone 6s/6s+', '2.4GHz'),
 
     'wifi4|probe:0,1,3,50|assoc:0,1,48,50|os:ipodtouch1':
         ('Marvell_W8686B22', 'iPod Touch 1st/2nd gen', '2.4GHz'),
@@ -311,6 +364,8 @@
 
     '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,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff,txpow:1504|os:ios':
         ('BCM4334', 'iPod Touch 5th gen', '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: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:1504|os:ios':
+        ('BCM4334', 'iPod Touch 5th 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:1706|os:ios':
         ('BCM4334', 'iPod Touch 5th 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:1706|os:ios':
@@ -327,16 +382,26 @@
         ('BCM4339', 'LG G3', '5GHz'),
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(506f9a,16),htcap:012c,htagg:03,htmcs:000000ff,extcap:000000800040|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a8201400000|oui:lg':
         ('BCM4339', 'LG G3', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(506f9a,16),htcap:012c,htagg:03,htmcs:000000ff,extcap:000000800040|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a8201400000|oui:lg':
+        ('BCM4339', 'LG G3', '2.4GHz'),
 
     'wifi4|probe:0,1,3,45,127,107,191,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088001400040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1d01,extcap:0000008001400040|oui:lg':
         ('BCM4339', 'LG G4', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088000400040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e008,extcap:0000008000400040|oui:lg':
+        ('BCM4339', 'LG G4', '5GHz'),
     'wifi4|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:112d,htagg:17,htmcs:000000ff,extcap:0000088001400040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:112d,htagg:17,htmcs:000000ff,txpow:1001,extcap:000000800140|oui:lg':
         ('BCM4339', 'LG G4', '2.4GHz'),
 
-    'wifi|probe:0,1,50,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,wps:LGMS323|assoc:0,1,50,48,45,221(0050f2,2),221(004096,3),htcap:012c':
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:LGL16C|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff':
+        ('', 'LG Lucky', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:LGMS323|assoc:0,1,50,48,45,221(0050f2,2),221(004096,3),htcap:012c,htagg:03,htmcs:000000ff':
         ('QCA_WCN3360', 'LG Optimus L70', '2.4GHz'),
 
-    'wifi|probe:0,1,50,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:LGLS660|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff':
+    'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:11ac,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:11ac,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:lgtv':
+        ('', 'LG Smart TV', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:LGLS660|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff':
         ('', 'LG Tribute', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:087e,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:087e,htagg:1b,htmcs:0000ffff,txpow:0f07|os:macos':
@@ -439,6 +504,8 @@
         ('BCM4339', 'Nexus 5', '5GHz'),
     'wifi4|probe:0,1,3,45,127,191,221(001018,2),221(00904c,51),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e003,extcap:0000000000000040|oui:lg':
         ('BCM4339', 'Nexus 5', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(001018,2),221(00904c,51),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e003,extcap:0000000000000040|oui:lg':
+        ('BCM4339', 'Nexus 5', '5GHz'),
     'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088001400040|assoc:0,1,33,36,48,45,127,70,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:016f,htagg:17,htmcs:000000ff,vhtcap:0f805932,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e003,extcap:0000008001400040|oui:lg':
         ('BCM4339', 'Nexus 5', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:112d,htagg:17,htmcs:000000ff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),htcap:112d,htagg:17,htmcs:000000ff,txpow:1303|oui:lg':
@@ -545,6 +612,8 @@
         ('BCM4354', 'Nexus 9', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:150b,extcap:000008800140':
         ('BCM4354', 'Nexus 9', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(00904c,4),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1309,extcap:000008800140|oui:samsung':
+        ('BCM4354', 'Nexus 9', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:01fe,htagg:1b,htmcs:0000ffff|assoc:0,1,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff|oui:samsung':
         ('', 'Nexus 10', '5GHz'),
@@ -553,6 +622,8 @@
 
     'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040,wps:Nexus_Player|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:0000088001400040':
         ('BCM4356', 'Nexus Player', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_Player|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
+        ('BCM4356', 'Nexus Player', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_Player|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
         ('BCM4356', 'Nexus Player', '2.4GHz'),
 
@@ -561,29 +632,73 @@
     'wifi4|probe:0,1,50,45,htcap:012c,htagg:1b,htmcs:000000ff|assoc:0,1,48,50,221(0050f2,2),45,51,127,htcap:012c,htagg:1b,htmcs:000000ff,extcap:0100000000000040|os:windows-phone':
         ('', 'Nokia Lumia 635', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,48,45,221(0050f2,2),htcap:016e,htagg:03,htmcs:000000ff|os:windows-phone':
+        ('', 'Nokia Lumia 920', '5GHz'),
+    'wifi4|probe:0,1,50,45,htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff|os:windows-phone':
+        ('', 'Nokia Lumia 920', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a0200000000|oui:oneplus':
+        ('', 'Oneplus X', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,48|assoc:0,1,33,36,50,221(0050f2,2),45,221(00037f,1),221(00037f,4),48,htcap:1004,htagg:1b,htmcs:0000ffff,txpow:0f0f|os:panasonictv':
+        ('', 'Panasonic TV', '2.4GHz'),
+
     'wifi4|probe:0,1,50|assoc:0,1,50,48,221(005043,1)|os:playstation':
         ('', 'Playstation 3 or 4', '2.4GHz'),
 
-    'wifi|probe:0,1,3,50|assoc:0,1,48,50,221(0050f2,2),45,htcap:112c,htagg:03,htmcs:0000ffff|os:playstation':
+    'wifi4|probe:0,1,3,50|assoc:0,1,33,48,50,221(0050f2,2),45,htcap:010c,htagg:03,htmcs:0000ffff,txpow:1209|os:playstation':
+        ('Marvell_88W8797', 'Playstation 4', '2.4GHz'),
+    'wifi4|probe:0,1,3,50|assoc:0,1,48,50,221(0050f2,2),45,htcap:112c,htagg:03,htmcs:0000ffff,txpow:0f06|os:playstation':
         ('Marvell_88W8797', 'Playstation 4', '2.4GHz'),
     'wifi4|probe:0,1,3,50|assoc:0,1,33,48,50,221(0050f2,2),45,htcap:112c,htagg:03,htmcs:0000ffff,txpow:0f06|os:playstation':
         ('Marvell_88W8797', 'Playstation 4', '2.4GHz'),
 
-    'wifi|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff|os:roku':
-        ('BCM43362', 'Roku HD', '2.4GHz'),
+    'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:RCT6303W87DK|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
+        ('', 'RCA 10 Viking Pro', '2.4GHz'),
 
-    'wifi|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:roku':
-        ('BCM4336', 'Roku 2 XD', '2.4GHz'),
+    # Roku model 1100, 2500 and LT model 2450
+    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff|os:roku':
+        ('BCM43362', 'Roku HD/LT', '2.4GHz'),
 
+    # Roku model 1101
+    'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:186e,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:186e,htagg:1a,htmcs:0000ffff,txpow:1208|os:roku':
+        ('', 'Roku HD-XR', '2.4GHz'),
+
+    # Roku Streaming Stick model 3400X
+    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:187c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:187c,htagg:1a,htmcs:0000ffff,txpow:1208|os:roku':
+        ('', 'Roku Streaming Stick', '2.4GHz'),
+
+    # Roku 1 models 2000, 2050, 2100, and "XD" (not sure of model number)
+    'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:186e,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:186e,htagg:1a,htmcs:0000ffff,txpow:1308|os:roku':
+        ('', 'Roku 1', '2.4GHz'),
+
+    # Roku 1 model 2710 and Roku LT model 2700
+    '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(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff|os:roku':
+        ('', 'Roku 1/LT', '2.4GHz'),
+
+    # Roku 2 models 3000, 3050, 3100, and Roku LT model 2400
+    '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:roku':
+        ('BCM4336', 'Roku 2/LT', '2.4GHz'),
+
+    # Roku 2 model 2720
+    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:187c,htagg:1a,htmcs:0000ffff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:187c,htagg:1a,htmcs:0000ffff|os:roku':
+        ('BCM4336', 'Roku 2', '2.4GHz'),
+
+    # Roku 3 model 4230, 4200, 4200X and Roku 2 model 4210 and Roku Streaming Stick model 3500
     'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:19bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:19bc,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
         ('BCM43236', 'Roku 3', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:193c,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:193c,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
         ('BCM43236', 'Roku 3', '2.4GHz'),
 
+    # Roku 4 model 4400
+    'wifi4|probe:0,1,45,127,191,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,199,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1109,extcap:0000000000000040|os:roku':
+        ('', 'Roku 4', '5GHz'),
     'wifi4|probe:0,1,45,191,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa|assoc:0,1,33,36,48,45,191,199,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1109|os:roku':
         ('', 'Roku 4', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1209|os:roku':
         ('', 'Roku 4', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1209|os:roku':
+        ('', 'Roku 4', '2.4GHz'),
 
     'wifi4|probe:0,1,50,3,45,htcap:0020,htagg:01,htmcs:000000ff|assoc:0,1,50,45,61,48,221(0050f2,2),htcap:0020,htagg:01,htmcs:000000ff|oui:samsung':
         ('', 'Samsung Galaxy Mini', '2.4GHz'),
@@ -640,6 +755,8 @@
 
     'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9118b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:00080f8401400040|assoc:0,1,33,36,48,45,127,191,199,221(00904c,4),221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9118b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102,extcap:0000000000000040|oui:samsung':
         ('BCM4359', 'Samsung Galaxy Note 5', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9118b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:00080f8401400040|assoc:0,1,33,36,48,70,45,127,191,199,221(00904c,4),221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9118b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1202,extcap:0000000000000040|oui:samsung':
+        ('BCM4359', 'Samsung Galaxy Note 5', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,extcap:00080f8401400040|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1202|oui:samsung':
         ('BCM4359', 'Samsung Galaxy Note 5', '2.4GHz'),
 
@@ -681,8 +798,14 @@
 
     'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040|assoc:0,1,33,36,48,45,127,107,191,221(00904c,4),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e20b,extcap:0000088001400040|oui:samsung':
         ('BCM4354', 'Samsung Galaxy S5', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140|assoc:0,1,33,36,48,45,127,107,191,221(00904c,4),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e20b,extcap:000008800140|oui:samsung':
+        ('BCM4354', 'Samsung Galaxy S5', '5GHz'),
     'wifi4|probe:0,1,45,191,221(00904c,4),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa|assoc:0,1,33,36,48,45,191,221(00904c,4),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e20b|oui:samsung':
         ('BCM4354', 'Samsung Galaxy S5', '5GHz'),
+    'wifi4|probe:0,1,45,191,221(00904c,4),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:9b40fffa,vhttxmcs:18dafffa|assoc:0,1,33,36,48,45,191,221(00904c,4),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:9b40fffa,vhttxmcs:18dafffa,txpow:e20b|oui:samsung':
+        ('BCM4354', 'Samsung Galaxy S5', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(00904c,4),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140|assoc:0,1,33,36,48,45,127,191,221(00904c,4),221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e20b,extcap:000008800140|oui:samsung':
+        ('BCM4354', 'Samsung Galaxy S5', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140|assoc:0,1,50,33,36,48,45,127,107,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140|oui:samsung':
         ('BCM4354', 'Samsung Galaxy S5', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040|assoc:0,1,50,33,36,48,45,127,107,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140|oui:samsung':
@@ -699,6 +822,18 @@
     'wifi4|probe:0,1,50,3,45,127,221(00904c,4),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1402,extcap:0000088001400040|oui:samsung':
         ('BCM4358', 'Samsung Galaxy S6', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:0163,htagg:17,htmcs:0000ffff,vhtcap:0f907032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:00080f8401400040|assoc:0,1,33,36,48,70,45,127,191,199,221(00904c,4),221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9118b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102,extcap:0000000000000040|oui:samsung':
+        ('', 'Samsung Galaxy S7', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,extcap:00080f8401400040|assoc:0,1,50,33,36,48,70,45,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1402|oui:samsung':
+        ('', 'Samsung Galaxy S7', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,extcap:00080f840140|assoc:0,1,50,33,36,48,70,45,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1402|oui:samsung':
+        ('', 'Samsung Galaxy S7', '2.4GHz'),
+
+    'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9178b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:00080f840140|assoc:0,1,33,36,48,70,45,191,199,221(00904c,4),221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f9118b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1102|oui:samsung':
+        ('', 'Samsung Galaxy S7 Edge', '5GHz'),
+    'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(00904c,4),221(0050f2,8),221(001018,2),htcap:1163,htagg:17,htmcs:0000ffff,extcap:00080f8401400040|assoc:0,1,50,33,36,48,70,45,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1302|oui:samsung':
+        ('', 'Samsung Galaxy S7 Edge', '2.4GHz'),
+
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:082c,htagg:1b,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:082c,htagg:1b,htmcs:000000ff,txpow:0f08|oui:samsung':
         ('BCM4329', 'Samsung Galaxy Tab', '5GHz'),
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:182c,htagg:1b,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:182c,htagg:1b,htmcs:000000ff,txpow:1208|oui:samsung':
@@ -711,9 +846,9 @@
     'wifi4|probe:0,1,3,45,50,htcap:0162,htagg:03,htmcs:00000000|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:012c,htagg:03,htmcs:000000ff,extcap:0000000000000140|oui:samsung':
         ('Marvell_88W8787', 'Samsung Galaxy Tab 3', '2.4GHz'),
 
-    'wifi|probe:0,1,45,221(0050f2,8),htcap:016e|assoc:0,1,33,36,48,45,221(0050f2,2),221(004096,3),htcap:016e|oui:samsung':
+    'wifi4|probe:0,1,45,221(0050f2,8),htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,33,36,48,45,221(0050f2,2),221(004096,3),htcap:016e,htagg:03,htmcs:000000ff,txpow:170d|oui:samsung':
         ('APQ8026', 'Samsung Galaxy Tab 4', '5GHz'),
-    'wifi|probe:0,1,50,3,45,221(0050f2,8),htcap:012c|assoc:0,1,50,48,45,221(0050f2,2),221(004096,3),htcap:012c|oui:samsung':
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),221(004096,3),htcap:012c,htagg:03,htmcs:000000ff|oui:samsung':
         ('APQ8026', 'Samsung Galaxy Tab 4', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(0050f2,4),221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff,wps:_|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:000c,htagg:19,htmcs:000000ff,txpow:0c0a|oui:samsung':
@@ -727,6 +862,15 @@
         ('', 'Samsung Smart TV', '5GHz'),
     'wifi4|probe:0,1,50,45,htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,45,127,48,221(0050f2,2),htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01|os:samsungtv':
         ('', 'Samsung Smart TV', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,221(002d25,32),htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01|os:samsungtv':
+        ('', 'Samsung Smart TV', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,htcap:0120,htagg:02,htmcs:000000ff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:0120,htagg:02,htmcs:000000ff,extcap:01|os:samsungtv':
+        ('', 'Samsung Smart TV', '2.4GHz'),
+
+    'wifi4|probe:0,1,45,221(0050f2,4),htcap:11ee,htagg:02,htmcs:0000ffff,wps:Sony_BRAVIA|assoc:0,1,33,36,48,221(0050f2,2),45,127,htcap:11ee,htagg:02,htmcs:0000ffff,txpow:0500,extcap:01':
+        ('', 'Sony Bravia TV', '5GHz'),
+    'wifi4|probe:0,1,50,45,127,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:01ed,htagg:13,htmcs:0000ffff,extcap:00,wps:BRAVIA_2015|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:008c,htagg:13,htmcs:0000ffff,extcap:00000a02':
+        ('', 'Sony Bravia TV', '2.4GHz'),
 
     'wifi4|probe:0,1,3,45,221(0050f2,8),191,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffc,vhttxmcs:0000fffc|assoc:0,1,33,36,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff|oui:sony':
         ('WCN3680', 'Sony Xperia Z Ultra', '5GHz'),
@@ -737,36 +881,64 @@
     'wifi4|probe:0,1,50,45,htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a0200000000|oui:sony':
         ('WCN3680', 'Sony Xperia Z Ultra', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040|assoc:0,1,33,36,48,70,45,127,107,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815032,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e007,extcap:0000088001400040|oui:sony':
+        ('', 'Sony Xperia Z4/Z5', '5GHz'),
     'wifi4|probe:0,1,45,127,107,191,221(506f9a,16),221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040|assoc:0,1,33,36,48,70,45,127,107,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e007,extcap:0000088001400040|oui:sony':
-        ('', 'Sony Xperia Z4 Tablet', '5GHz'),
+        ('', 'Sony Xperia Z4/Z5', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040|assoc:0,1,50,33,36,48,70,45,127,107,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1307,extcap:0000088001400040|oui:sony':
-        ('', 'Sony Xperia Z4 Tablet', '2.4GHz'),
+        ('', 'Sony Xperia Z4/Z5', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(0050f2,4),221(506f9a,10),221(506f9a,9),221(506f9a,16),htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a820040,wps:831C|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a8201400000':
+        ('', 'Sprint One M8', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(506f9a,16),221(0050f2,8),221(001018,2),htcap:1063,htagg:17,htmcs:000000ff,extcap:000008800140,wps:0PJA2|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:1063,htagg:17,htmcs:000000ff,txpow:1309,extcap:000008800140':
+        ('', 'Sprint One M9', '2.4GHz'),
 
     'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:12,htmcs:000000ff,extcap:01000000|os:visiotv':
         ('', 'Vizio Smart TV', '2.4GHz'),
-    'wifi|probe:0,1,50,45,127,221(0050f2,4),htcap:106e,htagg:12,htmcs:000000ff,wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:12,htmcs:000000ff,extcap:00000001|os:visiotv':
+    'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:13,htmcs:000000ff,extcap:01|os:visiotv':
+        ('', 'Vizio Smart TV', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,127,221(0050f2,4),htcap:106e,htagg:12,htmcs:000000ff,extcap:00,wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:12,htmcs:000000ff,extcap:01000000|os:visiotv':
         ('', 'Vizio Smart TV', '2.4GHz'),
     'wifi|probe:0,1,50,48|assoc:0,1,50,221(0050f2,2),45,51,127,48,htcap:012c,htagg:1b,htmcs:000000ff|os:visiotv':
         ('', 'Vizio Smart TV', '2.4GHz'),
 
-    'wifi|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:wii':
+    'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:wii':
         ('BCM4318', 'Wii', '2.4GHz'),
 
     'wifi4|probe:0,1,50,45,3,221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff|os:wii':
         ('BCM43362', 'Wii-U', '2.4GHz'),
 
-    'wifi|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff|oui:withings':
+    'wifi4|probe:0,1,50,45,htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,48,50,221(0050f2,2),45,221(00904c,51),127,htcap:016e,htagg:03,htmcs:000000ff,extcap:0100008000000000|os:windows':
+        ('', 'Windows 802.11n PC', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff|oui:withings':
         ('', 'Withings Scale', '2.4GHz'),
 
-    'wifi|probe:0,1,3,45,50,127,htcap:010c,htagg:1b,htmcs:0000ffff|assoc:0,1,45,48,50,221(0050f2,2),htcap:010c,htagg:1b,htmcs:000000ff|oui:microsoft':
+    'wifi4|probe:0,1,3,45,50,127,htcap:010c,htagg:1b,htmcs:0000ffff,extcap:00|assoc:0,1,45,48,50,221(0050f2,2),htcap:010c,htagg:1b,htmcs:000000ff|oui:microsoft':
+        ('', 'Xbox', '5GHz'),
+    'wifi4|probe:0,1,3|assoc:0,1,48,33,36,221(0050f2,2),txpow:1405|oui:microsoft':
         ('', 'Xbox', '5GHz'),
     'wifi|probe:0,1,3,45,50,htcap:016e,htagg:03,htmcs:0000ffff|assoc:0,1,33,48,50,127,221(0050f2,2),45,htcap:012c,htagg:03,htmcs:0000ffff,extcap:00000000|oui:microsoft':
         ('', 'Xbox', '5GHz'),
+    'wifi4|probe:0,1,50|assoc:0,1,3,33,36,50,221(0050f2,2),45,221(00037f,1),221(00037f,4),48,htcap:104c,htagg:00,htmcs:000000ff,txpow:0f0f|oui:microsoft':
+        ('', 'Xbox', '2.4GHz'),
+    'wifi4|probe:0,1,50,48|assoc:0,1,3,33,36,50,221(0050f2,2),45,221(00037f,1),221(00037f,4),48,htcap:104c,htagg:00,htmcs:0000ffff,txpow:0f0f|oui:microsoft':
+        ('', 'Xbox', '2.4GHz'),
+    'wifi4|probe:0,1,45,50,htcap:058f,htagg:03,htmcs:0000ffff|assoc:0,1,33,36,48,221(0050f2,2),45,htcap:058f,htagg:03,htmcs:0000ffff,txpow:1208|oui:microsoft':
+        ('', 'Xbox', '2.4GHz'),
 
     'wifi4|probe:0,1,3,45,50,htcap:058f,htagg:03,htmcs:0000ffff|assoc:0,1,48,50,221(0050f2,2),45,htcap:058d,htagg:03,htmcs:0000ffff|oui:microsoft':
         ('Marvell_88W8897', 'Xbox One', '2.4GHz'),
     'wifi4|probe:0,1,45,50,htcap:058f,htagg:03,htmcs:0000ffff|assoc:0,1,48,50,221(0050f2,2),45,htcap:058d,htagg:03,htmcs:0000ffff|oui:microsoft':
         ('Marvell_88W8897', 'Xbox One', '2.4GHz'),
+    'wifi4|probe:0,1|assoc:0,1,50,45,127,221(000c43,0),221(0050f2,2),33,48,htcap:008d,htagg:02,htmcs:0000ffff,txpow:0805,extcap:0100000000000000|oui:microsoft':
+        ('', 'Xbox One', '2.4GHz'),
+    'wifi4|probe:0,1,50|assoc:0,1,50,45,127,221(000c43,0),221(0050f2,2),33,48,htcap:008d,htagg:02,htmcs:0000ffff,txpow:0805,extcap:0100000000000000|oui:microsoft':
+        ('', 'Xbox One', '2.4GHz'),
+
+    'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,33,48,70,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff,txpow:170d|oui:xiaomi':
+        ('', 'Xiaomi Redmi 3', '2.4GHz'),
 }
 
 
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 10af967..b88ccac 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -99,6 +99,13 @@
     # 'apply_security_config' must be called instead.
     _qcsapi('apply_security_config', 'wifi0')
 
+    for _ in xrange(10):
+      if _qcsapi('get_status', 'wifi0') == 'Up':
+        break
+      time.sleep(1)
+    else:
+      raise utils.BinWifiException('wpa_supplicant failed to connect')
+
   return True
 
 
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index 72f0333..b68b239 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -16,7 +16,9 @@
 def fake_qcsapi(*args):
   calls.append(list(args))
   if args[0] == 'is_startprod_done':
-    return '1\n' if ['startprod', 'wifi0'] in calls else '0\n'
+    return '1' if ['startprod', 'wifi0'] in calls else '0'
+  if args[0] == 'get_status':
+    return 'Up' if ['get_status', 'wifi0'] in calls else 'Down'
 
 
 bridge_interfaces = set()