Merge "gfch100: add craft ui"
diff --git a/cmds/logos.c b/cmds/logos.c
index 2f453d0..6c186a5 100644
--- a/cmds/logos.c
+++ b/cmds/logos.c
@@ -25,7 +25,6 @@
  *  - cleans up control characters (ie. chars < 32).
  *  - makes sure output lines are in "facility: message" format.
  *  - doesn't rely on syslogd.
- *  - suppresses logging of MAC addresses.
  *  - suppresses logging of filenames of personal media.
  */
 #include <assert.h>
@@ -461,52 +460,6 @@
 }
 
 
-static int is_mac_address(const uint8_t *s, char sep) {
-  if ((s[2] == sep) && (s[5] == sep) && (s[8] == sep) &&
-      (s[11] == sep) && (s[14] == sep) &&
-      isxdigit(s[0]) && isxdigit(s[1]) &&
-      isxdigit(s[3]) && isxdigit(s[4]) &&
-      isxdigit(s[6]) && isxdigit(s[7]) &&
-      isxdigit(s[9]) && isxdigit(s[10]) &&
-      isxdigit(s[12]) && isxdigit(s[13]) &&
-      isxdigit(s[15]) && isxdigit(s[16])) {
-    return 1;
-  }
-
-  return 0;
-}
-
-
-static void blot_out_mac_address(uint8_t *s) {
-  s[12] = 'X';
-  s[13] = 'X';
-  s[15] = 'X';
-  s[16] = 'X';
-}
-
-
-/*
- * search for text patterns which look like MAC addresses,
- * and cross out the last two bytes with 'X' characters.
- * Ex: f8:8f:ca:00:00:01 and f8-8f-ca-00-00-01
- */
-#define MAC_ADDR_LEN 17
-static void suppress_mac_addresses(uint8_t *line, ssize_t len, char sep) {
-  uint8_t *s = line;
-
-  while (len >= MAC_ADDR_LEN) {
-    if (is_mac_address(s, sep)) {
-      blot_out_mac_address(s);
-      s += MAC_ADDR_LEN;
-      len -= MAC_ADDR_LEN;
-    } else {
-      s += 1;
-      len -= 1;
-    }
-  }
-}
-
-
 /*
  * Return true for a character which we expect to terminate a
  * media filename.
@@ -700,9 +653,6 @@
       uint8_t *start = buf, *next = buf + used, *end = buf + used + got, *p;
       while ((p = memchr(next, '\n', end - next)) != NULL) {
         ssize_t linelen = p - start;
-        suppress_mac_addresses(start, linelen, ':');
-        suppress_mac_addresses(start, linelen, '-');
-        suppress_mac_addresses(start, linelen, '_');
         suppress_media_filenames(start, linelen, "/var/media/pictures/");
         suppress_media_filenames(start, linelen, "/var/media/videos/");
         flush(header, headerlen, start, linelen);
diff --git a/cmds/test-logos.py b/cmds/test-logos.py
index c934c58..d930ccb 100755
--- a/cmds/test-logos.py
+++ b/cmds/test-logos.py
@@ -11,10 +11,6 @@
 from wvtest.wvtest import *
 
 
-def macAddressShapedString():
-  chars = '0123456789abcdef::::::'
-  return ''.join(random.choice(chars) for x in range(17))
-
 @wvtest
 def testLogos():
   # We use a SOCK_DGRAM here rather than a normal pipe, because datagram
@@ -90,25 +86,6 @@
   os.write(fd1, '\n')
   WVPASSEQ('<7>fac: booga!\n', _Read())
 
-  # MAC addresses
-  os.write(fd1, 'f8:8f:ca:00:00:01\n')
-  WVPASSEQ('<7>fac: f8:8f:ca:00:XX:XX\n', _Read())
-  os.write(fd1, '8:8f:ca:00:00:01\n')
-  WVPASSEQ('<7>fac: 8:8f:ca:00:00:01\n', _Read())
-  os.write(fd1, '8:8f:ca:00:00:01:\n')
-  WVPASSEQ('<7>fac: 8:8f:ca:00:00:01:\n', _Read())
-  os.write(fd1, ':::semicolons:f8:8f:ca:00:00:01:and:after\n')
-  WVPASSEQ('<7>fac: :::semicolons:f8:8f:ca:00:XX:XX:and:after\n', _Read())
-  os.write(fd1, 'f8-8f-ca-00-00-01\n')
-  WVPASSEQ('<7>fac: f8-8f-ca-00-XX-XX\n', _Read())
-
-  # Send in random strings to look for crashes.
-  for x in range(10):
-    mac = macAddressShapedString()
-    print 'Trying %s to check for crashes' % mac
-    os.write(fd1, mac + '\n')
-    print _Read()
-
   # Filenames
   os.write(fd1, 'Accessing /var/media/pictures/MyPicture.jpg for decode\n')
   WVPASSEQ('<7>fac: Accessing /var/media/pictures/XXXXXXXXXXXXX for decode\n',
diff --git a/cmds/wifi_files.c b/cmds/wifi_files.c
index 31f9d4a..49e77c6 100644
--- a/cmds/wifi_files.c
+++ b/cmds/wifi_files.c
@@ -777,26 +777,6 @@
 }
 
 
-static void TouchUpdateFile()
-{
-  char filename[PATH_MAX];
-  int fd;
-
-  snprintf(filename, sizeof(filename), "%s/updated.new", STATIONS_DIR);
-  if ((fd = open(filename, O_CREAT | O_WRONLY, 0666)) < 0) {
-    perror("TouchUpdatedFile open");
-    exit(1);
-  }
-
-  if (write(fd, "updated", 7) < 7) {
-    perror("TouchUpdatedFile write");
-    exit(1);
-  }
-
-  close(fd);
-} /* TouchUpdateFile */
-
-
 static void ClientStateToLog(gpointer key, gpointer value, gpointer user_data)
 {
   const client_state_t *state = (const client_state_t *)value;
@@ -850,7 +830,6 @@
 void UpdateAssociatedDevices()
 {
   g_hash_table_foreach(clients, ClientStateToJson, NULL);
-  TouchUpdateFile();
 }
 
 
@@ -887,12 +866,21 @@
   int i;
 
   for (i = 0; i < len; i++) {
-    if (isprint(data[i]) && data[i] != ' ' && data[i] != '\\')
-      fprintf(f, "%c", data[i]);
-    else if (data[i] == ' ' && (i != 0 && i != len -1))
-      fprintf(f, " ");
-    else
-      fprintf(f, "\\x%.2x", data[i]);
+    switch(data[i]) {
+      case '\\': fprintf(f, "\\\\"); break;
+      case '"': fprintf(f, "\\\""); break;
+      case '\b': fprintf(f, "\\b"); break;
+      case '\f': fprintf(f, "\\f"); break;
+      case '\n': fprintf(f, "\\n"); break;
+      case '\r': fprintf(f, "\\r"); break;
+      case '\t': fprintf(f, "\\t"); break;
+      default:
+        if ((data[i] <= 0x1f) || !isprint(data[i])) {
+          fprintf(f, "\\u00%02x", data[i]);
+        } else {
+          fprintf(f, "%c", data[i]); break;
+        }
+    }
   }
 }
 
@@ -1027,6 +1015,26 @@
 }
 
 #ifndef UNIT_TESTS
+static void TouchUpdateFile()
+{
+  char filename[PATH_MAX];
+  int fd;
+
+  snprintf(filename, sizeof(filename), "%s/updated.new", STATIONS_DIR);
+  if ((fd = open(filename, O_CREAT | O_WRONLY, 0666)) < 0) {
+    perror("TouchUpdatedFile open");
+    exit(1);
+  }
+
+  if (write(fd, "updated", 7) < 7) {
+    perror("TouchUpdatedFile write");
+    exit(1);
+  }
+
+  close(fd);
+} /* TouchUpdateFile */
+
+
 int main(int argc, char **argv)
 {
   int done = 0;
diff --git a/cmds/wifi_files_test.c b/cmds/wifi_files_test.c
index bb0043c..9d48dd1 100644
--- a/cmds/wifi_files_test.c
+++ b/cmds/wifi_files_test.c
@@ -34,8 +34,8 @@
 {
   FILE *f = tmpfile();
   char buf[32];
-  const uint8_t ssid[] = {'a', 'b', 0x86, ' ', 'c'};  /* not NUL terminated. */
-  const uint8_t expected[] = {'a', 'b', '\\', 'x', '8', '6', ' ', 'c'};
+  const uint8_t ssid[] = {'b', 0x86, ' ', 'c'};  /* not NUL terminated. */
+  const uint8_t expected[] = {'b', '\\', 'u', '0', '0', '8', '6', ' ', 'c'};
 
   printf("Testing \"%s\" in %s:\n", __FUNCTION__, __FILE__);
   memset(buf, 0, sizeof(buf));
diff --git a/logupload/client/Makefile b/logupload/client/Makefile
index f7a56d3..5e72977 100644
--- a/logupload/client/Makefile
+++ b/logupload/client/Makefile
@@ -10,7 +10,7 @@
 
 CFLAGS+=-Wall -Werror $(EXTRACFLAGS)
 LDFLAGS+=$(EXTRALDFLAGS)
-LIBS=-lrt -lcurl -lz -lm
+LIBS=-lrt -lcurl -lz -lm -lcrypto
 
 # Test Flags
 TEST_LDFLAGS=$(LDFLAGS)
diff --git a/logupload/client/log_uploader.c b/logupload/client/log_uploader.c
index 5b63174..8aa0990 100644
--- a/logupload/client/log_uploader.c
+++ b/logupload/client/log_uploader.c
@@ -1,3 +1,4 @@
+#include <ctype.h>
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
@@ -6,6 +7,10 @@
 #include <time.h>
 #include <unistd.h>
 #include <inttypes.h>
+#include <openssl/md5.h>
+#include <openssl/hmac.h>
+#include <sys/types.h>
+#include <sys/stat.h>
 
 #include "log_uploader.h"
 #include "utils.h"
@@ -108,6 +113,8 @@
       }
     }
 
+    num_read = suppress_mac_addresses(params->line_buffer, num_read);
+
     // Parse the data on the line to get the extra information
     if (parse_line_data(params->line_buffer, &parsed_line)) {
       // We don't want to be fatal if we fail to parse a line for some
@@ -177,3 +184,158 @@
   }
   return params->log_buffer;
 }
+
+const char SOFT[] = "AEIOUY" "V";
+const char HARD[] = "BCDFGHJKLMNPQRSTVWXYZ" "AEIOU";
+const char *consensus_key_file = "/tmp/waveguide/consensus_key";
+#define CONSENSUS_KEY_LEN 16
+uint8_t consensus_key[CONSENSUS_KEY_LEN];
+#define MAC_ADDR_LEN 17
+
+void default_consensus_key()
+{
+  int fd;
+
+  if ((fd = open("/dev/urandom", O_RDONLY)) >= 0) {
+    ssize_t siz = sizeof(consensus_key);
+    if (read(fd, consensus_key, siz) != siz) {
+      /* https://xkcd.com/221/ */
+      memset(consensus_key, time(NULL), siz);
+    }
+    close(fd);
+  }
+}
+
+/* Read the waveguide consensus_key, if any */
+static void get_consensus_key()
+{
+  static ino_t ino = 0;
+  static time_t mtime = 0;
+  struct stat statbuf;
+  int fd;
+
+  if (stat(consensus_key_file, &statbuf)) {
+    if ((statbuf.st_ino == ino) && (statbuf.st_mtime == mtime)) {
+      return;
+    }
+  }
+
+  fd = open(consensus_key_file, O_RDONLY);
+  if (fd >= 0) {
+    uint8_t new_key[sizeof(consensus_key)];
+    if (read(fd, new_key, sizeof(new_key)) == sizeof(new_key)) {
+      memcpy(consensus_key, new_key, sizeof(consensus_key));
+      ino = statbuf.st_ino;
+      mtime = statbuf.st_mtime;
+    }
+    close(fd);
+  }
+}
+
+/* Given a value from 0..4095, encode it as a cons+vowel+cons sequence. */
+static void trigraph(int num, char *out)
+{
+  int ns = sizeof(SOFT) - 1;
+  int nh = sizeof(HARD) - 1;
+  int c1, c2, c3;
+
+  c3 = num % nh;
+  c2 = (num / nh) % ns;
+  c1 = num / nh / ns;
+  out[0] = HARD[c1];
+  out[1] = SOFT[c2];
+  out[2] = HARD[c3];
+}
+
+static int hex_chr_to_int(char hex) {
+  switch(hex) {
+    case '0' ... '9':
+      return hex - '0';
+    case 'a' ... 'f':
+      return hex - 'a' + 10;
+    case 'A' ... 'F':
+      return hex - 'A' + 10;
+  }
+
+  return 0;
+}
+
+/*
+ * Convert a string of the form "00:11:22:33:44:55" to
+ * a binary array 001122334455.
+ */
+static void get_binary_mac(const char *mac, uint8_t *out) {
+  int i;
+  for (i = 0; i < MAC_ADDR_LEN; i += 3) {
+    *out = (hex_chr_to_int(mac[i]) << 4) | hex_chr_to_int(mac[i+1]);
+    out++;
+  }
+}
+
+static const char *get_anonid_for_mac(const char *mac, char *out) {
+  unsigned char digest[EVP_MAX_MD_SIZE];
+  unsigned int digest_len = sizeof(digest);
+  uint8_t macbin[6];
+  uint32_t num;
+
+  get_consensus_key();
+  get_binary_mac(mac, macbin);
+  HMAC(EVP_md5(), consensus_key, sizeof(consensus_key), macbin, sizeof(macbin),
+      digest, &digest_len);
+  num = (digest[0] << 16) | (digest[1] << 8) | digest[2];
+  trigraph((num >> 12) & 0x0fff, out);
+  trigraph((num      ) & 0x0fff, out + 3);
+
+  return out;
+}
+
+static ssize_t anonymize_mac_address(char *s, ssize_t len) {
+  char anonid[6];
+  ssize_t offset = MAC_ADDR_LEN - sizeof(anonid);
+
+  get_anonid_for_mac(s, anonid);
+  memcpy(s, anonid, sizeof(anonid));
+  s += sizeof(anonid);
+  len -= offset;
+  memmove(s, s + offset, len);
+  return offset;
+}
+
+static int is_mac_addr(const char *s, char sep) {
+  if ((s[2] == sep) && (s[5] == sep) && (s[8] == sep) &&
+      (s[11] == sep) && (s[14] == sep) &&
+      isxdigit(s[0]) && isxdigit(s[1]) &&
+      isxdigit(s[3]) && isxdigit(s[4]) &&
+      isxdigit(s[6]) && isxdigit(s[7]) &&
+      isxdigit(s[9]) && isxdigit(s[10]) &&
+      isxdigit(s[12]) && isxdigit(s[13]) &&
+      isxdigit(s[15]) && isxdigit(s[16])) {
+    return 1;
+  }
+
+  return 0;
+}
+
+/*
+ * search for text patterns which look like MAC addresses,
+ * and anonymize them.
+ * Ex: f8:8f:ca:00:00:01 to PEEVJB
+ */
+unsigned long suppress_mac_addresses(char *line, ssize_t len) {
+  char *s = line;
+  unsigned long new_len = len;
+  ssize_t reduce;
+
+  while (len >= MAC_ADDR_LEN) {
+    if (is_mac_addr(s, ':') || is_mac_addr(s, '-') || is_mac_addr(s, '_')) {
+      reduce = anonymize_mac_address(s, len);
+      len -= reduce;
+      new_len -= reduce;
+    } else {
+      s += 1;
+      len -= 1;
+    }
+  }
+
+  return new_len;
+}
diff --git a/logupload/client/log_uploader.h b/logupload/client/log_uploader.h
index bb6011c..4a3672f 100644
--- a/logupload/client/log_uploader.h
+++ b/logupload/client/log_uploader.h
@@ -49,6 +49,13 @@
 int logmark_once(const char* output_path, const char* version_path,
     const char* ntp_sync_path);
 
