Merge wifitv into master

Change-Id: Ia4edb3026ce2bb2a5beca30781a9d69786920113
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/Makefile b/Makefile
index 2f3aa95..f433d89 100644
--- a/Makefile
+++ b/Makefile
@@ -14,9 +14,11 @@
 BUILD_CRYPTDEV?=    # default off: needs libdevmapper
 BUILD_SIGNING?=     # default off: needs libgtest
 BUILD_JSONPOLL?=n
+BUILD_PRESTERASTATS?=n
 export BUILD_HNVRAM BUILD_SSDP BUILD_DNSSD BUILD_LOGUPLOAD \
 	BUILD_IBEACON BUILD_WAVEGUIDE BUILD_DVBUTILS BUILD_SYSMGR \
-	BUILD_STATUTILS BUILD_CRYPTDEV BUILD_SIGNING BUILD_JSONPOLL
+	BUILD_STATUTILS BUILD_CRYPTDEV BUILD_SIGNING BUILD_JSONPOLL \
+	BUILD_PRESTERASTATS
 
 # note: libgpio is not built here.  It's conditionally built
 # via buildroot/packages/google/google_platform/google_platform.mk
@@ -78,6 +80,10 @@
 DIRS+=conman
 endif
 
+ifeq ($(BUILD_PRESTERASTATS),y)
+DIRS+=presterastats
+endif
+
 PREFIX=/usr
 BINDIR=$(DESTDIR)$(PREFIX)/bin
 LIBDIR=$(DESTDIR)$(PREFIX)/lib
@@ -97,6 +103,15 @@
 install:
 	set -e; for d in $(DIRS); do $(MAKE) -C $$d install; done
 	$(MAKE) install-optionspy
+	mkdir -p $(BINDIR)
+	rm -fv $(BINDIR)/hnvram
+ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME), gfmn110)
+	ln -s /usr/bin/hnvram_wrapper $(BINDIR)/hnvram
+else ifeq ($(BR2_TARGET_GENERIC_PLATFORM_NAME), gflt110)
+	ln -s /usr/bin/hnvram_wrapper $(BINDIR)/hnvram
+else
+	ln -s /usr/bin/hnvram_binary $(BINDIR)/hnvram
+endif
 
 sysmgr/all: base/all libstacktrace/all libexperiments/all
 cmds/all: libstacktrace/all libexperiments/all
diff --git a/base/time.cc b/base/time.cc
index d1427c3..2672023 100644
--- a/base/time.cc
+++ b/base/time.cc
@@ -65,7 +65,7 @@
 }
 
 // Make sure someone calls it so that it gets initialized
