Merge "conman:  Route management changes."
diff --git a/cmds/Makefile b/cmds/Makefile
index 4f7db2b..24bc8a1 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -32,6 +32,7 @@
 	diskbench \
 	dnsck \
 	freemegs \
+	gfhd254_reboot \
 	gstatic \
 	http_bouncer \
 	ionice \
diff --git a/cmds/device_stats.proto b/cmds/device_stats.proto
index 30a344e..4f47b5e 100644
--- a/cmds/device_stats.proto
+++ b/cmds/device_stats.proto
@@ -17,5 +17,8 @@
 
   // Device serial number.
   optional string serial = 5;
+
+  // Public ipv6 address of onu
+  optional string ipv6 = 6;
 };
 
diff --git a/cmds/gfhd254_reboot.c b/cmds/gfhd254_reboot.c
new file mode 100644
index 0000000..fdc7e32
--- /dev/null
+++ b/cmds/gfhd254_reboot.c
@@ -0,0 +1,65 @@
+// GFHD254 has a bug where software reset doesn't reset the entire
+// chip, some state in the SAGE engine isn't getting reset.  This
+// drives a gpio that connects back to the chips own external reset
+// pin, resetting the chip with this pin works around the issue as
+// the SAGE engine is completely reset in this path.
+
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#define REG_BASE 0xf0410000
+#define REG_SIZE 0x8000
+
+
+#define GPIO_DATA (0x7404 / 4)
+#define GPIO_IODIR (0x7408 / 4)
+#define CTRL_MUX_0 (0x0700 / 4)
+#define CTRL_MUX_1 (0x0704 / 4)
+
+static void *mmap_(
+    void* addr, size_t size, int prot, int flags, int fd,
+    off_t offset) {
+#ifdef __ANDROID__
+  return mmap64(addr, size, prot, flags, fd,
+                (off64_t)(uint64_t)(uint32_t)offset);
+#else
+  return mmap(addr, size, prot, flags, fd, offset);
+#endif
+}
+
+// TODO(jnewlin):  Revist this after the exact gpio being used
+// is settled on.
+
+int main() {
+  int fd = open("/dev/mem", O_RDWR);
+  volatile uint32_t* reg;
+
+  if (fd < 0) {
+    perror("mmap");
+    return 1;
+  }
+
+  reg = mmap_(NULL, REG_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
+              fd, REG_BASE);
+  if (reg == MAP_FAILED) {
+    perror("mmap");
+    return 1;
+  }
+
+  // Set the pin mux to gpio, value of zero selects gpio mode, this
+  // is the reset value so this is probably not required, but just
+  // in case.
+  reg[CTRL_MUX_0] &= ~((0xf << 8) | (0xf << 12)); // aon_gio2 and 3
+  reg[CTRL_MUX_1] &= ~(0xf << 4); // aon_gio9
+
+
+  // Set the direction to be an output and drive it low.
+  reg[GPIO_IODIR] &= ~((1 << 2) | (1 << 3) | (1 << 9));
+  reg[GPIO_DATA] &= ~((1 << 2) | (1 << 3) | (1 << 9));
+
+  return 0;
+}
diff --git a/cmds/statcatcher.cc b/cmds/statcatcher.cc
index 64db2b6..bfd7033 100644
--- a/cmds/statcatcher.cc
+++ b/cmds/statcatcher.cc
@@ -142,7 +142,8 @@
 "onu_acs_contacted": %s,
 "onu_acs_contact_time": "%lld",
 "onu_uptime": %lld,
-"onu_serial": "%s"
+"onu_serial": "%s",
+"onu_ipv6": "%s"
 })";
     FILE *f = fopen(tmp_file.c_str(), "w");
     if (!f) {
@@ -155,7 +156,8 @@
             status.acs_contacted() ? "true" : "false",
             status.acs_contact_time(),
             status.uptime(),
-            status.serial().c_str());
+            status.serial().c_str(),
+            status.ipv6().c_str());
     fclose(f);
 
     if (rename(tmp_file.c_str(), stat_file.c_str()) != 0) {
diff --git a/cmds/statpitcher.cc b/cmds/statpitcher.cc
index 412be5d..990666d 100644
--- a/cmds/statpitcher.cc
+++ b/cmds/statpitcher.cc
@@ -16,8 +16,10 @@
 
 #include <fstream>
 #include <iostream>
+#include <sstream>
 #include <string>
 #include <vector>
+#include <memory>
 
 #include "device_stats.pb.h"
 
@@ -91,6 +93,71 @@
   return static_cast<int64_t>(up);
 }
 