+// Rewrite any MAC addresses of the form 00:11:22:33:44:55 (or similar)
+// as anonids like ABCDEF.
+unsigned long suppress_mac_addresses(char *line, ssize_t len);
+
+// initialize a random key for anonymization.
+void default_consensus_key();
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/logupload/client/log_uploader_main.c b/logupload/client/log_uploader_main.c
index 8d3afc4..7a4e0fb 100644
--- a/logupload/client/log_uploader_main.c
+++ b/logupload/client/log_uploader_main.c
@@ -170,6 +170,8 @@
   snprintf(config.upload_target, sizeof(config.upload_target), "%s",
         DEFAULT_UPLOAD_TARGET);
 
+  default_consensus_key();
+
   if (argc > 1) {
     if (parse_args(&config, argc, argv) < 0) {
       usage(argv[0]);
diff --git a/logupload/client/log_uploader_test.cc b/logupload/client/log_uploader_test.cc
index af10f41..ad3d61f 100644
--- a/logupload/client/log_uploader_test.cc
+++ b/logupload/client/log_uploader_test.cc
@@ -315,3 +315,30 @@
   remove(test_dev_kmsg_path);
   rmdir(tdir);
 }
+
+
+struct log_data test_MAC_data[] = {
+  { 1, 1000LL, 100LL, "-", "f8:8f:ca:00:00:01\n", NULL },
+  { 4, 1001LL, 101LL, "-", "8:8f:ca:00:00:01\n", NULL },
+  { 2, 1010LL, 102LL, "-", "8:8f:ca:00:00:01:\n", NULL },
+  { 5, 2030000LL, 104LL, "-", ":::semicolons:f8:8f:ca:00:00:01:and:after\n",
+      NULL },
+  { 3, 3030000LL, 105LL, "-", "f8-8f-ca-00-00-01\n", NULL },
+  { 3, 3030000LL, 105LL, "-", "f8_8f_ca_00_00_01\n", NULL },
+};
+int test_MAC_data_size = sizeof(test_MAC_data) / sizeof(struct log_data);
+
+
+TEST(LogUploader, anonymize_mac_addresses) {
+  struct log_parse_params* params = create_log_parse_params(test_MAC_data,
+      test_MAC_data_size);
+  char* res_buffer = parse_and_consume_log_data(params);
+
+  /* Verify that the MAC address has been anonymized. */
+  printf("%s\n", res_buffer);
+  EXPECT_TRUE(strstr(res_buffer, "f8:8f:ca:00:00:01") == NULL);
+  EXPECT_TRUE(strstr(res_buffer, "f8-8f-ca-00-00-01") == NULL);
+  EXPECT_TRUE(strstr(res_buffer, "f8_8f_ca_00_00_01") == NULL);
+
+  free_log_parse_params(params);
+}
diff --git a/speedtest/Makefile b/speedtest/Makefile
index 56dc3e1..af5ea27 100644
--- a/speedtest/Makefile
+++ b/speedtest/Makefile
@@ -4,27 +4,65 @@
 BINDIR=$(PREFIX)/bin
 DEBUG?=-g
 WARNINGS=-Wall -Werror -Wno-unused-result -Wno-unused-but-set-variable
-CXXFLAGS=$(DEBUG) $(WARNINGS) -O3 -DNDEBUG -std=c++11 $(EXTRACFLAGS)
+CXXFLAGS=$(DEBUG) $(WARNINGS) -DNDEBUG -std=c++11 $(EXTRACFLAGS)
+#CXXFLAGS=$(DEBUG) $(WARNINGS) -O3 -DNDEBUG -std=c++11 $(EXTRACFLAGS)
 LDFLAGS=$(DEBUG) $(EXTRALDFLAGS)
 
 GTEST_DIR=googletest
 GMOCK_DIR=googlemock
 TFLAGS=$(DEBUG) -isystem ${GTEST_DIR}/include -isystem $(GMOCK_DIR)/include -pthread -std=c++11
 
-LIBS=-lcurl -lpthread
+LIBS=-lcurl -lpthread -ljsoncpp
 TOBJS=curl_env.o url.o errors.o request.o utils.o
-OBJS=errors.o curl_env.o options.o request.o utils.o speedtest.o url.o
+OBJS=config.o \
+     curl_env.o \
+     download_task.o \
+     errors.o \
+     http_task.o \
+     options.o \
+     ping_task.o \
+     request.o \
+     speedtest.o \
+     task.o \
+     timed_runner.o \
+     transfer_runner.o \
+     transfer_task.o \
+     upload_task.o \
+     url.o \
+     utils.o
 
 all: speedtest
 
+config.o: config.cc config.h url.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
-utils.o: utils.cc options.h
-request.o: request.cc request.h curl_env.h url.h
-url.o: url.cc url.h
-speedtest.o: speedtest.cc speedtest.h curl_env.h options.h request.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
+speedtest.o: speedtest.cc \
+             speedtest.h \
+             config.h \
+             curl_env.h \
+             download_task.h \
+             options.h \
+             ping_task.h \
+             request.h \
+             task.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
+utils.o: utils.cc options.h
+url.o: url.cc url.h utils.h
+
 speedtest: speedtest_main.o $(OBJS)
 	$(CXX) -o $@ $< $(OBJS) $(LDFLAGS) $(LIBS)
 
@@ -45,14 +83,14 @@
 libspeedtesttest.a: $(TOBJS)
 	ar -rv libspeedtesttest.a $(TOBJS)
 
-%_test.o: %_test.cc %.h
+%_test.o: %_test.cc %.h %.cc
 	$(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
 	./$@
 
-test: options_test request_test url_test
+test: config_test options_test request_test url_test
 
 install: speedtest
 	$(INSTALL) -m 0755 speedtest $(BINDIR)/
diff --git a/speedtest/config.cc b/speedtest/config.cc
new file mode 100644
index 0000000..aede959
--- /dev/null
+++ b/speedtest/config.cc
@@ -0,0 +1,97 @@
+/*
+ * 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"
+
+// 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) {
+  if (!config) {
+    return false;
+  }
+
+  Json::Reader reader;
+  Json::Value root;
+  if (!reader.parse(json, root, false)) {
+    return false;
+  }
+
+  config->download_size = root["downloadSize"].asInt();
+  config->upload_size = root["uploadSize"].asInt();
+  config->interval_millis = root["intervalSize"].asInt();
+  config->location_name = root["locationName"].asString();
+  config->min_transfer_intervals = root["minTransferIntervals"].asInt();
+  config->max_transfer_intervals = root["maxTransferIntervals"].asInt();
+  config->min_transfer_runtime = root["minTransferRunTime"].asInt();
+  config->max_transfer_runtime = root["maxTransferRunTime"].asInt();
+  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->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;
+}
+
+void PrintConfig(const Config &config) {
+  PrintConfig(std::cout, config);
+}
+
+void PrintConfig(std::ostream &out, const Config &config) {
+  out << "Download size: " << config.download_size << " bytes\n"
+      << "Upload size: " << config.upload_size << " bytes\n"
+      << "Interval size: " << config.interval_millis << " ms\n"
+      << "Location name: " << config.location_name << "\n"
+      << "Min transfer intervals: " << config.min_transfer_intervals << "\n"
+      << "Max transfer intervals: " << config.max_transfer_intervals << "\n"
+      << "Min transfer runtime: " << config.min_transfer_runtime << " ms\n"
+      << "Max transfer runtime: " << config.max_transfer_runtime << " ms\n"
+      << "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"
+      << "Transfer port start: " << config.transfer_port_start << "\n"
+      << "Transfer port end: " << config.transfer_port_end << "\n";
+}
+
+}  // namespace
diff --git a/speedtest/config.h b/speedtest/config.h
new file mode 100644
index 0000000..2988484
--- /dev/null
+++ b/speedtest/config.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_CONFIG_H
+#define SPEEDTEST_CONFIG_H
+
+#include <iostream>
+#include <string>
+#include <vector>
+#include "url.h"
+
+namespace speedtest {
+
+struct Config {
+  int download_size = 0;
+  int upload_size = 0;
+  int interval_millis = 0;
+  std::string location_name;
+  int min_transfer_intervals = 0;
+  int max_transfer_intervals = 0;
+  int min_transfer_runtime = 0;
+  int 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;
+  int transfer_port_start = 0;
+  int transfer_port_end = 0;
+};
+
+// 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);
+
+void PrintConfig(const Config &config);
+void PrintConfig(std::ostream &out, const Config &config);
+
+}  // namespace speedtest
+
+#endif //SPEEDTEST_CONFIG_H
diff --git a/speedtest/config_test.cc b/speedtest/config_test.cc
new file mode 100644
index 0000000..5924921
--- /dev/null
+++ b/speedtest/config_test.cc
@@ -0,0 +1,132 @@
+/*
+ * 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>
+
+namespace speedtest {
+namespace {
+
+const char *kValidConfig = R"CONFIG(
+{
+    "downloadSize": 10000000,
+    "intervalSize": 200,
+    "locationName": "Kansas City",
+    "maxTransferIntervals": 25,
+    "maxTransferRunTime": 20000,
+    "maxTransferVariance": 0.08,
+    "minTransferIntervals": 10,
+    "minTransferRunTime": 5000,
+    "numConcurrentDownloads": 20,
+    "numConcurrentUploads": 15,
+    "pingRunTime": 3000,
+    "pingTimeout": 300,
+    "transferPortEnd": 3023,
+    "transferPortStart": 3004,
+    "uploadSize": 20000000
+}
+)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));
+}
+
+TEST(ParseConfigTest, EmptyJson_Invalid) {
+  Config config;
+  EXPECT_FALSE(ParseConfig("", &config));
+}
+
+TEST(ParseConfigTest, InvalidJson_Invalid) {
+  Config config;
+  EXPECT_FALSE(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_EQ(20, config.num_downloads);
+  EXPECT_EQ(15, config.num_uploads);
+  EXPECT_EQ(200, config.interval_millis);
+  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(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 c02835c..eed370f 100644
--- a/speedtest/curl_env.cc
+++ b/speedtest/curl_env.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -18,34 +18,106 @@
 
 #include <cstdlib>
 #include <iostream>
-#include <curl/curl.h>
 #include "errors.h"
 #include "request.h"
 
 namespace http {
+namespace {
 
-CurlEnv::CurlEnv() {
-  init(CURL_GLOBAL_NOTHING);
+void LockFn(CURL *handle,
+            curl_lock_data data,
+            curl_lock_access access,
+            void *userp) {
+  CurlEnv *env = static_cast<CurlEnv *>(userp);
+  env->Lock(data);
 }
 
-CurlEnv::CurlEnv(int init_options) {
-  init(init_options);
+void UnlockFn(CURL *handle, curl_lock_data data, void *userp) {
+  CurlEnv *env = static_cast<CurlEnv *>(userp);
+  env->Unlock(data);
 }
 
-CurlEnv::~CurlEnv() {
-  curl_global_cleanup();
+}  // namespace
+
+std::shared_ptr<CurlEnv> CurlEnv::NewCurlEnv(const Options &options) {
+  return std::shared_ptr<CurlEnv>(new CurlEnv(options));
 }
 
-std::unique_ptr<Request> CurlEnv::NewRequest() {
-  return std::unique_ptr<Request>(new Request(shared_from_this()));
-}
-
-void CurlEnv::init(int init_flags) {
-  CURLcode status = curl_global_init(init_flags);
+CurlEnv::CurlEnv(const Options &options)
+    : options_(options),
+      set_max_connections_(false),
+      share_(nullptr) {
+  CURLcode status;
+  {
+    std::lock_guard <std::mutex> lock(curl_mutex_);
+    status = curl_global_init(options_.curl_options);
+  }
   if (status != 0) {
     std::cerr << "Curl initialization failed: " << ErrorString(status);
     std::exit(1);
   }
+  if (!options_.disable_dns_cache) {
+    share_ = curl_share_init();
+    curl_share_setopt(share_, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
+    curl_share_setopt(share_, CURLSHOPT_USERDATA, this);
+    curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &LockFn);
+    curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &UnlockFn);
+  }
+}
+
+CurlEnv::~CurlEnv() {
+  curl_share_cleanup(share_);
+  share_ = nullptr;
+  curl_global_cleanup();
+}
+
+std::unique_ptr<Request> 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.
+  std::lock_guard <std::mutex> lock(curl_mutex_);
+
+  // We use an aliasing constructor on a shared_ptr to keep a reference to
+  // CurlEnv as when the refcount drops to 0 we want to do global cleanup.
+  // So the CURL handle for this shared_ptr is _unmanaged_ and the Request
+  // object is responsible for cleaning it up, which involves calling
+  // curl_easy_cleanup().
+  //
+  // This way Request doesn't need to know about CurlEnv at all, while
+  // all Request instances will still keep an implicit reference to
+  // CurlEnv.
+  std::shared_ptr<CURL> handle(shared_from_this(), curl_easy_init());
+
+  // For some reason libcurl sets the max connections on a handle.
+  // According to the docs, doing so when there are open connections may
+  // close them so we maintain this boolean so as to set the maximum
+  // number of connections on the connection pool associated with this
+  // handle before any connections are opened.
+  if (!set_max_connections_ && options_.max_connections > 0) {
+    curl_easy_setopt(handle.get(),
+                     CURLOPT_MAXCONNECTS,
+                     options_.max_connections);
+    set_max_connections_ = true;
+  }
+
+  curl_easy_setopt(handle.get(), CURLOPT_SHARE, share_);
+  return std::unique_ptr<Request>(new Request(handle, url));
+}
+
+void CurlEnv::Lock(curl_lock_data lock_type) {
+  if (lock_type == CURL_LOCK_DATA_DNS) {
+    // It is ill-advised to call lock directly but libcurl uses
+    // separate lock/unlock functions.
+    dns_mutex_.lock();
+  }
+}
+
+void CurlEnv::Unlock(curl_lock_data lock_type) {
+  if (lock_type == CURL_LOCK_DATA_DNS) {
+    // It is ill-advised to call lock directly but libcurl uses
+    // separate lock/unlock functiounknns.
+    dns_mutex_.unlock();
+  }
 }
 
 }  // namespace http
diff --git a/speedtest/curl_env.h b/speedtest/curl_env.h
index 09511fa..6a70f28 100644
--- a/speedtest/curl_env.h
+++ b/speedtest/curl_env.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -17,27 +17,45 @@
 #ifndef HTTP_CURL_ENV_H
 #define HTTP_CURL_ENV_H
 
+#include <curl/curl.h>
 #include <memory>
+#include <mutex>
+#include "url.h"
 
 namespace http {
 
 class Request;
 
-// Curl initialization to cleanup automatically
 class CurlEnv : public std::enable_shared_from_this<CurlEnv> {
  public:
-  CurlEnv();
-  explicit CurlEnv(int init_options);
+  struct Options {
+    int curl_options = CURL_GLOBAL_NOTHING;
+    bool disable_dns_cache = false;
+    int max_connections = 0;
+  };
+
+  static std::shared_ptr<CurlEnv> NewCurlEnv(const Options &options);
   virtual ~CurlEnv();
 
-  std::unique_ptr<Request> NewRequest();
+  std::unique_ptr<Request> NewRequest(const Url &url);
+
+  void Lock(curl_lock_data lock_type);
+  void Unlock(curl_lock_data lock_type);
 
  private:
-  void init(int flags);
+  explicit CurlEnv(const Options &options);
+
+  Options options_;
+
+  // used to lock on curl global state
+  std::mutex curl_mutex_;
+  bool set_max_connections_;
+
+  std::mutex dns_mutex_;
+  CURLSH *share_;  // owned
 
   // disable
   CurlEnv(const CurlEnv &other) = delete;
-
   void operator=(const CurlEnv &other) = delete;
 };
 
diff --git a/speedtest/download_task.cc b/speedtest/download_task.cc
new file mode 100644
index 0000000..a643725
--- /dev/null
+++ b/speedtest/download_task.cc
@@ -0,0 +1,78 @@
+/*
+ * 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
new file mode 100644
index 0000000..2b65478
--- /dev/null
+++ b/speedtest/download_task.h
@@ -0,0 +1,51 @@
+/*
+ * 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/errors.cc b/speedtest/errors.cc
index 05f382b..43c94f8 100644
--- a/speedtest/errors.cc
+++ b/speedtest/errors.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
diff --git a/speedtest/errors.h b/speedtest/errors.h
index 334689d..4c70003 100644
--- a/speedtest/errors.h
+++ b/speedtest/errors.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
diff --git a/speedtest/http_task.cc b/speedtest/http_task.cc
new file mode 100644
index 0000000..1275aa4
--- /dev/null
+++ b/speedtest/http_task.cc
@@ -0,0 +1,24 @@
+/*
+ * 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
new file mode 100644
index 0000000..a54e4ba
--- /dev/null
+++ b/speedtest/http_task.h
@@ -0,0 +1,43 @@
+/*
+ * 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/options.cc b/speedtest/options.cc
index 4d07099..133b857 100644
--- a/speedtest/options.cc
+++ b/speedtest/options.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -25,14 +25,23 @@
 
 namespace speedtest {
 
-const char* kDefaultHost = "speedtest.googlefiber.net";
+const char* kDefaultHost = "any.speed.gfsvc.com";
 
 namespace {
 
-bool ParseLong(const char *s, char **endptr, long *size) {
-  assert(s != nullptr);
-  assert(size != nullptr);
-  *size = strtol(s, endptr, 10);
+bool ParseLong(const char *s, char **endptr, long *number) {
+  assert(s);
+  assert(endptr);
+  assert(number != nullptr);
+  *number = strtol(s, endptr, 10);
+  return !**endptr;
+}
+
+bool ParseDouble(const char *s, char **endptr, double *number) {
+  assert(s);
+  assert(endptr);
+  assert(number != nullptr);
+  *number = strtod(s, endptr);
   return !**endptr;
 }
 
@@ -58,67 +67,129 @@
   return true;
 }
 
-const char *kShortOpts = "d:u:t:n:p:s:vh";
+const int kOptDisableDnsCache = 1000;
+const int kOptMaxConnections = 1001;
+const int kOptExponentialMovingAverage = 1002;
+
+const int kOptMinTransferTime = 1100;
+const int kOptMaxTransferTime = 1101;
+const int kOptMinTransferIntervals = 1102;
+const int kOptMaxTransferIntervals = 1103;
+const int kOptMaxTransferVariance = 1104;
+const int kOptIntervalMillis = 1105;
+const int kOptPingRuntime = 1106;
+const int kOptPingTimeout = 1107;
+
+const char *kShortOpts = "hvg:a:d:s:t:u:p:";
+
 struct option kLongOpts[] = {
     {"help", no_argument, nullptr, 'h'},
-    {"number", required_argument, nullptr, 'n'},
-    {"download_size", required_argument, nullptr, 'd'},
-    {"upload_size", required_argument, nullptr, 'u'},
-    {"progress", required_argument, nullptr, 'p'},
-    {"serverid", required_argument, nullptr, 's'},  // ignored
-    {"time", required_argument, nullptr, 't'},
     {"verbose", no_argument, nullptr, 'v'},
+    {"global_host", 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},
+
+    {"num_downloads", required_argument, nullptr, 'd'},
+    {"download_size", required_argument, nullptr, 's'},
+    {"num_uploads", required_argument, nullptr, 'u'},
+    {"upload_size", required_argument, nullptr, 't'},
+    {"min_transfer_runtime", required_argument, nullptr, kOptMinTransferTime},
+    {"max_transfer_runtime", required_argument, nullptr, kOptMaxTransferTime},
+    {"min_transfer_intervals", required_argument, nullptr,
+        kOptMinTransferIntervals},
+    {"max_transfer_intervals", required_argument, nullptr,
+        kOptMaxTransferIntervals},
+    {"max_transfer_variance", required_argument, nullptr,
+        kOptMaxTransferVariance},
+    {"interval_millis", required_argument, nullptr, kOptIntervalMillis},
+    {"ping_runtime", required_argument, nullptr, kOptPingRuntime},
+    {"ping_timeout", required_argument, nullptr, kOptPingTimeout},
     {nullptr, 0, nullptr, 0},
 };
-const int kDefaultNumber = 10;
-const int kDefaultDownloadSize = 10000000;
-const int kDefaultUploadSize = 10000000;
-const int kDefaultTimeMillis = 5000;
 const int kMaxNumber = 1000;
 const int kMaxProgress = 1000000;
 
+const char *kSpeedtestHelp = R"USAGE(: run an HTTP speedtest.
+
+If no hosts are specified, the global host is queried for a list
+of servers to use, otherwise the list of supplied hosts will be
+used. Each will be pinged several times and the one with the
+lowest ping time will be used. If only one host is supplied, it
+will be used without pinging.
+
+Usage: speedtest [options] [host ...]
+ -h, --help                    This help text
+ -v, --verbose                 Verbose output
+ -g, --global_host 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
+
+These options override the speedtest config parameters:
+ -d, --num_downloads NUM       Number of simultaneous downloads
+ -s, --download_size SIZE      Download size in bytes
+ -t, --upload_size SIZE        Upload size in bytes
+ -u, --num_uploads NUM         Number of simultaneous uploads
+ --min_transfer_runtime TIME   Minimum transfer time in milliseconds
+ --max_transfer_runtime TIME   Maximum transfer time in milliseconds
+ --min_transfer_intervals NUM  Short moving average intervals
+ --max_transfer_intervals NUM  Long moving average intervals
+ --max_transfer_variance NUM   Max difference between moving averages
+ --interval_millis TIME        Interval size in milliseconds
+ --ping_runtime TIME           Ping runtime in milliseconds
+ --ping_timeout TIME           Ping timeout in milliseconds
+)USAGE";
+
 }  // namespace
 
 bool ParseOptions(int argc, char *argv[], Options *options) {
   assert(options != nullptr);
-  options->number = kDefaultNumber;
-  options->download_size = kDefaultDownloadSize;
-  options->upload_size = kDefaultUploadSize;
-  options->time_millis = kDefaultTimeMillis;
-  options->progress_millis = 0;
-  options->verbose = false;
   options->usage = false;
+  options->verbose = false;
+  options->global_host = 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->num_downloads = 0;
+  options->download_size = 0;
+  options->num_uploads = 0;
+  options->upload_size = 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->hosts.clear();
 
+  if (!options->global_host.ok()) {
+    std::cerr << "Invalid global host " << kDefaultHost << "\n";
+    return false;
+  }
+
   // Manually set this to 0 to allow repeated calls
   optind = 0;
-
   int opt = 0, long_index = 0;
   while ((opt = getopt_long(argc, argv,
                             kShortOpts, kLongOpts, &long_index)) != -1) {
     switch (opt) {
-      case 'd':
-        if (!ParseSize(optarg, &options->download_size)) {
-          std::cerr << "Invalid download size '" << optarg << "'\n";
-          return false;
-        }
+      case 'a':
+        options->user_agent = optarg;
         break;
-      case 'u':
-        if (!ParseSize(optarg, &options->upload_size)) {
-          std::cerr << "Invalid upload size '" << optarg << "'\n";
-          return false;
-        }
-        break;
-      case 't':
-        options->time_millis = atoi(optarg);
-        break;
-      case 'v':
-        options->verbose = true;
-        break;
-      case 'h':
-        options->usage = true;
-        return true;
-      case 'n': {
+      case 'd': {
         long number;
         char *endptr;
         if (!ParseLong(optarg, &endptr, &number)) {
@@ -127,12 +198,24 @@
         }
         if (number < 1 || number > kMaxNumber) {
           std::cerr << "Number must be between 1 and " << kMaxNumber
-          << ", got '" << optarg << "'\n";
+                    << ", got '" << optarg << "'\n";
           return false;
         }
-        options->number = static_cast<int>(number);
+        options->num_downloads = static_cast<int>(number);
         break;
       }
+      case 'g': {
+        http::Url url(optarg);
+        if (!url.ok()) {
+          std::cerr << "Invalid global host " << optarg << "\n";
+          return false;
+        }
+        options->global_host = url;
+        break;
+      }
+      case 'h':
+        options->usage = true;
+        return true;
       case 'p': {
         long progress;
         char *endptr;
@@ -142,16 +225,186 @@
         }
         if (progress < 0 || progress > kMaxProgress) {
           std::cerr << "Number must be between 0 and " << kMaxProgress
-          << ", got '" << optarg << "'\n";
+                    << ", got '" << optarg << "'\n";
           return false;
         }
         options->progress_millis = static_cast<int>(progress);
         break;
       }
       case 's':
-        // serverid is an argument supported by the older speedtest
-        // implementation. It is ignored here to ease the transition.
+        if (!ParseSize(optarg, &options->download_size)) {
+          std::cerr << "Invalid download size '" << optarg << "'\n";
+          return false;
+        }
         break;
+      case 't':
+        if (!ParseSize(optarg, &options->upload_size)) {
+          std::cerr << "Invalid upload size '" << optarg << "'\n";
+          return false;
+        }
+        break;
+      case 'u': {
+        long number;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &number)) {
+          std::cerr << "Could not parse number '" << optarg << "'\n";
+          return false;
+        }
+        if (number < 1 || number > kMaxNumber) {
+          std::cerr << "Number must be between 1 and " << kMaxNumber
+                    << ", got '" << optarg << "'\n";
+          return false;
+        }
+        options->num_uploads = static_cast<int>(number);
+        break;
+      }
+      case 'v':
+        options->verbose = true;
+        break;
+      case kOptDisableDnsCache:
+        options->disable_dns_cache = true;
+        break;
+      case kOptMaxConnections: {
+        long max_connections;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &max_connections)) {
+          std::cerr << "Could not parse max connections '" << optarg << "'\n";
+          return false;
+        }
+        if (max_connections < 0) {
+          std::cerr << "Max connections must be nonnegative, got "
+          << optarg << "'\n";
+          return false;
+        }
+        options->max_connections = static_cast<int>(max_connections);
+        break;
+      }
+      case kOptExponentialMovingAverage:
+        options->exponential_moving_average = true;
+        break;
+      case kOptMinTransferTime: {
+        long transfer_time;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &transfer_time)) {
+          std::cerr << "Could not parse minimum transfer time '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (transfer_time < 0) {
+          std::cerr << "Minimum transfer runtime must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->min_transfer_runtime = static_cast<int>(transfer_time);
+        break;
+      }
+      case kOptMaxTransferTime: {
+        long transfer_time;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &transfer_time)) {
+          std::cerr << "Could not parse maximum transfer time '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (transfer_time < 0) {
+          std::cerr << "Maximum transfer runtime must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->max_transfer_runtime = static_cast<int>(transfer_time);
+        break;
+      }
+      case kOptMinTransferIntervals: {
+        long intervals;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &intervals)) {
+          std::cerr << "Could not parse minimum transfer intervals '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (intervals < 0) {
+          std::cerr << "Minimum transfer intervals must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->min_transfer_intervals = static_cast<int>(intervals);
+        break;
+      }
+      case kOptMaxTransferIntervals: {
+        long intervals;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &intervals)) {
+          std::cerr << "Could not parse maximum transfer intervals '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (intervals < 0) {
+          std::cerr << "Maximum transfer intervals must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->max_transfer_intervals = static_cast<int>(intervals);
+        break;
+      }
+      case kOptMaxTransferVariance: {
+        double variance;
+        char *endptr;
+        if (!ParseDouble(optarg, &endptr, &variance)) {
+          std::cerr << "Could not parse variance '" << optarg << "'\n";
+          return false;
+        }
+        if (variance < 0) {
+          std::cerr << "Variances must be nonnegative, got " << optarg << "'\n";
+          return false;
+        }
+        options->max_transfer_variance = variance;
+        break;
+      }
+      case kOptIntervalMillis: {
+        long interval_millis;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &interval_millis)) {
+          std::cerr << "Could not parse interval time '" << optarg << "'\n";
+          return false;
+        }
+        if (interval_millis < 0) {
+          std::cerr << "Interval time must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->interval_millis = static_cast<int>(interval_millis);
+        break;
+      }
+      case kOptPingRuntime: {
+        long ping_runtime;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &ping_runtime)) {
+          std::cerr << "Could not parse ping time '" << optarg << "'\n";
+          return false;
+        }
+        if (ping_runtime < 0) {
+          std::cerr << "Ping time must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->ping_runtime = static_cast<int>(ping_runtime);
+        break;
+      }
+      case kOptPingTimeout: {
+        long ping_timeout;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &ping_timeout)) {
+          std::cerr << "Could not parse ping timeout '" << optarg << "'\n";
+          return false;
+        }
+        if (ping_timeout < 0) {
+          std::cerr << "Ping timeout must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->ping_timeout = static_cast<int>(ping_timeout);
+        break;
+      }
       default:
         return false;
     }
@@ -174,7 +427,7 @@
     }
   }
   if (options->hosts.empty()) {
-    options->hosts.emplace_back(http::Url(kDefaultHost));
+    options->global = true;
   }
   return true;
 }
@@ -184,13 +437,29 @@
 }
 
 void PrintOptions(std::ostream &out, const Options &options) {
-  out << "Number: " << options.number << "\n"
-      << "Upload size: " << options.upload_size << "\n"
-      << "Download size: " << options.download_size << "\n"
-      << "Time: " << options.time_millis << " ms\n"
-      << "Progress interval: " << options.progress_millis << " ms\n"
+  out << "Usage: " << (options.usage ? "true" : "false") << "\n"
       << "Verbose: " << (options.verbose ? "true" : "false") << "\n"
-      << "Usage: " << (options.usage ? "true" : "false") << "\n"
+      << "Global host: " << options.global_host.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"
+      << "Number of downloads: " << options.num_downloads << "\n"
+      << "Download size: " << options.download_size << " bytes\n"
+      << "Number of uploads: " << options.num_uploads << "\n"
+      << "Upload size: " << options.upload_size << " 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"
       << "Hosts:\n";
   for (const http::Url &host : options.hosts) {
     out << "  " << host.url() << "\n";
@@ -205,15 +474,7 @@
   assert(app_path != nullptr);
   const char *last_slash = strrchr(app_path, '/');
   const char *app_name = last_slash == nullptr ? app_path : last_slash + 1;
-  out << basename(app_name) << ": run an HTTP speedtest\n\n"
-      << "Usage: speedtest [options] <host> ...\n"
-      << " -h, --help                This help text\n"
-      << " -n, --number NUM          Number of simultaneous transfers\n"
-      << " -d, --download_size SIZE  Download size in bytes\n"
-      << " -u, --upload_size SIZE    Upload size in bytes\n"
-      << " -t, --time TIME           Time per test in milliseconds\n"
-      << " -p, --progress TIME       Progress intervals in milliseconds\n"
-      << " -v, --verbose             Verbose output\n";
+  out << basename(app_name) << kSpeedtestHelp;
 }
 
 }  // namespace speedtest
diff --git a/speedtest/options.h b/speedtest/options.h
index 0996798..9028f70 100644
--- a/speedtest/options.h
+++ b/speedtest/options.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -27,14 +27,31 @@
 extern const char* kDefaultHost;
 
 struct Options {
+  bool usage = false;
+  bool verbose = false;
+  http::Url global_host;
+  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;
+
+  // A value of 0 means use the speedtest config parameters
+  int num_downloads = 0;
+  long download_size = 0;
+  int num_uploads = 0;
+  long upload_size = 0;
+  int min_transfer_runtime = 0;
+  int 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;
+
   std::vector<http::Url> hosts;
-  int number;
-  long download_size;
-  long upload_size;
-  int time_millis;
-  int progress_millis;
-  bool verbose;
-  bool usage;
 };
 
 // Parse command line options putting results into 'options'
diff --git a/speedtest/options_test.cc b/speedtest/options_test.cc
index 2754552..601984a 100644
--- a/speedtest/options_test.cc
+++ b/speedtest/options_test.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 
+#include "options.h"
+
 #include <gtest/gtest.h>
 #include <gmock/gmock.h>
 #include <mutex>
 #include <string.h>
 #include <vector>
-#include "options.h"
+#include "url.h"
 
 namespace speedtest {
 namespace {
@@ -81,8 +83,28 @@
 TEST(OptionsTest, Empty_ValidDefault) {
   Options options;
   TestValidOptions({}, &options);
-  EXPECT_FALSE(options.verbose);
   EXPECT_FALSE(options.usage);
+  EXPECT_FALSE(options.verbose);
+  EXPECT_TRUE(options.global);
+  EXPECT_EQ(http::Url("any.speed.gfsvc.com"), options.global_host);
+  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_EQ(0, options.num_downloads);
+  EXPECT_EQ(0, options.download_size);
+  EXPECT_EQ(0, options.num_uploads);
+  EXPECT_EQ(0, options.upload_size);
+  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());
 }
 
 TEST(OptionsTest, Usage_Valid) {
@@ -104,35 +126,94 @@
   EXPECT_THAT(options.hosts, testing::ElementsAre(http::Url("efgh")));
 }
 
-TEST(OptionsTest, FullShort_Valid) {
+TEST(OptionsTest, ShortOptions_Valid) {
   Options options;
-  TestValidOptions({"-d", "5122",
-                    "-u", "7653",
-                    "-t", "123",
-                    "-n", "15",
-                    "-p", "500"},
+  TestValidOptions({"-v",
+                    "-s", "5122",
+                    "-t", "7653",
+                    "-d", "20",
+                    "-u", "15",
+                    "-p", "500",
+                    "-g", "speed.gfsvc.com",
+                    "-a", "CrOS",
+                    "foo.speed.googlefiber.net",
+                    "bar.speed.googlefiber.net"},
                     &options);
+  EXPECT_TRUE(options.verbose);
+  EXPECT_EQ(20, options.num_downloads);
   EXPECT_EQ(5122, options.download_size);
+  EXPECT_EQ(15, options.num_uploads);
   EXPECT_EQ(7653, options.upload_size);
-  EXPECT_EQ(123, options.time_millis);
-  EXPECT_EQ(15, options.number);
   EXPECT_EQ(500, options.progress_millis);
+  EXPECT_FALSE(options.global);
+  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
+  EXPECT_EQ("CrOS", options.user_agent);
+
+  EXPECT_EQ(0, options.max_connections);
+  EXPECT_FALSE(options.disable_dns_cache);
+  EXPECT_FALSE(options.exponential_moving_average);
+  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::UnorderedElementsAre(
+      http::Url("foo.speed.googlefiber.net"),
+      http::Url("bar.speed.googlefiber.net")));
 }
 
-TEST(OptionsTest, FullLong_Valid) {
+TEST(OptionsTest, LongOptions_Valid) {
   Options options;
-  TestValidOptions({"--download_size", "5122",
+  TestValidOptions({"--verbose",
+                    "--global_host", "speed.gfsvc.com",
+                    "--user_agent", "CrOS",
+                    "--progress_millis", "1000",
+                    "--disable_dns_cache",
+                    "--max_connections", "23",
+                    "--exponential_moving_average",
+                    "--num_downloads", "16",
+                    "--download_size", "5122",
+                    "--num_uploads", "12",
                     "--upload_size", "7653",
-                    "--time", "123",
-                    "--progress", "1000",
-                    "--number", "12"},
+                    "--min_transfer_runtime", "7500",
+                    "--max_transfer_runtime", "13500",
+                    "--min_transfer_intervals", "13",
+                    "--max_transfer_intervals", "22",
+                    "--max_transfer_variance", "0.12",
+                    "--interval_millis", "250",
+                    "--ping_runtime", "2500",
+                    "--ping_timeout", "300",
+                    "foo.speed.googlefiber.net",
+                    "bar.speed.googlefiber.net"},
                     &options);
-  EXPECT_EQ(5122, options.download_size);
-  EXPECT_EQ(7653, options.upload_size);
-  EXPECT_EQ(123, options.time_millis);
-  EXPECT_EQ(12, options.number);
+  EXPECT_TRUE(options.verbose);
+  EXPECT_FALSE(options.global);
+  EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
+  EXPECT_EQ("CrOS", options.user_agent);
   EXPECT_EQ(1000, options.progress_millis);
-  EXPECT_THAT(options.hosts, testing::ElementsAre(http::Url(kDefaultHost)));
+  EXPECT_TRUE(options.disable_dns_cache);
+  EXPECT_EQ(23, options.max_connections);
+  EXPECT_TRUE(options.exponential_moving_average);
+  EXPECT_EQ(16, options.num_downloads);
+  EXPECT_EQ(5122, options.download_size);
+  EXPECT_EQ(12, options.num_uploads);
+  EXPECT_EQ(7653, options.upload_size);
+  EXPECT_EQ("CrOS", options.user_agent);
+  EXPECT_EQ(7500, options.min_transfer_runtime);
+  EXPECT_EQ(13500, options.max_transfer_runtime);
+  EXPECT_EQ(13, options.min_transfer_intervals);
+  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(
+      http::Url("foo.speed.googlefiber.net"),
+      http::Url("bar.speed.googlefiber.net")));
 }
 
 }  // namespace
diff --git a/speedtest/ping_task.cc b/speedtest/ping_task.cc
new file mode 100644
index 0000000..7a1c7be
--- /dev/null
+++ b/speedtest/ping_task.cc
@@ -0,0 +1,115 @@
+/*
+ * 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
new file mode 100644
index 0000000..b2923a8
--- /dev/null
+++ b/speedtest/ping_task.h
@@ -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.
+ */
+
+#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/request.cc b/speedtest/request.cc
index b0a2b89..ef46d2d 100644
--- a/speedtest/request.cc
+++ b/speedtest/request.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -64,17 +64,14 @@
 
 const int kDefaultQueryStringSize = 200;
 
+const Request::DownloadFn noop = [](void *, size_t) { };
+
 }  // namespace
 
-Request::Request(std::shared_ptr<CurlEnv> env)
-    : curl_headers_(nullptr),
-      env_(env) {
-  assert(env_);
-  handle_ = curl_easy_init();
-  if (!handle_) {
-    std::cerr << "Failed to create handle\n";
-    std::exit(1);
-  }
+Request::Request(std::shared_ptr<CURL> handle, const Url &url)
+    : handle_(handle),
+      curl_headers_(nullptr),
+      url_(url) {
 }
 
 Request::~Request() {
@@ -82,35 +79,40 @@
     curl_slist_free_all(curl_headers_);
     curl_headers_ = nullptr;
   }
-  curl_easy_cleanup(handle_);
+
+  curl_easy_cleanup(handle_.get());
+}
+
+CURLcode Request::Get() {
+  return Get(noop);
 }
 
 CURLcode Request::Get(DownloadFn download_fn) {
   CommonSetup();
   if (download_fn) {
-    curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, &WriteCallback);
-    curl_easy_setopt(handle_, CURLOPT_WRITEDATA, &download_fn);
+    curl_easy_setopt(handle_.get(), CURLOPT_WRITEFUNCTION, &WriteCallback);
+    curl_easy_setopt(handle_.get(), CURLOPT_WRITEDATA, &download_fn);
   }
   return Execute();
 }
 
 CURLcode Request::Post(UploadFn upload_fn) {
   CommonSetup();
-  curl_easy_setopt(handle_, CURLOPT_UPLOAD, 1);
-  curl_easy_setopt(handle_, CURLOPT_READFUNCTION, &ReadCallback);
-  curl_easy_setopt(handle_, CURLOPT_READDATA, &upload_fn);
+  curl_easy_setopt(handle_.get(), CURLOPT_UPLOAD, 1);
+  curl_easy_setopt(handle_.get(), CURLOPT_READFUNCTION, &ReadCallback);
+  curl_easy_setopt(handle_.get(), CURLOPT_READDATA, &upload_fn);
   return Execute();
 }
 
 CURLcode Request::Post(const char *data, curl_off_t data_len) {
   CommonSetup();
-  curl_easy_setopt(handle_, CURLOPT_POSTFIELDSIZE_LARGE, data_len);
-  curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, data);
+  curl_easy_setopt(handle_.get(), CURLOPT_POSTFIELDSIZE_LARGE, data_len);
+  curl_easy_setopt(handle_.get(), CURLOPT_POSTFIELDS, data);
   return Execute();
 }
 
 void Request::Reset() {
-  curl_easy_reset(handle_);
+  curl_easy_reset(handle_.get());
   clear_progress_fn();
   clear_headers();
   clear_params();
@@ -156,6 +158,10 @@
   params_.clear();
 }
 