-static uint32 ignore = StartTime();
+static uint32 __attribute__((used)) ignore = StartTime();
 
 uint32 TimeAfter(int32 elapsed) {
   ASSERT(elapsed >= 0);
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 87%
rename from cmds/asus_hosts.c
rename to cmds/asustax.cc
index 96ba81f..d4c665f 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,10 @@
 int main(int argc, char **argv)
 {
   int s, opt, i;
-  char *ifname = "br0";
+  const char *ifname = "br0";
+
+  setlinebuf(stdout);
+  alarm(30);
 
   while ((opt = getopt(argc, argv, "i:")) != -1) {
     switch (opt) {
@@ -213,7 +231,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..ee0ca24
--- /dev/null
+++ b/cmds/dhcpvendortax.c
@@ -0,0 +1,418 @@
+/*
+ * 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>
+#include <unistd.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];
+
+  setlinebuf(stdout);
+  alarm(30);
+  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/isoping.c b/cmds/isoping.c
index d54a6b8..a669883 100644
--- a/cmds/isoping.c
+++ b/cmds/isoping.c
@@ -210,6 +210,8 @@
   int sock = -1, want_timestamps = 0, quiet = 0, ttl = 2;
   double packets_per_sec = DEFAULT_PACKETS_PER_SEC, prints_per_sec = -1;
 
+  setvbuf(stdout, NULL, _IOLBF, 0);
+
   int c;
   while ((c = getopt(argc, argv, "f:r:t:qTh?")) >= 0) {
     switch (c) {
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..1c99553
--- /dev/null
+++ b/cmds/ssdptax.cc
@@ -0,0 +1,595 @@
+/*
+ * 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);
+  alarm(30);
+
+  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/cmds/test-mmap.sh b/cmds/test-mmap.sh
index 85b3004..32710d9 100755
--- a/cmds/test-mmap.sh
+++ b/cmds/test-mmap.sh
@@ -18,7 +18,14 @@
     $PREFIX ../host-mmap $ARGS < INPUT > GOT 2>&1
     status=$?
     if [ -n "$PREFIX" ]; then
-      sleep .5      # script mysteriously delays output
+      # /usr/bin/script mysteriously delays output (child writes cached?)
+      # sleep up to 4 seconds waiting for the output
+      for n in $(seq 1 40); do
+        if [ -s GOT ]; then
+          break
+        fi
+        sleep .1
+      done
     fi
     if [ "$status" != "$EXIT" ]; then
       echo "exit code: expected '$EXIT', got '$status'"
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index e9a2796..aa1b320 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -430,6 +430,7 @@
       # This interface is not connected to the WLAN, so scan for potential
       # routes to the ACS for provisioning.
       if (not self.acs() and
+          not getattr(wifi, 'last_successful_bss_info', None) and
           time.time() > wifi.last_wifi_scan_time + self._wifi_scan_period_s):
         logging.debug('Performing scan on %s.', wifi.name)
         self._wifi_scan(wifi)
@@ -463,7 +464,10 @@
         logging.debug('Unable to join WLAN on %s', wifi.name)
         self._status.connected_to_wlan = False
         if self.acs():
-          logging.debug('Connected to ACS')
+          logging.debug('Connected to ACS on %s', wifi.name)
+          wifi.last_successful_bss_info = getattr(wifi,
+                                                  'last_attempted_bss_info',
+                                                  None)
           now = time.time()
           if (self._wlan_configuration and
               hasattr(wifi, 'waiting_for_acs_since')):
@@ -502,9 +506,23 @@
     return result
 
   def _update_interfaces_and_routes(self):
+    """Touch each interface via update_routes."""
+
     self.bridge.update_routes()
     for wifi in self.wifi:
       wifi.update_routes()
+      # If wifi is connected to something that's not the WLAN, it must be a
+      # provisioning attempt, and in particular that attempt must be via
+      # last_attempted_bss_info.  If that is the same as the
+      # last_successful_bss_info (i.e. the last attempt was successful) and we
+      # aren't connected to the ACS after calling update_routes (which expires
+      # the connection status cache), then this BSS is no longer successful.
+      if (wifi.wpa_supplicant and
+          not self._connected_to_wlan(wifi) and
+          (getattr(wifi, 'last_successful_bss_info', None) ==
+           getattr(wifi, 'last_attempted_bss_info', None)) and
+          not wifi.acs()):
+        wifi.last_successful_bss_info = None
 
     # Make sure these get called semi-regularly so that exported status is up-
     # to-date.
@@ -663,19 +681,22 @@
     if not hasattr(wifi, 'cycler'):
       return False
 
-    bss_info = wifi.cycler.next()
+    last_successful_bss_info = getattr(wifi, 'last_successful_bss_info', None)
+    bss_info = last_successful_bss_info or wifi.cycler.next()
     if bss_info is not None:
+      logging.debug('Attempting to connect to SSID %s for provisioning',
+                    bss_info.ssid)
       self._status.trying_open = True
-      connected = subprocess.call(self.WIFI_SETCLIENT +
-                                  ['--ssid', bss_info.ssid,
-                                   '--band', wifi.bands[0],
-                                   '--bssid', bss_info.bssid]) == 0
+      connected = self._try_bssid(wifi, bss_info)
       if connected:
         self._status.connected_to_open = True
         now = time.time()
         wifi.waiting_for_acs_since = now
         wifi.complain_about_acs_at = now + 5
         logging.info('Attempting to provision via SSID %s', bss_info.ssid)
+      # If we can no longer connect to this, it's no longer successful.
+      elif bss_info == last_successful_bss_info:
+        wifi.last_successful_bss_info = None
       return connected
     else:
       # TODO(rofrankel):  There are probably more cases in which this should be
@@ -687,6 +708,13 @@
 
     return False
 
+  def _try_bssid(self, wifi, bss_info):
+    wifi.last_attempted_bss_info = bss_info
+    return subprocess.call(self.WIFI_SETCLIENT +
+                           ['--ssid', bss_info.ssid,
+                            '--band', wifi.bands[0],
+                            '--bssid', bss_info.bssid]) == 0
+
   def _connected_to_wlan(self, wifi):
     return (wifi.wpa_supplicant and
             any(config.client_up for band, config
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index 7b202b7..d96022e 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -291,7 +291,11 @@
 
     self.interface_with_scan_results = None
     self.scan_results_include_hidden = False
+    # Should we be able to connect to open network s2?
     self.can_connect_to_s2 = True
+    self.can_connect_to_s3 = True
+    # Will s2 fail rather than providing ACS access?
+    self.s2_fail = False
 
   def create_wifi_interfaces(self):
     super(ConnectionManager, self).create_wifi_interfaces()
@@ -304,16 +308,18 @@
     return ['echo'] + ['%s LOWER_UP' % ifc
                        for ifc in self.interfaces_already_up]
 
-  def _try_next_bssid(self, wifi):
-    if hasattr(wifi, 'cycler'):
-      bss_info = wifi.cycler.peek()
-      if bss_info:
-        self.last_provisioning_attempt = bss_info
-        # pylint: disable=protected-access
-        if wifi._wpa_control:
-          wifi._wpa_control.ssid_testonly = bss_info.ssid
+  def _update_access_point(self, wlan_configuration):
+    client_was_up = wlan_configuration.client_up
+    super(ConnectionManager, self)._update_access_point(wlan_configuration)
+    if wlan_configuration.access_point_up:
+      if client_was_up:
+        wifi = self.wifi_for_band(wlan_configuration.band)
+        wifi.add_terminating_event()
 
-    super(ConnectionManager, self)._try_next_bssid(wifi)
+  def _try_bssid(self, wifi, bss_info):
+    self.last_provisioning_attempt = bss_info
+
+    super(ConnectionManager, self)._try_bssid(wifi, bss_info)
 
     def connect(connection_check_result):
       # pylint: disable=protected-access
@@ -331,10 +337,10 @@
       return True
 
     if bss_info and bss_info.ssid == 's2' and self.can_connect_to_s2:
-      connect('succeed')
+      connect('fail' if self.s2_fail else 'succeed')
       return True
 
-    if bss_info and bss_info.ssid == 's3':
+    if bss_info and bss_info.ssid == 's3' and self.can_connect_to_s3:
       connect('restricted')
       return True
 
@@ -649,8 +655,11 @@
   for _ in range(3):
     c.run_once()
     wvtest.WVPASS(c.has_status_files([status.P.CONNECTED_TO_OPEN]))
-  wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's2')
-  wvtest.WVPASSEQ(c.last_provisioning_attempt.bssid, '01:23:45:67:89:ab')
+
+  last_bss_info = c.wifi_for_band(band).last_attempted_bss_info
+  wvtest.WVPASSEQ(last_bss_info.ssid, 's2')
+  wvtest.WVPASSEQ(last_bss_info.bssid, '01:23:45:67:89:ab')
+
   # Wait for the connection to be processed.
   c.run_once()
   wvtest.WVPASS(c.acs())
@@ -747,6 +756,76 @@
   wvtest.WVFAIL(c.wifi_for_band(band).current_route())
   wvtest.WVPASS(c.bridge.current_route())
 
+  # Now delete the config and bring down the bridge and make sure we reprovision
+  # via the last working BSS.
+  c.delete_wlan_config(band)
+  c.bridge.set_connection_check_result('fail')
+  scan_count_for_band = c.wifi_for_band(band).wifi_scan_counter
+  c.run_until_interface_update()
+  wvtest.WVFAIL(c.acs())
+  wvtest.WVFAIL(c.internet())
+  # s3 is not what the cycler would suggest trying next.
+  wvtest.WVPASSNE('s3', c.wifi_for_band(band).cycler.peek())
+  # Run only once, so that only one BSS can be tried.  It should be the s3 one,
+  # since that worked previously.
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  # Make sure we didn't scan on `band`.
+  wvtest.WVPASSEQ(scan_count_for_band, c.wifi_for_band(band).wifi_scan_counter)
+
+  # Now re-create the WLAN config, connect to the WLAN, and make sure that s3 is
+  # unset as last_successful_bss_info, since it is no longer available.
+  c.write_wlan_config(band, ssid, psk)
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  wvtest.WVPASS(c.internet())
+
+  c.can_connect_to_s3 = False
+  c.scan_results_include_hidden = False
+  c.delete_wlan_config(band)
+  c.run_once()
+  wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, None)
+
+  # Now do the same, except this time s2 is connected to but doesn't provide ACS
+  # access.  This requires first re-establishing s2 as successful, so there are
+  # four steps:
+  #
+  # 1) Connect to WLAN.
+  # 2) Disconnect, reprovision via s2 (establishing it as successful).
+  # 3) Reconnect to WLAN so that we can trigger re-provisioning by
+  #    disconnecting.
+  # 4) Connect to s2 but get no ACS access; see that last_successful_bss_info is
+  #    unset.
+  c.write_wlan_config(band, ssid, psk)
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  wvtest.WVPASS(c.internet())
+
+  c.delete_wlan_config(band)
+  c.run_once()
+  wvtest.WVFAIL(c.wifi_for_band(band).acs())
+
+  c.can_connect_to_s2 = True
+  # Give it time to try all BSSIDs.
+  for _ in range(3):
+    c.run_once()
+  s2_bss = iw.BssInfo('01:23:45:67:89:ab', 's2')
+  wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
+
+  c.s2_fail = True
+  c.write_wlan_config(band, ssid, psk)
+  c.run_once()
+  wvtest.WVPASS(c.acs())
+  wvtest.WVPASS(c.internet())
+
+  wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, s2_bss)
+  c.delete_wlan_config(band)
+  # Run once so that c will reconnect to s2.
+  c.run_once()
+  # Now run until it sees the lack of ACS access.
+  c.run_until_interface_update()
+  wvtest.WVPASSEQ(c.wifi_for_band(band).last_successful_bss_info, None)
+
 
 @wvtest.wvtest
 @connection_manager_test(WIFI_SHOW_OUTPUT_MARVELL8897)
diff --git a/conman/iw.py b/conman/iw.py
index b014220..f4932f1 100755
--- a/conman/iw.py
+++ b/conman/iw.py
@@ -37,7 +37,7 @@
 
   def __eq__(self, other):
     # pylint: disable=protected-access
-    return self.__attrs() == other.__attrs()
+    return isinstance(other, BssInfo) and self.__attrs() == other.__attrs()
 
   def __ne__(self, other):
     return not self.__eq__(other)
diff --git a/craftui/.gitignore b/craftui/.gitignore
new file mode 100644
index 0000000..1a5f7ef
--- /dev/null
+++ b/craftui/.gitignore
@@ -0,0 +1,4 @@
+*.swp
+.started
+.sim.extracted
+sim
diff --git a/craftui/HOW.restart_if_changed b/craftui/HOW.restart_if_changed
index 1f3343e..8c17154 100644
--- a/craftui/HOW.restart_if_changed
+++ b/craftui/HOW.restart_if_changed
@@ -6,10 +6,13 @@
 
 restart() {
   [ -n "$pid" ] && kill $pid
+  echo "######################################################################"
   echo "# starting craftui"
+  gpylint *.py
+  make test
   ./craftui &
   pid=$!
-  touch started
+  touch .started
 }
 
 onExit() {
@@ -25,7 +28,7 @@
     restart
     continue
   fi
-  f=$(find . -type f -newer started)
+  f=$(find . -name '*.swp' -prune -o -type f -newer .started -print)
   if [ -n "$f" ]; then
     restart
     continue
diff --git a/craftui/HOW.updatesim b/craftui/HOW.updatesim
new file mode 100644
index 0000000..8ae3f04
--- /dev/null
+++ b/craftui/HOW.updatesim
@@ -0,0 +1,26 @@
+#! /bin/sh
+
+ssh chimera '
+	rm -rf /tmp/sim;
+	mkdir -p /tmp/sim/proc && cat /proc/uptime > /tmp/sim/proc/uptime;
+	for n in /sys/class/net/*/statistics/*; do
+		mkdir -p /tmp/sim/$(dirname $n);
+		test ! -d $n && cat $n > /tmp/sim/$n;
+	done;
+	ip -o -d link > /tmp/sim/ip.link.txt;
+	ip -o addr > /tmp/sim/ip.addr.txt;
+	presterastats > /tmp/sim/presterastats.json;
+	'
+
+ssh chimera cd / "&&" tar czf - -C / \
+	config/settings \
+	etc/platform \
+	etc/serial \
+	etc/softwaredate \
+	etc/version \
+	tmp/glaukus \
+	tmp/serial \
+	tmp/platform \
+	tmp/gpio/ledstate \
+	tmp/sim \
+	> sim.tgz
diff --git a/craftui/Makefile b/craftui/Makefile
index 6b6757b..b946953 100644
--- a/craftui/Makefile
+++ b/craftui/Makefile
@@ -15,12 +15,18 @@
 install-libs:
 	@echo "No libs to install."
 
-test: lint
+.sim.extracted: sim.tgz
+	rm -rf sim
+	rsync -av sim-tools/ sim
+	tar xf sim.tgz -C sim
+	touch $@
+
+test: .sim.extracted lint
 	set -e; \
-	for pytest in $(wildcard *_test.py); do \
+	for n in $(wildcard ./*_test.*); do \
 		echo; \
-		echo "Testing $$pytest"; \
-		$(PYTHON) $$pytest; \
+		echo "Testing $$n"; \
+		$$n; \
 	done
 
 clean:
diff --git a/craftui/README b/craftui/README
new file mode 100644
index 0000000..a5a8cef
--- /dev/null
+++ b/craftui/README
@@ -0,0 +1,34 @@
+HOW.restart_if_changed
+	Use this script during development.  It runs the craftui in the
+	desktop, restarting on writes to the files, for quick development.
+	Use a browser pointed to localhost:8888 to see UI and reload when
+	it restarts.
+
+HOW.updatesim
+	This script creates the sim.tgz from a real device. Use it to
+	snapshot a new sim when tools like presterastats or glaukusd change
+	files formats.
+
+sim.tgz
+	This is a tar file screated by HOW.updatesim containing snapshots
+	of various files used in the craftui simulation for unit tests
+
+craftui_test.sh
+	starts the craftui and does simple sanity tests
+
+craftui
+	This is the wrapper that launches the craftui, used in both
+	testing and device runtime.  If tun on the desktop, it starts
+	craftui_fortesting.py instead of craftui.py, to catch exceptions.
+
+craftui_fortesting.py
+	wrapped to start the craftui with all exceptions caught.
+
+craftui.py
+	The craftui python script.
+
+sim-tools
+	tools used to simulate a real device
+
+www
+	the www tree with html and javascript for the UI
diff --git a/craftui/craftui b/craftui/craftui
index c78bc35..2f5e143 100755
--- a/craftui/craftui
+++ b/craftui/craftui
@@ -1,6 +1,6 @@
 #! /bin/sh
 
-bin=/bin
+pycode=/bin/craftui.py
 cw=/usr/catawampus
 devcw=../../../../vendor/google/catawampus
 localwww=./www
@@ -14,7 +14,8 @@
 if [ "$isdev" = 1 ]; then
   cw="$devcw"
   args="$args --port=8888 --sim=./sim"
-  bin=.
+  pycode=./craftui_fortesting.py
+  export PATH="$PWD/sim/bin:$PATH"
 fi
 
 # for debugging on the device, use the local (/tmp/www?) web tree
@@ -22,5 +23,10 @@
   args="$args --www=$localwww"
 fi
 
+# enable debugger
+if [ "$1" = -d ]; then
+  debug="-m pdb"
+fi
+
 export PYTHONPATH="$cw/tr/vendor/tornado:$PYTHONPATH"
-exec python $bin/craftui.py $args
+exec python -u $debug $pycode $args
diff --git a/craftui/craftui.py b/craftui/craftui.py
index 8e06e65..d25b40b 100755
--- a/craftui/craftui.py
+++ b/craftui/craftui.py
@@ -20,15 +20,287 @@
 import getopt
 import json
 import os
+import re
+import subprocess
 import sys
 import urllib2
 import tornado.ioloop
 import tornado.web
 
 
+class ConfigError(Exception):
+  """Configuration errors to pass to browser."""
+
+  def __init__(self, message):
+    super(ConfigError, self).__init__(message)
+
+
+class Validator(object):
+  """Validate the user value and convert to safe config value."""
+  pattern = r'^(.*)$'
+  example = 'any string'
+
+  def __init__(self):
+    self.Reset()
+
+  def Reset(self):
+    self.fields = ()
+    self.config = ''
+
+  def Validate(self, value):
+    self.Reset()
+    self.value = value
+    m = re.search(self.pattern, value)
+    if not m:
+      raise ConfigError('value "%s" does not match pattern "%s", eg: "%s"' %
+                        (value, self.pattern, self.example))
+    self.fields = m.groups()
+    self.config = self.fields[0]
+
+
+class VInt(Validator):
+  """Validate as integer."""
+  pattern = r'^(\d+)$'
+  example = '123'
+
+
+class VRange(VInt):
+  """Validate as integer in a range."""
+
+  def __init__(self, low, high):
+    super(VRange, self).__init__()
+    self.low = low
+    self.high = high
+
+  def Validate(self, value):
+    super(VRange, self).Validate(value)
+    self.CheckInRange(int(self.config), self.low, self.high)
+
+  @staticmethod
+  def CheckInRange(num, low, high):
+    if num < low or num > high:
+      raise ConfigError('number %d is out of range %d-%d' % (num, low, high))
+
+
+class VSlash(Validator):
+  """Validate as slash notation (eg 192.168.1.1/24)."""
+  pattern = r'^((\d+).(\d+).(\d+).(\d+)/(\d+))$'
+  example = '192.168.1.1/24'
+
+  def __init__(self):
+    super(VSlash, self).__init__()
+
+  def Validate(self, value):
+    super(VSlash, self).Validate(value)
+    mask = int(self.fields[5])
+    VRange.CheckInRange(mask, 0, 32)
+    for dotted_quad_part in self.fields[1:4]:
+      num = int(dotted_quad_part)
+      VRange.CheckInRange(num, 0, 255)
+
+
+class VVlan(VRange):
+  """Validate as vlan."""
+
+  def __init__(self):
+    super(VVlan, self).__init__(0, 4095)
+
+
+class VFreqHi(VRange):
+  """Validate as Hi E-Band frequency."""
+
+  def __init__(self):
+    super(VFreqHi, self).__init__(82000000, 85000000)
+
+
+class VFreqLo(VRange):
+  """Validate as Low E-Band frequency."""
+
+  def __init__(self):
+    super(VFreqLo, self).__init__(72000000, 75000000)
+
+
+class VPower(VRange):
+  """Validate as PA power level."""
+
+  def __init__(self):
+    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):
+  """Validate as member of dict."""
+  dict = {}
+
+  def Validate(self, value):
+    super(VDict, self).Validate(value)
+    if value not in self.dict:
+      keys = self.dict.keys()
+      raise ConfigError('value "%s" must be one of "%s"' % (value, keys))
+    self.config = self.dict[value]
+
+
+class VTx(VDict):
+  """Validate: tx/rx."""
+  dict = {'tx': 'tx', 'rx': 'rx'}
+
+
+class VTrueFalse(VDict):
+  """Validate as true or false."""
+  dict = {'true': 'true', 'false': 'false'}
+
+
+class Config(object):
+  """Configure the device after validation."""
+
+  def __init__(self, validator):
+    self.validator = validator()
+
+  def Validate(self, value):
+    self.validator.Validate(value)
+
+  def Configure(self):
+    raise Exception('override Config.Configure')
+
+  @staticmethod
+  def Run(command):
+    """Run a command."""
+    print 'running: %s' % command
+    try:
+      subprocess.check_output(command)
+    except subprocess.CalledProcessError as e:
+      print 'Run: ', str(e)
+      raise ConfigError('command failed with %d' % e.returncode)
+
+
+class PtpConfig(Config):
+  """Configure using ptp-config."""
+
+  def __init__(self, validator, key):
+    super(PtpConfig, self).__init__(validator)
+    self.key = key
+
+  def Configure(self):
+    Config.Run(['ptp-config', '-s', self.key, self.validator.config])
+
+
+class PtpActivate(Config):
+  """Configure using ptp-config."""
+
+  def __init__(self, validator, key):
+    super(PtpActivate, self).__init__(validator)
+    self.key = key
+
+  def Configure(self):
+    Config.Run(['ptp-config', '-i', self.key])
+
+
+class Glaukus(Config):
+  """Configure using glaukus json api."""
+
+  def __init__(self, validator, api, fmt):
+    super(Glaukus, self).__init__(validator)
+    self.api = api
+    self.fmt = fmt
+
+  def Configure(self):
+    """Handle a JSON request to glaukusd."""
+    url = 'http://localhost:8080' + self.api
+    payload = self.fmt % self.validator.config
+    print 'Glaukus: ', url, payload
+    try:
+      fd = urllib2.urlopen(url, payload)
+    except urllib2.URLError as ex:
+      print 'Connection to %s failed: %s' % (url, ex.reason)
+      raise ConfigError('failed to contact glaukus')
+    response = fd.read()
+    j = json.loads(response)
+    print j
+    if j['code'] != 'SUCCESS':
+      if j['message']:
+        raise ConfigError(j.message)
+      raise ConfigError('failed to configure glaukus')
+
+
+class Reboot(Config):
+  """Reboot."""
+
+  def Configure(self):
+    if self.validator.config == 'true':
+      Config.Run(['reboot'])
+
+
 class CraftUI(object):
   """A web server that configures and displays Chimera data."""
 
+  handlers = {
+      'craft_ipaddr': PtpConfig(VSlash, 'craft_ipaddr'),
+      'link_ipaddr': PtpConfig(VSlash, 'local_ipaddr'),
+      '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_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',
+      'sw0.ooband': 'ooband',
+      'sw0.inband': 'inband',
+      'sw0.peer': 'link',
+  }
+  ifvlan = [
+      'sw0.ooband',
+      'sw0.inband',
+      'sw0.peer'
+  ]
+  stats = [
+      'multicast',
+      'collisions',
+      'rx_bytes',
+      'rx_packets',
+      'rx_errors',
+      'rx_dropped',
+      'tx_bytes',
+      'tx_packets',
+      'tx_errors',
+      'tx_dropped'
+  ]
+
   def __init__(self, wwwroot, port, sim):
     """initialize."""
     self.wwwroot = wwwroot
@@ -37,7 +309,30 @@
     self.data = {}
     self.data['refreshCount'] = 0
 
+  def ApplyChanges(self, changes):
+    """Apply changes to system."""
+    if 'config' not in changes:
+      raise ConfigError('missing required config array')
+    conf = changes['config']
+    try:
+      # dry run to validate all
+      for c in conf:
+        for k, v in c.items():
+          if k not in self.handlers:
+            raise ConfigError('unknown key "%s"' % k)
+          h = self.handlers[k]
+          h.Validate(v)
+      # do it again for real
+      for c in conf:
+        for k, v in c.items():
+          h = self.handlers[k]
+          h.Validate(v)
+          h.Configure()
+    except ConfigError as e:
+      raise ConfigError('key "%s": %s' % (k, e))
+
   def ReadFile(self, filepath):
+    """cat file."""
     text = ''
     try:
       with open(filepath) as fd:
@@ -55,12 +350,75 @@
     js = '{"platform":' + pj + ',"modem":' + mj + ',"radio":' + rj + '}'
     return js
 
+  def AddIpAddr(self, data):
+    """Run ip addr and parse results."""
+    ipaddr = ''
+    try:
+      ipaddr = subprocess.check_output(['ip', '-o', 'addr'])
+    except subprocess.CalledProcessError as e:
+      print 'warning: "ip -o addr" failed: ', e
+    v = {}
+    for line in ipaddr.splitlines():
+      f = line.split()
+      ifname = re.sub(r'[@:].*', '', f[1])
+      m = re.search(r'scope (global|link)', line)
+      scope = m.group(1) if m else 'noscope'
+      v[ifname + ':' + f[2] + ':' + scope] = f[3]
+      m = re.search(r'link/ether (\S+)', line)
+      if m:
+        mac = m.group(1)
+        v[ifname + ':' + 'mac'] = mac
+    for ifname, uiname in self.ifmap.items():
+      mac = v.get(ifname + ':mac')
+      data[uiname + '_mac'] = mac if mac else 'unknown'
+      for inet in ('inet', 'inet6'):
+        kglobal = ifname + ':' + inet + ':' + 'global'
+        vdata = v.get(kglobal, 'unknown')
+        kdata = 'active_' + uiname + '_' + inet
+        data[kdata] = vdata
+
+  def AddInterfaceStats(self, data):
+    """Get if stats."""
+    for ifname, uiname in self.ifmap.items():
+      d = self.sim + '/sys/class/net/' + ifname + '/statistics/'
+      for stat in self.stats:
+        k = uiname + '_' + stat
+        data[k] = self.ReadFile(d + stat)
+
+  def AddSwitchStats(self, data):
+    """Run presterastats and send json."""
+    stats = ''
+    try:
+      stats = subprocess.check_output(['presterastats'])
+    except subprocess.CalledProcessError as e:
+      print 'warning: "presterastats" failed: ', e
+    data['switch'] = json.loads(stats)['port-interface-statistics']
+
+  def AddVlans(self, data):
+    """Run ip -d link and parse results for vlans."""
+    iplink = ''
+    try:
+      iplink = subprocess.check_output(['ip', '-o', '-d', 'link'])
+    except subprocess.CalledProcessError as e:
+      print 'warning: "ip -o -d link" failed: ', e
+    v = {}
+    for line in iplink.splitlines():
+      m = re.search(r'^\d+: ([\w\.]+)@\w+: .* vlan id (\w+)', line)
+      if m:
+        v[m.group(1)] = m.group(2)
+    for ifname in self.ifvlan:
+      uiname = self.ifmap[ifname]
+      vdata = v.get(ifname, 'unknown')
+      kdata = 'active_' + uiname + '_vlan'
+      data[kdata] = vdata
+
   def GetPlatformData(self):
     """Get platform data, return a json string."""
     data = self.data
     sim = self.sim
 
     if data['refreshCount'] == 0:
+      data['serialno'] = self.ReadFile(sim + '/etc/serial')
       data['version'] = self.ReadFile(sim + '/etc/version')
       data['platform'] = self.ReadFile(sim + '/etc/platform')
       data['softwaredate'] = self.ReadFile(sim + '/etc/softwaredate')
@@ -69,10 +427,15 @@
     data['ledstate'] = self.ReadFile(sim + '/tmp/gpio/ledstate')
     cs = '/config/settings/'
     data['craft_ipaddr'] = self.ReadFile(sim + cs + 'craft_ipaddr')
-    data['local_ipaddr'] = self.ReadFile(sim + cs + 'local_ipaddr')
+    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_peer'] = self.ReadFile(sim + cs + 'vlan_peer')
+    data['vlan_ooband'] = self.ReadFile(sim + cs + 'vlan_ooband')
+    data['vlan_link'] = self.ReadFile(sim + cs + 'vlan_peer')
+    self.AddIpAddr(data)
+    self.AddInterfaceStats(data)
+    self.AddSwitchStats(data)
+    self.AddVlans(data)
     return json.dumps(data)
 
   def GetModemData(self):
@@ -104,12 +467,20 @@
     return response
 
   class MainHandler(tornado.web.RequestHandler):
-    """Displays the UI."""
+    """Displays the Craft UI."""
 
     def get(self):
       ui = self.settings['ui']
       print 'GET craft HTML page'
-      self.render(ui.wwwroot + '/index.thtml', peerurl='http://TODO')
+      self.render(ui.wwwroot + '/index.thtml', peerurl='/?peer=1')
+
+  class ConfigHandler(tornado.web.RequestHandler):
+    """Displays the Config page."""
+
+    def get(self):
+      ui = self.settings['ui']
+      print 'GET config HTML page'
+      self.render(ui.wwwroot + '/config.thtml', peerurl='/config/?peer=1')
 
   class RestartHandler(tornado.web.RequestHandler):
     """Restart the box."""
@@ -135,12 +506,40 @@
       self.write(jsonstring)
       self.finish()
 
+    def post(self):
+      print 'POST JSON data for craft page'
+      request = self.request.body
+      result = {}
+      result['error'] = 0
+      result['errorstring'] = ''
+      try:
+        try:
+          json_args = json.loads(request)
+          request = json.dumps(json_args)
+        except ValueError as e:
+          print e
+          raise ConfigError('json format error')
+        ui = self.settings['ui']
+        ui.ApplyChanges(json_args)
+      except ConfigError as e:
+        print e
+        result['error'] += 1
+        result['errorstring'] += str(e)
+
+      response = json.dumps(result)
+      print 'request: ', request
+      print 'response: ', response
+      self.set_header('Content-Type', 'application/json')
+      self.write(response)
+      self.finish()
+
   def RunUI(self):
     """Create the web server and run forever."""
     handlers = [
-        (r'/', CraftUI.MainHandler),
-        (r'/content.json', CraftUI.JsonHandler),
-        (r'/restart', CraftUI.RestartHandler),
+        (r'/', self.MainHandler),
+        (r'/config', self.ConfigHandler),
+        (r'/content.json', self.JsonHandler),
+        (r'/restart', self.RestartHandler),
         (r'/static/([^/]*)$', tornado.web.StaticFileHandler,
          {'path': self.wwwroot + '/static'}),
     ]
diff --git a/craftui/craftui_test.py b/craftui/craftui_fortesting.py
similarity index 75%
rename from craftui/craftui_test.py
rename to craftui/craftui_fortesting.py
index ae20df7..82bf147 100644
--- a/craftui/craftui_test.py
+++ b/craftui/craftui_fortesting.py
@@ -17,6 +17,14 @@
 
 __author__ = 'edjames@google.com (Ed James)'
 
+import traceback
+import craftui
 
 if __name__ == '__main__':
-  print 'TODO(edjames)'
+  try:
+    craftui.main()
+  # pylint: disable=broad-except
+  except Exception as e:
+    traceback.print_exc()
+    # exit cleanly to close the socket so next listen doesn't fail with in-use
+    exit(1)
diff --git a/craftui/craftui_test.sh b/craftui/craftui_test.sh
new file mode 100755
index 0000000..9147945
--- /dev/null
+++ b/craftui/craftui_test.sh
@@ -0,0 +1,107 @@
+#! /bin/sh
+
+# some unit tests for the craft UI
+
+# save stdout to 3, dup stdout to a file
+log=.testlog.$$
+exec 3>&1
+exec >$log 2>&1
+
+failcount=0
+passcount=0
+
+fail() {
+	echo "FAIL: $*" >&3
+	echo "FAIL: $*"
+	((failcount++))
+}
+
+pass() {
+	echo "PASS: $*" >&3
+	echo "PASS: $*"
+	((passcount++))
+}
+
+testname() {
+	test="$*"
+	echo "---------------------------------------------------------"
+	echo "starting test $test"
+}
+
+check_success() {
+	status=$?
+	echo "check_success: last return code was $status, wanted 0"
+	if [ $status = 0 ]; then
+		pass $test
+	else
+		fail $test
+	fi
+}
+
+check_failure() {
+	status=$?
+	echo "check_failure: last return code was $status, wanted not-0"
+	if [ $status != 0 ]; then
+		pass $test
+	else
+		fail $test
+	fi
+}
+
+onexit() {
+	testname "process running at exit"
+	kill -0 $pid
+	check_success
+
+	# cleanup
+	kill -9 $pid
+
+	exec 1>&3
+	echo "SUMMARY: pass=$passcount fail=$failcount"
+	if [ $failcount -eq 0 ]; then
+		echo "SUCCESS: $passcount tests passed."
+	else
+		echo "FAILURE: $failcount tests failed."
+		echo "details follow:"
+		cat $log
+	fi
+	rm -f $log
+
+	exit $failcount
+}
+
+trap onexit 0 1 2 3
+
+testname "server not running"
+curl -s http://localhost:8888/
+check_failure
+
+./craftui > /tmp/LOG 2>&1 &
+pid=$!
+
+testname "process running"
+kill -0 $pid
+check_success
+
+sleep 1
+
+testname true
+true
+check_success
+
+testname false
+false
+check_failure
+
+testname "main web page"
+curl -s http://localhost:8888/ > /dev/null
+check_success
+
+testname "404 not found"
+curl -s http://localhost:8888/notexist | grep '404: Not Found'
+check_success
+
+testname "json"
+curl -s http://localhost:8888/content.json | grep '"platform": "GFCH100"'
+check_success
+
diff --git a/craftui/sim-tools/bin/ip b/craftui/sim-tools/bin/ip
new file mode 100755
index 0000000..82bcaae
--- /dev/null
+++ b/craftui/sim-tools/bin/ip
@@ -0,0 +1,9 @@
+#! /bin/sh
+
+dir=$(dirname $0)
+
+if [ "$3" = link ]; then
+  cat $dir/../tmp/sim/ip.link.txt
+else
+  cat $dir/../tmp/sim/ip.addr.txt
+fi
diff --git a/craftui/sim-tools/bin/presterastats b/craftui/sim-tools/bin/presterastats
new file mode 100755
index 0000000..ab71721
--- /dev/null
+++ b/craftui/sim-tools/bin/presterastats
@@ -0,0 +1,4 @@
+#! /bin/sh
+
+dir=$(dirname $0)
+cat $dir/../tmp/sim/presterastats.json
diff --git a/craftui/sim-tools/bin/ptp-config b/craftui/sim-tools/bin/ptp-config
new file mode 100755
index 0000000..4031019
--- /dev/null
+++ b/craftui/sim-tools/bin/ptp-config
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+echo "TODO: $0 $*"
diff --git a/craftui/sim-tools/bin/reboot b/craftui/sim-tools/bin/reboot
new file mode 100755
index 0000000..60ee746
--- /dev/null
+++ b/craftui/sim-tools/bin/reboot
@@ -0,0 +1,3 @@
+#! /bin/sh
+
+echo running: $0 $*
diff --git a/craftui/sim-tools/proc b/craftui/sim-tools/proc
new file mode 120000
index 0000000..2b2cb87
--- /dev/null
+++ b/craftui/sim-tools/proc
@@ -0,0 +1 @@
+tmp/sim/proc
\ No newline at end of file
diff --git a/craftui/sim-tools/sys b/craftui/sim-tools/sys
new file mode 120000
index 0000000..cdcfee0
--- /dev/null
+++ b/craftui/sim-tools/sys
@@ -0,0 +1 @@
+tmp/sim/sys
\ No newline at end of file
diff --git a/craftui/sim.tgz b/craftui/sim.tgz
new file mode 100644
index 0000000..136253f
--- /dev/null
+++ b/craftui/sim.tgz
Binary files differ
diff --git a/craftui/sim/config/settings/craft_ipaddr b/craftui/sim/config/settings/craft_ipaddr
deleted file mode 100644
index a1f2875..0000000
--- a/craftui/sim/config/settings/craft_ipaddr
+++ /dev/null
@@ -1 +0,0 @@
-192.168.5.99/24
diff --git a/craftui/sim/config/settings/local_ipaddr b/craftui/sim/config/settings/local_ipaddr
deleted file mode 100644
index 6f5d511..0000000
--- a/craftui/sim/config/settings/local_ipaddr
+++ /dev/null
@@ -1 +0,0 @@
-192.168.2.1/24
diff --git a/craftui/sim/config/settings/peer_ipaddr b/craftui/sim/config/settings/peer_ipaddr
deleted file mode 100644
index 85be23f..0000000
--- a/craftui/sim/config/settings/peer_ipaddr
+++ /dev/null
@@ -1 +0,0 @@
-192.168.2.2/24
diff --git a/craftui/sim/config/settings/vlan_inband b/craftui/sim/config/settings/vlan_inband
deleted file mode 100644
index 6b61c08..0000000
--- a/craftui/sim/config/settings/vlan_inband
+++ /dev/null
@@ -1 +0,0 @@
-4090
diff --git a/craftui/sim/config/settings/vlan_peer b/craftui/sim/config/settings/vlan_peer
deleted file mode 100644
index 8bd1af1..0000000
--- a/craftui/sim/config/settings/vlan_peer
+++ /dev/null
@@ -1 +0,0 @@
-2000
diff --git a/craftui/sim/etc/platform b/craftui/sim/etc/platform
deleted file mode 100644
index 91bb929..0000000
--- a/craftui/sim/etc/platform
+++ /dev/null
@@ -1 +0,0 @@
-GFCH100
diff --git a/craftui/sim/etc/softwaredate b/craftui/sim/etc/softwaredate
deleted file mode 100644
index 8bdcf18..0000000
--- a/craftui/sim/etc/softwaredate
+++ /dev/null
@@ -1,2 +0,0 @@
-1457565485
-2016-03-09 15:18:05 -0800
diff --git a/craftui/sim/etc/version b/craftui/sim/etc/version
deleted file mode 100644
index 67debcc..0000000
--- a/craftui/sim/etc/version
+++ /dev/null
@@ -1 +0,0 @@
-gfch100-47-pre5-44951-g1e8917f-ed
diff --git a/craftui/sim/proc/uptime b/craftui/sim/proc/uptime
deleted file mode 100644
index 47bea91..0000000
--- a/craftui/sim/proc/uptime
+++ /dev/null
@@ -1 +0,0 @@
-86648.37 85251.17
diff --git a/craftui/sim/tmp/glaukus/modem.json b/craftui/sim/tmp/glaukus/modem.json
deleted file mode 100644
index ba7f021..0000000
--- a/craftui/sim/tmp/glaukus/modem.json
+++ /dev/null
@@ -1 +0,0 @@
-{"firmware":"\/etc\/glaukus\/firmware\/bcm85100mc_1007128.fw","network":{"rxCounters":{"broadcast":15449,"bytes":5662990,"crcErrors":0,"frames":17417,"frames1024_1518":0,"frames128_255":1968,"frames256_511":15449,"frames512_1023":0,"frames64":0,"frames65_127":0,"framesJumbo":0,"framesUndersized":0,"multicast":1968,"unicast":0},"status":0,"statusStr":"UP","txCounters":{"broadcast":0,"bytes":0,"crcErrors":0,"frames":0,"frames1024_1518":0,"frames128_255":0,"frames256_511":0,"frames512_1023":0,"frames64":0,"frames65_127":0,"framesJumbo":0,"framesUndersized":0,"multicast":0,"unicast":0}},"profile":"\/etc\/glaukus\/profiles\/C01_1007128_1500_48_C1_260X300_3c3f2.bin","status":{"absoluteMse":-32768,"acmEngineRxSensorsEnabled":true,"acmEngineTxSwitchEnabled":true,"acquireStatus":0,"acquireStatusStr":"Acquire in progress","carrierOffset":-3749990,"debugIndications":2,"externalAgc":122,"internalAgc":560,"lastAcquireError":3,"lastAcquireErrorStr":"Acquisition failed at frequency sweep.","normalizedMse":-32693,"radialMse":-32663,"resPhNoiseVal":0,"rxAcmProfile":0,"rxSymbolRate":374830203,"txAcmProfile":6,"txSymbolRate":375000000},"temperature":50.19281005859375,"transmitter":{"dcLeakageI":0,"dcLeakageQ":0,"mode":0,"modeStr":"NORMAL","sweepTime":0,"toneFreq":0,"toneSecFreq":0},"version":{"build":128,"chipType":"BCM85100IFSBG","major":100,"minor":7}}
\ No newline at end of file
diff --git a/craftui/sim/tmp/glaukus/radio.json b/craftui/sim/tmp/glaukus/radio.json
deleted file mode 100644
index 4946225..0000000
--- a/craftui/sim/tmp/glaukus/radio.json
+++ /dev/null
@@ -1 +0,0 @@
-{"heaterEnabled":false,"hiTransceiver":{"epot":{"control":"auto","driver":0,"lna":0,"pa":0},"icModel":"BGT80","mode":"tx","pll":{"frequency":85500000,"lockCounts":0,"locked":true},"temp":1481},"loTransceiver":{"epot":{"control":"auto","driver":0,"lna":0,"pa":0},"icModel":"BGT70","mode":"rx","pll":{"frequency":75500000,"lockCounts":0,"locked":true},"temp":1671},"paLnaPowerEnabled":true,"paLnaPowerStatus":"normal","rx":{"agcDigitalGain":42,"agcDigitalGainIndex":3,"lnaCurrentMeas":0,"lnaCurrentSet":0,"rssi":1342},"transceiversPowerEnabled":true,"tx":{"dcI":63,"dcQ":63,"driverCurrentMeas":0,"driverCurrentSet":0,"paCurrentMeas":1428,"paCurrentSet":0,"paTemp":4084,"txPowerControl":"auto","txPowerMeas":2895,"txPowerSet":0,"vgaGain":14},"version":{"hardware":{"major":1,"minor":1,"type":"Chimera V1 000001"},"software":{"build":0,"major":0,"minor":1}}}
\ No newline at end of file
diff --git a/craftui/sim/tmp/gpio/ledstate b/craftui/sim/tmp/gpio/ledstate
deleted file mode 100644
index 80e3de0..0000000
--- a/craftui/sim/tmp/gpio/ledstate
+++ /dev/null
@@ -1 +0,0 @@
-IPV6ACQUIRED
diff --git a/craftui/www/config.thtml b/craftui/www/config.thtml
new file mode 100644
index 0000000..ddca423
--- /dev/null
+++ b/craftui/www/config.thtml
@@ -0,0 +1,206 @@
+<html>
+<head>
+  <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+  <meta content="utf-8" http-equiv="encoding">
+  <script src="static/jquery-2.1.4.min.js"></script>
+  <link rel="stylesheet" type="text/css" href="static/craft.css">
+  <link rel=icon href=static/favicon.ico>
+  <link rel=stylesheet href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700&amp;lang=en">
+  <link rel=stylesheet href=static/default.css>
+</head>
+<body>
+  <header>
+    <section>
+      <h1><img src=static/logo.png alt="Google Fiber"></h1>
+      <nav>
+        <ul>
+          <li ><a href=/>GFCH100</a></li>
+          <li class=active><a href=/config>Configuration</a></li>
+          <li ><a href={{ peerurl }} target=_blank>Peer</a></li>
+        </ul>
+      </nav>
+    </section>
+  </header>
+  <br>
+  <div class="tabs">
+    <div class="tab">
+      <input type="radio" id="tab-1" name="tab-group-1" checked>
+      <label for="tab-1">Site Configuration</label>
+      <div class="content">
+	<b>Platform Parameters:</b>
+	<table>
+	  <tr>
+	    <td align=center><b>Parameter
+	    <td align=center><b>Active Value
+	    <td align=center><b>Last Configured
+	    <td align=center><b>Configure
+	    <td align=center><b>Status
+
+	  <tr>
+	    <td><b>Craft IP Address
+	    <td align=right><span id="platform/active_craft_inet">...</span>
+	    <td align=right>
+	      <span id="platform/craft_ipaddr">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('craft_ipaddr', 1)">
+	    <td>
+	      <input id=craft_ipaddr type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('craft_ipaddr')">
+	    <td>
+	      <span id=craft_ipaddr_result>...</span>
+
+	  <tr>
+	    <td><b>Link IP Address
+	    <td align=right><span id="platform/active_link_inet">...</span>
+	    <td align=right>
+	      <span id="platform/link_ipaddr">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('link_ipaddr', 1)">
+	    <td>
+	      <input id=link_ipaddr type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('link_ipaddr')">
+	    <td>
+	      <span id=link_ipaddr_result>...</span>
+
+	  <tr>
+	    <td><b>Peer IP Address
+	    <td align=right>See Peer
+	    <td align=right>
+	      <span id="platform/peer_ipaddr">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('peer_ipaddr', 1)">
+	    <td>
+	      <input id=peer_ipaddr type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('peer_ipaddr')">
+	    <td>
+	      <span id=peer_ipaddr_result>...</span>
+
+	  <tr>
+	    <td><b>In-band Management VLAN
+	    <td align=right><span id="platform/active_inband_vlan">...</span>
+	    <td align=right>
+	      <span id="platform/vlan_inband">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('vlan_inband', 1)">
+	    <td>
+	      <input id=vlan_inband type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('vlan_inband')">
+	    <td>
+	      <span id=vlan_inband_result>...</span>
+
+	  <tr>
+	    <td><b>Out-of-band Management VLAN
+	    <td align=right><span id="platform/active_ooband_vlan">...</span>
+	    <td align=right>
+	      <span id="platform/vlan_ooband">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('vlan_ooband', 1)">
+	    <td>
+	      <input id=vlan_ooband type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('vlan_ooband')">
+	    <td>
+	      <span id=vlan_ooband_result>...</span>
+
+	  <tr>
+	    <td><b>Link VLAN (to peer)
+	    <td align=right><span id="platform/active_link_vlan">...</span>
+	    <td align=right>
+	      <span id="platform/vlan_link">...</span>
+	      <input type=submit value="Apply Now" onclick="CraftUI.config('vlan_peer', 1)">
+	    <td>
+	      <input id=vlan_peer type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('vlan_peer')">
+	    <td>
+	      <span id=vlan_peer_result>...</span>
+
+	</table>
+	<b>Radio Parameters:</b>
+	<table>
+	  <tr>
+	    <td align=center><b>Parameter
+	    <td align=center><b>Active Value
+	    <td align=center><b>Configure and Apply
+	    <td align=center><b>Status
+
+	  <tr>
+	    <td><b>High Frequency
+	    <td align=right><span id="radio/hiTransceiver/pll/frequency">...</span>
+	    <td>
+	      <input id=freq_hi type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('freq_hi')">
+	    <td>
+	      <span id=freq_hi_result>...</span>
+
+	  <tr>
+	    <td><b>Low Frequency
+	    <td align=right><span id="radio/loTransceiver/pll/frequency">...</span>
+	    <td>
+	      <input id=freq_lo type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('freq_lo')">
+	    <td>
+	      <span id=freq_lo_result>...</span>
+
+	  <tr>
+	    <td><b>High Frequency Mode
+	    <td align=right><span id="radio/hiTransceiver/mode">...</span>
+	    <td>
+	      <input id=mode_hi type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('mode_hi')">
+	    <td>
+	      <span id=mode_hi_result>...</span>
+
+	  <tr>
+	    <td><b>Transmit Power (dB x 100)
+	    <td align=right><span id="radio/tx/paPowerSet">...</span>
+	    <td>
+	      <input id=tx_powerlevel type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('tx_powerlevel')">
+	    <td>
+	      <span id=tx_powerlevel_result>...</span>
+
+	  <tr>
+	    <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=palna_on type=text value="">
+	      <input type=submit value=Configure onclick="CraftUI.config('palna_on')">
+	    <td>
+	      <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>
+    </div>
+    <div class="tab">
+      <input type="radio" id="tab-2" name="tab-group-1">
+      <label for="tab-2">Debug</label>
+      <div class="content">
+        <b>refreshCount:</b><span class="values" id="platform/refreshCount">...</span><br>
+        <b>unhandled xml:</b><span class="values" id="unhandled"></span>
+      </div>
+    </div>
+  </div>
+  <script src="static/craft.js"></script>
+</body>
+</html>
diff --git a/craftui/www/index.thtml b/craftui/www/index.thtml
index 4388f77..20bba69 100644
--- a/craftui/www/index.thtml
+++ b/craftui/www/index.thtml
@@ -11,11 +11,11 @@
 <body>
   <header>
     <section>
-      <h1><a href=https://fiber.google.com/myfiber/><img src=static/logo.png alt="Google Fiber"></a></h1>
+      <h1><img src=static/logo.png alt="Google Fiber"></h1>
       <nav>
         <ul>
           <li class=active><a href=/>GFCH100</a></li>
-          <li ><a href=craft/config/>Configuration</a></li>
+          <li ><a href=/config>Configuration</a></li>
           <li ><a href={{ peerurl }} target=_blank>Peer</a></li>
         </ul>
       </nav>
@@ -27,6 +27,7 @@
       <input type="radio" id="tab-1" name="tab-group-1" checked>
       <label for="tab-1">Platform</label>
       <div class="content">
+        <b>Serial Number:</b><span class="values" id="platform/serialno">...</span><br>
         <b>Platform:</b><span class="values" id="platform/platform">...</span><br>
         <b>Software Version:</b><span class="values" id="platform/version">...</span><br>
         <b>Software Date:</b><span class="values" id="platform/softwaredate">...</span><br>
@@ -38,68 +39,286 @@
       <input type="radio" id="tab-2" name="tab-group-1">
       <label for="tab-2">Network</label>
       <div class="content">
-        <b>Craft Port IP Address:</b><span class="values" id="platform/craft_ipaddr">...</span><br>
-        <b>Local IP Address:</b><span class="values" id="platform/local_ipaddr">...</span><br>
-        <b>Peer IP Address:</b><span class="values" id="platform/peer_ipaddr">...</span><br>
-        <b>Inband Vlan:</b><span class="values" id="platform/vlan_inband">...</span><br>
-        <b>Peer Vlan:</b><span class="values" id="platform/vlan_peer">...</span><br>
+        <b>IP Addresses:</b>
+	<table>
+	  <tr>
+            <td align=center><b>Port</b></td>
+            <td align=center><b>MAC</b></td>
+            <td align=center><b>VLAN</b></td>
+            <td align=center><b>IPv4</b></td>
+            <td align=center><b>IPv6</b></td></tr>
+	  <tr>
+            <td><b>Craft</b></td>
+	    <td align=right><span id="platform/craft_mac">...</span></td>
+	    <td align=right>-</td>
+	    <td align=right><span id="platform/active_craft_inet">...</span></td>
+	    <td align=right><span id="platform/active_craft_inet6">...</span></td></tr>
+	  <tr>
+            <td><b>In-Band</b></td>
+	    <td align=right><span id="platform/bridge_mac">...</span></td>
+	    <td align=right><span id="platform/active_inband_vlan">...</span></td>
+	    <td align=right><span id="platform/active_bridge_inet">...</span></td>
+	    <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/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>
+	    <td align=right><span id="platform/active_link_vlan">...</span></td>
+	    <td align=right><span id="platform/active_link_inet">...</span></td>
+	    <td align=right><span id="platform/active_link_inet6">...</span></td></tr>
+	</table>
+        <b>Packet Counters:</b>
         <table>
           <tr>
             <td><b></b></td>
-            <td colspan=3><b>received</b></td>
-            <td colspan=3><b>transmitted</b></td></tr>
+            <td colspan=5 align=center><b>received</b></td>
+            <td colspan=5 align=center><b>transmitted</b></td>
+            <td colspan=9 align=center><b>errors</b></td></tr>
           <tr>
-            <td><b>interface</b></td>
-            <td><b>bytes (UC BC MC)</b></td>
-            <td><b>frames (<64 <128 <256 <512 <1024 jumbo)</b></td>
-            <td><b>errors</b></td>
-            <td><b>bytes (UC BC MC)</b></td>
-            <td><b>frames</b></td>
-            <td><b>errors</b></td></tr>
+            <td align=center><b>interface</b></td>
+
+            <td align=center><b>bytes</b></td>
+            <td align=center><b>frames</b></td>
+            <td align=center><b>multicast</b></td>
+            <td align=center><b>broadcast</b></td>
+            <td align=center><b>unicast</b></td>
+
+            <td align=center><b>bytes</b></td>
+            <td align=center><b>frames</b></td>
+            <td align=center><b>multicast</b></td>
+            <td align=center><b>broadcast</b></td>
+            <td align=center><b>unicast</b></td>
+
+            <td align=center><b>rx errors</b></td>
+            <td align=center><b>rx dropped</b></td>
+            <td align=center><b>rx CRC</b></td>
+            <td align=center><b>rx Undersize</b></td>
+            <td align=center><b>tx errors</b></td>
+            <td align=center><b>tx dropped</b></td>
+            <td align=center><b>tx CRC</b></td>
+            <td align=center><b>tx Undersize</b></td>
+            <td align=center><b>collisions</b></td>
           <tr>
-            <td>modem (from/to switch)</td>
-            <td>
-              <span id="modem/network/rxCounters/bytes">...</span>
-              (<span id="modem/network/rxCounters/unicast">...</span>
-              <span id="modem/network/rxCounters/broadcast">...</span>
-              <span id="modem/network/rxCounters/multicast">...</span>)</td>
-            <td>
-              <span id="modem/network/rxCounters/frames">...</span>
-              (<span id="modem/network/rxCounters/frames64">...</span>
-              <span id="modem/network/rxCounters/frames65_127">...</span>
-              <span id="modem/network/rxCounters/frames128_255">...</span>
-              <span id="modem/network/rxCounters/frames256_511">...</span>
-              <span id="modem/network/rxCounters/frames512_1023">...</span>
-              <span id="modem/network/rxCounters/frames1024_1518">...</span>
-              <span id="modem/network/rxCounters/framesJumbo">...</span>)</td>
-            <td>
-              CRC: <span id="modem/network/rxCounters/crcErrors">...</span>
-              Undersize: <span id="modem/network/rxCounters/framesUndersized">...</span></td>
-            <td>
-              <span id="modem/network/txCounters/bytes">...</span>
-              (<span id="modem/network/txCounters/unicast">...</span>
-              <span id="modem/network/txCounters/broadcast">...</span>
-              <span id="modem/network/txCounters/multicast">...</span>)</td>
-            <td>
-              <span id="modem/network/txCounters/frames">...</span>
-              (<span id="modem/network/txCounters/frames64">...</span>
-              <span id="modem/network/txCounters/frames65_127">...</span>
-              <span id="modem/network/txCounters/frames128_255">...</span>
-              <span id="modem/network/txCounters/frames256_511">...</span>
-              <span id="modem/network/txCounters/frames512_1023">...</span>
-              <span id="modem/network/txCounters/frames1024_1518">...</span>
-              <span id="modem/network/txCounters/framesJumbo">...</span>)</td>
-            <td>
-              CRC: <span id="modem/network/txCounters/crcErrors">...</span>
-              Undersize: <span id="modem/network/txCounters/framesUndersized">...</span></td></tr>
+            <td><b>Modem (from/to switch)<b></td>
+            <td align=right><span id="modem/network/rxCounters/bytes">...</span></td>
+            <td align=right><span id="modem/network/rxCounters/frames">...</span></td>
+	    <td align=right><span id="modem/network/rxCounters/multicast">...</span></td>
+	    <td align=right><span id="modem/network/rxCounters/broadcast">...</span></td>
+	    <td align=right><span id="modem/network/rxCounters/unicast">...</span></td>
+
+            <td align=right><span id="modem/network/txCounters/bytes">...</span></td>
+            <td align=right><span id="modem/network/txCounters/frames">...</span></td>
+	    <td align=right><span id="modem/network/txCounters/multicast">...</span></td>
+	    <td align=right><span id="modem/network/txCounters/broadcast">...</span></td>
+	    <td align=right><span id="modem/network/txCounters/unicast">...</span></td>
+
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="modem/network/rxCounters/crcErrors">...</span></td>
+            <td align=right><span id="modem/network/rxCounters/framesUndersized">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="modem/network/txCounters/crcErrors">...</span></td>
+            <td align=right><span id="modem/network/txCounters/framesUndersized">...</span></td>
+            <td align=right>-</td></tr>
+
+	  <tr>
+            <td><b>Craft<b></td>
+            <td align=right><span id="platform/craft_rx_bytes">...</span></td>
+            <td align=right><span id="platform/craft_rx_packets">...</span></td>
+            <td align=right><span id="platform/craft_multicast">...</span></td>
+            <td align=right><span id="platform/craft_broadcast">...</span></td>
+            <td align=right><span id="platform/craft_unicast">...</span></td>
+
+            <td align=right><span id="platform/craft_tx_bytes">...</span></td>
+            <td align=right><span id="platform/craft_tx_packets">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
+            <td align=right><span id="platform/craft_rx_errors">...</span></td>
+            <td align=right><span id="platform/craft_rx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/craft_tx_errors">...</span></td>
+            <td align=right><span id="platform/craft_tx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/craft_collisions">...</span></td>
+
+	  <tr>
+            <td><b>In-Band<b></td>
+            <td align=right><span id="platform/bridge_rx_bytes">...</span></td>
+            <td align=right><span id="platform/bridge_rx_packets">...</span></td>
+            <td align=right><span id="platform/bridge_multicast">...</span></td>
+            <td align=right><span id="platform/bridge_broadcast">...</span></td>
+            <td align=right><span id="platform/bridge_unicast">...</span></td>
+
+            <td align=right><span id="platform/bridge_tx_bytes">...</span></td>
+            <td align=right><span id="platform/bridge_tx_packets">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
+            <td align=right><span id="platform/bridge_rx_errors">...</span></td>
+            <td align=right><span id="platform/bridge_rx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/bridge_tx_errors">...</span></td>
+            <td align=right><span id="platform/bridge_tx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/bridge_collisions">...</span></td>
+
+	  <tr>
+            <td><b>Out-of-Band (PoE)<b></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/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/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/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/ooband_collisions">...</span></td>
+
+	  <tr>
+            <td><b>Link (to peer)<b></td>
+            <td align=right><span id="platform/link_rx_bytes">...</span></td>
+            <td align=right><span id="platform/link_rx_packets">...</span></td>
+            <td align=right><span id="platform/link_multicast">...</span></td>
+            <td align=right><span id="platform/link_broadcast">...</span></td>
+            <td align=right><span id="platform/link_unicast">...</span></td>
+
+            <td align=right><span id="platform/link_tx_bytes">...</span></td>
+            <td align=right><span id="platform/link_tx_packets">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
+            <td align=right><span id="platform/link_rx_errors">...</span></td>
+            <td align=right><span id="platform/link_rx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/link_tx_errors">...</span></td>
+            <td align=right><span id="platform/link_tx_dropped">...</span></td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/link_collisions">...</span></td>
+
           <tr>
-            <td>SOC (from/to switch)</td>
+            <td><b>Switch Port 0/0 (PoE)</b></td>
+            <td align=right><span id="platform/switch/0/0/bytes_received">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/0/multicast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/0/broadcast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/0/unicast_packets_received">...</span></td>
+
+            <td align=right><span id="platform/switch/0/0/bytes_sent">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/0/multicast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/0/broadcast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/0/unicast_packets_sent">...</span></td>
+
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
           <tr>
-            <td>SOC (port 0)</td>
+            <td><b>Switch Port 0/4 (SOC)</b></td>
+            <td align=right><span id="platform/switch/0/4/bytes_received">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/4/multicast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/4/broadcast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/4/unicast_packets_received">...</span></td>
+
+            <td align=right><span id="platform/switch/0/4/bytes_sent">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/4/multicast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/4/broadcast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/4/unicast_packets_sent">...</span></td>
+
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
           <tr>
-            <td>POE (port 0)</td>
+            <td><b>Switch Port 0/24 (modem)</b></td>
+            <td align=right><span id="platform/switch/0/24/bytes_received">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/24/multicast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/24/broadcast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/24/unicast_packets_received">...</span></td>
+
+            <td align=right><span id="platform/switch/0/24/bytes_sent">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/24/multicast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/24/broadcast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/24/unicast_packets_sent">...</span></td>
+
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
           <tr>
-            <td>POE (port 0)</td>
+            <td><b>Switch Port 0/25 (SFP+)</b></td>
+            <td align=right><span id="platform/switch/0/25/bytes_received">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/25/multicast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/25/broadcast_packets_received">...</span></td>
+            <td align=right><span id="platform/switch/0/25/unicast_packets_received">...</span></td>
+
+            <td align=right><span id="platform/switch/0/25/bytes_sent">...</span></td>
+            <td align=right>-</td>
+            <td align=right><span id="platform/switch/0/25/multicast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/25/broadcast_packets_sent">...</span></td>
+            <td align=right><span id="platform/switch/0/25/unicast_packets_sent">...</span></td>
+
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+            <td align=right>-</td>
+
         </table>
       </div>
     </div>
@@ -107,10 +326,13 @@
       <input type="radio" id="tab-3" name="tab-group-1">
       <label for="tab-3">Modem</label>
       <div class="content">
-        <b>Chip:</b><span class="values" id="modem/version/chipType">...</span><br>
+        <b>Chip:</b><span class="values" id="modem/version/api/chipType">...</span><br>
         <b>Firmware:</b><span class="values" id="modem/firmware">...</span><br>
-        <b>Version:</b><span class="values">
-          <span id="modem/version/major">...</span>.<span id="modem/version/minor">?</span>.<span id="modem/version/build">?</span>
+        <b>Bootloader Version:</b><span class="values">
+          <span id="modem/version/bootloader/major">...</span>.<span id="modem/version/bootloader/minor">?</span>.<span id="modem/version/bootloader/build">?</span>
+        </span><br>
+        <b>API Version:</b><span class="values">
+          <span id="modem/version/api/major">...</span>.<span id="modem/version/api/minor">?</span>.<span id="modem/version/api/build">?</span>
         </span><br>
         <b>Profile:</b><span class="values" id="modem/profile">...</span><br>
         <b>Temperature:</b><span class="values" id="modem/temperature">...</span><br>
@@ -159,6 +381,7 @@
           Q: <span id="modem/transmitter/dcLeakageQ">...</span>&nbsp;&nbsp;
         </span><br>
         <b>Transmitter:</b><span class="values">
+          signal gain: <span id="modem/transmitter/signalGain">...</span>&nbsp;&nbsp;
           sweep time: <span id="modem/transmitter/sweepTime">...</span>&nbsp;&nbsp;
           tone freq: <span id="modem/transmitter/toneFreq">...</span>&nbsp;&nbsp;
           tone sec freq: <span id="modem/transmitter/toneSecFreq">...</span>&nbsp;&nbsp;
diff --git a/craftui/www/static/craft.js b/craftui/www/static/craft.js
index 620ff71..81501aa 100644
--- a/craftui/www/static/craft.js
+++ b/craftui/www/static/craft.js
@@ -68,9 +68,6 @@
       CraftUI.flattenAndUpdateFields(list, '');
     }
     CraftUI.updateField('unhandled', self.unhandled);
-    if (self.unhandled.length > 0) {
-      console.log(self.unhandled);
-    }
   };
   var payload = [];
   payload.push('checksum=' + encodeURIComponent(CraftUI.info.checksum));
@@ -79,4 +76,38 @@
   xhr.send();
 };
 
+CraftUI.config = function(key, activate) {
+  // POST as json
+  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 } ] };
+  }
+  var txt = JSON.stringify(data);
+  var resultid = key + "_result"
+  var el = document.getElementById(resultid);
+  xhr.onload = function(e) {
+    var json = JSON.parse(xhr.responseText);
+    if (json.error == 0) {
+      el.innerHTML = action + " successfully.";
+    } else {
+      el.innerHTML = "Error: " + json.errorstring;
+    }
+    CraftUI.getInfo();
+  }
+  xhr.onerror = function(e) {
+    el.innerHTML = xhr.statusText + xhr.responseText;
+  }
+  el.innerHTML = "sending...";
+  xhr.send(txt);
+};
+
 new CraftUI();
diff --git a/ginstall/ginstall.py b/ginstall/ginstall.py
index 1d233c6..983d76c 100755
--- a/ginstall/ginstall.py
+++ b/ginstall/ginstall.py
@@ -771,13 +771,9 @@
   Raises:
     Fatal: if install fails
   """
-  if GetPlatform() == 'GFHD254' and GetMemTotal() < 4*1e9:
-    print 'Skipping bootloader on 2GB lockdown.'
-    return
-
   loader_start = loader.filelike.tell()
   installed = False
-  for i in ['cfe', 'loader', 'loader0', 'loader1', 'flash0.bolt']:
+  for i in ['cfe', 'loader', 'loader0', 'loader1', 'flash0.bolt', 'uboot']:
     mtd = GetMtdDevForNameOrNone(i)
     if mtd:
       WriteLoaderToMtd(loader, loader_start, mtd, 'loader')
diff --git a/gpio-mailbox/broadcom.c b/gpio-mailbox/broadcom.c
index 315b8f8..575f197 100644
--- a/gpio-mailbox/broadcom.c
+++ b/gpio-mailbox/broadcom.c
@@ -26,6 +26,11 @@
 
 #define DEVMEM          "/dev/mem"
 
+/* This value, from old code, controls the pwm period. The duty cycle
+  is defined as on/(period + 1) and on is defined as (on/Fv). Fv is
+  the frequency of the variable rate PWM.*/
+static const int PWM_CYCLE_PERIOD = 0x63;
+
 struct PinHandle_s {
   int   unused;
 };
@@ -61,8 +66,7 @@
   int old_val;
 };
 
-
-struct Fan {
+struct PwmControl {
   int is_present;
   int open_drain;
   unsigned int offset_data;
@@ -70,33 +74,35 @@
   int old_percent;
 };
 
-
 struct Temp {
   int is_present;
   unsigned int offset_data;
   double (*get_temp)(struct Temp* t);
 };
 
-
 struct Voltage {
   int is_present;
   unsigned int offset_data;
   double (*get_voltage)(struct Voltage* v);
 };
 
+struct Leds {
+  struct Gpio led_red;
+  struct Gpio led_blue;
+  struct Gpio led_activity;
+  struct Gpio led_standby;
+  struct PwmControl led_brightness;
+};
 
 struct platform_info {
   const char *name;
   off_t mmap_base;
   size_t mmap_size;
   void (*init)(struct platform_info* p);
-  struct Gpio led_red;
-  struct Gpio led_blue;
-  struct Gpio led_activity;
-  struct Gpio led_standby;
+  struct Leds leds;
   struct Gpio reset_button;
   struct Gpio fan_tick;
-  struct Fan fan_control;
+  struct PwmControl fan_control;
   struct Temp temp_monitor;
   struct Voltage voltage_monitor;
 };
@@ -113,49 +119,51 @@
     .name = "GFHD100",
     .mmap_base = 0x10400000,            // base of many brcm registers
     .mmap_size = 0x40000,
-    .led_red = {
-      .is_present = 1,                  // GPIO 17
-      .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
-      .offset_data = 0x94c4,            // GIO_AON_DATA_LO
-      .mask = 0x00020000,               // 1<<17
-      .shift = 17,
-      .off_value = 0,
-      .on_value = 1,
-      .direction_value = 0,
-      .old_val = -1,
-    },
-    .led_blue = {
-      .is_present = 1,                  // GPIO 12
-      .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
-      .offset_data = 0x94c4,            // GIO_AON_DATA_LO
-      .mask = 0x00001000,               // 1<<12
-      .shift = 12,
-      .off_value = 0,
-      .on_value = 1,
-      .direction_value = 0,
-      .old_val = -1,
-    },
-    .led_activity = {
-      .is_present = 1,                  // GPIO 13
-      .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
-      .offset_data = 0x94c4,            // GIO_AON_DATA_LO
-      .mask = 0x00002000,               // 1<<13
-      .shift = 13,
-      .off_value = 0,
-      .on_value = 1,
-      .direction_value = 0,
-      .old_val = -1,
-    },
-    .led_standby = {
-      .is_present = 1,                  // GPIO 10
-      .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
-      .offset_data = 0x94c4,            // GIO_AON_DATA_LO
-      .mask = 0x00000400,               // 1<<10
-      .shift = 10,
-      .off_value = 0,
-      .on_value = 1,
-      .direction_value = 0,
-      .old_val = -1,
+    .leds = {
+      .led_red = {
+        .is_present = 1,                  // GPIO 17
+        .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
+        .offset_data = 0x94c4,            // GIO_AON_DATA_LO
+        .mask = 0x00020000,               // 1<<17
+        .shift = 17,
+        .off_value = 0,
+        .on_value = 1,
+        .direction_value = 0,
+        .old_val = -1,
+      },
+      .led_blue = {
+        .is_present = 1,                  // GPIO 12
+        .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
+        .offset_data = 0x94c4,            // GIO_AON_DATA_LO
+        .mask = 0x00001000,               // 1<<12
+        .shift = 12,
+        .off_value = 0,
+        .on_value = 1,
+        .direction_value = 0,
+        .old_val = -1,
+      },
+      .led_activity = {
+        .is_present = 1,                  // GPIO 13
+        .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
+        .offset_data = 0x94c4,            // GIO_AON_DATA_LO
+        .mask = 0x00002000,               // 1<<13
+        .shift = 13,
+        .off_value = 0,
+        .on_value = 1,
+        .direction_value = 0,
+        .old_val = -1,
+      },
+      .led_standby = {
+        .is_present = 1,                  // GPIO 10
+        .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
+        .offset_data = 0x94c4,            // GIO_AON_DATA_LO
+        .mask = 0x00000400,               // 1<<10
+        .shift = 10,
+        .off_value = 0,
+        .on_value = 1,
+        .direction_value = 0,
+        .old_val = -1,
+      },
     },
     .reset_button = {
       .is_present = 1,                  // GPIO 4
@@ -200,33 +208,35 @@
     .name = "GFMS100",
     .mmap_base = 0x10400000,            // base of many brcm registers
     .mmap_size = 0x40000,
-    .led_red = {
-      .is_present = 1,                  // GPIO 17
-      .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
-      .offset_data = 0x94c4,            // GIO_AON_DATA_LO 0..17
-      .mask = 0x00020000,               // 1<<17
-      .shift = 17,
-      .off_value = 0,
-      .on_value = 1,
-      .direction_value = 0,
-      .old_val = -1,
-    },
-    .led_blue = {
-      .is_present = 0,
-    },
-    .led_activity = {
-      .is_present = 1,                  // GPIO 13
-      .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
-      .offset_data = 0x94c4,            // GIO_AON_DATA_LO
-      .mask = 0x00002000,               // 1<<13
-      .shift = 13,
-      .off_value = 0,
-      .on_value = 1,
-      .direction_value = 0,
-      .old_val = -1,
-    },
-    .led_standby = {
-      .is_present = 0,
+    .leds = {
+      .led_red = {
+        .is_present = 1,                  // GPIO 17
+        .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
+        .offset_data = 0x94c4,            // GIO_AON_DATA_LO 0..17
+        .mask = 0x00020000,               // 1<<17
+        .shift = 17,
+        .off_value = 0,
+        .on_value = 1,
+        .direction_value = 0,
+        .old_val = -1,
+      },
+      .led_blue = {
+        .is_present = 0,
+      },
+      .led_activity = {
+        .is_present = 1,                  // GPIO 13
+        .offset_direction = 0x94c8,       // GIO_AON_IODIR_LO
+        .offset_data = 0x94c4,            // GIO_AON_DATA_LO
+        .mask = 0x00002000,               // 1<<13
+        .shift = 13,
+        .off_value = 0,
+        .on_value = 1,
+        .direction_value = 0,
+        .old_val = -1,
+      },
+      .led_standby = {
+        .is_present = 0,
+      },
     },
     .reset_button = {
       .is_present = 1,                  // GPIO 4
@@ -272,35 +282,37 @@
     .init = init_gfhd200,
     .mmap_base = 0x10400000,            // AON_PIN_CTRL ...
     .mmap_size = 0x30000,
-    .led_red = {
-      .is_present = 1,                  // GPIO 5
-      .pinmux_offset = 0x8500,          // PIN_MUX_CTRL_0
-      .pinmux_mask = 0xf0000000,
-      .pinmux_value = 0x10000000,       // LED_LD1 (segment 1 on led digit1)
-      .offset_data = 0x9018,            // GIO_AON_DATA_LO
-      .mask = 0x00000002,               // 1<<1
-      .shift = 1,
-      .off_value =1,
-      .on_value = 0,
-      .old_val = -1,
-    },
-    .led_blue = {
-      .is_present = 0,
-    },
-    .led_activity = {
-      .is_present = 1,                  // GPIO 4
-      .pinmux_offset = 0x8500,          // PIN_MUX_CTRL_0
-      .pinmux_mask = 0x0f000000,
-      .pinmux_value = 0x01000000,       // LED_LD0 (segment 0 on led digit1)
-      .offset_data = 0x9018,            // GIO_AON_DATA_LO
-      .mask = 0x00000001,               // 1<<0
-      .shift = 0,
-      .off_value = 1,
-      .on_value = 0,
-      .old_val = -1,
-    },
-    .led_standby = {
-      .is_present = 0,
+    .leds = {
+      .led_red = {
+        .is_present = 1,                  // GPIO 5
+        .pinmux_offset = 0x8500,          // PIN_MUX_CTRL_0
+        .pinmux_mask = 0xf0000000,
+        .pinmux_value = 0x10000000,       // LED_LD1 (segment 1 on led digit1)
+        .offset_data = 0x9018,            // GIO_AON_DATA_LO
+        .mask = 0x00000002,               // 1<<1
+        .shift = 1,
+        .off_value =1,
+        .on_value = 0,
+        .old_val = -1,
+      },
+      .led_blue = {
+        .is_present = 0,
+      },
+      .led_activity = {
+        .is_present = 1,                  // GPIO 4
+        .pinmux_offset = 0x8500,          // PIN_MUX_CTRL_0
+        .pinmux_mask = 0x0f000000,
+        .pinmux_value = 0x01000000,       // LED_LD0 (segment 0 on led digit1)
+        .offset_data = 0x9018,            // GIO_AON_DATA_LO
+        .mask = 0x00000001,               // 1<<0
+        .shift = 0,
+        .off_value = 1,
+        .on_value = 0,
+        .old_val = -1,
+      },
+      .led_standby = {
+        .is_present = 0,
+      },
     },
     .reset_button = {
       .is_present = 1,                  // GPIO 3
@@ -332,35 +344,44 @@
     .init = init_gfhd254,
     .mmap_base = 0xf0400000,            // AON_PIN_CTRL ...
     .mmap_size =    0xe0000,
-    .led_red = {
-      .is_present = 1,                  // AON_GPIO_05
-      .pinmux_offset = 0x10700,         // PIN_MUX_CTRL_0
-      .pinmux_mask =  0x00f00000,
-      .pinmux_value = 0x00200000,       // LED_LD_13
-      .offset_data = 0x1701c,           // LDK_DIGIT1
-      .mask = 1<<13,                    // 1<<13
-      .shift = 13,
-      .off_value =1,
-      .on_value = 0,
-      .old_val = -1,
-    },
-    .led_blue = {
-      .is_present = 0,
-    },
-    .led_activity = {
-      .is_present = 1,                  // AON_GPIO_04
-      .pinmux_offset = 0x10700,         // PIN_MUX_CTRL_0
-      .pinmux_mask = 0x000f0000,
-      .pinmux_value = 0x00020000,       // LED_LD_12
-      .offset_data = 0x1701c,           // LDK_DIGIT1
-      .mask = 1<<12,                    // 1<<12
-      .shift = 12,
-      .off_value = 1,
-      .on_value = 0,
-      .old_val = -1,
-    },
-    .led_standby = {
-      .is_present = 0,
+    .leds = {
+      .led_red = {
+        .is_present = 1,                  // AON_GPIO_05
+        .pinmux_offset = 0x10700,         // PIN_MUX_CTRL_0
+        .pinmux_mask =  0x00f00000,
+        .pinmux_value = 0x00200000,       // LED_LD_13
+        .offset_data = 0x1701c,           // LDK_DIGIT1
+        .mask = 1<<13,                    // 1<<13
+        .shift = 13,
+        .off_value =1,
+        .on_value = 0,
+        .old_val = -1,
+      },
+      .led_blue = {
+        .is_present = 0,
+      },
+      .led_activity = {
+        .is_present = 1,                  // AON_GPIO_04
+        .pinmux_offset = 0x10700,         // PIN_MUX_CTRL_0
+        .pinmux_mask = 0x000f0000,
+        .pinmux_value = 0x00020000,       // LED_LD_12
+        .offset_data = 0x1701c,           // LDK_DIGIT1
+        .mask = 1<<12,                    // 1<<12
+        .shift = 12,
+        .off_value = 1,
+        .on_value = 0,
+        .old_val = -1,
+      },
+      .led_standby = {
+        .is_present = 0,
+      },
+      .led_brightness = {
+        .is_present = 1,                // GPIO_098
+        .open_drain = 0,
+        .offset_data = 0x9000,          // PWM_2
+        .channel = 0,
+        .old_percent = -1,
+      },
     },
     .reset_button = {
       .is_present = 1,                  // GPIO_009
@@ -408,79 +429,21 @@
 
 struct platform_info *platform = NULL;
 
-/* set LED/Keypad timings to control LED brightness */
-static void init_gfhd200(UNUSED struct platform_info* p) {
-  volatile uint32_t* reg;
+/* PWM operates on either channel 0 or 1. We want to get the duty cycle value
+   by calculating it from the "ON" register, located offset 6 for channel 0
+   and 8 for channel 1.
 
-  reg = mmap_addr + 0x9034;     // LDK_CONTROL
-  *reg = 0x01;                  // reset
-  *reg = 0x18;                  // ver=1 inv_led=1
-
-  reg = mmap_addr + 0x9008;     // LDK_PRESCHI, LO (clock divisor)
-  reg[0] = 0x00;
-  reg[1] = 0x10;                // tick = clock / 0x0010, not sure what clock is
-
-  reg = mmap_addr + 0x9010;     // LDK_DUTYOFF, ON
-  reg[0] = 0x40;
-  reg[1] = 0xc0;                // 0x40 off ticks then 0xc0 on ticks == 75% brightness
-}
-
-/* set LED/Keypad timings to control LED brightness */
-static void init_gfhd254(UNUSED struct platform_info* p) {
-  volatile uint32_t* reg;
-
-  // The led display controller works like this:
-  //  - there are 16 gpios (we connect our leds to 2 of these)
-  //  - the controller steps through digit1-4 and then status
-  //  - bit0 in a register maps to a particular gpio
-  //     when digit1 is being displayed the controller uses digit1_bit[15:0] to
-  //     drive the gpios.  When digit 2 is displayed digit2[15:0] and so forth.
-  //  - duty_on controls how many clocks a digit is displayed
-  //  - duty_off controls number of clocks of all off time when switching
-  //    between digits
-  //
-  //  To get 100% brightness you set all of digit1-4 and status to 1 for the led
-  //  you are drivng, and set duty_off to 0.
-  //
-  //  Here we also invert the values, so a 1 means off, and 0 means on, this is
-  //  done because for unknown reasons the time between status and digit1 is on,
-  //  so we can't get the brightness to 0 unless we invert.
-  //
-  //  For simplicity we enable only one of the digits because the leds are
-  //  already insanely bright, and then to disable an led we simply toggle the
-  //  bit in that one digit register.
-  //
-  //  The red led is attached to bit 13 and blue led is attached to bit 12.
-
-  reg = mmap_addr + 0x17034;     // LDK_CONTROL
-  *reg = 0x01;                   // reset
-  *reg = 0x18;                   // ver=1
-
-  reg = mmap_addr + 0x17018;
-  reg[0] = 0xffff;               // LDK_DIGIT2
-  reg[1] = 0xcfff;               // LDK_DIGIT1
-  reg[2] = 0xffff;               // LDK_DIGIT4
-  reg[3] = 0xffff;               // LDK_DIGIT3
-  reg[5] = 0xffff;               // LDK_STATUS
-
-  reg = mmap_addr + 0x17008;     // LDK_PRESCHI, LO (clock divisor)
-  reg[0] = 0x00;
-  reg[1] = 0x10;                 // tick = clock / 0x0010, not sure what clock is
-
-  reg = mmap_addr + 0x17010;     // LDK_DUTYOFF, ON
-  reg[0] = 0x40;
-  reg[1] = 0xc0;                 // 0x40 off ticks then 0xc0 on ticks to dim a bit more.
-
-
-  // The fan is connected to PWM3, the register PWM3_CWORD_LSB is set to 1,
-  // this is the frequency of the PWM, the other pwm register control
-  // the duty cycle.
-  reg = mmap_addr + 0x9014;       // PWM3_CWORD_LSB
-  reg[0] = 1;
+   Duty cycle is calculated by ON / Period.
+*/
+static UNUSED int get_pwm(struct PwmControl *f) {
+  volatile uint32_t* reg = mmap_addr + f->offset_data;
+  uint8_t offset = f->channel ? 8 : 6;
+  uint32_t val = reg[offset];
+  return ((uint64_t)val * 100) / PWM_CYCLE_PERIOD;
 }
 
 // Set the given PWM (pulse width modulator) to the given percent duty cycle.
-static void set_pwm(struct Fan *f, int percent) {
+static void set_pwm(struct PwmControl *f, int percent) {
   volatile uint32_t* reg;
   uint32_t mask0, val0, mask1, val1, on;
 
@@ -509,8 +472,8 @@
   }
   reg[0] = (reg[0] & mask0) | val0;
   reg[1] = (reg[1] & mask1) | val1;
-  reg[on] = 0x63 * percent/100;         // 0x63 is what old code used
-  reg[on+1] = 0x63;
+  reg[on] = (PWM_CYCLE_PERIOD * percent)/100;         // 0x63 is what old code used
+  reg[on+1] = PWM_CYCLE_PERIOD;
 }
 
 // Get the CPU temperature.  I think it's in Celsius.
@@ -771,51 +734,138 @@
 }
 
 int has_red_led(void) {
-  return (platform->led_red.is_present);
+  return (platform->leds.led_red.is_present);
 }
 
 int has_blue_led(void) {
-  return (platform->led_blue.is_present);
+  return (platform->leds.led_blue.is_present);
 }
 
 int has_activity_led(void) {
-  return (platform->led_activity.is_present);
+  return (platform->leds.led_activity.is_present);
 }
 
 int has_standby_led(void) {
-  return (platform->led_standby.is_present);
+  return (platform->leds.led_standby.is_present);
 }
 
 int get_red_led(void) {
-  return get_gpio(&platform->led_red);
+  return get_gpio(&platform->leds.led_red);
 }
 
 int get_blue_led(void) {
-  return get_gpio(&platform->led_blue);
+  return get_gpio(&platform->leds.led_blue);
 }
 
 int get_activity_led(void) {
-  return get_gpio(&platform->led_activity);
+  return get_gpio(&platform->leds.led_activity);
 }
 
 int get_standby_led(void) {
-  return get_gpio(&platform->led_standby);
+  return get_gpio(&platform->leds.led_standby);
 }
 
+/* TODO(doughorn): The set_*_led functions should apply the brightness
+   as well */
 void set_red_led(int level) {
-  set_gpio(&platform->led_red, level ? 1 : 0);
+  set_gpio(&platform->leds.led_red, level ? 1 : 0);
 }
 
 void set_blue_led(int level) {
-  set_gpio(&platform->led_blue, level ? 1 : 0);
+  set_gpio(&platform->leds.led_blue, level ? 1 : 0);
 }
 
 void set_activity_led(int level) {
-  set_gpio(&platform->led_activity, level ? 1 : 0);
+  set_gpio(&platform->leds.led_activity, level ? 1 : 0);
 }
 
 void set_standby_led(int level) {
-  set_gpio(&platform->led_standby, level ? 1 : 0);
+  set_gpio(&platform->leds.led_standby, level ? 1 : 0);
+}
+
+void set_led_brightness(int level) {
+  set_pwm(&platform->leds.led_brightness, level);
+}
+
+/* set LED/Keypad timings to control LED brightness */
+static void init_gfhd200(UNUSED struct platform_info* p) {
+  volatile uint32_t* reg;
+
+  reg = mmap_addr + 0x9034;     // LDK_CONTROL
+  *reg = 0x01;                  // reset
+  *reg = 0x18;                  // ver=1 inv_led=1
+
+  reg = mmap_addr + 0x9008;     // LDK_PRESCHI, LO (clock divisor)
+  reg[0] = 0x00;
+  reg[1] = 0x10;                // tick = clock / 0x0010, not sure what clock is
+
+  reg = mmap_addr + 0x9010;     // LDK_DUTYOFF, ON
+  reg[0] = 0x40;
+  reg[1] = 0xc0;                // 0x40 off ticks then 0xc0 on ticks == 75% brightness
+}
+
+/* set LED/Keypad timings to control LED brightness */
+static void init_gfhd254(UNUSED struct platform_info* p) {
+  volatile uint32_t* reg;
+
+  // The following comment explains how the LED controller works on <= EVT3.
+  //  For EVT4+, the LED controller was changed to control via PWM. We currently
+  //  configure both. The EVT3 specific code can be removed at a later date.
+  //
+  // The led display controller works like this:
+  //  - there are 16 gpios (we connect our leds to 2 of these)
+  //  - the controller steps through digit1-4 and then status
+  //  - bit0 in a register maps to a particular gpio
+  //     when digit1 is being displayed the controller uses digit1_bit[15:0] to
+  //     drive the gpios.  When digit 2 is displayed digit2[15:0] and so forth.
+  //  - duty_on controls how many clocks a digit is displayed
+  //  - duty_off controls number of clocks of all off time when switching
+  //    between digits
+  //
+  //  To get 100% brightness you set all of digit1-4 and status to 1 for the led
+  //  you are drivng, and set duty_off to 0.
+  //
+  //  Here we also invert the values, so a 1 means off, and 0 means on, this is
+  //  done because for unknown reasons the time between status and digit1 is on,
+  //  so we can't get the brightness to 0 unless we invert.
+  //
+  //  For simplicity we enable only one of the digits because the leds are
+  //  already insanely bright, and then to disable an led we simply toggle the
+  //  bit in that one digit register.
+  //
+  //  The red led is attached to bit 13 and blue led is attached to bit 12.
+  reg = mmap_addr + 0x17034;     // LDK_CONTROL
+  *reg = 0x01;                   // reset
+  *reg = 0x18;                   // ver=1
+
+  reg = mmap_addr + 0x17018;
+  reg[0] = 0xffff;               // LDK_DIGIT2
+  reg[1] = 0xcfff;               // LDK_DIGIT1
+  reg[2] = 0xffff;               // LDK_DIGIT4
+  reg[3] = 0xffff;               // LDK_DIGIT3
+  reg[5] = 0xffff;               // LDK_STATUS
+
+  reg = mmap_addr + 0x17008;     // LDK_PRESCHI, LO (clock divisor)
+  reg[0] = 0x00;
+  reg[1] = 0x10;                 // tick = clock / 0x0010, not sure what clock is
+
+  reg = mmap_addr + 0x17010;     // LDK_DUTYOFF, ON
+  reg[0] = 0x40;
+  reg[1] = 0xc0;                 // 0x40 off ticks then 0xc0 on ticks to dim a bit more.
+
+  // The fan is connected to PWM3, the register PWM3_CWORD_LSB is set to 1,
+  // this is the frequency of the PWM, the other pwm register control
+  // the duty cycle.
+  reg = mmap_addr + 0x9014;       // PWM3_CWORD_LSB
+  reg[0] = 1;
+
+  // LEDs are connected to PWM2. Setting CWORD_LSB to 0xf to control
+  // the output freq of the var rate clock.
+  reg = mmap_addr + 0x900c;
+  reg[0] = 0xf;
+
+  // Default the LED brightness to 50.
+  set_led_brightness(50);
 }
 
 static void init_platform(struct platform_info* p) {
@@ -827,15 +877,15 @@
 static void initialize_gpios(void) {
   init_platform(platform);
 
-  set_pinmux(&platform->led_red);
-  set_pinmux(&platform->led_blue);
-  set_pinmux(&platform->led_activity);
-  set_pinmux(&platform->led_standby);
+  set_pinmux(&platform->leds.led_red);
+  set_pinmux(&platform->leds.led_blue);
+  set_pinmux(&platform->leds.led_activity);
+  set_pinmux(&platform->leds.led_standby);
 
-  set_direction(&platform->led_red);
-  set_direction(&platform->led_blue);
-  set_direction(&platform->led_activity);
-  set_direction(&platform->led_standby);
+  set_direction(&platform->leds.led_red);
+  set_direction(&platform->leds.led_blue);
+  set_direction(&platform->leds.led_activity);
+  set_direction(&platform->leds.led_standby);
   set_direction(&platform->reset_button);
   set_direction(&platform->fan_tick);
 }
diff --git a/hnvram/Makefile b/hnvram/Makefile
index c3368cb..2790f4d 100644
--- a/hnvram/Makefile
+++ b/hnvram/Makefile
@@ -7,7 +7,8 @@
 AR:=$(CROSS_COMPILE)ar
 RANLIB:=$(CROSS_COMPILE)ranlib
 STRIP:=$(CROSS_COMPILE)strip
-BINDIR=$(DESTDIR)/bin
+PREFIX=/usr
+BINDIR=$(DESTDIR)$(PREFIX)/bin
 
 HUMAX_UPGRADE_DIR ?= ../../../humax/misc/libupgrade
 CFLAGS += -g -Os -I$(HUMAX_UPGRADE_DIR) $(EXTRACFLAGS)
@@ -29,7 +30,7 @@
 
 install:
 	mkdir -p $(BINDIR)
-	cp hnvram $(BINDIR)
+	cp hnvram $(BINDIR)/hnvram_binary
 
 install-libs:
 	@echo "No libs to install."
diff --git a/hnvram/hnvram_main.c b/hnvram/hnvram_main.c
index 6e86f26..b850048 100644
--- a/hnvram/hnvram_main.c
+++ b/hnvram/hnvram_main.c
@@ -79,7 +79,10 @@
   {"PAIRED_DISK",          NVRAM_FIELD_PAIRED_DISK,       HNVRAM_STRING},
   {"PARTITION_VER",        NVRAM_FIELD_PARTITION_VER,     HNVRAM_STRING},
   {"HW_VER",               NVRAM_FIELD_HW_VER,            HNVRAM_UINT8},
-  {"UITYPE",               NVRAM_FIELD_UITYPE,            HNVRAM_STRING}
+  {"UITYPE",               NVRAM_FIELD_UITYPE,            HNVRAM_STRING},
+  {"LASER_CHANNEL",        NVRAM_FIELD_LASER_CHANNEL,     HNVRAM_STRING},
+  {"MAC_ADDR_PON",         NVRAM_FIELD_MAC_ADDR_PON,      HNVRAM_MAC},
+  {"PRODUCTION_UNIT",      NVRAM_FIELD_PRODUCTION_UNIT,   HNVRAM_STRING},
 };
 
 const hnvram_field_t* get_nvram_field(const char* name) {
diff --git a/libexperiments/experiments.cc b/libexperiments/experiments.cc
index d3dcb2b..89fec64 100644
--- a/libexperiments/experiments.cc
+++ b/libexperiments/experiments.cc
@@ -64,13 +64,17 @@
   register_func_ = register_func;
   min_time_between_refresh_usec_ = min_time_between_refresh_usec;
 
-  if (!Register_Locked(names_to_register))
-    return false;
+  initialized_ = true;  // initialization part succeeded at this point
 
-  // initial read of registered experiments states
-  Refresh();
+  // register any provided experiments
+  if (!names_to_register.empty()) {
+    if (!Register_Locked(names_to_register))
+      return false;
 
-  initialized_ = true;
+    // initial read of registered experiments states
+    Refresh();
+  }
+
   return true;
 }
 
@@ -156,7 +160,7 @@
 
   experiments = new Experiments();
   return experiments->Initialize(config_dir, min_time_between_refresh_usec,
-                                 register_func, {""});
+                                 register_func, {});
 }
 
 int experiments_is_initialized() {
@@ -171,6 +175,10 @@
   return experiments ? experiments->IsRegistered(name) : false;
 }
 
+int experiments_get_num_of_registered_experiments() {
+  return experiments ? experiments->GetNumOfRegisteredExperiments() : 0;
+}
+
 int experiments_is_enabled(const char *name) {
   return experiments ? experiments->IsEnabled(name) : false;
 }
diff --git a/libexperiments/experiments.h b/libexperiments/experiments.h
index 54c6389..1f086ba 100644
--- a/libexperiments/experiments.h
+++ b/libexperiments/experiments.h
@@ -84,8 +84,10 @@
         last_time_refreshed_usec_(0) {}
   virtual ~Experiments() {}
 
-  // Initializes the instance:
-  // * Sets the provided experiments config directory and register function.
+  // Initializes the instance and registers any provided experiments. In detail:
+  // * Sets the provided experiments config directory and register function and
+  //   makes sure they are valid. If successful the instance is marked as
+  //   initialized.
   // * Calls the register function for the provided experiment names.
   // * Scans the config folder to determine initial state of all registered
   //   experiments.
@@ -113,6 +115,10 @@
     return Register(names);
   }
 
+  int GetNumOfRegisteredExperiments() const {
+    return registered_experiments_.size();
+  }
+
   // Returns true if the given experiment is registered.
   bool IsRegistered(const std::string &name);
 
@@ -205,6 +211,9 @@
 // else 0 (boolean false).
 int experiments_is_registered(const char *name);
 
+// Returns the number of experiments registered.
+int experiments_get_num_of_registered_experiments();
+
 // Returns non-zero (boolean true) if the given experiment is active, else 0
 // (boolean false). If the minimum time between refreshes has passed, re-scans
 // the config folder for updates first.
diff --git a/libexperiments/experiments_test.cc b/libexperiments/experiments_test.cc
index e0370cc..5f0178d 100644
--- a/libexperiments/experiments_test.cc
+++ b/libexperiments/experiments_test.cc
@@ -131,6 +131,7 @@
   ASSERT_TRUE(e.Initialize(test_folder_path_, 0, &DummyExperimentsRegisterFunc,
                            {"exp1"}));
   EXPECT_TRUE(e.IsRegistered("exp1"));
+  EXPECT_EQ(1, e.GetNumOfRegisteredExperiments());
 
   // add one more
   EXPECT_FALSE(e.IsRegistered("exp2"));
@@ -160,6 +161,7 @@
   ASSERT_TRUE(e.Initialize(test_folder_path_, 0, &DummyExperimentsRegisterFunc,
                            {"exp1"}));
   EXPECT_FALSE(e.IsEnabled("exp1"));
+  EXPECT_EQ(1, e.GetNumOfRegisteredExperiments());
 
   EXPECT_TRUE(SetActive(&e, "exp1"));
   EXPECT_TRUE(e.IsEnabled("exp1"));
@@ -178,6 +180,7 @@
   Experiments e;
   ASSERT_TRUE(e.Initialize(test_folder_path_, 0, &DummyExperimentsRegisterFunc,
                            {"exp1", "exp2", "exp3"}));
+  EXPECT_EQ(3, e.GetNumOfRegisteredExperiments());
   EXPECT_FALSE(e.IsEnabled("exp1"));
   EXPECT_FALSE(e.IsEnabled("exp2"));
   EXPECT_FALSE(e.IsEnabled("exp3"));
@@ -233,6 +236,7 @@
   Experiments e;
   ASSERT_TRUE(e.Initialize(test_folder_path_, kMinTimeBetweenRefresh,
                            &DummyExperimentsRegisterFunc, {"exp1"}));
+  EXPECT_EQ(1, e.GetNumOfRegisteredExperiments());
   EXPECT_FALSE(e.IsEnabled("exp1"));
   EXPECT_TRUE(SetActive(&e, "exp1"));
 
@@ -263,9 +267,11 @@
   // initialize
   EXPECT_TRUE(test_experiments_initialize(test_folder_path_));
   EXPECT_TRUE(test_experiments_is_initialized());
+  EXPECT_EQ(0, experiments_get_num_of_registered_experiments());
 
   EXPECT_TRUE(test_experiments_register("exp1"));
   EXPECT_TRUE(test_experiments_is_registered("exp1"));
+  EXPECT_EQ(1, experiments_get_num_of_registered_experiments());
 
   EXPECT_FALSE(test_experiments_is_enabled("exp1"));
   EXPECT_TRUE(SetActive(experiments, "exp1"));
diff --git a/presterastats/Makefile b/presterastats/Makefile
new file mode 100644
index 0000000..af578cd
--- /dev/null
+++ b/presterastats/Makefile
@@ -0,0 +1,31 @@
+default:
+
+PREFIX=/
+BINDIR=$(DESTDIR)$(PREFIX)/bin
+PYTHON?=python
+
+all:
+
+install:
+	mkdir -p $(BINDIR)
+	cp presterastats.py $(BINDIR)/presterastats
+	cp prestera_periodic.py $(BINDIR)/prestera_periodic
+
+install-libs:
+	@echo "No libs to install."
+
+test: lint
+	set -e; \
+	for pytest in $(wildcard *_test.py); do \
+		echo; \
+		echo "Testing $$pytest"; \
+		$(PYTHON) $$pytest; \
+	done
+
+clean:
+	rm -rf *.pyc
+
+lint:
+	for n in $(filter-out options.py, $(wildcard *.py)); do \
+		gpylint $$n || exit 1; \
+	done
diff --git a/presterastats/options.py b/presterastats/options.py
new file mode 100644
index 0000000..7fb5bcf
--- /dev/null
+++ b/presterastats/options.py
@@ -0,0 +1,274 @@
+# Copyright 2010-2012 Avery Pennarun and options.py contributors.
+# All rights reserved.
+#
+# (This license applies to this file but not necessarily the other files in
+# this package.)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+#    1. Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in
+#       the documentation and/or other materials provided with the
+#       distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY AVERY PENNARUN ``AS IS'' AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+"""Command-line options parser.
+With the help of an options spec string, easily parse command-line options.
+
+An options spec is made up of two parts, separated by a line with two dashes.
+The first part is the synopsis of the command and the second one specifies
+options, one per line.
+
+Each non-empty line in the synopsis gives a set of options that can be used
+together.
+
+Option flags must be at the begining of the line and multiple flags are
+separated by commas. Usually, options have a short, one character flag, and a
+longer one, but the short one can be omitted.
+
+Long option flags are used as the option's key for the OptDict produced when
+parsing options.
+
+When the flag definition is ended with an equal sign, the option takes one
+string as an argument. Otherwise, the option does not take an argument and
+corresponds to a boolean flag that is true when the option is given on the
+command line.
+
+The option's description is found at the right of its flags definition, after
+one or more spaces. The description ends at the end of the line. If the
+description contains text enclosed in square brackets, the enclosed text will
+be used as the option's default value.
+
+Options can be put in different groups. Options in the same group must be on
+consecutive lines. Groups are formed by inserting a line that begins with a
+space. The text on that line will be output after an empty line.
+"""
+import sys, os, textwrap, getopt, re, struct
+
+
+def _invert(v, invert):
+    if invert:
+        return not v
+    return v
+
+
+def _remove_negative_kv(k, v):
+    if k.startswith('no-') or k.startswith('no_'):
+        return k[3:], not v
+    return k,v
+
+
+class OptDict(object):
+    """Dictionary that exposes keys as attributes.
+
+    Keys can be set or accessed with a "no-" or "no_" prefix to negate the
+    value.
+    """
+    def __init__(self, aliases):
+        self._opts = {}
+        self._aliases = aliases
+
+    def _unalias(self, k):
+        k, reinvert = _remove_negative_kv(k, False)
+        k, invert = self._aliases[k]
+        return k, invert ^ reinvert
+
+    def __setitem__(self, k, v):
+        k, invert = self._unalias(k)
+        self._opts[k] = _invert(v, invert)
+
+    def __getitem__(self, k):
+        k, invert = self._unalias(k)
+        return _invert(self._opts[k], invert)
+
+    def __getattr__(self, k):
+        return self[k]
+
+
+def _default_onabort(msg):
+    sys.exit(97)
+
+
+def _intify(v):
+    try:
+        vv = int(v or '')
+        if str(vv) == v:
+            return vv
+    except ValueError:
+        pass
+    return v
+
+
+def _atoi(v):
+    try:
+        return int(v or 0)
+    except ValueError:
+        return 0
+
+
+def _tty_width():
+    s = struct.pack("HHHH", 0, 0, 0, 0)
+    try:
+        import fcntl, termios
+        s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s)
+    except (IOError, ImportError):
+        return _atoi(os.environ.get('WIDTH')) or 70
+    (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s)
+    return xsize or 70
+
+
+class Options:
+    """Option parser.
+    When constructed, a string called an option spec must be given. It
+    specifies the synopsis and option flags and their description.  For more
+    information about option specs, see the docstring at the top of this file.
+
+    Two optional arguments specify an alternative parsing function and an
+    alternative behaviour on abort (after having output the usage string).
+
+    By default, the parser function is getopt.gnu_getopt, and the abort
+    behaviour is to exit the program.
+    """
+    def __init__(self, optspec, optfunc=getopt.gnu_getopt,
+                 onabort=_default_onabort):
+        self.optspec = optspec
+        self._onabort = onabort
+        self.optfunc = optfunc
+        self._aliases = {}
+        self._shortopts = 'h?'
+        self._longopts = ['help', 'usage']
+        self._hasparms = {}
+        self._defaults = {}
+        self._usagestr = self._gen_usage()  # this also parses the optspec
+
+    def _gen_usage(self):
+        out = []
+        lines = self.optspec.strip().split('\n')
+        lines.reverse()
+        first_syn = True
+        while lines:
+            l = lines.pop()
+            if l == '--': break
+            out.append('%s: %s\n' % (first_syn and 'usage' or '   or', l))
+            first_syn = False
+        out.append('\n')
+        last_was_option = False
+        while lines:
+            l = lines.pop()
+            if l.startswith(' '):
+                out.append('%s%s\n' % (last_was_option and '\n' or '',
+                                       l.lstrip()))
+                last_was_option = False
+            elif l:
+                (flags,extra) = (l + ' ').split(' ', 1)
+                extra = extra.strip()
+                if flags.endswith('='):
+                    flags = flags[:-1]
+                    has_parm = 1
+                else:
+                    has_parm = 0
+                g = re.search(r'\[([^\]]*)\]$', extra)
+                if g:
+                    defval = _intify(g.group(1))
+                else:
+                    defval = None
+                flagl = flags.split(',')
+                flagl_nice = []
+                flag_main, invert_main = _remove_negative_kv(flagl[0], False)
+                self._defaults[flag_main] = _invert(defval, invert_main)
+                for _f in flagl:
+                    f,invert = _remove_negative_kv(_f, 0)
+                    self._aliases[f] = (flag_main, invert_main ^ invert)
+                    self._hasparms[f] = has_parm
+                    if f == '#':
+                        self._shortopts += '0123456789'
+                        flagl_nice.append('-#')
+                    elif len(f) == 1:
+                        self._shortopts += f + (has_parm and ':' or '')
+                        flagl_nice.append('-' + f)
+                    else:
+                        f_nice = re.sub(r'\W', '_', f)
+                        self._aliases[f_nice] = (flag_main,
+                                                 invert_main ^ invert)
+                        self._longopts.append(f + (has_parm and '=' or ''))
+                        self._longopts.append('no-' + f)
+                        flagl_nice.append('--' + _f)
+                flags_nice = ', '.join(flagl_nice)
+                if has_parm:
+                    flags_nice += ' ...'
+                prefix = '    %-20s  ' % flags_nice
+                argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(),
+                                                initial_indent=prefix,
+                                                subsequent_indent=' '*28))
+                out.append(argtext + '\n')
+                last_was_option = True
+            else:
+                out.append('\n')
+                last_was_option = False
+        return ''.join(out).rstrip() + '\n'
+
+    def usage(self, msg=""):
+        """Print usage string to stderr and abort."""
+        sys.stderr.write(self._usagestr)
+        if msg:
+            sys.stderr.write(msg)
+        e = self._onabort and self._onabort(msg) or None
+        if e:
+            raise e
+
+    def fatal(self, msg):
+        """Print an error message to stderr and abort with usage string."""
+        msg = '\nerror: %s\n' % msg
+        return self.usage(msg)
+
+    def parse(self, args):
+        """Parse a list of arguments and return (options, flags, extra).
+
+        In the returned tuple, "options" is an OptDict with known options,
+        "flags" is a list of option flags that were used on the command-line,
+        and "extra" is a list of positional arguments.
+        """
+        try:
+            (flags,extra) = self.optfunc(args, self._shortopts, self._longopts)
+        except getopt.GetoptError, e:
+            self.fatal(e)
+
+        opt = OptDict(aliases=self._aliases)
+
+        for k,v in self._defaults.iteritems():
+            opt[k] = v
+
+        for (k,v) in flags:
+            k = k.lstrip('-')
+            if k in ('h', '?', 'help', 'usage'):
+                self.usage()
+            if (self._aliases.get('#') and
+                  k in ('0','1','2','3','4','5','6','7','8','9')):
+                v = int(k)  # guaranteed to be exactly one digit
+                k, invert = self._aliases['#']
+                opt['#'] = v
+            else:
+                k, invert = opt._unalias(k)
+                if not self._hasparms[k]:
+                    assert(v == '')
+                    v = (opt._opts.get(k) or 0) + 1
+                else:
+                    v = _intify(v)
+            opt[k] = _invert(v, invert)
+        return (opt,flags,extra)
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/presterastats/presterastats.py b/presterastats/presterastats.py
new file mode 100755
index 0000000..bb703c5
--- /dev/null
+++ b/presterastats/presterastats.py
@@ -0,0 +1,113 @@
+#!/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.
+
+"""Retrieve packet statistics from cpss, emit in JSON format."""
+
+__author__ = 'poist@google.com (Gregory Poist)'
+
+import itertools
+import json
+import os
+import signal
+import subprocess
+import sys
+import textwrap
+import threading
+
+import options
+
+
+optspec = """
+presterastats [options]
+--
+ports=           prestera ports to collect [0/0,0/4,0/24,0/25]
+timeout=         seconds to wait for cpss response [5]
+"""
+
+
+class PresteraStats(object):
+  """Class wrapping a cpss command to request stats."""
+
+  def __init__(self, ports, timeout):
+    self.ports = ports
+    self.timeout = timeout
+
+  def WriteToStderr(self, msg, is_json=False):
+    """Write a message to stderr."""
+    if is_json:
+      # Make the json easier to parse from the logs.
+      json_data = json.loads(msg)
+      json_str = json.dumps(json_data, sort_keys=True, indent=2,
+                            separators=(',', ': '))
+      # Logging pretty-printed json is like logging one huge line. Logos is
+      # configured to limit lines to 768 characters. Split the logged output at
+      # half of that to make sure logos doesn't clip our output.
+      sys.stderr.write('\n'.join(textwrap.wrap(json_str, width=384)))
+      sys.stderr.flush()
+    else:
+      sys.stderr.write(msg)
+      sys.stderr.flush()
+
+  def StartCpssSubprocess(self):
+    """Start execution of the cpss_cmd sub-process."""
+    return subprocess.Popen(['cpss_cmd'],
+                            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+                            preexec_fn=os.setsid)
+
+  def GetMibStats(self):
+    """Extract statistics from cpss_cmd output."""
+    result = None
+    proc = self.StartCpssSubprocess()
+    if not proc:
+      self.WriteToStderr('Failed to start subprocess.')
+      return
+    kill_proc = lambda p: os.killpg(os.getpgid(p.pid), signal.SIGTERM)
+    timer = threading.Timer(self.timeout, kill_proc, [proc])
+    cpss_cmd_prefix = 'show interfaces mac json-counters ethernet '
+    try:
+      timer.start()
+      cpssout, _ = proc.communicate(input=cpss_cmd_prefix + self.ports + '\n')
+
+      # itertools magic to take only the lines between JSONSTART and JSONEND.
+      it = itertools.dropwhile(lambda line: line.strip() != 'JSONSTART',
+                               cpssout.splitlines())
+      it = itertools.islice(it, 1, None)
+      it = itertools.takewhile(lambda line: line.strip() != 'JSONEND', it)
+
+      # smack itertools iterable down to a string
+      result = ''.join(it)
+    finally:
+      timer.cancel()
+
+    if result:
+      return json.loads(result)
+
+
+def main():
+  o = options.Options(optspec)
+  (opt, unused_flags, unused_extra) = o.parse(sys.argv[1:])
+  prestera = PresteraStats(opt.ports, opt.timeout)
+  results = prestera.GetMibStats()
+
+  if results:
+    print json.dumps(results, sort_keys=True,
+                     indent=2, separators=(',', ': '))
+    sys.exit(0)
+
+  sys.exit(1)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/presterastats/presterastats_test.py b/presterastats/presterastats_test.py
new file mode 100644
index 0000000..b96e019
--- /dev/null
+++ b/presterastats/presterastats_test.py
@@ -0,0 +1,113 @@
+#!/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 presterastats."""
+
+__author__ = 'poist@google.com (Gregory Poist)'
+
+import json
+import os
+import subprocess
+import time
+import unittest
+
+import presterastats
+
+VALID_JSON_RESPONSE = """
+garbage here
+JSONSTART
+{ "valid": {
+  "0/0": {
+    "unicast_packets_sent": 19
+  }
+}}
+JSONEND
+garbage there
+"""
+
+VALID_JSON_CONTENT = """
+{ "valid": {
+  "0/0": {
+    "unicast_packets_sent": 19
+  }
+}}
+"""
+
+NO_START_BLOCK_RESPONSE = """
+blah
+blah
+blah
+no json here
+"""
+
+
+class FakePresteraStats(presterastats.PresteraStats):
+  """Mock PresteraStats."""
+
+  def StartCpssSubprocess(self):
+    return subprocess.Popen(self.command.split(),
+                            stdin=self.cpss_in, stdout=self.cpss_out,
+                            preexec_fn=os.setsid)
+
+
+class PresteraStatsTest(unittest.TestCase):
+
+  def setUp(self):
+    self.poller = FakePresteraStats('0/0,0/4', 1)
+    self.poller.command = ''
+    self.poller.cpss_in = subprocess.PIPE
+    self.poller.cpss_out = subprocess.PIPE
+
+  def testRequestStatsTimeout(self):
+    start_time = time.time()
+    self.poller.command = '/bin/sleep 30'
+    self.poller.subproc_response_fd = subprocess.PIPE
+    result = self.poller.GetMibStats()
+    end_time = time.time()
+
+    self.assertIsNone(result)
+    self.assertTrue(end_time - start_time < 30)
+
+  def testValidJsonBlock(self):
+    self.poller.command = '/bin/cat'
+    self.poller.cpss_in, out_fd = os.pipe()
+    os.write(out_fd, VALID_JSON_RESPONSE)
+    os.close(out_fd)
+    result = self.poller.GetMibStats()
+    os.close(self.poller.cpss_in)
+
+    self.assertEquals(result, json.loads(VALID_JSON_CONTENT))
+
+  def testNoJsonBlock(self):
+    self.poller.command = '/bin/cat'
+    self.poller.cpss_in, out_fd = os.pipe()
+    os.write(out_fd, NO_START_BLOCK_RESPONSE)
+    os.close(out_fd)
+    result = self.poller.GetMibStats()
+    os.close(self.poller.cpss_in)
+
+    self.assertIsNone(result)
+
+  def testBogusCommand(self):
+    self.poller.command = 'aintgottimeforthat'
+    try:
+      _ = self.poller.GetMibStats()
+      self.fail('Should explode')
+    except OSError:
+      pass
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/signing/repack.py b/signing/repack.py
index 031b20d..54dce21 100755
--- a/signing/repack.py
+++ b/signing/repack.py
@@ -146,7 +146,7 @@
 def RealSignBolt(hostdir, fname):
   """Sign the kernel image with the bolt signing tool.
 
-  This uses the broadcom signing tool to sign a kerel for
+  This uses the broadcom signing tool to sign a kernel for
   secure boot.  The function expects to be called with the
   current directory equal to out/build/images.
 
@@ -159,12 +159,15 @@
   """
 
   tool_path = os.path.join(hostdir, 'usr/bin/boltsigning/gfiber')
-  shutil.copy('signing/gfhd254_private.pem', '/dev/shm/gfhd254_private.pem')
+  # Remove any existing file before trying to copy the key over.
+  key_path = '/dev/shm/gfhd254_private.pem'
+  subprocess.call(['shred', '-fuz', key_path])
+  shutil.copy('signing/gfhd254_private.pem', key_path)
   shutil.copy(fname, os.path.join(tool_path, 'kernel.img'))
   exit_code = subprocess.call(
       ['wine', '../imagetool.exe', '-L', 'kernel', '-O', 'kernel.cfg',
        '-K', 'signing=true'], cwd=tool_path)
-  subprocess.call(['shred', '-fuz', '/dev/shm/gfhd254_private.pem'])
+  subprocess.call(['shred', '-fuz', key_path])
   if exit_code:
     raise Exception('bolt signing tool returned exit code %d' % (exit_code,))
   shutil.copy(os.path.join(tool_path, 'kernel.img.signed'), fname)
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..7267856 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,12 @@
 
 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 kOptServerId = 1007;
 
 const int kOptMinTransferTime = 1100;
 const int kOptMaxTransferTime = 1101;
@@ -79,19 +87,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 +120,9 @@
     {"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},
+    {"serverid", required_argument, nullptr, kOptServerId},  // ignored
     {nullptr, 0, nullptr, 0},
 };
 const int kMaxNumber = 1000;
@@ -124,12 +139,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 +162,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 +171,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 +233,7 @@
           std::cerr << "Invalid global host " << optarg << "\n";
           return false;
         }
-        options->global_host = url;
+        options->global_url = url;
         break;
       }
       case 'h':
@@ -228,17 +251,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 +302,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 +330,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 +346,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 +407,7 @@
                     << optarg << "'\n";
           return false;
         }
-        options->interval_millis = static_cast<int>(interval_millis);
+        options->interval_millis = interval_millis;
         break;
       }
       case kOptPingRuntime: {
@@ -387,7 +422,7 @@
                     << optarg << "'\n";
           return false;
         }
-        options->ping_runtime = static_cast<int>(ping_runtime);
+        options->ping_runtime_millis = ping_runtime;
         break;
       }
       case kOptPingTimeout: {
@@ -402,9 +437,15 @@
                     << 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;
+      case kOptServerId:
+        // --serverid is accepted but ignored, for backwards compatibility.
+        break;
       default:
         return false;
     }
@@ -423,10 +464,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 +480,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 580b54b..d3c00d0 100644
--- a/speedtest/utils.cc
+++ b/speedtest/utils.cc
@@ -62,12 +62,24 @@
   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;
   }
   std::istringstream n(str);
-  return n >> *result;
+  return !(n >> *result).fail();
 }
 
 // Trim from start in place
@@ -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/sysmgr/peripheral/fancontrol.cc b/sysmgr/peripheral/fancontrol.cc
index a19afa9..8e81fe1 100644
--- a/sysmgr/peripheral/fancontrol.cc
+++ b/sysmgr/peripheral/fancontrol.cc
@@ -226,6 +226,16 @@
                           temp_overheat : 97,
                         };
 
+const FanControlParams FanControl::kGFLT300FanCtrlSocDefaults = {
+                          temp_setpt    : 0,  /* No fan */
+                          temp_max      : 0,
+                          temp_step     : 0,
+                          duty_cycle_min: 0,
+                          duty_cycle_max: 0,
+                          pwm_step      : 0,
+                          temp_overheat : 97,
+                        };
+
 FanControl::~FanControl() {
   Terminate();
 }
@@ -310,6 +320,9 @@
     case BRUNO_GFLT110:
       pfan_ctrl_params_[BRUNO_SOC] = kGFLT110FanCtrlSocDefaults;
       break;
+    case BRUNO_GFLT300:
+      pfan_ctrl_params_[BRUNO_SOC] = kGFLT300FanCtrlSocDefaults;
+      break;
     case BRUNO_UNKNOWN:
       LOG(LS_ERROR) << "Invalid platform type, ignore ... " << platform_;
       break;
diff --git a/sysmgr/peripheral/fancontrol.h b/sysmgr/peripheral/fancontrol.h
index 2e552e3..06e8499 100644
--- a/sysmgr/peripheral/fancontrol.h
+++ b/sysmgr/peripheral/fancontrol.h
@@ -81,6 +81,7 @@
   static const FanControlParams kGFHD254FanCtrlAux1Defaults;
 
   static const FanControlParams kGFLT110FanCtrlSocDefaults;
+  static const FanControlParams kGFLT300FanCtrlSocDefaults;
 
   explicit FanControl(Platform *platform)
       : state_(OFF),
diff --git a/sysmgr/peripheral/platform.cc b/sysmgr/peripheral/platform.cc
index 23572a0..ba239fd 100644
--- a/sysmgr/peripheral/platform.cc
+++ b/sysmgr/peripheral/platform.cc
@@ -20,6 +20,7 @@
   Platform("GFLT110", BRUNO_GFLT110, false, false, false),
   Platform("GFLT120", BRUNO_GFLT110, false, false, false),
   Platform("GFHD254", BRUNO_GFHD254, false, true, true),
+  Platform("GFLT300", BRUNO_GFLT300, false, false, false),
   Platform("UNKNOWN PLATFORM", BRUNO_UNKNOWN, false, false,  false),
 };
 
diff --git a/sysmgr/peripheral/platform.h b/sysmgr/peripheral/platform.h
index 80e0a74..91a5925 100644
--- a/sysmgr/peripheral/platform.h
+++ b/sysmgr/peripheral/platform.h
@@ -26,6 +26,7 @@
   BRUNO_GFHD200,          /* Camaro */
   BRUNO_GFLT110,          /* Fiber Jack */
   BRUNO_GFHD254,          /* Lockdown */
+  BRUNO_GFLT300,          /* Go-Long FiberJack */
   BRUNO_UNKNOWN
 };
 
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 cab824c..e75a337 100644
--- a/taxonomy/dhcp.py
+++ b/taxonomy/dhcp.py
@@ -36,13 +36,18 @@
 
     '6,3,1,15,66,67,13,44,2,42,12': ['brotherprinter'],
 