+std::string IPAddress() {
+  std::ifstream infile;
+  infile.open("/proc/net/if_inet6");
+
+  if (!infile.good()) {
+    perror("error reading ipv6 from file");
+    exit(1);
+  }
+
+  std::string line;
+  int found = 0;
+  while (!infile.eof()) {
+    getline(infile, line);
+    // Want Ipv6 address on man interface
+    if (line.find("man") == std::string::npos) {
+      continue;
+    }
+    // Avoid local ipv6
+    if (line.substr(0, 4) == "0100" || // Discard prefix RFC 6666
+        line.substr(0, 2) == "fc" || // Unique local addresses
+        line.substr(0, 2) == "fd" ||
+        line.substr(0, 4) == "fe80" || // Link-local addresses
+        line.substr(0, 4) == "fec0") { // Old, deprecated local address range
+      continue;
+    }
+    found = 1;
+    break;
+  }
+
+  infile.close();
+  if (!found || line.size() < 32) {
+    perror("ipv6 address on man not found in file");
+    return "::1";
+  }
+
+  // Add colons
+  std::stringstream ipv6;
+  line = line.substr(0, 32);
+  for (unsigned int i = 0; i < line.size(); i++) {
+    if (i != 0 && i % 4 == 0) {
+      ipv6 << ':';
+    }
+    ipv6 << line[i];
+  }
+
+  // Format canonically
+  struct in6_addr ipv6_struct;
+  if (!inet_pton(AF_INET6, ipv6.str().c_str(), &ipv6_struct)) {
+    std::string errmsg = "unable to parse ipv6 address to inet_pton: " +
+        ipv6.str();
+    perror(errmsg.c_str());
+    exit(1);
+  }
+  char address[INET6_ADDRSTRLEN];
+  if (!inet_ntop(AF_INET6, &ipv6_struct, address, INET6_ADDRSTRLEN)) {
+    std::string errmsg = "unable to parse ipv6 address from inet_pton struct "
+        "created from: " + ipv6.str();
+    perror(errmsg.c_str());
+    exit(1);
+  }
+
+  std::string result(address);
+  return result;
+}
+
 void MakePacket(std::vector<uint8_t>* pkt) {
   devstatus::Status status;
 
@@ -101,6 +168,7 @@
   status.set_acs_contact_time(acs_contact_time);
   status.set_uptime(Uptime());
   status.set_serial(serial_number);
+  status.set_ipv6(IPAddress());
 
   pkt->resize(status.ByteSize());
   status.SerializeToArray(&(*pkt)[0], status.ByteSize());
diff --git a/cmds/test-http_bouncer.sh b/cmds/test-http_bouncer.sh
index 4129d52..9cc0d9e 100755
--- a/cmds/test-http_bouncer.sh
+++ b/cmds/test-http_bouncer.sh
@@ -40,10 +40,6 @@
 INPUTS[3]=$(printf "\n\n"; printf "$SENTINEL")
 OUTPUTS[3]=$(printf "HTTP/1.0 302 Found\r\nLocation: $URL\r\n\r\n"; printf "$SENTINEL")
 
-INPUTS[4]=$(printf "GET /GIAG2.crl HTTP/1.0\r\nHost: pki.google.com\r\n\r\n"; printf "$SENTINEL")
-OUTPUTS[4]=$(curl "http://pki.google.com/GIAG2.crl"; printf "$SENTINEL")
-STRIP_HEADER[4]=1
-
 WVSTART "http_bouncer test"
 
 # fail with no arguments
@@ -59,10 +55,13 @@
 i=0
 while [ $i -lt ${#INPUTS[@]} ]; do
   output=$(echo -n "${INPUTS[$i]}" | nc localhost $PORT; printf "$SENTINEL")
-  if [ ${STRIP_HEADER[$i]} ]; then
-    output=$(echo -n "$output" | sed '1,/^\r$/d')
-  fi
-
   WVPASSEQ "$output" "${OUTPUTS[$i]}"
   i=$(expr $i + 1)
 done
+
+# Make sure we can download a CRL even through the bouncer.
+# Some Internet Explorer versions will refuse to connect if we can't.
+WVPASS printf "GET /GIAG2.crl HTTP/1.0\r\nHost: pki.google.com\r\n\r\n" |\
+  nc localhost $PORT |\
+  sed '1,/^\r$/d' |\
+  openssl crl -inform DER
diff --git a/cmds/wifi_files.c b/cmds/wifi_files.c
index 49e77c6..1700cda 100644
--- a/cmds/wifi_files.c
+++ b/cmds/wifi_files.c
@@ -77,6 +77,11 @@
  * client for a while longer than that.
  */
 typedef struct client_state {
+  #define MAC_STR_LEN 18
+  char macstr[MAC_STR_LEN];
+  #define IFNAME_STR_LEN 16
+  char ifname[IFNAME_STR_LEN];
+
   double inactive_since;
 
   uint64_t rx_drop64;
@@ -106,14 +111,22 @@
   uint32_t tx_failed;
   uint32_t expected_mbps;
 
-  int sample_index;
 #define MAX_SAMPLE_INDEX 150
+  int rx_sample_index;
   uint8_t rx_ht_mcs_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_vht_mcs_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_width_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_ht_nss_samples[MAX_SAMPLE_INDEX];
   uint8_t rx_vht_nss_samples[MAX_SAMPLE_INDEX];
-  uint8_t short_gi_samples[MAX_SAMPLE_INDEX];
+  uint8_t rx_short_gi_samples[MAX_SAMPLE_INDEX];
+
+  int tx_sample_index;
+  uint8_t tx_ht_mcs_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_vht_mcs_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_width_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_ht_nss_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_vht_nss_samples[MAX_SAMPLE_INDEX];
+  uint8_t tx_short_gi_samples[MAX_SAMPLE_INDEX];
 
   /*
    * Clients spend a lot of time mostly idle, where they
@@ -131,7 +144,14 @@
   uint8_t rx_width;
   uint8_t rx_ht_nss;
   uint8_t rx_vht_nss;
-  uint8_t short_gi;
+  uint8_t rx_short_gi;
+
+  uint8_t tx_ht_mcs;
+  uint8_t tx_vht_mcs;
+  uint8_t tx_width;
+  uint8_t tx_ht_nss;
+  uint8_t tx_vht_nss;
+  uint8_t tx_short_gi;
 
   /* Track the largest value we've ever seen from this client. This
    * shows client capabilities, even if current interference
@@ -141,7 +161,14 @@
   uint8_t rx_max_width;
   uint8_t rx_max_ht_nss;
   uint8_t rx_max_vht_nss;
-  uint8_t ever_short_gi;
+  uint8_t ever_rx_short_gi;
+
+  uint8_t tx_max_ht_mcs;
+  uint8_t tx_max_vht_mcs;
+  uint8_t tx_max_width;
+  uint8_t tx_max_ht_nss;
+  uint8_t tx_max_vht_nss;
+  uint8_t ever_tx_short_gi;
 
   int8_t signal;
   int8_t signal_avg;
@@ -153,11 +180,6 @@
   uint8_t mfp:1;
   uint8_t tdls_peer:1;
   uint8_t preamble_length:1;
-
-  #define MAC_STR_LEN 18
-  char macstr[MAC_STR_LEN];
-  #define IFNAME_STR_LEN 16
-  char ifname[IFNAME_STR_LEN];
 } client_state_t;
 
 
@@ -176,6 +198,16 @@
 static FILE *wifi_info_handle = NULL;
 
 
+static void ClearClientStateCounters(client_state_t *state)
+{
+  char macstr[MAC_STR_LEN];
+
+  memcpy(macstr, state->macstr, sizeof(macstr));
+  memset(state, 0, sizeof(*state));
+  memcpy(state->macstr, macstr, sizeof(state->macstr));
+}
+
+
 static int GetIfIndex(const char *ifname)
 {
   int fd;
@@ -349,7 +381,7 @@
 }
 
 
-static void GetRxMCS(struct nlattr *attr,
+static void GetMCS(struct nlattr *attr,
     int *mcs, int *vht_mcs, int *width, int *short_gi, int *vht_nss)
 {
   int w160 = 0, w80_80 = 0, w80 = 0, w40 = 0;
@@ -418,7 +450,7 @@
 }
 
 
-static int RxHtMcsToNss(int rxmcs)
+static int HtMcsToNss(int rxmcs)
 {
   /* https://en.wikipedia.org/wiki/IEEE_802.11n-2009 */
   switch(rxmcs) {
@@ -488,6 +520,12 @@
 
   mac = (uint8_t *)nla_data(tb[NL80211_ATTR_MAC]);
   state = FindClientState(mac);
+
+  if (strcasecmp(state->ifname, ifname) != 0) {
+    /* Client moved from one interface to another */
+    ClearClientStateCounters(state);
+  }
+
   state->last_seen = monotime();
   snprintf(state->ifname, sizeof(state->ifname), "%s", ifname);
 
@@ -502,20 +540,20 @@
   }
 
   if (si[NL80211_STA_INFO_RX_BITRATE]) {
-    int rx_ht_mcs=0, rx_vht_mcs=0, rx_vht_nss=0, rx_width=0, short_gi=0;
+    int rx_ht_mcs=0, rx_vht_mcs=0, rx_vht_nss=0, rx_width=0, rx_short_gi=0;
     int ht_nss;
-    int n = state->sample_index + 1;
+    int n = state->rx_sample_index + 1;
 
     if (n >= MAX_SAMPLE_INDEX) n = 0;
 
     state->rx_bitrate = GetBitrate(si[NL80211_STA_INFO_RX_BITRATE]);
-    GetRxMCS(si[NL80211_STA_INFO_RX_BITRATE], &rx_ht_mcs, &rx_vht_mcs,
-        &rx_width, &short_gi, &rx_vht_nss);
+    GetMCS(si[NL80211_STA_INFO_RX_BITRATE], &rx_ht_mcs, &rx_vht_mcs,
+        &rx_width, &rx_short_gi, &rx_vht_nss);
 
     state->rx_ht_mcs_samples[n] = rx_ht_mcs;
     if (rx_ht_mcs > state->rx_max_ht_mcs) state->rx_max_ht_mcs = rx_ht_mcs;
 
-    ht_nss = RxHtMcsToNss(rx_ht_mcs);
+    ht_nss = HtMcsToNss(rx_ht_mcs);
     state->rx_ht_nss_samples[n] = ht_nss;
     if (ht_nss > state->rx_max_ht_nss) state->rx_max_ht_nss = ht_nss;
 
@@ -525,13 +563,13 @@
     state->rx_vht_nss_samples[n] = rx_vht_nss;
     if (rx_vht_nss > state->rx_max_vht_nss) state->rx_max_vht_nss = rx_vht_nss;
 
-    state->short_gi_samples[n] = short_gi;
-    if (short_gi) state->ever_short_gi = 1;
+    state->rx_short_gi_samples[n] = rx_short_gi;
+    if (rx_short_gi) state->ever_rx_short_gi = 1;
 
     state->rx_width_samples[n] = rx_width;
     if (rx_width > state->rx_max_width) state->rx_max_width = rx_width;
 
-    state->sample_index = n;
+    state->rx_sample_index = n;
   }
   if (si[NL80211_STA_INFO_RX_BYTES]) {
     uint32_t last_rx_bytes = state->rx_bytes;
@@ -544,7 +582,36 @@
     state->rx_packets64 += (state->rx_packets - last_rx_packets);
   }
   if (si[NL80211_STA_INFO_TX_BITRATE]) {
+    int tx_ht_mcs=0, tx_vht_mcs=0, tx_vht_nss=0, tx_width=0, tx_short_gi=0;
+    int ht_nss;
+    int n = state->tx_sample_index + 1;
+
+    if (n >= MAX_SAMPLE_INDEX) n = 0;
+
     state->tx_bitrate = GetBitrate(si[NL80211_STA_INFO_TX_BITRATE]);
+    GetMCS(si[NL80211_STA_INFO_TX_BITRATE], &tx_ht_mcs, &tx_vht_mcs,
+        &tx_width, &tx_short_gi, &tx_vht_nss);
+
+    state->tx_ht_mcs_samples[n] = tx_ht_mcs;
+    if (tx_ht_mcs > state->tx_max_ht_mcs) state->tx_max_ht_mcs = tx_ht_mcs;
+
+    ht_nss = HtMcsToNss(tx_ht_mcs);
+    state->tx_ht_nss_samples[n] = ht_nss;
+    if (ht_nss > state->tx_max_ht_nss) state->tx_max_ht_nss = ht_nss;
+
+    state->tx_vht_mcs_samples[n] = tx_vht_mcs;
+    if (tx_vht_mcs > state->tx_max_vht_mcs) state->tx_max_vht_mcs = tx_vht_mcs;
+
+    state->tx_vht_nss_samples[n] = tx_vht_nss;
+    if (tx_vht_nss > state->tx_max_vht_nss) state->tx_max_vht_nss = tx_vht_nss;
+
+    state->tx_short_gi_samples[n] = tx_short_gi;
+    if (tx_short_gi) state->ever_tx_short_gi = 1;
+
+    state->tx_width_samples[n] = tx_width;
+    if (tx_width > state->tx_max_width) state->tx_max_width = tx_width;
+
+    state->tx_sample_index = n;
   }
   if (si[NL80211_STA_INFO_TX_BYTES]) {
     uint32_t last_tx_bytes = state->tx_bytes;
@@ -655,7 +722,9 @@
   client_state_t *state = (client_state_t *)value;
   int i;
   uint8_t rx_ht_mcs=0, rx_vht_mcs=0, rx_width=0, rx_ht_nss=0;
-  uint8_t rx_vht_nss=0, short_gi=0;
+  uint8_t rx_vht_nss=0, rx_short_gi=0;
+  uint8_t tx_ht_mcs=0, tx_vht_mcs=0, tx_width=0, tx_ht_nss=0;
+  uint8_t tx_vht_nss=0, tx_short_gi=0;
 
   for (i = 0; i < MAX_SAMPLE_INDEX; ++i) {
     if (state->rx_ht_mcs_samples[i] > rx_ht_mcs) {
@@ -673,8 +742,27 @@
     if (state->rx_vht_nss_samples[i] > rx_vht_nss) {
       rx_vht_nss = state->rx_vht_nss_samples[i];
     }
-    if (state->short_gi_samples[i] > short_gi) {
-      short_gi = state->short_gi_samples[i];
+    if (state->rx_short_gi_samples[i] > rx_short_gi) {
+      rx_short_gi = state->rx_short_gi_samples[i];
+    }
+
+    if (state->tx_ht_mcs_samples[i] > tx_ht_mcs) {
+      tx_ht_mcs = state->tx_ht_mcs_samples[i];
+    }
+    if (state->tx_vht_mcs_samples[i] > tx_vht_mcs) {
+      tx_vht_mcs = state->tx_vht_mcs_samples[i];
+    }
+    if (state->tx_width_samples[i] > tx_width) {
+      tx_width = state->tx_width_samples[i];
+    }
+    if (state->tx_ht_nss_samples[i] > tx_ht_nss) {
+      tx_ht_nss = state->tx_ht_nss_samples[i];
+    }
+    if (state->tx_vht_nss_samples[i] > tx_vht_nss) {
+      tx_vht_nss = state->tx_vht_nss_samples[i];
+    }
+    if (state->tx_short_gi_samples[i] > tx_short_gi) {
+      tx_short_gi = state->tx_short_gi_samples[i];
     }
   }
 
@@ -683,7 +771,14 @@
   state->rx_width = rx_width;
   state->rx_ht_nss = rx_ht_nss;
   state->rx_vht_nss = rx_vht_nss;
-  state->short_gi = short_gi;
+  state->rx_short_gi = rx_short_gi;
+
+  state->tx_ht_mcs = tx_ht_mcs;
+  state->tx_vht_mcs = tx_vht_mcs;
+  state->tx_width = tx_width;
+  state->tx_ht_nss = tx_ht_nss;
+  state->tx_vht_nss = tx_vht_nss;
+  state->tx_short_gi = tx_short_gi;
 }
 
 
@@ -738,8 +833,8 @@
   fprintf(f, "  \"rx max vht_nss\": %u,\n", state->rx_max_vht_nss);
 
   #define BOOL(x) (x ? "true" : "false")
-  fprintf(f, "  \"rx SHORT_GI\": %s,\n", BOOL(state->short_gi));
-  fprintf(f, "  \"rx SHORT_GI seen\": %s,\n", BOOL(state->ever_short_gi));
+  fprintf(f, "  \"rx SHORT_GI\": %s,\n", BOOL(state->rx_short_gi));
+  fprintf(f, "  \"rx SHORT_GI seen\": %s,\n", BOOL(state->ever_rx_short_gi));
   #undef BOOL
 
   fprintf(f, "  \"signal\": %hhd,\n", state->signal);
@@ -797,6 +892,8 @@
       "%s %s %ld %" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64 ",%" PRIu64
       " %c,%hhd,%hhd,%u,%u,%u,%u,%u,%d"
       " %u,%u,%u,%u,%u,%d"
+      " %u,%u,%u,%u,%u,%d"
+      " %u,%u,%u,%u,%u,%d"
       "\n",
       state->macstr, state->ifname,
       ((mono_now - state->last_seen) + (state->inactive_msec / 1000)),
@@ -810,12 +907,21 @@
       state->signal, state->signal_avg,
       state->rx_ht_mcs, state->rx_ht_nss,
       state->rx_vht_mcs, state->rx_vht_nss,
-      state->rx_width, state->short_gi,
+      state->rx_width, state->rx_short_gi,
 
       /* information about the maximum we've ever seen from this client. */
       state->rx_max_ht_mcs, state->rx_max_ht_nss,
       state->rx_max_vht_mcs, state->rx_max_vht_nss,
-      state->rx_max_width, state->ever_short_gi);
+      state->rx_max_width, state->ever_rx_short_gi,
+
+      state->tx_ht_mcs, state->tx_ht_nss,
+      state->tx_vht_mcs, state->tx_vht_nss,
+      state->tx_width, state->tx_short_gi,
+
+      /* information about the maximum we've ever seen from this client. */
+      state->tx_max_ht_mcs, state->tx_max_ht_nss,
+      state->tx_max_vht_mcs, state->tx_max_vht_nss,
+      state->tx_max_width, state->ever_tx_short_gi);
 }
 
 
@@ -878,8 +984,9 @@
         if ((data[i] <= 0x1f) || !isprint(data[i])) {
           fprintf(f, "\\u00%02x", data[i]);
         } else {
-          fprintf(f, "%c", data[i]); break;
+          fprintf(f, "%c", data[i]);
         }
+        break;
     }
   }
 }