+void Request::set_timeout_millis(long millis) {
+  curl_easy_setopt(handle_.get(), CURLOPT_TIMEOUT_MS, millis);
+}
+
 void Request::UpdateUrl() {
   std::string query_string;
   query_string.reserve(kDefaultQueryStringSize);
@@ -165,10 +171,10 @@
     if (!query_string.empty()) {
       query_string.append("&");
     }
-    char *name = curl_easy_escape(handle_,
+    char *name = curl_easy_escape(handle_.get(),
                                   iter->first.data(),
                                   iter->first.length());
-    char *value = curl_easy_escape(handle_,
+    char *value = curl_easy_escape(handle_.get(),
                                    iter->second.data(),
                                    iter->second.length());
     query_string.append(name);
@@ -183,12 +189,14 @@
 void Request::CommonSetup() {
   UpdateUrl();
   std::string request_url = url_.url();
-  curl_easy_setopt(handle_, CURLOPT_URL, request_url.c_str());
-  curl_easy_setopt(handle_, CURLOPT_USERAGENT, user_agent_.c_str());
+  curl_easy_setopt(handle_.get(), CURLOPT_URL, request_url.c_str());
+  curl_easy_setopt(handle_.get(), CURLOPT_USERAGENT, user_agent_.c_str());
   if (progress_fn_) {
-    curl_easy_setopt(handle_, CURLOPT_NOPROGRESS, 0);
-    curl_easy_setopt(handle_, CURLOPT_XFERINFOFUNCTION, &ProgressCallback);
-    curl_easy_setopt(handle_, CURLOPT_XFERINFODATA, &progress_fn_);
+    curl_easy_setopt(handle_.get(), CURLOPT_NOPROGRESS, 0);
+    curl_easy_setopt(handle_.get(),
+                     CURLOPT_XFERINFOFUNCTION,
+                     &ProgressCallback);
+    curl_easy_setopt(handle_.get(), CURLOPT_XFERINFODATA, &progress_fn_);
   }
   if (!headers_.empty()) {
     struct curl_slist *headers = nullptr;
@@ -200,12 +208,12 @@
       header.append(iter->second);
       headers = curl_slist_append(headers, header.c_str());
     }
-    curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, headers);
+    curl_easy_setopt(handle_.get(), CURLOPT_HTTPHEADER, headers);
   }
 }
 
 CURLcode Request::Execute() {
-  return curl_easy_perform(handle_);
+  return curl_easy_perform(handle_.get());
 }
 
 }  // namespace http