+    '1,3,6,15,44,47': ['canonprinter'],
+
     '1,121,33,3,6,12,15,26,28,51,54,58,59,119,252': ['chromeos'],
     '1,121,33,3,6,12,15,26,28,51,54,58,59,119': ['chromeos'],
 
-    '1,3,6': ['dashbutton'],
+    '1,3,6': ['dashbutton', 'canonprinter'],
+
+    '1,3,6,28': ['ecobee'],
 
     '1,3,6,12,15,17,28,40,41,42': ['epsonprinter'],
 
+    '6,3,1,15,66,67,13,44': ['hpprinter'],
     '6,3,1,15,66,67,13,44,12': ['hpprinter'],
     '6,3,1,15,66,67,13,44,12,81': ['hpprinter'],
     '6,3,1,15,66,67,13,44,119,12,81,252': ['hpprinter'],
@@ -52,22 +57,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'],
+    '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 b62f0a2..c3673bb 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -24,21 +24,30 @@
 # Galaxy S4.
 database = {
     '00:bb:3a': ['amazon'],
+    '0c:47:c9': ['amazon'],
     '10:ae:60': ['amazon'],
     '28:ef:01': ['amazon'],
+    '44:65:0d': ['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'],
     '74:d0:2b': ['asus'],
     'ac:22:0b': ['asus'],
     'bc:ee:7b': ['asus'],
     'd8:50:e6': ['asus'],
+    'f8:32:e4': ['asus'],
 
     '30:8c:fb': ['dropcam'],
 
@@ -51,17 +60,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'],
@@ -69,29 +83,49 @@
     'd8:b3:77': ['htc'],
     'e8:99:c4': ['htc'],
 
+    '00:34:da': ['lg'],
     '0c:48:85': ['lg'],
     '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'],
     'a0:91:69': ['lg'],
     '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:0d:3a': ['microsoft'],