diff --git a/cmds/wifi_files_test.c b/cmds/wifi_files_test.c
index 9d48dd1..902cd73 100644
--- a/cmds/wifi_files_test.c
+++ b/cmds/wifi_files_test.c
@@ -50,6 +50,27 @@
 }
 
 
+void testPrintSsidEscapedQuoteBackslash()
+{
+  FILE *f = tmpfile();
+  char buf[32];
+  const uint8_t ssid[] = {'"', '\\'};  /* not NUL terminated. */
+  const uint8_t expected[] = {'\\', '"', '\\', '\\'};
+
+  printf("Testing \"%s\" in %s:\n", __FUNCTION__, __FILE__);
+  memset(buf, 0, sizeof(buf));
+  TEST_ASSERT(f != NULL);
+  print_ssid_escaped(f, sizeof(ssid), ssid);
+  fflush(f);
+  rewind(f);
+  TEST_ASSERT(fread(buf, 1, sizeof(buf), f) > 0);
+  printf("%s\n", buf);
+  TEST_ASSERT(memcmp(buf, expected, sizeof(expected)) == 0);
+  fclose(f);
+  printf("! %s:%d\t%s\tok\n", __FILE__, __LINE__, __FUNCTION__);
+}
+
+
 void testFrequencyToChannel()
 {
   printf("Testing \"%s\" in %s:\n", __FUNCTION__, __FILE__);
@@ -171,6 +192,7 @@
   clients = g_hash_table_new(g_str_hash, g_str_equal);
 
   testPrintSsidEscaped();
+  testPrintSsidEscapedQuoteBackslash();
   testFrequencyToChannel();
   testClientStateToJson();
   testAgeOutClients();
diff --git a/conman/interface.py b/conman/interface.py
index fd0df6f..7fda644 100755
--- a/conman/interface.py
+++ b/conman/interface.py
@@ -563,7 +563,8 @@
       client_mode = self._qcsapi('get_mode', 'wifi0') == 'Station'
       ssid = self._qcsapi('get_ssid', 'wifi0')
       status = self._qcsapi('get_status', 'wifi0')
-      security = self._qcsapi('ssid_get_authentication_mode', 'wifi0', ssid)
+      security = (self._qcsapi('ssid_get_authentication_mode', 'wifi0', ssid)
+                  if ssid else None)
     except subprocess.CalledProcessError:
       # If QCSAPI failed, skip update.
       return
diff --git a/hnvram/hnvram_main.c b/hnvram/hnvram_main.c
index b850048..323d62e 100644
--- a/hnvram/hnvram_main.c
+++ b/hnvram/hnvram_main.c
@@ -83,6 +83,7 @@
   {"LASER_CHANNEL",        NVRAM_FIELD_LASER_CHANNEL,     HNVRAM_STRING},
   {"MAC_ADDR_PON",         NVRAM_FIELD_MAC_ADDR_PON,      HNVRAM_MAC},
   {"PRODUCTION_UNIT",      NVRAM_FIELD_PRODUCTION_UNIT,   HNVRAM_STRING},
+  {"BOOT_TARGET",          NVRAM_FIELD_BOOT_TARGET,       HNVRAM_STRING},
 };
 
 const hnvram_field_t* get_nvram_field(const char* name) {
diff --git a/ledpattern/Makefile b/ledpattern/Makefile
index 705a398..ad3456d 100644
--- a/ledpattern/Makefile
+++ b/ledpattern/Makefile
@@ -1,7 +1,7 @@
 default:
 
-PREFIX=/
-BINDIR=$(DESTDIR)$(PREFIX)/bin
+ETCDIR=$(DESTDIR)/etc
+BINDIR=$(DESTDIR)/bin
 PYTHON?=python
 
 all:
@@ -9,6 +9,9 @@
 install:
 	mkdir -p $(BINDIR)
 	cp ledpattern.py $(BINDIR)/ledpattern
+	cp ledtapcode.sh $(BINDIR)/ledtapcode
+	cp ledpatterns $(ETCDIR)/ledpatterns
+	chmod +x $(BINDIR)/ledtapcode
 
 install-libs:
 	@echo "No libs to install."
diff --git a/ledpattern/ledpatterns b/ledpattern/ledpatterns
new file mode 100644
index 0000000..2e0ab63
--- /dev/null
+++ b/ledpattern/ledpatterns
@@ -0,0 +1,12 @@
+HALTED,P,R
+NO_LASER_CHANNEL,P,P
+SET_LASER_FAILED,P,R,R
+LOSLOF_ALARM,P,R,B
+OTHER_ALARM,P,R,P
+GPON_INITIAL,P,B,R
+GPON_STANDBY,P,B,P
+GPON_SERIAL,P,P,R
+GPON_RANGING,P,P,B
+WAIT_ACS,P,B,B
+ALL_OK,P,B,B,B
+UNKNOWN_ERROR,P,R,R,R
diff --git a/ledpattern/ledtapcode.sh b/ledpattern/ledtapcode.sh
new file mode 100755
index 0000000..6841f2f
--- /dev/null
+++ b/ledpattern/ledtapcode.sh
@@ -0,0 +1,107 @@
+#!/bin/sh
+
+. /etc/utils.sh
+
+LEDPATTERN="ledpattern /etc/ledpatterns"
+SYSFS_GPON_PATH="/sys/devices/platform/gpon"
+MONITOR_PATH="/tmp/gpio/ledcontrol"
+LASER_STATUS_FILE="/tmp/laser_i2c_status"
+ALARM_GPON_FILE="$SYSFS_GPON_PATH/info/alarmGpon"
+GPON_INFO_FILE="$SYSFS_GPON_PATH/info/infoGpon"
+HALTED_FILE="$MONITOR_PATH/halted"
+HW_FAILURE="$MONITOR_PATH/hardware_failure"
+LASER_CHANNEL_FILE="$SYSFS_GPON_PATH/misc/laserChannel"
+ACS_FILE="$MONITOR_PATH/acsconnected"
+
+PlayPatternAndExit()
+{
+  state="$1"
+  # ledpattern takes care of all the LED management and state selection.
+  result="$($LEDPATTERN $state)"
+  if [ "$?" -ne 0 ]; then
+    echo "Failed to display pattern $state: $result"
+    exit 1
+  fi
+  exit 0
+}
+
+if [ ! -f "$ALARM_GPON_FILE" ]; then
+  echo "$ALARM_GPON_FILE does not exist"
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
+
+if [ ! -f "$GPON_INFO_FILE" ]; then
+  echo "$GPON_INFO_FILE does not exist"
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
+
+if [ ! -f "$LASER_CHANNEL_FILE" ]; then
+  echo "$LASER_CHANNEL_FILE does not exist"
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
+
+# It is a valid state that there may not be a LASER_STATUS_FILE yet.
+if [ -f "$LASER_STATUS_FILE" ]; then
+  laser_status=$(cat "$LASER_STATUS_FILE")
+  if [ "$laser_status" -ne 0 ]; then
+    echo "Playing SET_LASER_FAILED pattern"
+    PlayPatternAndExit SET_LASER_FAILED
+  fi
+fi
+
+if [ -f "$HW_FAILURE" ]; then
+  echo "Playing HALTED pattern on HW_FAILURE"
+  PlayPatternAndExit HALTED
+fi
+
+if [ -f "$HALTED_FILE" ]; then
+  echo "Playing HALTED pattern on HALTED_FILE"
+  PlayPatternAndExit HALTED
+fi
+
+# Chop the table headers off the output using tail, otherwise grep gets
+# confused later.
+alarm_info=$(cat "$ALARM_GPON_FILE" | tail -n+7)
+los_output=$(echo "$alarm_info" | grep "LOS" | grep "ON")
+lof_output=$(echo "$alarm_info" | grep "LOF" | grep "ON")
+if [ -n "$los_output" ] || [ -n "$lof_output" ]; then
+  echo "Playing LOSLOF_ALARM pattern"
+  PlayPatternAndExit LOSLOF_ALARM
+fi
+other_alarm=$(echo "$alarm_info" | grep "ON")
+if [ -n "$other_alarm" ]; then
+  echo "Playing OTHER_ALARM pattern"
+  PlayPatternAndExit OTHER_ALARM
+fi
+
+gpon_info=$(cat "$GPON_INFO_FILE" | grep "ONU STATE")
+if contains "$gpon_info" "INITIAL"; then
+  echo "Playing GPON_INITIAL pattern"
+  PlayPatternAndExit GPON_INITIAL
+elif contains "$gpon_info" "STANDBY"; then
+  echo "Playing GPON_STANDBY pattern"
+  PlayPatternAndExit GPON_STANDBY
+elif contains "$gpon_info" "SERIAL"; then
+  echo "Playing GPON_SERIAL pattern"
+  PlayPatternAndExit GPON_SERIAL
+elif contains "$gpon_info" "RANGING"; then
+  echo "Playing GPON_RANGING pattern"
+  PlayPatternAndExit GPON_RANGING
+fi
+
+laser_channel=$(cat "$LASER_CHANNEL_FILE")
+if [ ! -f "$ACS_FILE" ] && [ "$laser_channel" -eq "-1" ]; then
+  echo "Playing NO_LASER_CHANNEL pattern"
+  PlayPatternAndExit NO_LASER_CHANNEL
+elif [ ! -f "$ACS_FILE" ] && [ $laser_channel -ne "-1" ]; then
+  echo "Playing WAIT_ACS pattern"
+  PlayPatternAndExit WAIT_ACS
+elif [ -f "$ACS_FILE" ] && [ $laser_channel -ne "-1" ]; then
+  echo "Playing ALL_OK pattern"
+  PlayPatternAndExit ALL_OK
+else
+  # If we get all the way here and nothing triggered on the way then this really
+  # is an unknown error...
+  echo "Nothing triggered? Playing UNKNOWN_ERROR pattern..."
+  PlayPatternAndExit UNKNOWN_ERROR
+fi
diff --git a/wifi/iw.py b/wifi/iw.py
index ae2a8b6..c0bbf57 100644
--- a/wifi/iw.py
+++ b/wifi/iw.py
@@ -226,14 +226,26 @@
       return interface
 
 
-def find_all_interfaces_from_phy(phy):
+def find_all_interfaces_from_phy(phy, interface_type=None):
+  """Finds the names of all interfaces on a given phy.
+
+  Args:
+    phy: The name of a phy, e.g. 'phy0'.
+    interface_type: An INTERFACE_TYPE value (optional).
+
+  Returns:
+    A list of all interfaces found.
+  """
   interfaces = []
-  for interface_type in INTERFACE_TYPE:
+  interface_types = INTERFACE_TYPE
+  if interface_type:
+    interface_types = [interface_type]
+  for interface_type in interface_types:
     pattern = re.compile(r'w%s[0-9]\w*\Z' % re.escape(interface_type))
     interfaces.extend(interface for interface
                       in dev_parsed()[phy]['interfaces']
                       if pattern.match(interface))
-  return interfaces
+  return set(interfaces)
 
 
 def find_interface_from_band(band, interface_type, interface_suffix):
@@ -254,6 +266,23 @@
   return find_interface_from_phy(phy, interface_type, interface_suffix)
 
 
+def find_all_interfaces_from_band(band, interface_type=None):
+  """Finds the names of all interface on a given band.
+
+  Args:
+    band: The band for which you want the interface.
+    interface_type: An INTERFACE_TYPE value (optional).
+
+  Returns:
+    A list of all interfaces found.
+  """
+  phy = find_phy(band, 'auto')
+  if phy is None:
+    return []
+
+  return find_all_interfaces_from_phy(phy, interface_type)
+
+
 def find_width_and_channel(interface):
   """Finds the width and channel being used by a given interface.
 
diff --git a/wifi/iw_test.py b/wifi/iw_test.py
index 4a7ef4c..2293954 100755
--- a/wifi/iw_test.py
+++ b/wifi/iw_test.py
@@ -514,6 +514,19 @@
 
 
 @wvtest.wvtest
+def find_all_interfaces_from_phy_test():
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal', 'wcli0']),
+                  iw.find_all_interfaces_from_phy('phy0'))
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal']),
+                  iw.find_all_interfaces_from_phy('phy0', iw.INTERFACE_TYPE.ap))
+  wvtest.WVPASSEQ(set(['wcli0']),
+                  iw.find_all_interfaces_from_phy('phy0',
+                                                  iw.INTERFACE_TYPE.client))
+  wvtest.WVPASSEQ(set(['wlan1', 'wlan1_portal']),
+                  iw.find_all_interfaces_from_phy('phy1'))
+
+
+@wvtest.wvtest
 def find_interface_from_band_test():
   wvtest.WVPASSEQ('wlan0',
                   iw.find_interface_from_band('2.4', iw.INTERFACE_TYPE.ap, ''))
@@ -529,6 +542,19 @@
 
 
 @wvtest.wvtest
+def find_all_interfaces_from_band_test():
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal', 'wcli0']),
+                  iw.find_all_interfaces_from_band('2.4'))
+  wvtest.WVPASSEQ(set(['wlan0', 'wlan0_portal']),
+                  iw.find_all_interfaces_from_band('2.4', iw.INTERFACE_TYPE.ap))
+  wvtest.WVPASSEQ(set(['wcli0']),
+                  iw.find_all_interfaces_from_band('2.4',
+                                                   iw.INTERFACE_TYPE.client))
+  wvtest.WVPASSEQ(set(['wlan1', 'wlan1_portal']),
+                  iw.find_all_interfaces_from_band('5'))
+
+
+@wvtest.wvtest
 def info_parsed_test():
   wvtest.WVPASSEQ({
       'wdev': '0x3',
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 8797633..142010c 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -50,7 +50,7 @@
 X,extra-short-timeouts            Use shorter key rotations; 1=rotate PTK, 2=rotate often
 Y,yottasecond-timeouts            Don't rotate any keys: PTK, GTK, or GMK
 P,persist                         For set commands, persist options so we can restore them with 'wifi restore'.  For stop commands, remove persisted options.
-S,interface-suffix=               Interface suffix []
+S,interface-suffix=               Interface suffix (defaults to ALL for stop commands; use NONE to specify no suffix) []
 lock-timeout=                     How long, in seconds, to wait for another /bin/wifi process to finish before giving up. [60]
 scan-ap-force                     (Scan only) scan when in AP mode
 scan-passive                      (Scan only) do not probe, scan passively
@@ -381,18 +381,25 @@
     if band == '5' and quantenna.stop_ap_wifi(opt):
       continue
 
-    interface = iw.find_interface_from_band(
-        band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
-    if interface is None:
-      utils.log('No AP interface for %s GHz; nothing to stop', band)
+    interfaces = []
+    if opt.interface_suffix == 'ALL':
+      interfaces = iw.find_all_interfaces_from_band(band, iw.INTERFACE_TYPE.ap)
+    else:
+      interface = iw.find_interface_from_band(
+          band, iw.INTERFACE_TYPE.ap, opt.interface_suffix)
+      if interface:
+        interfaces = [interface]
+    if not interfaces:
+      utils.log('No AP interfaces for %s GHz; nothing to stop', band)
       continue
 
-    if _stop_hostapd(interface):
-      if opt.persist:
-        persist.delete_options('hostapd', band)
-    else:
-      utils.log('Failed to stop hostapd on interface %s', interface)
-      success = False
+    for interface in interfaces:
+      if _stop_hostapd(interface):
+        if opt.persist:
+          persist.delete_options('hostapd', band)
+      else:
+        utils.log('Failed to stop hostapd on interface %s', interface)
+        success = False
 
   return success
 
@@ -989,18 +996,26 @@
     if band == '5' and quantenna.stop_client_wifi(opt):
       continue
 
-    interface = iw.find_interface_from_band(
-        band, iw.INTERFACE_TYPE.client, opt.interface_suffix)
-    if interface is None:
-      utils.log('No client interface for %s GHz; nothing to stop', band)
+    interfaces = []
+    if opt.interface_suffix == 'ALL':
+      interfaces = iw.find_all_interfaces_from_band(
+          band, iw.INTERFACE_TYPE.client)
+    else:
+      interface = iw.find_interface_from_band(
+          band, iw.INTERFACE_TYPE.client, opt.interface_suffix)
+      if interface:
+        interfaces = [interface]
+    if not interfaces:
+      utils.log('No client interfaces for %s GHz; nothing to stop', band)
       continue
 
-    if _stop_wpa_supplicant(interface):
-      if opt.persist:
-        persist.delete_options('wpa_supplicant', band)
-    else:
-      utils.log('Failed to stop wpa_supplicant on interface %s', interface)
-      success = False
+    for interface in interfaces:
+      if _stop_wpa_supplicant(interface):
+        if opt.persist:
+          persist.delete_options('wpa_supplicant', band)
+      else:
+        utils.log('Failed to stop wpa_supplicant on interface %s', interface)
+        success = False
 
   return success
 
@@ -1044,10 +1059,18 @@
     parser.fatal('Must specify a command (see usage for details).')
     return 1
 
+  command = extra[0]
+
   # set and setclient have a different default for -b.
-  if extra[0].startswith('set') and ' ' in opt.band:
+  if command.startswith('set') and ' ' in opt.band:
     opt.band = '2.4'
 
+  if command == 'off' or command.startswith('stop'):
+    if not opt.interface_suffix:
+      opt.interface_suffix = 'ALL'
+    elif opt.interface_suffix == 'NONE':
+      opt.interface_suffix = ''
+
   try:
     function = {
         'set': set_wifi,