diff --git a/speedtest/request.h b/speedtest/request.h
index 837eecc..8588e29 100644
--- a/speedtest/request.h
+++ b/speedtest/request.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -22,8 +22,6 @@
 #include <map>
 #include <memory>
 #include <string>
-
-#include "curl_env.h"
 #include "url.h"
 
 namespace http {
@@ -44,10 +42,12 @@
                                         curl_off_t,
                                         curl_off_t,
                                         curl_off_t)>;
+  using Ptr = std::unique_ptr<Request>;
 
-  explicit Request(std::shared_ptr<CurlEnv> env);
+  Request(std::shared_ptr<CURL> handle, const Url &url);
   virtual ~Request();
 
+  CURLcode Get();
   CURLcode Get(DownloadFn download_fn);
   CURLcode Post(UploadFn upload_fn);
   CURLcode Post(const char *data, curl_off_t data_len);
@@ -80,24 +80,25 @@
   void set_progress_fn(ProgressFn progress_fn) { progress_fn_ = progress_fn; }
   void clear_progress_fn() { progress_fn_ = nullptr; }
 
+  // Request timeout
+  void set_timeout_millis(long millis);
+
   void UpdateUrl();
 
  private:
   void CommonSetup();
+
   CURLcode Execute();
 
   // owned