+    '00:12:5a': ['microsoft'],
+    '00:17:fa': ['microsoft'],
+    '00:1d:d8': ['microsoft'],
+    '00:22:48': ['microsoft'],
+    '00:25:ae': ['microsoft'],
+    '00:50:f2': ['microsoft'],
     '28:18:78': ['microsoft'],
+    '30:59:b7': ['microsoft'],
+    '4c:0b:be': ['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'],
+    'c0:33:5e': ['microsoft'],
 
+    '14:1a:a3': ['motorola'],
     '14:30:c6': ['motorola'],
     '1c:56:fe': ['motorola'],
     '24:da:9b': ['motorola'],
@@ -133,62 +167,90 @@
 
     '18:b4:30': ['nest'],
 
-    '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'],
+    '18:22:7e': ['samsung'],
+    '20:6e:9c': ['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'],
+    '3c:a1:0d': ['samsung'],
     '40:0e:85': ['samsung'],
     '48:5a:3f': ['samsung', 'wisol'],
+    '4c:bc:a5': ['samsung'],
+    '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'],
     'ec:9b:f3': ['samsung'],
     'f0:25:b7': ['samsung'],
     'f4:09:d8': ['samsung'],
+    'fc:f1:36': ['samsung'],
 
     '00:d9:d1': ['sony'],
     '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/tests/wifi_test.py b/taxonomy/tests/wifi_test.py