-  CURL *handle_;
+  std::shared_ptr<CURL> handle_;
   struct curl_slist *curl_headers_;
-
-  // ref-count CURL global config
-  std::shared_ptr<CurlEnv> env_;
   Url url_;
 
   std::string user_agent_;
   Headers headers_;
   QueryStringParams params_;
-  ProgressFn progress_fn_;  // unowned
+  ProgressFn progress_fn_;
 
   // disable
   Request(const Request &) = delete;
diff --git a/speedtest/request_test.cc b/speedtest/request_test.cc
index d8b11e9..ae7b74f 100644
--- a/speedtest/request_test.cc
+++ b/speedtest/request_test.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -14,10 +14,9 @@
  * limitations under the License.
  */
 
-#include <gtest/gtest.h>
-
 #include "request.h"
 
+#include <gtest/gtest.h>
 #include <memory>
 #include "curl_env.h"
 
@@ -30,8 +29,8 @@
   std::unique_ptr<Request> request;
 
   void SetUp() override {
-    env = std::make_shared<CurlEnv>();
-    request = env->NewRequest();
+    env = CurlEnv::NewCurlEnv({});
+    request = env->NewRequest(http::Url("http://example.com/foo"));
   }
 
   void VerifyQueryString(const char *expected,
@@ -92,9 +91,9 @@
 }
 
 TEST_F(RequestTest, Url_OneParamTwoValues_Ok) {
-  VerifyUrl("http://example.com/?abc=def&abc=ghi",
+  VerifyUrl("http://example.com/?abc=def&abc=def",
             "http://example.com",
-            {{"abc", "def"}, {"abc", "ghi"}});
+            {{"abc", "def"}, {"abc", "def"}});
 }
 
 TEST_F(RequestTest, Url_EscapeParam_Ok) {
diff --git a/speedtest/speedtest.cc b/speedtest/speedtest.cc
index bf8fe3c..ed1263d 100644
--- a/speedtest/speedtest.cc
+++ b/speedtest/speedtest.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -16,241 +16,416 @@
 
 #include "speedtest.h"
 
-#include <assert.h>
 #include <chrono>
 #include <cstring>
 #include <limits>
 #include <random>
 #include <thread>
 #include <iomanip>
+#include <fstream>
+#include <streambuf>
 
 #include "errors.h"
+#include "timed_runner.h"
+#include "transfer_runner.h"
 #include "utils.h"
 
 namespace speedtest {
+namespace {
 
-Speedtest::Speedtest(const Options &options)
-    : options_(options),
-      bytes_downloaded_(0),
-      bytes_uploaded_(0) {
-  assert(!options_.hosts.empty());
-  env_ = std::make_shared<http::CurlEnv>();
+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);
-  char *data = new char[options_.upload_size];
-  for (int i = 0; i < options_.upload_size; ++i) {
-    data[i] = uniform_dist(random_engine);
+  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);
   }
-  send_data_ = data;
+  return std::move(random_data);
+}
+
+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>());
+}
+
+}  // 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() {
-  delete[] send_data_;
 }
 
 void Speedtest::Run() {
-  if (!RunPingTest()) {
+  InitUserAgent();
+  LoadServerList();
+  if (servers_.empty()) {
+    std::cerr << "No servers found in global server list\n";
+    std::exit(1);
+  }
+  FindNearestServer();
+  if (!server_url_) {
     std::cout << "No servers responded. Exiting\n";
     return;
   }
+  std::string json = LoadConfig(*server_url_);
+  if (!ParseConfig(json, &config_)) {
+    std::cout << "Could not parse config\n";
+    return;
+  }
+  if (options_.verbose) {
+    std::cout << "Server config:\n";
+    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;
+      }
+    }
+  } else {
+    user_agent_ = options_.user_agent;
+    return;
+  }
+  if (options_.verbose) {
+    std::cout << "Setting user agent to " << user_agent_ << "\n";
+  }
+}
+
+void Speedtest::LoadServerList() {
+  servers_.clear();
+  if (!options_.global) {
+    if (options_.verbose) {
+      std::cout << "Explicit server list:\n";
+      for (const auto &url : options_.hosts) {
+        std::cout << "  " << url.url() << "\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";
+    }
+  }
+}
+
+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();
+  if (options_.verbose) {
+    std::cout << "There are " << hosts.size() << " ping URLs:\n";
+    for (const auto &host : hosts) {
+      std::cout << "  " << host.url() << "\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() {
-  end_download_ = false;
-  long start_time = SystemTimeMicros();
-  bytes_downloaded_ = 0;
-  std::thread threads[options_.number];
-  for (int i = 0; i < options_.number; ++i) {
-    threads[i] = std::thread([=]() {
-      RunDownload(i);
-    });
+  if (options_.verbose) {
+    std::cout << "Starting download test to " << config_.location_name
+              << " (" << server_url_->url() << ")\n";
   }
-  std::thread timer([&]{
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(options_.time_millis));
-    end_download_ = true;
-  });
-  timer.join();
-  for (auto &thread : threads) {
-    thread.join();
+  DownloadTask::Options download_options;
+  download_options.verbose = options_.verbose;
+  download_options.num_transfers = NumDownloads();
+  download_options.download_size = DownloadSize();
+  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();
+  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 "
+                << round(speed_variance, 4) << ")\n";
+    };
   }
-  long end_time = speedtest::SystemTimeMicros();
-
-  double running_time = (end_time - start_time) / 1000000.0;
-  double megabits = bytes_downloaded_ * 8 / 1000000.0 / running_time;
-  std::cout << "Downloaded " << bytes_downloaded_
-            << " bytes in " << running_time * 1000 << " ms ("
-            << megabits << " Mbps)\n";
-}
-
-void Speedtest::RunDownload(int id) {
-  auto download = MakeRequest(id, "/download");
-  http::Request::DownloadFn noop = [](void *, size_t) {};
-  while (!end_download_) {
-    long downloaded = 0;
-    download->set_param("i", speedtest::to_string(id));
-    download->set_param("size", speedtest::to_string(options_.download_size));
-    download->set_param("time", speedtest::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_downloaded_ += dlnow - downloaded;
-        downloaded = dlnow;
-      }
-      return end_download_;
-    });
-    download->Get(noop);
-    download->Reset();
+  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";
 }
 
 void Speedtest::RunUploadTest() {
-  end_upload_ = false;
-  long start_time = SystemTimeMicros();
-  bytes_uploaded_ = 0;
-  std::thread threads[options_.number];
-  for (int i = 0; i < options_.number; ++i) {
-    threads[i] = std::thread([=]() {
-      RunUpload(i);
-    });
-  }
-  std::thread timer([&]{
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(options_.time_millis));
-    end_upload_ = true;
-  });
-  timer.join();
-  for (auto &thread : threads) {
-    thread.join();
-  }
-  long end_time = speedtest::SystemTimeMicros();
-
-  double running_time = (end_time - start_time) / 1000000.0;
-  double megabits = bytes_uploaded_ * 8 / 1000000.0 / running_time;
-  std::cout << "Uploaded " << bytes_uploaded_
-            << " bytes in " << running_time * 1000 << " ms ("
-            << megabits << " Mbps)\n";
-}
-
-void Speedtest::RunUpload(int id) {
-  auto upload = MakeRequest(id, "/upload");
-  while (!end_upload_) {
-    long uploaded = 0;
-    upload->set_progress_fn([&](curl_off_t,
-                                curl_off_t,
-                                curl_off_t,
-                                curl_off_t ulnow) -> bool {
-      if (ulnow > uploaded) {
-        bytes_uploaded_ += ulnow - uploaded;
-        uploaded = ulnow;
-      }
-      return end_upload_;
-    });
-
-    // 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(send_data_, options_.upload_size);
-    upload->Reset();
-  }
-}
-
-bool Speedtest::RunPingTest() {
-  end_ping_ = false;
-  size_t num_hosts = options_.hosts.size();
-  std::thread threads[num_hosts];
-  min_ping_micros_.clear();
-  min_ping_micros_.resize(num_hosts);
-  for (size_t i = 0; i < num_hosts; ++i) {
-    threads[i] = std::thread([=]() {
-      RunPing(i);
-    });
-  }
-  std::thread timer([&]{
-    std::this_thread::sleep_for(
-        std::chrono::milliseconds(options_.time_millis));
-    end_ping_ = true;
-  });
-  timer.join();
-  for (auto &thread : threads) {
-    thread.join();
-  }
   if (options_.verbose) {
-    std::cout << "Pinged " << num_hosts << " "
-              << (num_hosts == 1 ? "host" : "hosts:") << "\n";
+    std::cout << "Starting upload test to " << config_.location_name
+              << " (" << server_url_->url() << ")\n";
   }
-  size_t min_index = 0;
-  for (size_t i = 0; i < num_hosts; ++i) {
-    if (options_.verbose) {
-      std::cout << "  " << options_.hosts[i].url() << ": ";
-      if (min_ping_micros_[i] == std::numeric_limits<long>::max()) {
-        std::cout << "no packets received";
-      } else {
-        double ping_ms = min_ping_micros_[i] / 1000.0;
-        if (ping_ms < 10) {
-          std::cout << std::fixed << std::setprecision(1);
-        } else {
-          std::cout << std::fixed << std::setprecision(0);
-        }
-        std::cout << ping_ms << " ms";
-      }
-      std::cout << "\n";
-    }
-    if (min_ping_micros_[i] < min_ping_micros_[min_index]) {
-      min_index = i;
-    }
+  UploadTask::Options upload_options;
+  upload_options.verbose = options_.verbose;
+  upload_options.num_transfers = NumUploads();
+  upload_options.payload = MakeRandomData(UploadSize());
+  upload_options.request_factory = [this](int id) -> http::Request::Ptr{
+    return MakeTransferRequest(id, "/upload");
+  };
+
+  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();
+  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 "
+                << round(speed_variance, 4) << ")\n";
+    };
   }
-  if (min_ping_micros_[min_index] == std::numeric_limits<long>::max()) {
-    // no servers respondeded
-    return false;
+  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";
   }
-  url_ = options_.hosts[min_index];
-  std::cout << "Host for Speedtest: " << url_.url() << " (";
-  double ping_ms = min_ping_micros_[min_index] / 1000.0;
-  if (ping_ms < 10) {
-    std::cout << std::fixed << std::setprecision(1);
-  } else {
-    std::cout << std::fixed << std::setprecision(0);
-  }
-  std::cout << ping_ms << " ms)\n";
-  return true;
+  std::cout << "Upload speed: "
+            << round(runner.GetSpeedInMegabits(), 2) << " Mbps\n";
 }
 
-void Speedtest::RunPing(size_t index) {
-  http::Request::DownloadFn noop = [](void *, size_t) {};
-  min_ping_micros_[index] = std::numeric_limits<long>::max();
-  http::Url url(options_.hosts[index]);
-  url.set_path("/ping");
-  auto ping = env_->NewRequest();
-  ping->set_url(url);
-  while (!end_ping_) {
-    long req_start = SystemTimeMicros();
-    if (ping->Get(noop) == CURLE_OK) {
-      long req_end = SystemTimeMicros();
-      long ping_time = req_end - req_start;
-      min_ping_micros_[index] = std::min(min_ping_micros_[index], ping_time);
-    }
-    ping->Reset();
-    std::this_thread::sleep_for(std::chrono::milliseconds(100));
-  }
+int Speedtest::NumDownloads() const {
+  return options_.num_downloads
+         ? options_.num_downloads
+         : config_.num_downloads;
 }
 
-std::unique_ptr<http::Request> Speedtest::MakeRequest(int id,
-                                                      const std::string &path) {
-  auto request = env_->NewRequest();
-  http::Url url(url_);
-  int port = (id % 20) + url.port() + 1;
-  url.set_port(port);
-  url.set_path(path);
-  request->set_url(url);
+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_);
+  }
   return std::move(request);
 }
 
+http::Request::Ptr Speedtest::MakeBaseRequest(
+    int id, const std::string &path) {
+  http::Url url(*server_url_);
+  url.set_path(path);
+  return MakeRequest(url);
+}
+
+http::Request::Ptr Speedtest::MakeTransferRequest(
+    int id, const std::string &path) {
+  http::Url url(*server_url_);
+  int port_start = config_.transfer_port_start;
+  int port_end = config_.transfer_port_end;
+  int num_ports = port_end - port_start + 1;
+  if (num_ports > 0) {
+    url.set_port(port_start + (id % num_ports));
+  }
+  url.set_path(path);
+  return MakeRequest(url);
+}
+
 }  // namespace speedtest
diff --git a/speedtest/speedtest.h b/speedtest/speedtest.h
index 01e6f75..fb32355 100644
--- a/speedtest/speedtest.h
+++ b/speedtest/speedtest.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -21,8 +21,12 @@
 #include <memory>
 #include <string>
 
+#include "config.h"
 #include "curl_env.h"
+#include "download_task.h"
 #include "options.h"
+#include "ping_task.h"
+#include "upload_task.h"
 #include "url.h"
 #include "request.h"
 
@@ -34,27 +38,40 @@
   virtual ~Speedtest();
 
   void Run();
-  void RunDownloadTest();
-  void RunUploadTest();
-  bool RunPingTest();
 
  private:
-  void RunDownload(int id);
-  void RunUpload(int id);
-  void RunPing(size_t host_index);
+  void InitUserAgent();
+  void LoadServerList();
+  void FindNearestServer();
+  std::string LoadConfig(const http::Url &url);
+  void RunPingTest();
+  void RunDownloadTest();
+  void RunUploadTest();
 
-  std::unique_ptr<http::Request> MakeRequest(int id, const std::string &path);
+  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;
 
-  std::shared_ptr<http::CurlEnv> env_;
+  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);
+
+  std::shared_ptr <http::CurlEnv> env_;
   Options options_;
-  http::Url url_;
-  std::atomic_bool end_ping_;
-  std::atomic_bool end_download_;
-  std::atomic_bool end_upload_;
-  std::atomic_long bytes_downloaded_;
-  std::atomic_long bytes_uploaded_;
-  std::vector<long> min_ping_micros_;
-  const char *send_data_;
+  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_;
 
   // disable
   Speedtest(const Speedtest &) = delete;
diff --git a/speedtest/speedtest_main.cc b/speedtest/speedtest_main.cc
index 8a9c2c9..d756c4b 100644
--- a/speedtest/speedtest_main.cc
+++ b/speedtest/speedtest_main.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
diff --git a/speedtest/task.cc b/speedtest/task.cc
new file mode 100644
index 0000000..84d12c9
--- /dev/null
+++ b/speedtest/task.cc
@@ -0,0 +1,127 @@
+/*
+ * 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
new file mode 100644
index 0000000..429b078
--- /dev/null
+++ b/speedtest/task.h
@@ -0,0 +1,80 @@
+/*
+ * 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
new file mode 100644
index 0000000..bf7c4cc
--- /dev/null
+++ b/speedtest/timed_runner.cc
@@ -0,0 +1,35 @@
+/*
+ * 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
new file mode 100644
index 0000000..02e673f
--- /dev/null
+++ b/speedtest/timed_runner.h
@@ -0,0 +1,29 @@
+/*
+ * 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_RUNNER_H
+#define SPEEDTEST_RUNNER_H
+
+#include "task.h"
+
+namespace speedtest {
+
+// Run a task for a set duration
+void RunTimed(Task *task, long millis);
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_RUNNER_H
diff --git a/speedtest/transfer_runner.cc b/speedtest/transfer_runner.cc
new file mode 100644
index 0000000..d37f087
--- /dev/null
+++ b/speedtest/transfer_runner.cc
@@ -0,0 +1,171 @@
+/*
+ * 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_runner.h"
+
+#include <algorithm>
+#include <cassert>
+#include <chrono>
+#include <iostream>
+#include <thread>
+#include "transfer_task.h"
+#include "utils.h"
+
+namespace speedtest {
+namespace {
+
+const int kDefaultIntervalMillis = 200;
+
+}  // 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) {
+    return 0.0;
+  }
+  Interval last_interval = GetLastInterval();
+  double percent = 2.0d / (num_intervals + 1);
+  return GetSimpleAverage(1) * percent +
+      last_interval.short_megabits * (1 - percent);
+}
+
+double TransferRunner::GetLongEma(int num_intervals) {
+  if (intervals_.empty() || num_intervals <= 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);
+}
+
+double TransferRunner::GetSimpleAverage(int num_intervals) {
+  if (intervals_.empty() || num_intervals <= 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);
+}
+
+}  // namespace
diff --git a/speedtest/transfer_runner.h b/speedtest/transfer_runner.h
new file mode 100644
index 0000000..793c8ec
--- /dev/null
+++ b/speedtest/transfer_runner.h
@@ -0,0 +1,84 @@
+/*
+ * 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_RUNNER_H
+#define SPEEDTEST_TRANSFER_RUNNER_H
+
+#include <functional>
+#include <mutex>
+#include <thread>
+#include <vector>
+#include "task.h"
+#include "transfer_task.h"
+
+namespace speedtest {
+
+struct Interval {
+  long bytes = 0;
+  long running_time = 0;
+  double short_megabits = 0.0;
+  double long_megabits = 0.0;
+};
+
+// 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;
+  };
+
+  explicit TransferRunner(const Options &options);
+
+  double GetSpeedInMegabits() const;
+  Interval GetLastInterval() const;
+
+ protected:
+  void RunInternal() override;
+  void StopInternal() override;
+
+ private:
+  const Interval &AddInterval();
+  double GetSimpleAverage(int num_intervals);
+  double GetShortEma(int num_intervals);
+  double GetLongEma(int num_intervals);
+
+  Options options_;
+
+  mutable std::mutex mutex_;
+  std::vector<Interval> intervals_;
+  std::vector<std::thread> threads_;
+  double speed_;
+
+  // disallowed
+  TransferRunner(const TransferRunner &) = delete;
+  void operator=(const TransferRunner &) = delete;
+};
+
+}  // namespace
+
+#endif //SPEEDTEST_TRANSFER_RUNNER_H
diff --git a/speedtest/transfer_task.cc b/speedtest/transfer_task.cc
new file mode 100644
index 0000000..d742d87
--- /dev/null
+++ b/speedtest/transfer_task.cc
@@ -0,0 +1,51 @@
+/*
+ * 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
new file mode 100644
index 0000000..83cff9e
--- /dev/null
+++ b/speedtest/transfer_task.h
@@ -0,0 +1,55 @@
+/*
+ * 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_task.cc b/speedtest/upload_task.cc
new file mode 100644
index 0000000..251fc41
--- /dev/null
+++ b/speedtest/upload_task.cc
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#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
new file mode 100644
index 0000000..323f904
--- /dev/null
+++ b/speedtest/upload_task.h
@@ -0,0 +1,53 @@
+/*
+ * 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/url.cc b/speedtest/url.cc
index c0934a0..61588a0 100644
--- a/speedtest/url.cc
+++ b/speedtest/url.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -132,6 +132,10 @@
   return url1.url() == url2.url();
 }
 
+std::ostream &operator<<(std::ostream &os, const Url &url) {
+  return os << (url.ok() ? url.url() : "{invalid URL}");
+}
+
 Url::Url(): parsed_(false), absolute_(false), port_(0) {
 }
 
@@ -150,7 +154,8 @@
   fragment_ = other.fragment_;
 }
 
-Url::Url(const char *url): parsed_(false), absolute_(false), port_(0) {
+Url::Url(const std::string &url)
+    : parsed_(false), absolute_(false), port_(0) {
   Parse(url);
 }
 
@@ -411,8 +416,9 @@
     return false;
   }
   std::string port(start, iter);
-  int portnum = speedtest::stoi(port);
-  if (portnum < 1 || portnum > 65535) {
+  int portnum;
+  if (!speedtest::ParseInt(port, &portnum) ||
+      portnum < 1 || portnum > 65535) {
     return false;
   }
   current_ = iter;
diff --git a/speedtest/url.h b/speedtest/url.h
index 6aeb68a..4844916 100644
--- a/speedtest/url.h
+++ b/speedtest/url.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -24,6 +24,7 @@
 class Url;
 
 bool operator==(const Url &url1, const Url &url2);
+std::ostream &operator<<(std::ostream &os, const Url &url);
 
 // Partial implementation of a URL parser. This is needed because URLs need
 // to be manipulated for creating URLs for Speedtest, which is otherwise
@@ -43,7 +44,7 @@
  public:
   Url();
   Url(const Url &other);
-  explicit Url(const char *url);
+  explicit Url(const std::string &url);
   Url &operator=(const Url &other);
 
   bool Parse(const std::string &url);
@@ -75,6 +76,7 @@
   std::string url() const;
 
   friend bool operator==(const Url &url1, const Url &url2);
+  friend std::ostream &operator<<(std::ostream &os, const Url &url);
 
  private:
   using Iter = std::string::const_iterator;
diff --git a/speedtest/url_test.cc b/speedtest/url_test.cc
index 06c2d72..f2945a5 100644
--- a/speedtest/url_test.cc
+++ b/speedtest/url_test.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-#include <gtest/gtest.h>
-
 #include "url.h"
 
+#include <gtest/gtest.h>
+
 namespace http {
 namespace {
 
diff --git a/speedtest/utils.cc b/speedtest/utils.cc
index 012e1eb..580b54b 100644
--- a/speedtest/utils.cc
+++ b/speedtest/utils.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -16,9 +16,13 @@
 
 #include "utils.h"
 
+#include <algorithm>
+#include <cctype>
 #include <cstdlib>
+#include <functional>
 #include <iostream>
 #include <stdexcept>
+#include <stdio.h>
 #include <string>
 #include <sstream>
 
@@ -33,24 +37,62 @@
   return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
 }
 
-std::string to_string(long n)
-{
+std::string to_string(long n) {
   std::ostringstream s;
   s << n;
   return s.str();
 }
 
-int stoi(const std::string& str)
-{
-  int rc;
-  std::istringstream n(str);
+std::string round(double d, int digits) {
+  char buf[20];
+  sprintf(buf, "%.*f", digits, d);
+  return buf;
+}
 
-  if (!(n >> rc)) {
-    std::cerr << "Not a number: " << str;
-    std::exit(1);
+double variance(double d1, double d2) {
+  if (d2 == 0) {
+    return 0.0;
   }
+  double smaller = std::min(d1, d2);
+  double larger = std::max(d1, d2);
+  return 1.0 - smaller / larger;
+}
 
-  return rc;
+double ToMegabits(long bytes, long micros) {
+  return (8.0d * bytes) / micros;
+}
+
+bool ParseInt(const std::string &str, int *result) {
+  if (!result) {
+    return false;
+  }
+  std::istringstream n(str);
+  return n >> *result;
+}
+
+// Trim from start in place
+// Caller retains ownership
+void LeftTrim(std::string *s) {
+  s->erase(s->begin(),
+           std::find_if(s->begin(),
+                        s->end(),
+                        std::not1(std::ptr_fun<int, int>(std::isspace))));
+}
+
+// Trim from end in place
+// Caller retains ownership
+void RightTrim(std::string *s) {
+  s->erase(std::find_if(s->rbegin(),
+                        s->rend(),
+                        std::not1(std::ptr_fun<int, int>(std::isspace))).base(),
+           s->end());
+}
+
+// Trim from both ends in place
+// Caller retains ownership
+void Trim(std::string *s) {
+  LeftTrim(s);
+  RightTrim(s);
 }
 
 }  // namespace speedtest
diff --git a/speedtest/utils.h b/speedtest/utils.h
index 7ad4289..7e8d251 100644
--- a/speedtest/utils.h
+++ b/speedtest/utils.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * 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.
@@ -28,8 +28,31 @@
 // Return a string representation of n
 std::string to_string(long n);
 
-// return an integer value from the string str.
-int stoi(const std::string& str);
+// Round a double to a minimum number of significant digits
+std::string round(double d, int digits);
+
+// Return 1 - (shorter / larger)
+double variance(double d1, double d2);
+
+// Convert bytes and time in micros to speed in megabits
+double ToMegabits(long bytes, 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.
+bool ParseInt(const std::string &str, int *result);
+
+// Trim from start in place
+// Caller retains ownership
+void LeftTrim(std::string *s);
+
+// Trim from end in place
+// Caller retains ownership
+void RightTrim(std::string *s);
+
+// Trim from both ends in place
+// Caller retains ownership
+void Trim(std::string *s);
 
 }  // namespace speedtst
 
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index d0aaf45..e746f13 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -37,6 +37,8 @@
     '5c:ff:35': ['asus'],
     '74:d0:2b': ['asus'],
     'ac:22:0b': ['asus'],
+    'bc:ee:7b': ['asus'],
+    'd8:50:e6': ['asus'],
 
     '30:8c:fb': ['dropcam'],
 
@@ -49,6 +51,7 @@
 
     # These are registered to AzureWave, but used for Chromecast v1.
     '6c:ad:f8': ['azurewave', 'google'],
+    'b0:ee:45': ['azurewave', 'google'],
     'd0:e7:82': ['azurewave', 'google'],
 
     '00:23:76': ['htc'],
@@ -69,6 +72,7 @@
     '0c:48:85': ['lg'],
     '10:68:3f': ['lg'],
     '2c:54:cf': ['lg'],
+    '34:fc:ef': ['lg'],
     '40:b0:fa': ['lg'],
     '58:3f:54': ['lg'],
     '64:89:9a': ['lg'],
@@ -91,6 +95,7 @@
     '1c:56:fe': ['motorola'],
     '24:da:9b': ['motorola'],
     '3c:43:8e': ['motorola'],
+    '40:78:6a': ['motorola'],
     '44:80:eb': ['motorola'],
     '5c:51:88': ['motorola'],
     '60:be:b5': ['motorola'],
@@ -100,26 +105,29 @@
     '98:4b:4a': ['motorola'],
     '9c:d9:17': ['motorola'],
     'cc:c3:ea': ['motorola'],
+    'ec:88:92': ['motorola'],
     'e8:91:20': ['motorola'],
     'f8:7b:7a': ['motorola'],
     'f8:cf:c5': ['motorola'],
     'f8:e0:79': ['motorola'],
     'f8:f1:b6': ['motorola'],
 
-    '00:26:e8': ['murata'],
-    '10:a5:d0': ['murata'],
-    '14:7d:c5': ['murata'],
-    '1c:99:4c': ['murata'],
-    '20:02:af': ['murata'],
-    '40:f3:08': ['murata'],
-    '44:a7:cf': ['murata'],
-    '5c:da:d4': ['murata'],
-    '78:4b:87': ['murata'],
-    '90:b6:86': ['murata'],
-    '98:f1:70': ['murata'],
-    'f0:27:65': ['murata'],
-    'fc:c2:de': ['murata'],
-    'fc:db:b3': ['murata'],
+    '00:26:e8': ['murata', 'samsung'],
+    '00:ae:fa': ['murata', 'samsung'],
+    '10:a5:d0': ['murata', 'samsung'],
+    '14:7d:c5': ['murata', 'samsung'],
+    '1c:99:4c': ['murata', 'samsung'],
+    '20:02:af': ['murata', 'samsung'],
+    '40:f3:08': ['murata', 'samsung'],
+    '44:a7:cf': ['murata', 'samsung'],
+    '5c:da:d4': ['murata', 'samsung'],
+    '5c:f8:a1': ['murata', 'samsung'],
+    '78:4b:87': ['murata', 'samsung'],
+    '90:b6:86': ['murata', 'samsung'],
+    '98:f1:70': ['murata', 'samsung'],
+    'f0:27:65': ['murata', 'samsung'],
+    'fc:c2:de': ['murata', 'samsung'],
+    'fc:db:b3': ['murata', 'samsung'],
 
     '18:b4:30': ['nest'],
 
@@ -137,12 +145,14 @@
     '3c:8b:fe': ['samsung'],
     '40:0e:85': ['samsung'],
     '48:5a:3f': ['samsung', 'wisol'],
+    '54:88:0e': ['samsung'],
     '5c:0a:5b': ['samsung'],
     '5c:f6:dc': ['samsung'],
     '6c:2f:2c': ['samsung'],
     '6c:83:36': ['samsung'],
     '78:d6:f0': ['samsung'],
     '80:65:6d': ['samsung'],
+    '84:11:9e': ['samsung'],
     '84:25:db': ['samsung'],
     '84:38:38': ['samsung'],
     '88:32:9b': ['samsung'],
@@ -155,6 +165,7 @@
     'b0:df:3a': ['samsung'],
     'b0:ec:71': ['samsung'],
     'b4:07:f9': ['samsung'],
+    'bc:20:a4': ['samsung'],
     'c0:bd:d1': ['samsung'],
     'c4:42:02': ['samsung'],
     'cc:07:ab': ['samsung'],
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index ffc7161..bb76e15 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -38,6 +38,10 @@
   ('Unknown', './testdata/pcaps/MediaTek MT7610U 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Motorola Droid 2 2.4GHz.pcap'),
   ('Unknown', './testdata/pcaps/Motorola Droid 3 2.4GHz.pcap'),
+  ('Unknown', './testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Broadcast Probe.pcap'),
+  ('Unknown', './testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Specific Probe.pcap'),
+  ('Unknown', './testdata/pcaps/Motorola Droid Razr 2.4GHz XT910.pcap'),
+  ('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'),
diff --git a/taxonomy/ssdp.py b/taxonomy/ssdp.py
index f16b3ce..f359b1b 100644
--- a/taxonomy/ssdp.py
+++ b/taxonomy/ssdp.py
@@ -21,8 +21,11 @@
 
 
 database = {
+    'Canon IJ-UPnP/1.0 UPnP/1.0 UPnP-Device-Host/1.0': 'Canon Printer',
     'OpenRG/6.0.7.1.4 UPnP/1.0': 'Google Fiber GFRG1x0',
     'HDHomeRun/1.0 UPnP/1.0': 'HDHomeRun',
+    'Linux UPnP/1.0 Sonos/31.9-26010 (ZPS1)': 'Sonos ZPS1',
+    'Linux UPnP/1.0 Sonos/31.9-26010 (ZPS5)': 'Somos ZPS5',
     'Linux UPnP/1.0 Sonos/28.1-83040 (ZP90)': 'Sonos ZP90',
     'Linux UPnP/1.0 Sonos/28.1-83040 (ZP120)': 'Sonos ZP120',
     'WNDR3700v2 UPnP/1.0 miniupnpd/1.0': 'Netgear WNDR3700',
diff --git a/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Broadcast Probe.pcap b/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Broadcast Probe.pcap
new file mode 100644
index 0000000..07b4d99
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Broadcast Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Specific Probe.pcap b/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Specific Probe.pcap
new file mode 100644
index 0000000..7cd4d3b
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910 Specific Probe.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910.pcap b/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910.pcap
new file mode 100644
index 0000000..77cb52a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Motorola Droid Razr 2.4GHz XT910.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Motorola Droid Razr 5GHz XT910.pcap b/taxonomy/testdata/pcaps/Motorola Droid Razr 5GHz XT910.pcap
new file mode 100644
index 0000000..d52dcda
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Motorola Droid Razr 5GHz XT910.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Nexus Player 2.4GHz.pcap b/taxonomy/testdata/pcaps/Nexus Player 2.4GHz.pcap
new file mode 100644
index 0000000..429dc07
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus Player 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Nexus Player 5GHz.pcap b/taxonomy/testdata/pcaps/Nexus Player 5GHz.pcap
new file mode 100644
index 0000000..aaff1e4
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Nexus Player 5GHz.pcap
Binary files differ
diff --git a/taxonomy/tests/wifi_test.py b/taxonomy/tests/wifi_test.py
index 8fae201..c16824d 100755
--- a/taxonomy/tests/wifi_test.py
+++ b/taxonomy/tests/wifi_test.py
@@ -48,16 +48,18 @@
     self.assertEqual('802.11n n:2,w:40', taxonomy[2])
 
   def testNameLookup(self):
-    signature = ('wifi|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,'
-                 '221(001018,2),221(0050f2,2)')
+    signature = ('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')
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual(3, len(taxonomy))
     self.assertEqual('Unknown', taxonomy[1])
     taxonomy = wifi.identify_wifi_device(signature, '00:00:01:00:00:01')
     self.assertEqual(3, len(taxonomy))
     self.assertEqual('Unknown', taxonomy[1])
-    taxonomy = wifi.identify_wifi_device(signature, '2c:1f:23:ff:ff:01')
-    self.assertEqual('iPod Touch 3rd gen', taxonomy[1])
+    taxonomy = wifi.identify_wifi_device(signature, 'c8:69:cd:5e:b5:43')
+    self.assertEqual('Apple TV (3rd gen)', taxonomy[1])
 
   def testChecksumWhenNoIdentification(self):
     taxonomy = wifi.identify_wifi_device('wifi|probe:1,2,3,4,htcap:0|assoc:1',
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index c8e224f..c46191a 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -59,7 +59,9 @@
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:180c,htagg:1b,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:180c,htagg:1b,htmcs:000000ff,txpow:1308|os:ios':
         ('BCM4329', 'Apple TV (2nd gen)', '2.4GHz'),
 
-    'wifi|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0100|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100|name:Apple-TV':
+    '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|name:appletv':
+        ('BCM4330', 'Apple TV (3rd gen)', '2.4GHz'),
+    '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,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100,htagg:19,htmcs:000000ff,txpow:180f|name:appletv':
         ('BCM4330', 'Apple TV (3rd 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:1907|os:ios':
@@ -76,7 +78,7 @@
     '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'),
 
-    'wifi|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':
+    '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,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':
@@ -109,12 +111,11 @@
         ('Marvell_88W8887', 'Chromecast v2', '5GHz'),
     'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,33,36,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,extcap:0400000000000140|oui:google':
         ('Marvell_88W8887', 'Chromecast v2', '5GHz'),
+    'wifi4|probe:0,1,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,33,36,48,127,221(0050f2,2),45,191,htcap:006e,htagg:03,htmcs:000000ff,vhtcap:33c07030,vhtrxmcs:0186fffe,vhttxmcs:0186fffe,txpow:1308,extcap:0400000000000140|oui:google':
+        ('Marvell_88W8887', 'Chromecast v2', '5GHz'),
     '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,45,221(001018,2),221(00904c,51),htcap:007c|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:007c':
-        ('', 'DirecTV HR-44', ''),
-
     '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':
         ('', 'Dropcam', '2.4GHz'),
 
@@ -173,7 +174,9 @@
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:1800,htagg:1b,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1800,htagg:1b,htmcs:000000ff,txpow:1108|os:ios':
         ('BCM4329', 'iPad (2nd gen)', '2.4GHz'),
 
-    'wifi|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0100|assoc:0,1,33,36,48,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100|os:ios':
+    '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,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0100,htagg:19,htmcs:000000ff,txpow:180f|os:ios':
+        ('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':
         ('BCM4330', 'iPad (3rd gen)', '2.4GHz'),
@@ -278,6 +281,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:0000ffff,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: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: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':
@@ -296,7 +301,7 @@
     'wifi4|probe:0,1,3,50|assoc:0,1,48,50|os:ipodtouch1':
         ('Marvell_W8686B22', 'iPod Touch 1st/2nd gen', '2.4GHz'),
 
-    'wifi|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2),221(0050f2,2)|name:ipod':
+    'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2),221(0050f2,2)|name:ipod':
         ('BCM4329', 'iPod Touch 3rd gen', '2.4GHz'),
 
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:180c,htagg:1b,htmcs:000000ff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:180c,htagg:1b,htmcs:000000ff|os:ios':
@@ -304,6 +309,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,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':
         ('BCM4334', 'iPod Touch 5th gen', '2.4GHz'),
 
@@ -330,17 +337,12 @@
     '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':
         ('', 'LG Tribute', '2.4GHz'),
 
-    'wifi|probe:0,1,50,45,221(00904c,51),htcap:182c|assoc:0,1,33,36,48,50,45,221(00904c,51),221(0050f2,2),htcap:182c|os:macos':
-        ('BCM4322', 'MacBook late 2008 (A1278)', '5GHz'),
-    'wifi|probe:0,1,50,3,45,221(00904c,51),htcap:182c|assoc:0,1,33,36,48,50,45,221(00904c,51),221(0050f2,2),htcap:182c|os:macos':
-        ('BCM4322', 'MacBook late 2008 (A1278)', '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':
         ('BCM43224', 'MacBook Air late 2010 (A1369)', '5GHz'),
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:187c,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:187c,htagg:1b,htmcs:0000ffff,txpow:1207|os:macos':
         ('BCM43224', 'MacBook Air late 2010 (A1369)', '2.4GHz'),
 
-    'wifi|probe:0,1,45,221(00904c,51),htcap:086e,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(00904c,51),221(0050f2,2),htcap:086e,htagg:1b,htmcs:0000ffff|os:macos':
+    'wifi4|probe:0,1,45,221(00904c,51),htcap:086e,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(00904c,51),221(0050f2,2),htcap:086e,htagg:1b,htmcs:0000ffff,txpow:0f07|os:macos':
         ('BCM4322', 'MacBook Air late 2011', '5GHz'),
 
     'wifi4|probe:0,1,45,221(00904c,51),htcap:09ef,htagg:1b,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(00904c,51),221(0050f2,2),htcap:09ef,htagg:1b,htmcs:0000ffff,txpow:0005|os:macos':
@@ -432,6 +434,8 @@
         ('QCA6174', 'Nexus 5X', '5GHz'),
     'wifi4|probe:0,1,45,221(0050f2,8),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:000000000000004080|assoc:0,1,48,45,221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:000000000000004080|oui:lg':
         ('QCA6174', 'Nexus 5X', '5GHz'),
+    'wifi4|probe:0,1,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a020100004080|assoc:0,1,48,45,221(0050f2,2),191,127,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:339071b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:000000000000004080|oui:lg':
+        ('QCA6174', 'Nexus 5X', '5GHz'),
     'wifi4|probe:0,1,50,127,45,191,htcap:01ef,htagg:03,htmcs:0000ffff,vhtcap:338061b2,vhtrxmcs:030cfffa,vhttxmcs:030cfffa,extcap:00000a0201000040|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,extcap:0000000000000000|oui:lg':
         ('QCA6174', 'Nexus 5X', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,htcap:01ad,htagg:03,htmcs:0000ffff,extcap:000000000000000080|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:01ad,htagg:03,htmcs:0000ffff,extcap:000000000000000080|oui:lg':
@@ -443,11 +447,17 @@
         ('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,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':
         ('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,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':
         ('BCM4356', 'Nexus 6', '2.4GHz'),
+    'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:000008800140,wps:Nexus_6|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1209,extcap:000008800140':
+        ('BCM4356', 'Nexus 6', '2.4GHz'),
 
+    'wifi4|probe:0,1,45,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,wps:Nexus_6P|assoc:0,1,33,36,48,45,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002':
+        ('BCM4358', 'Nexus 6P', '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_6P|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:0000088001400040':
         ('BCM4358', 'Nexus 6P', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_6P|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1402,extcap:0000088001400040':
@@ -468,6 +478,14 @@
         ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
     'wifi4|probe:0,1,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,extcap:00000a02':
         ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
+    'wifi4|probe:0,1,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,33,36,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:1e0d,extcap:00000a02':
+        ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
+    'wifi4|probe:0,1,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a02':
+        ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
+    'wifi4|probe:0,1,45,221(0050f2,8),127,221(0050f2,4),221(506f9a,10),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,extcap:00000a02,wps:Nexus_7|assoc:0,1,33,36,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:1e0d,extcap:00000a02':
+        ('QCA_WCN3660', 'Nexus 7 (2013)', '5GHz'),
+    'wifi4|probe:0,1,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,wps:Nexus_7|assoc:0,1,33,36,48,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:1e0d,extcap:00000a02':
+        ('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':
@@ -479,19 +497,17 @@
         ('BCM4354', 'Nexus 9', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0000088001400040,wps:Nexus_9|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1309,extcap:000008800140':
         ('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,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'),
-    '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:murata':
-        ('', 'Nexus 10', '5GHz'),
     'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:01bc,htagg:1b,htmcs:0000ffff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff|oui:samsung':
         ('', 'Nexus 10', '2.4GHz'),
-    'wifi4|probe:0,1,50,3,45,221(001018,2),221(00904c,51),htcap:01bc,htagg:1b,htmcs:0000ffff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:01bc,htagg:1b,htmcs:0000ffff|oui:murata':
-        ('', 'Nexus 10', '2.4GHz'),
 
-    'wifi|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:006f,vhtcap:0f815832,wps:Nexus_Player|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,vhtcap:0f815832':
+    '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'),
-    'wifi|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:002d,wps:Nexus_Player|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d':
+    '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'),
 
     'wifi4|probe:0,1,50,45,51,127,htcap:012c,htagg:1b,htmcs:000000ff,extcap:0100000000000040|assoc:0,1,48,50,221(0050f2,2),45,51,127,htcap:012c,htagg:1b,htmcs:000000ff,extcap:0100000000000040|os:windows-phone':
@@ -513,7 +529,7 @@
     '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'),
 
-    'wifi|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:19bc,htagg:16,htmcs:0000ffff,extcap:00000000|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:19bc,htagg:16,htmcs:0000ffff,extcap:00000000|os:roku':
+    'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:19bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:19bc,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
         ('BCM43236', 'Roku 3', '2.4GHz'),
 
     'wifi4|probe:0,1,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':
@@ -523,8 +539,6 @@
 
     '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'),
-    '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:murata':
-        ('', 'Samsung Galaxy Mini', '2.4GHz'),
 
     'wifi4|probe:0,1,45,3,221(0050f2,4),221(001018,2),221(00904c,51),htcap:010c,htagg:19,htmcs:000000ff,wps:Galaxy_Nexus|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:010c,htagg:19,htmcs:000000ff,txpow:0f09':
         ('BCM4330', 'Samsung Galaxy Nexus', '5GHz'),
@@ -532,236 +546,130 @@
         ('BCM4330', 'Samsung Galaxy Nexus', '5GHz'),
     'wifi4|probe:0,1,45,3,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:0f0a|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Nexus', '5GHz'),
-    'wifi4|probe:0,1,45,3,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:0f0a|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Nexus', '5GHz'),
     'wifi4|probe:0,1,50,45,221(0050f2,4),221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff,wps:Galaxy_Nexus|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff,txpow:1209':
         ('BCM4330', 'Samsung Galaxy Nexus', '2.4GHz'),
     'wifi4|probe:0,1,50,45,3,221(0050f2,4),221(001018,2),221(00904c,51),htcap:110c,htagg:19,htmcs:000000ff,wps:Galaxy_Nexus|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:110c,htagg:19,htmcs:000000ff,txpow:1209':
         ('BCM4330', 'Samsung Galaxy Nexus', '2.4GHz'),
     'wifi4|probe:0,1,50,45,221(0050f2,4),221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff,wps:_|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Nexus', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,221(0050f2,4),221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff,wps:_|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Nexus', '2.4GHz'),
 
     'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:000c,htagg:19,htmcs:000000ff,txpow:0f09|oui:samsung':
         ('', 'Samsung Galaxy Note or S2+', '5GHz'),
-    'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:000c,htagg:19,htmcs:000000ff,txpow:0f09|oui:murata':
-        ('', 'Samsung Galaxy Note or S2+', '5GHz'),
 
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:1409|oui:samsung':
         ('', 'Samsung Galaxy Note', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:1409|oui:murata':
-        ('', 'Samsung Galaxy Note', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
-    'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
-    'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
     'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
-    'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
     'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
-    'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:0e09|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Note 2', '5GHz'),
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:1020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1020,htagg:1a,htmcs:000000ff,txpow:1209|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Note 2', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:1020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1020,htagg:1a,htmcs:000000ff,txpow:1209|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Note 2', '2.4GHz'),
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:1020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1020,htagg:1a,htmcs:000000ff,txpow:1209|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Note 2', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:1020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1020,htagg:1a,htmcs:000000ff,txpow:1209|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Note 2', '2.4GHz'),
 
     'wifi4|probe:0,1,3,45,127,191,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:0000080000000040|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:0000000000000040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy Note 3', '5GHz'),
-    'wifi4|probe:0,1,3,45,127,191,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:0000080000000040|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:0000000000000040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy Note 3', '5GHz'),
     'wifi4|probe:0,1,45,127,191,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:0000080000000040|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:0000000000000040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy Note 3', '5GHz'),
-    'wifi4|probe:0,1,45,127,191,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:0000080000000040|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:0000000000000040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy Note 3', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:112d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),htcap:112d,htagg:17,htmcs:000000ff,txpow:1208|oui:samsung':
         ('BCM4335', 'Samsung Galaxy Note 3', '2.4GHz'),
-    'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:112d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),htcap:112d,htagg:17,htmcs:000000ff,txpow:1208|oui:murata':
-        ('BCM4335', 'Samsung Galaxy Note 3', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:112d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:112d,htagg:17,htmcs:000000ff,txpow:1208|oui:samsung':
         ('BCM4335', 'Samsung Galaxy Note 3', '2.4GHz'),
-    'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:112d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:112d,htagg:17,htmcs:000000ff,txpow:1208|oui:murata':
-        ('BCM4335', 'Samsung Galaxy Note 3', '2.4GHz'),
 
     '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:e009,extcap:0000088001400040|oui:samsung':
         ('BCM4358', 'Samsung Galaxy Note 4', '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: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:e009,extcap:0000088001400040|oui:murata':
-        ('BCM4358', 'Samsung Galaxy Note 4', '5GHz'),
     'wifi4|probe:0,1,45,127,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,70,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:e009,extcap:0000088001400040|oui:samsung':
         ('BCM4358', 'Samsung Galaxy Note 4', '5GHz'),
-    'wifi4|probe:0,1,45,127,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,70,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:e009,extcap:0000088001400040|oui:murata':
-        ('BCM4358', 'Samsung Galaxy Note 4', '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:0000088001400040|assoc:0,1,50,33,36,48,45,127,107,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1509,extcap:0000088001400040|oui:samsung':
         ('BCM4358', 'Samsung Galaxy Note 4', '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:1509,extcap:0000088001400040|oui:murata':
-        ('BCM4358', 'Samsung Galaxy Note 4', '2.4GHz'),
     'wifi4|probe:0,1,50,3,45,127,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,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1509,extcap:0000088001400040|oui:samsung':
         ('BCM4358', 'Samsung Galaxy Note 4', '2.4GHz'),
-    'wifi4|probe:0,1,50,3,45,127,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,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1509,extcap:0000088001400040|oui:murata':
-        ('BCM4358', 'Samsung Galaxy Note 4', '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: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,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:murata':
-        ('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'),
-    '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:murata':
-        ('BCM4359', 'Samsung Galaxy Note 5', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
         ('', 'Samsung Galaxy S2', '5GHz'),
-    'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:murata':
-        ('', 'Samsung Galaxy S2', '5GHz'),
     'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
         ('', 'Samsung Galaxy S2', '5GHz'),
-    'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:000c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:000c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:murata':
-        ('', 'Samsung Galaxy S2', '5GHz'),
     'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:samsung':
         ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
-    'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:murata':
-        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:samsung':
         ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
-    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:120a|oui:murata':
-        ('', 'Samsung Galaxy S2 or Infuse','2.4GHz'),
 
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:1209|oui:samsung':
         ('', 'Samsung Galaxy S2+', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:100c,htagg:19,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:1209|oui:murata':
-        ('', 'Samsung Galaxy S2+', '2.4GHz'),
 
     'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1409|oui:samsung':
         ('BCM4334', 'Samsung Galaxy S3', '5GHz'),
-    'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1409|oui:murata':
-        ('BCM4334', 'Samsung Galaxy S3', '5GHz'),
     'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1409|oui:samsung':
         ('BCM4334', 'Samsung Galaxy S3', '5GHz'),
-    'wifi4|probe:0,1,45,3,221(001018,2),221(00904c,51),htcap:0062,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0062,htagg:1a,htmcs:000000ff,txpow:1409|oui:murata':
-        ('BCM4334', 'Samsung Galaxy S3', '5GHz'),
     'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:1020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1020,htagg:1a,htmcs:000000ff,txpow:1409|oui:samsung':
         ('BCM4334', 'Samsung Galaxy S3', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:1020,htagg:1a,htmcs:000000ff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:1020,htagg:1a,htmcs:000000ff,txpow:1409|oui:murata':
-        ('BCM4334', 'Samsung Galaxy S3', '2.4GHz'),
 
     '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,extcap:0000080000000040|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,txpow:e001,extcap:0000000000000040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
-    '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,extcap:0000080000000040|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,txpow:e001,extcap:0000000000000040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
     'wifi4|probe:0,1,3,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,extcap:0000080000000040|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,txpow:e001,extcap:0000000000000040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
-    'wifi4|probe:0,1,3,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,extcap:0000080000000040|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,txpow:e001,extcap:0000000000000040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '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:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088000400040|assoc:0,1,33,36,48,45,127,107,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e001,extcap:0000008000400040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '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:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088000400040|assoc:0,1,33,36,48,45,127,107,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e001,extcap:0000008000400040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
     '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:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088000400040|assoc:0,1,33,36,48,45,127,107,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e001,extcap:0000008000400040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
-    '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:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0000088000400040|assoc:0,1,33,36,48,45,127,107,191,221(001018,2),221(00904c,4),221(0050f2,2),htcap:006f,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e001,extcap:0000008000400040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
     '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,extcap:0000080000400040|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,txpow:e001,extcap:0000000000400040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
-    '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,extcap:0000080000400040|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,txpow:e001,extcap:0000000000400040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '5GHz'),
     'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,extcap:000000800040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
-    'wifi4|probe:0,1,50,3,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,extcap:000000800040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
     'wifi4|probe:0,1,50,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,127,221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000080000000040|assoc:0,1,33,36,48,50,45,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
     'wifi4|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,extcap:000000800040|oui:samsung':
         ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
-    'wifi4|probe:0,1,50,45,127,107,221(506f9a,16),221(001018,2),221(00904c,51),221(00904c,4),221(0050f2,8),htcap:102d,htagg:17,htmcs:000000ff,extcap:0000088000400040|assoc:0,1,33,36,48,50,45,127,107,221(001018,2),221(0050f2,2),htcap:102d,htagg:17,htmcs:000000ff,txpow:1201,extcap:000000800040|oui:murata':
-        ('BCM4335', 'Samsung Galaxy S4', '2.4GHz'),
 
     '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: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:murata':
-        ('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: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:murata':
-        ('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: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:murata':
-        ('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,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:murata':
-        ('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,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:murata':
-        ('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,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1402,extcap:0000088001400040|oui:murata':
-        ('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,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:murata':
-        ('BCM4358', 'Samsung Galaxy S6', '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,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:murata':
-        ('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':
         ('BCM4329', 'Samsung Galaxy Tab', '2.4GHz'),
-    '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:murata':
-        ('BCM4329', 'Samsung Galaxy Tab', '2.4GHz'),
 
     'wifi4|probe:0,1,45,50,htcap:0162,htagg:03,htmcs:00000000|assoc:0,1,48,127,221(0050f2,2),45,htcap:016e,htagg:03,htmcs:000000ff,extcap:0400000000000140|oui:samsung':
         ('Marvell_88W8787', 'Samsung Galaxy Tab 3', '5GHz'),
-    'wifi4|probe:0,1,45,50,htcap:0162,htagg:03,htmcs:00000000|assoc:0,1,48,127,221(0050f2,2),45,htcap:016e,htagg:03,htmcs:000000ff,extcap:0400000000000140|oui:murata':
-        ('Marvell_88W8787', 'Samsung Galaxy Tab 3', '5GHz'),
     'wifi4|probe:0,1,45,50,htcap:0162,htagg:03,htmcs:00000000|assoc:0,1,33,36,48,127,221(0050f2,2),45,htcap:016e,htagg:03,htmcs:000000ff,txpow:1208,extcap:0400000000000140|oui:samsung':
         ('Marvell_88W8787', 'Samsung Galaxy Tab 3', '5GHz'),
-    'wifi4|probe:0,1,45,50,htcap:0162,htagg:03,htmcs:00000000|assoc:0,1,33,36,48,127,221(0050f2,2),45,htcap:016e,htagg:03,htmcs:000000ff,txpow:1208,extcap:0400000000000140|oui:murata':
-        ('Marvell_88W8787', 'Samsung Galaxy Tab 3', '5GHz'),
     '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'),
-    '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:murata':
-        ('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':
         ('APQ8026', 'Samsung Galaxy Tab 4', '5GHz'),
-    '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:murata':
-        ('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':
         ('APQ8026', 'Samsung Galaxy Tab 4', '2.4GHz'),
-    '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:murata':
-        ('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':
         ('BCM4330', 'Samsung Galaxy Tab 10.1', '5GHz'),
-    '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:murata':
-        ('BCM4330', 'Samsung Galaxy Tab 10.1', '5GHz'),
     'wifi4|probe:0,1,45,3,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':
         ('BCM4330', 'Samsung Galaxy Tab 10.1', '5GHz'),
-    'wifi4|probe:0,1,45,3,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:murata':
-        ('BCM4330', 'Samsung Galaxy Tab 10.1', '5GHz'),
     '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,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:samsung':
         ('BCM4330', 'Samsung Galaxy Tab 10.1', '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,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:100c,htagg:19,htmcs:000000ff,txpow:0f0a|oui:murata':
-        ('BCM4330', 'Samsung Galaxy Tab 10.1', '2.4GHz'),
 
     'wifi4|probe:0,1,45,htcap:11ee,htagg:02,htmcs:0000ffff|assoc:0,1,45,127,33,36,48,221(0050f2,2),htcap:11ee,htagg:02,htmcs:0000ffff,txpow:1100,extcap:01|os:samsungtv':
         ('', 'Samsung Smart TV', '5GHz'),
diff --git a/wifi/iw.py b/wifi/iw.py
index b1d49b5..db6590f 100644
--- a/wifi/iw.py
+++ b/wifi/iw.py
@@ -46,11 +46,15 @@
 
 
 def _info(interface, **kwargs):
-  return subprocess.check_output(('iw', interface, 'info'), **kwargs)
+  return subprocess.check_output(('iw', 'dev', interface, 'info'), **kwargs)
 
 
 def _link(interface, **kwargs):
-  return subprocess.check_output(('iw', interface, 'link'), **kwargs)
+  return subprocess.check_output(('iw', 'dev', interface, 'link'), **kwargs)
+
+
+def _scan(interface, **kwargs):
+  return subprocess.check_output(('iw', 'dev', interface, 'scan'), **kwargs)
 
 
 _WIPHY_RE = re.compile(r'Wiphy (?P<phy>\S+)')
@@ -356,3 +360,8 @@
       result.add(band)
 
   return result
+
+
+def scan(interface):
+  """Return 'iw scan' output for printing."""
+  return _scan(interface)
diff --git a/wifi/wifi.py b/wifi/wifi.py
index f57e09e..c4fde30 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -30,8 +30,9 @@
 {bin} stopclient    Disable wifi clients.  Takes -b, -P, -S.
 {bin} restore       Restore saved client and access point options.  Takes -b, -S.
 {bin} show          Print all known parameters.  Takes -b, -S.
+{bin} scan          Print 'iw scan' results for a single band.  Takes -b, -S.
 --
-b,band=                           Wifi band(s) to use (5 GHz and/or 2.4 GHz).  set commands have a default of 2.4 and cannot take multiple-band values.  [2.4 5]
+b,band=                           Wifi band(s) to use (5 GHz and/or 2.4 GHz).  set, setclient, and scan have a default of 2.4 and cannot take multiple-band values.  [2.4 5]
 c,channel=                        Channel to use [auto]
 a,autotype=                       Autochannel method to use (LOW, HIGH, DFS, NONDFS, ANY,OVERLAP) [NONDFS]
 s,ssid=                           SSID to use [{ssid}]
@@ -491,6 +492,30 @@
   return True
 
 
+@iw.requires_iw
+def scan_wifi(opt):
+  """Prints 'iw scan' results.
+
+  Args:
+    opt: The OptDict parsed from command line options.
+
+  Returns:
+    True.
+
+  Raises:
+    BinWifiException: If an expected interface is not found.
+  """
+  band = opt.band.split()[0]
+  interface = iw.find_interface_from_band(
+      band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
+  if interface is None:
+    raise utils.BinWifiException('No client interface for band %s', band)
+
+  print(iw.scan(interface))
+
+  return True
+
+
 def _is_hostapd_running(interface):
   return utils.subprocess_quiet(
       ('hostapd_cli', '-i', interface, 'status'), no_stdout=True) == 0
@@ -974,6 +999,7 @@
         'setclient': set_client_wifi,
         'stopclient': stop_client_wifi,
         'stopap': stop_ap_wifi,
+        'scan': scan_wifi,
     }[extra[0]]
   except KeyError:
     parser.fatal('Unrecognized command %s' % extra[0])