index 5ccb745..13eb234 100755
--- a/taxonomy/tests/wifi_test.py
+++ b/taxonomy/tests/wifi_test.py
@@ -30,7 +30,7 @@
     dhcp.DHCP_SIGNATURE_FILE = 'testdata/dhcp.signatures'
 
   def testLookup(self):
-    signature = ('wifi|probe:0,1,50,45,htcap:186e|assoc:0,1,50,48,'
+    signature = ('wifi4|probe:0,1,50,45,htcap:186e|assoc:0,1,50,48,'
                  '221(0050f2,2),45,127,htcap:086c,htmcs:000000ff')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual(3, len(taxonomy))
@@ -114,76 +114,76 @@
     self.assertIn('Unknown', taxonomy[1])
 
   def testUnknown(self):
-    signature = 'wifi|probe:0,1,2,vhtcap:0033|assoc:3,4,vhtcap:0033'
+    signature = 'wifi4|probe:0,1,2,vhtcap:0033|assoc:3,4,vhtcap:0033'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertIn('802.11ac', taxonomy[2])
     self.assertNotIn('802.11n', taxonomy[2])
     self.assertNotIn('802.11a/b/g', taxonomy[2])
-    signature = 'wifi|probe:0,1,2,htcap:0033|assoc:3,4,htcap:0033'
+    signature = 'wifi4|probe:0,1,2,htcap:0033|assoc:3,4,htcap:0033'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertNotIn('802.11ac', taxonomy[2])
     self.assertIn('802.11n', taxonomy[2])
     self.assertNotIn('802.11a/b/g', taxonomy[2])
-    signature = 'wifi|probe:0,1,2|assoc:3,4'
+    signature = 'wifi4|probe:0,1,2|assoc:3,4'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertNotIn('802.11ac', taxonomy[2])
     self.assertNotIn('802.11n', taxonomy[2])
     self.assertIn('802.11a/b/g', taxonomy[2])
 
   def test802_11n_NssWidth(self):
-    signature = 'wifi|probe:0|assoc:1,htcap:012c,htagg:03,htmcs:000000ff'
+    signature = 'wifi4|probe:0|assoc:1,htcap:012c,htagg:03,htmcs:000000ff'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11n n:1,w:20', taxonomy[2])
-    signature = 'wifi|probe:0|assoc:1,htcap:0102,htagg:03,htmcs:0000ffff'
+    signature = 'wifi4|probe:0|assoc:1,htcap:0102,htagg:03,htmcs:0000ffff'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11n n:2,w:40', taxonomy[2])
-    signature = 'wifi|probe:0|assoc:1,htcap:0200,htagg:03,htmcs:00ffffff'
+    signature = 'wifi4|probe:0|assoc:1,htcap:0200,htagg:03,htmcs:00ffffff'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11n n:3,w:20', taxonomy[2])
-    signature = 'wifi|probe:0|assoc:1,htcap:0302,htagg:03,htmcs:ffffffff'
+    signature = 'wifi4|probe:0|assoc:1,htcap:0302,htagg:03,htmcs:ffffffff'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11n n:4,w:40', taxonomy[2])
-    signature = 'wifi|probe:0|assoc:1'
+    signature = 'wifi4|probe:0|assoc:1'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11a/b/g n:1,w:20', taxonomy[2])
 
   def test802_11ac_Width(self):
-    signature = ('wifi|probe:0|assoc:1,htcap:0302,htmcs:000000ff,'
+    signature = ('wifi4|probe:0|assoc:1,htcap:0302,htmcs:000000ff,'
                  'vhtcap:00000000,vhtrxmcs:0000ffaa,vhttxmcs:0000ffaa')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11ac n:4,w:80', taxonomy[2])
-    signature = ('wifi|probe:0|assoc:1,htcap:0200,htmcs:000000ff,'
+    signature = ('wifi4|probe:0|assoc:1,htcap:0200,htmcs:000000ff,'
                  'vhtcap:00000004,vhtrxmcs:0000ffea,vhttxmcs:0000ffea')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11ac n:3,w:160', taxonomy[2])
-    signature = ('wifi|probe:0|assoc:1,htcap:0200,htmcs:000000ff,'
+    signature = ('wifi4|probe:0|assoc:1,htcap:0200,htmcs:000000ff,'
                  'vhtcap:00000004,vhtrxmcs:0000fffa,vhttxmcs:0000fffa')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11ac n:2,w:160', taxonomy[2])
-    signature = ('wifi|probe:0|assoc:1,htcap:0200,htmcs:000000ff,'
+    signature = ('wifi4|probe:0|assoc:1,htcap:0200,htmcs:000000ff,'
                  'vhtcap:00000004,vhtrxmcs:0000fffe,vhttxmcs:0000fffe')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11ac n:1,w:160', taxonomy[2])
-    signature = 'wifi|probe:0|assoc:1,vhtcap:00000008'
+    signature = 'wifi4|probe:0|assoc:1,vhtcap:00000008'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11ac n:?,w:80+80', taxonomy[2])
-    signature = 'wifi|probe:0|assoc:1,vhtcap:0000000c'
+    signature = 'wifi4|probe:0|assoc:1,vhtcap:0000000c'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11ac n:?,w:??', taxonomy[2])
 
   def testPerformanceInfoBroken(self):
-    signature = ('wifi|probe:0,htmcs:000000ff|assoc:0,htmcs:000000ff')
+    signature = ('wifi4|probe:0,htmcs:000000ff|assoc:0,htmcs:000000ff')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11a/b/g n:1,w:20', taxonomy[2])
-    signature = ('wifi|probe:0,htcap:wrong,htmcs:ffffffff|'
+    signature = ('wifi4|probe:0,htcap:wrong,htmcs:ffffffff|'
                  'assoc:0,htcap:wrong,htmcs:ffffffff')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11n n:4,w:??', taxonomy[2])
-    signature = ('wifi|probe:0,htcap:012c,htmcs:wrong|'
+    signature = ('wifi4|probe:0,htcap:012c,htmcs:wrong|'
                  'assoc:0,htcap:012c,htmcs:wrong')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11n n:?,w:20', taxonomy[2])
-    signature = ('wifi|probe:0,htcap:wrong,htmcs:wrong|'
+    signature = ('wifi4|probe:0,htcap:wrong,htmcs:wrong|'
                  'assoc:0,htcap:wrong,htmcs:wrong')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual('802.11n n:?,w:??', taxonomy[2])
@@ -191,13 +191,13 @@
   def testRealClientsPerformance(self):
     """Test the performance information for a few real clients."""
     # Nest Thermostat
-    sig = ('wifi|probe:0,1,50,45,htcap:0130,htagg:18,htmcs:000000ff|assoc:'
+    sig = ('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')
     taxonomy = wifi.identify_wifi_device(sig, '18:b4:30:00:00:01')
     self.assertEqual('802.11n n:1,w:20', taxonomy[2])
     # Samsung Galaxy S4
     sig = (
-        'wifi|probe:0,1,45,127,191,221(001018,2),221(00904c,51),221(00904c,'
+        'wifi4|probe:0,1,45,127,191,221(001018,2),221(00904c,51),221(00904c,'
         '4),221(0050f2,8),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,'
         'vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,33,36,48,45,127,191,'
         '221(001018,2),221(00904c,4),221(0050f2,2),htcap:006f,htagg:17,htmcs:'
@@ -206,7 +206,7 @@
     self.assertEqual('802.11ac n:1,w:80', taxonomy[2])
     # MacBook Pro 802.11ac
     sig = (
-        'wifi|probe:0,1,45,127,191,221(00904c,51),htcap:09ef,htagg:17,'
+        'wifi4|probe:0,1,45,127,191,221(00904c,51),htcap:09ef,htagg:17,'
         'htmcs:0000ffff,vhtcap:0f8259b2,vhtrxmcs:0000ffea,vhttxmcs:0000ffea|'
         'assoc:0,1,33,36,48,45,127,191,221(00904c,51),221(0050f2,2),htcap:09ef,'
         'htagg:17,htmcs:0000ffff,vhtcap:0f8259b2,vhtrxmcs:0000ffea,'
@@ -225,7 +225,7 @@
     look at the Association for determining client
     performance characteristics.
     """
-    signature = ('wifi|probe:0,1,50,45,221(0050f2,8),191,221(0050f2,4),'
+    signature = ('wifi4|probe:0,1,50,45,221(0050f2,8),191,221(0050f2,4),'
                  '221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,'
                  'vhtcap:31811120,vhtrxmcs:01b2fffc,vhttxmcs:01b2fffc,'
                  'wps:Nexus_4|assoc:0,1,50,48,45,221(0050f2,2),'
@@ -234,76 +234,13 @@
     self.assertEqual('802.11n n:1,w:20', taxonomy[2])
 
   def testCorruptFiles(self):
-    signature = 'wifi|probe:0|assoc:1,htcap:this_is_not_a_number'
+    signature = 'wifi4|probe:0|assoc:1,htcap:this_is_not_a_number'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertIn('802.11n', taxonomy[2])
-    signature = 'wifi|probe:0|assoc:1,vhtcap:this_is_not_a_number'
+    signature = 'wifi4|probe:0|assoc:1,vhtcap:this_is_not_a_number'
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertIn('802.11ac', taxonomy[2])
 
-  def testV1Signature(self):
-    sig = ('wifi|probe:0,1,50,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),'
-           'htcap:012c,htagg:03,htmcs:000000ff,wps:Nexus_4|assoc:0,1,50,48,45,'
-           '221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff')
-    expected = (
-        'wifi|probe:0,1,50,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),'
-        'htcap:012c,wps:Nexus_4|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c')
-    v1 = wifi.make_v1_signature(sig)
-    self.assertEqual(v1, expected)
-    sig = ('wifi|probe:0,1,45,127,191,221(001018,2),221(00904c,51),'
-           '221(00904c,4),221(0050f2,8),htcap:006f,htagg:17,htmcs:000000ff,'
-           'vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe|assoc:0,1,33,'
-           '36,48,45,127,191,221(001018,2),221(00904c,4),221(0050f2,2),'
-           'htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,'
-           'vhtrxmcs:0000fffe,vhttxmcs:0000fffe')
-    expected = ('wifi|probe:0,1,45,127,191,221(001018,2),221(00904c,51),'
-                '221(00904c,4),221(0050f2,8),htcap:006f,vhtcap:0f805832|assoc:'
-                '0,1,33,36,48,45,127,191,221(001018,2),221(00904c,4),'
-                '221(0050f2,2),htcap:006f,vhtcap:0f805832')
-    v1 = wifi.make_v1_signature(sig)
-    self.assertEqual(v1, expected)
-
-  def testV2Signature(self):
-    sig = 'wifi4|probe:0,1,50|assoc:0,1,50,htcap:012c,txpow:3210'
-    exp = 'wifi|probe:0,1,50|assoc:0,1,50,htcap:012c'
-    v2 = wifi.make_v2_signature(sig)
-    self.assertEqual(v2, exp)
-    sig = 'wifi4|probe:0,1,50|assoc:0,1,50,extcap:0123456789abcdef'
-    exp = 'wifi|probe:0,1,50|assoc:0,1,50,extcap:67452301'
-    v2 = wifi.make_v2_signature(sig)
-    self.assertEqual(v2, exp)
-    # iPhone 6s signature
-    sig = ('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,45,127,221(001018,2),'
-           '221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,'
-           'txpow:1202,extcap:0000000000000040|os:ios')
-    exp = ('wifi|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),'
-           'htcap:002d,htagg:17,htmcs:0000ffff,'
-           'extcap:84080004|assoc:0,1,50,33,36,48,45,127,221(001018,2),'
-           '221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,'
-           'extcap:00000000|os:ios')
-    v2 = wifi.make_v2_signature(sig)
-    self.assertEqual(v2, exp)
-
-  def testV2SignatureSmallExtcap(self):
-    sig = 'wifi4|probe:0,1,50|assoc:0,1,50,extcap:01234567'
-    exp = 'wifi|probe:0,1,50|assoc:0,1,50,extcap:67452301'
-    v2 = wifi.make_v2_signature(sig)
-    self.assertEqual(v2, exp)
-    sig = 'wifi4|probe:0,1,50|assoc:0,1,50,extcap:012345'
-    exp = 'wifi|probe:0,1,50|assoc:0,1,50,extcap:452301'
-    v2 = wifi.make_v2_signature(sig)
-    self.assertEqual(v2, exp)
-    sig = 'wifi4|probe:0,1,50|assoc:0,1,50,extcap:0123'
-    exp = 'wifi|probe:0,1,50|assoc:0,1,50,extcap:2301'
-    v2 = wifi.make_v2_signature(sig)
-    self.assertEqual(v2, exp)
-    sig = 'wifi4|probe:0,1,50|assoc:0,1,50,extcap:01'
-    exp = 'wifi|probe:0,1,50|assoc:0,1,50,extcap:01'
-    v2 = wifi.make_v2_signature(sig)
-    self.assertEqual(v2, exp)
-
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index afbd709..ee4aee2 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -26,18 +26,28 @@
     '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,50,45,127,221(0050f2,4),221(506f9a,9),htcap:01ed,htagg:1f,htmcs:0000ffff,extcap:00,wps:AFTS|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,127,htcap:008c,htagg:1f,htmcs:0000ffff,extcap:00000a02':
+        ('', 'Amazon Fire TV', '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':
         ('', 'Amazon Fire TV Stick', '5GHz'),
     'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:003c,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),htcap:003c,htagg:1b,htmcs:0000ffff,txpow:170c|oui:amazon':
@@ -78,9 +88,18 @@
     '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'),
 
+    'wifi4|probe:0,1,3,45,50,htcap:007e,htagg:00,htmcs:000000ff|assoc:0,1,45,48,50,221(0050f2,2),htcap:000c,htagg:1b,htmcs:000000ff|os:canonprinter':
+        ('', 'Canon Printer', '2.4GHz'),
+
     'wifi4|probe:0,1,45,191,htcap:11e2,htagg:17,htmcs:0000ffff,vhtcap:038071a0,vhtrxmcs:0000fffa,vhttxmcs:0000fffa|assoc:0,1,48,45,127,191,221(0050f2,2),htcap:11e6,htagg:17,htmcs:0000ffff,vhtcap:038001a0,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000000000000040|os:chromeos':
         ('Intel_7260', 'Chromebook Pixel 2', '5GHz'),
     'wifi4|probe:0,1,45,191,htcap:11e2,htagg:17,htmcs:0000ffff,vhtcap:038071a0,vhtrxmcs:0000fffa,vhttxmcs:0000fffa|assoc:0,1,48,45,127,191,221(0050f2,2),htcap:11ee,htagg:17,htmcs:0000ffff,vhtcap:038001a0,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000000000000040|os:chromeos':
@@ -97,9 +116,6 @@
     'wifi4|probe:0,1,50,3,45,htcap:11ef,htagg:1b,htmcs:0000ffff|assoc:0,1,50,48,45,221(0050f2,2),htcap:11ef,htagg:1b,htmcs:0000ffff|os:chromeos':
         ('AR5822', 'Chromebook 14" HP', '2.4GHz'),
 
-    'wifi|probe:0,1,3,45,50,htcap:01ff|assoc:0,1,50,127,221(0050f2,1),221(0050f2,2),45,htcap:01ff|os:chromeos':
-        ('Marvell_88W8897', 'Chromebook 14" HP (Tegra)', '2.4GHz'),
-
     'wifi4|probe:0,1,45,50,htcap:016e,htagg:03,htmcs:0000ffff|assoc:0,1,48,127,221(0050f2,2),45,htcap:016e,htagg:03,htmcs:0000ffff,extcap:00':
         ('AR9382', 'Chromebook 11" Samsung', '5GHz'),
     'wifi4|probe:0,1,3,45,50,htcap:016e,htagg:03,htmcs:0000ffff|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:016e,htagg:03,htmcs:0000ffff,extcap:00':
@@ -116,22 +132,37 @@
     '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'),
 
-    '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':
+    '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'),
+
+    'wifi4|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,extcap:00|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':
+    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff|os:hpprinter':
         ('', 'HP Printer', '2.4GHz'),
-    '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'),
-    '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':
+    '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'),
+    'wifi4|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,extcap:00|os:hpprinter':
         ('', 'HP Printer', '2.4GHz'),
     'wifi4|probe:0,1,3,45,50,127,htcap:010c,htagg:1b,htmcs:0000ffff,extcap:00|assoc:0,1,45,48,127,50,221(0050f2,2),htcap:016c,htagg:1b,htmcs:000000ff,extcap:00|os:hpprinter':
         ('', 'HP Printer', '2.4GHz'),
+    'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:hpprinter':
+        ('', 'HP Printer', '2.4GHz'),
 
     'wifi4|probe:0,1,45,127,191,221(001018,2),221(00904c,51),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:03800032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:03800032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e008,extcap:0000000000000040|oui:htc':
         ('BCM4335', 'HTC One', '5GHz'),
@@ -151,6 +182,8 @@
         ('WCN3680', 'HTC One M8', '5GHz'),
     'wifi4|probe:0,1,3,45,221(0050f2,8),191,127,107,221(0050f2,4),221(506f9a,9),221(506f9a,16),htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a820040,wps:HTC_One_M8|assoc:0,1,48,45,221(0050f2,2),191,127,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:00000a8201400040':
         ('WCN3680', 'HTC One M8', '5GHz'),
+    'wifi4|probe:0,1,3,45,221(0050f2,8),191,127,107,221(0050f2,4),221(506f9a,10),221(506f9a,9),221(506f9a,16),htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:000000800040,wps:HTC_One_M8|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:1e0d,extcap:0000008001400040':
+        ('WCN3680', 'HTC One M8', '5GHz'),
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(0050f2,4),221(506f9a,9),221(506f9a,16),htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a820040,wps:HTC_One_M8|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a8201400000':
         ('WCN3680', 'HTC One M8', '2.4GHz'),
     '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:00000a8201400000|oui:htc':
@@ -163,6 +196,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,13 +221,15 @@
         ('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':
         ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
         ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
+    'wifi4|probe:0,1,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01fe,htagg:1b,htmcs:0000ffff,extcap:00000004|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01fe,htagg:1b,htmcs:0000ffff,txpow:e708|os:ios':
+        ('BCM4334', 'iPad (4th gen or Air)', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
         ('BCM4334', 'iPad (4th gen or Air)', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:01bc,htagg:1b,htmcs:0000ffff,extcap:00000804|assoc:0,1,33,36,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff,txpow:1805|os:ios':
@@ -229,6 +272,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 +300,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 +341,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 +357,10 @@
         ('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,50,3,45,127,107,221(00904c,51),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: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 +373,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 +391,29 @@
         ('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,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,wps:LG_V400|assoc:0,1,33,36,48,70,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a0200000000':
+        ('', 'LG Pad v400', '5GHz'),
+
+    '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 +516,10 @@
         ('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':
         ('BCM4339', 'Nexus 5', '2.4GHz'),
     'wifi4|probe:0,1,50,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':
@@ -466,8 +547,6 @@
         ('QCA6174', 'Nexus 5X', '2.4GHz'),
     'wifi4|probe:0,1,50,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,extcap:000000000000000080|oui:lg':
         ('QCA6174', 'Nexus 5X', '2.4GHz'),
-    'wifi4|probe:0,1,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,33,36,48,70,45,221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,txpow:1e08,extcap:000000000000004080|oui:lg':
-        ('QCA6174', 'Nexus 5X', '2.4GHz'),
     'wifi4|probe:0,1,50,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,txpow:1e08,extcap:000000000000000080|oui:lg':
         ('QCA6174', 'Nexus 5X', '2.4GHz'),
     'wifi4|probe:0,1,50,127,extcap:00000a020100004080|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,txpow:1e08,extcap:000000000000000080|oui:lg':
@@ -481,6 +560,10 @@
         ('BCM4356', 'Nexus 6', '5GHz'),
     'wifi4|probe:0,1,45,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,wps:Nexus_6|assoc:0,1,33,36,48,45,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009':
         ('BCM4356', 'Nexus 6', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000088001400040,wps:Nexus_6|assoc:0,1,33,36,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 6', '5GHz'),
+    'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140,wps:Nexus_6|assoc:0,1,33,36,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:000008800140':
+        ('BCM4356', 'Nexus 6', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_6|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 6', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_6|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
@@ -524,6 +607,8 @@
         ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
     'wifi4|probe:0,1,45,221(0050f2,8),htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,33,36,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:1e0d,extcap:00000a02|oui:asus':
         ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
+    'wifi4|probe:0,1,45,htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,33,36,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:1e0d,extcap:00000a02|oui:asus':
+        ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
     'wifi4|probe:0,1,50,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02':
         ('QCA_WCN3660', 'Nexus 7 (2013)', '2.4GHz'),
     'wifi4|probe:0,1,50,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02':
@@ -532,6 +617,10 @@
         ('QCA_WCN3660', 'Nexus 7 (2013)', '2.4GHz'),
     '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:00000a02|oui:asus':
         ('QCA_WCN3660', 'Nexus 7 (2013)', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02|oui:asus':
+        ('QCA_WCN3660', 'Nexus 7 (2013)', '2.4GHz'),
+    'wifi4|probe:0,1,50,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:Nexus_7|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff':
+        ('QCA_WCN3660', 'Nexus 7 (2013)', '2.4GHz'),
 
     'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:000008800140,wps:Nexus_9|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e009,extcap:000008800140':
         ('BCM4354', 'Nexus 9', '5GHz'),
@@ -539,6 +628,10 @@
         ('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(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:1309,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'),
@@ -547,6 +640,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'),
 
@@ -555,29 +650,77 @@
     '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,45,221(0050f2,4),htcap:01ad,htagg:02,htmcs:0000ffff,wps:WPS_SUPPLICANT_STATION|assoc:0,1,50,45,48,221(0050f2,2),htcap:01ad,htagg:02,htmcs:0000ffff|os:panasonictv':
+        ('', 'Panasonic TV', '2.4GHz'),
+
     'wifi4|probe:0,1,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,45,127,221(001018,2),221(00904c,51),htcap:09bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:09bc,htagg:16,htmcs:0000ffff,txpow:100a,extcap:0000000000000040|os:roku':
+        ('BCM43236', 'Roku 3/SS', '5GHz'),
     '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'),
+        ('BCM43236', 'Roku 3/SS', '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'),
+        ('BCM43236', 'Roku 3/SS', '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'),
@@ -634,6 +777,10 @@
 
     '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,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: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:1202|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'),
 
@@ -675,20 +822,44 @@
 
     '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':
         ('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:000008800140|assoc:0,1,50,33,36,48,70,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,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:0000088001400040|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:e002,extcap:0000088001400040|oui:samsung':
         ('BCM4358', 'Samsung Galaxy S6', '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:0000088001400040|assoc:0,1,33,36,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:e002,extcap:0000088001400040|oui:samsung':
         ('BCM4358', 'Samsung Galaxy S6', '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:e002|oui:samsung':
+        ('BCM4358', 'Samsung Galaxy S6', '5GHz'),
     '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,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,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':
@@ -701,9 +872,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':
@@ -717,6 +888,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'),
@@ -727,115 +907,69 @@
     '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'),
-    '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':
+    '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'),
+    'wifi4|probe:0,1,50,48|assoc:0,1,50,221(0050f2,2),45,51,127,48,htcap:012c,htagg:1b,htmcs:000000ff,extcap:01|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'),
-    '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':
+    'wifi4|probe:0,1,3|assoc:0,1,48,33,36,221(0050f2,2),txpow:1405|oui:microsoft':
         ('', 'Xbox', '5GHz'),
+    'wifi4|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,txpow:1208,extcap:0000000000000140|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,50|assoc:0,1,33,48,50,127,127,221(0050f2,2),45,htcap:012c,htagg:03,htmcs:0000ffff,txpow:1208,extcap:0000000000000140|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'),
 }
 
 
-def make_v2_signature(sig):
-  """Degrade a v3 / v4 signature to match the equivalent v2 signature.
-
-  v3/v4 signatures include additional information from the Wifi MLME
-  frames, based on experience of working with v1 and v2. Return a string
-  which matches what the v2 signature would have been, by removing
-  the additional information.
-
-  This allows us to retain the base of v2 signatures.
-
-  Args:
-    sig: the text signature.
-
-  Returns:
-    a v2 signature.
-  """
-  new_sig = []
-  for s in sig.split('|'):
-    fields = s.split(',')
-    new_fields = []
-    for x in fields:
-      if x.startswith('extcap:'):
-        val = x[7:]
-        # v2 sig includes only the first 4 bytes, swapped.
-        swapped = ''
-        if len(val) >= 8:
-          swapped = swapped + val[6:8]
-        if len(val) >= 6:
-          swapped = swapped + val[4:6]
-        if len(val) >= 4:
-          swapped = swapped + val[2:4]
-        if len(val) >= 2:
-          swapped = swapped + val[0:2]
-        x = 'extcap:' + swapped
-      if x.startswith('cap') or x.startswith('txpow'):
-        # new fields in v3, omit from v2 sig
-        continue
-      new_fields.append(x)
-    new_sig.append(','.join(new_fields))
-  new_sig[0] = 'wifi'
-  return '|'.join(new_sig)
-
-
-def v2only(field):
-  """Return true if field only occurs in a v2 signature."""
-  labels = set(['htagg', 'htmcs', 'vhtrxmcs', 'vhttxmcs', 'intwrk', 'extcap'])
-  for l in labels:
-    if l in field:
-      return True
-  return False
-
-
-def make_v1_signature(sig):
-  """Degrade a v2 signature to match the equivalent v1 signature.
-
-  v2 signatures include additional information from the Wifi MLME
-  frames, based on experience of working with v1. Return a string
-  which matches what the v1 signature would have been, by removing
-  the additional information.
-
-  This allows us to retain the base of v1 signatures.
-
-  Args:
-    sig: the text signature.
-
-  Returns:
-    a v1 signature.
-  """
-  new_sig = []
-  for s in sig.split('|'):
-    fields = s.split(',')
-    new_fields = [x for x in fields if not v2only(x)]
-    new_sig.append(','.join(new_fields))
-  return '|'.join(new_sig)
-
-
 def performance_characteristics(signature):
   """Parse 802.11n/ac capabilities bitmasks from sig.
 
@@ -951,10 +1085,8 @@
     return (SHA256, 'Unknown', PerformanceInfo)
   """
 
-  v4_sig = signature.strip()
-  v2_sig = make_v2_signature(v4_sig)
-  v1_sig = make_v1_signature(v2_sig)
-  perf = performance_info(*performance_characteristics(v4_sig))
+  sig = signature.strip()
+  perf = performance_info(*performance_characteristics(sig))
   name = dhcp.LookupHostname(mac)
   opersys = dhcp.LookupOperatingSystem(mac)
   oui = ethernet.LookupOUI(mac)
@@ -966,11 +1098,10 @@
   for o in oui:
     suffixes.append('|oui:' + o)
   suffixes.append('')
-  for sig in [v4_sig, v2_sig, v1_sig]:
-    for suffix in suffixes:
-      result = database.get(sig + suffix, None)
-      if result is not None:
-        return (result[0], result[1], perf)
+  for suffix in suffixes:
+    result = database.get(sig + suffix, None)
+    if result is not None:
+      return (result[0], result[1], perf)
 
   # We have no idea what the client is.
   slug = 'SHA:' + hashlib.sha256(signature).hexdigest()
@@ -983,13 +1114,5 @@
     # Remove os, oui, etc qualifiers if present.
     a = k.split('|')
     if len(a) > 3:
-      v4_sig = '|'.join(a[0:3])
-      print 'SHA:' + hashlib.sha256(v4_sig).hexdigest() + ' ' + v[1] + ' (unqualified)'
-
-      v2_sig = make_v2_signature(v4_sig)
-      if v2_sig != v4_sig:
-        print 'SHA:' + hashlib.sha256(v2_sig).hexdigest() + ' ' + v[1] + ' (unqualified, v2)'
-
-      v1_sig = make_v1_signature(v2_sig)
-      if v1_sig != v2_sig:
-        print 'SHA:' + hashlib.sha256(v1_sig).hexdigest() + ' ' + v[1] + ' (unqualified, v1)'
+      sig = '|'.join(a[0:3])
+      print 'SHA:' + hashlib.sha256(sig).hexdigest() + ' ' + v[1] + ' (unqualified)'
diff --git a/waveguide/clientinfo_test.py b/waveguide/clientinfo_test.py
index 282ec62..e7471f3 100755
--- a/waveguide/clientinfo_test.py
+++ b/waveguide/clientinfo_test.py
@@ -13,6 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# One version of gpylint wants to see clientinfo first and taxonomy last.
+# Another wants to see the reverse. Cannot satisfy both, so tell both of them
+# to shove the error so far up their stdin that it should never trouble us
+# again.
+# pylint:disable=g-bad-import-order
 import taxonomy
 import clientinfo
 from wvtest import wvtest
diff --git a/waveguide/waveguide.py b/waveguide/waveguide.py
index db2f9ed..cbc9fe8 100755
--- a/waveguide/waveguide.py
+++ b/waveguide/waveguide.py
@@ -783,7 +783,7 @@
         a = wgdata.Assoc(mac=mac, rssi=rssi, last_seen=last_seen, can5G=can5G)
         if mac not in self.assoc_list:
           self.Debug('Added: %r', a)
-          self.wifiblaster_controller.Measure(self.vdevname, mac)
+          self.wifiblaster_controller.MeasureOnAssociation(self.vdevname, mac)
         assoc_list[mac] = a
 
     for line in stdout.split('\n'):
@@ -904,16 +904,18 @@
 
   - Scheduling parameters
 
-    wifiblaster.enable      Enable WiFi performance measurement.
-    wifiblaster.interval    Average time between automated measurements in
-                            seconds, or 0 to disable automated measurements.
-    wifiblaster.measureall  Unix time at which to measure all clients.
+    wifiblaster.enable         Enable WiFi performance measurement.
+    wifiblaster.interval       Average time between automated measurements in
+                               seconds, or 0 to disable automated measurements.
+    wifiblaster.measureall     Unix time at which to measure all clients.
+    wifiblaster.onassociation  Enable WiFi performance measurement after clients
+                               associate.
 
   - Measurement parameters
 
-    wifiblaster.duration    Measurement duration in seconds.
-    wifiblaster.fraction    Number of samples per measurement.
-    wifiblaster.size        Packet size in bytes.
+    wifiblaster.duration       Measurement duration in seconds.
+    wifiblaster.fraction       Number of samples per measurement.
+    wifiblaster.size           Packet size in bytes.
   """
 
   def __init__(self, managers, basedir):
@@ -985,6 +987,12 @@
                     '-f', str(fraction), '-s', str(size),
                     helpers.DecodeMAC(client)])
 
+  def MeasureOnAssociation(self, interface, client):
+    """Measures the performance of a client after association."""
+    onassociation = self._ReadParameter('onassociation', self._StrToBool)
+    if onassociation:
+      self.Measure(interface, client)
+
   def Poll(self, now):
     """Polls the state machine."""
 
diff --git a/waveguide/wifiblaster_controller_test.py b/waveguide/wifiblaster_controller_test.py
index 04e71f8..6b562b1 100755
--- a/waveguide/wifiblaster_controller_test.py
+++ b/waveguide/wifiblaster_controller_test.py
@@ -208,6 +208,15 @@
       wvtest.WVPASSEQ(CountRuns(), 1)
       wc.Poll(501)
       wvtest.WVPASSEQ(CountRuns(), 0)
+
+      # Measure on association only if enabled.
+      wc.MeasureOnAssociation(manager.vdevname,
+                              manager.GetState().assoc[0].mac)
+      wvtest.WVPASSEQ(CountRuns(), 0)
+      WriteConfig('onassociation', 'True')
+      wc.MeasureOnAssociation(manager.vdevname,
+                              manager.GetState().assoc[0].mac)
+      wvtest.WVPASSEQ(CountRuns(), 1)
   finally:
     time.time = oldtime
     shutil.rmtree(d)