wifi_files: client interface stats, convert to C++

1. client interface stats
   Rearrange code to move stats to a separate struct.
   For AP, output every associated client.
   For STA, output stats of local client interface.

2. convert to C++, remove libglib dependency
   libglib is one of the largest files in the GFMN100
   filesystem, and appears to only be used by wifi_files
   for g_hash support. Convert wifi_files to C++ and use
   unordered_map instead. libstdc++ is even larger than
   libglib but is used by a bunch of stuff in the system
   already, so we're better off being dependant on it
   than on glib.

Change-Id: Ib454830cf6298729c23cf1caddc0ff880f573a66
diff --git a/cmds/Makefile b/cmds/Makefile
index aabebe5..43362dc 100644
--- a/cmds/Makefile
+++ b/cmds/Makefile
@@ -252,9 +252,9 @@
 netusage: CFLAGS += -Wno-sign-compare
 host-netusage_test: host-netusage_test.o
 wifi_files: wifi_files.o
-wifi_files: LIBS += -lnl-3 -lnl-genl-3 -lglib-2.0
+wifi_files: LIBS += -lnl-3 -lnl-genl-3 -lstdc++ -lm
 host-wifi_files_test: host-wifi_files_test.o
-host-wifi_files_test: LIBS += -lnl-3 -lnl-genl-3 -lglib-2.0
+host-wifi_files_test: LIBS += -lnl-3 -lnl-genl-3 -lstdc++ -lm
 dhcpvendortax: dhcpvendortax.o dhcpvendorlookup.tmp.o
 dhcpvendorlookup.tmp.c: dhcpvendorlookup.gperf
 	$(GPERF) -G -C -t -L ANSI-C -N exact_match -K vendor_class \
diff --git a/cmds/wifi_files.c b/cmds/wifi_files.c
deleted file mode 100644
index 1700cda..0000000
--- a/cmds/wifi_files.c
+++ /dev/null
@@ -1,1206 +0,0 @@
-/*
- * Portions of this code are derived from iw-3.17.
- *
- * Copyright (c) 2007, 2008 Johannes Berg
- * Copyright (c) 2007   Andy Lutomirski
- * Copyright (c) 2007   Mike Kershaw
- * Copyright (c) 2008-2009    Luis R. Rodriguez
- * Copyright (c) 2015   Google, Inc.
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-#include <ctype.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <getopt.h>
-#include <glib.h>
-#include <inttypes.h>
-#include <limits.h>
-#include <linux/if_ether.h>
-#include <linux/nl80211.h>
-#include <math.h>
-#include <net/if.h>
-#include <netlink/attr.h>
-#include <netlink/genl/genl.h>
-#include <netlink/genl/ctrl.h>
-#include <netlink/msg.h>
-#include <netlink/netlink.h>
-#include <netlink/socket.h>
-#include <stdio.h>
-#include <sys/ioctl.h>
-#include <sys/resource.h>
-#include <sys/select.h>
-#include <sys/stat.h>
-#include <sys/time.h>
-#include <sys/types.h>
-#include <time.h>
-#include <unistd.h>
-
-
-#ifndef UNIT_TESTS
-#define STATIONS_DIR "/tmp/stations"
-#define WIFIINFO_DIR "/tmp/wifi/wifiinfo"
-#endif
-
-
-// Hash table of known Wifi clients.
-GHashTable *clients = NULL;
-#define MAX_CLIENT_AGE_SECS  (4 * 60 * 60)
-
-
-#ifndef UNIT_TESTS
-static time_t monotime(void) {
-  struct timespec ts;
-  if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
-    return time(NULL);
-  } else {
-    return ts.tv_sec;
-  }
-}
-#endif
-
-
-/*
- * Saved state for each associated Wifi device. Wifi clients drop out
- * after 5 minutes inactive, we want to export information about the
- * 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;
-
-  // Accumulated values from the 32 bit counters.
-  uint64_t rx_bytes64;
-  uint64_t tx_bytes64;
-  uint64_t rx_packets64;
-  uint64_t tx_packets64;
-  uint64_t tx_retries64;
-  uint64_t tx_failed64;
-
-  time_t first_seen;  // CLOCK_MONOTONIC
-  time_t last_seen;  // CLOCK_MONOTONIC
-
-  uint32_t inactive_msec;
-  uint32_t connected_secs;
-
-  uint32_t rx_bitrate;
-  uint32_t rx_bytes;
-  uint32_t rx_packets;
-
-  uint32_t tx_bitrate;
-  uint32_t tx_bytes;
-  uint32_t tx_packets;
-  uint32_t tx_retries;
-  uint32_t tx_failed;
-  uint32_t expected_mbps;
-
-#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 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
-   * are only sending management frames and ACKs. These
-   * tend to be sent at much lower MCS rates than bulk data;
-   * if we report that MCS rate it gives a misleading
-   * picture of what the client is capable of getting.
-   *
-   * Instead, we choose the largest sample over the reporting
-   * interval. This is more likely to report a meaningful
-   * MCS rate.
-   */
-  uint8_t rx_ht_mcs;
-  uint8_t rx_vht_mcs;
-  uint8_t rx_width;
-  uint8_t rx_ht_nss;
-  uint8_t rx_vht_nss;
-  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
-   * conditions don't allow it to use its full capability. */
-  uint8_t rx_max_ht_mcs;
-  uint8_t rx_max_vht_mcs;
-  uint8_t rx_max_width;
-  uint8_t rx_max_ht_nss;
-  uint8_t rx_max_vht_nss;
-  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;
-
-  uint8_t authorized:1;
-  uint8_t authenticated:1;
-  uint8_t preamble:1;
-  uint8_t wmm_wme:1;
-  uint8_t mfp:1;
-  uint8_t tdls_peer:1;
-  uint8_t preamble_length:1;
-} client_state_t;
-
-
-typedef struct callback_data {
-  time_t mono_now;
-} callback_data_t;
-
-
-/* List of wifi interfaces in the system. */
-#define NINTERFACES 16
-int ifindexes[NINTERFACES] = {0};
-const char *interfaces[NINTERFACES] = {0};
-int ninterfaces = 0;
-
-/* FILE handle to /tmp/wifi/wifiinfo, while open. */
-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;
-  struct ifreq ifr;
-
-  if (strlen(ifname) >= sizeof(ifr.ifr_name)) {
-    fprintf(stderr, "interface name %s is too long\n", ifname);
-    exit(1);
-  }
-
-  if ((fd = socket(AF_PACKET, SOCK_DGRAM, 0)) < 0) {
-    perror("socket");
-    exit(1);
-  }
-
-  memset(&ifr, 0, sizeof(ifr));
-  snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
-
-  if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {
-    char errbuf[128];
-    snprintf(errbuf, sizeof(errbuf), "SIOCGIFINDEX %s", ifname);
-    perror(errbuf);
-    close(fd);
-    exit(1);
-  }
-
-  close(fd);
-  return ifr.ifr_ifindex;
-}  /* GetIfIndex */
-
-
-static int InterfaceListCallback(struct nl_msg *msg, void *arg)
-{
-  struct nlattr *il[NL80211_ATTR_MAX + 1];
-  struct genlmsghdr *gh = nlmsg_data(nlmsg_hdr(msg));
-
-  nla_parse(il, NL80211_ATTR_MAX, genlmsg_attrdata(gh, 0),
-      genlmsg_attrlen(gh, 0), NULL);
-
-  if (il[NL80211_ATTR_IFNAME]) {
-    const char *name = nla_get_string(il[NL80211_ATTR_IFNAME]);
-    if (interfaces[ninterfaces] != NULL) {
-      free((void *)interfaces[ninterfaces]);
-    }
-    interfaces[ninterfaces] = strdup(name);
-    ifindexes[ninterfaces] = GetIfIndex(name);
-    ninterfaces++;
-  }
-
-  return NL_OK;
-}
-
-
-static void HandleNLCommand(struct nl_sock *nlsk, int nl80211_id, int n,
-                            int cb(struct nl_msg *, void *),
-                            int cmd, int flag)
-{
-  struct nl_msg *msg;
-  int ifindex = n >= 0 ? ifindexes[n] : -1;
-  const char *ifname = n>=0 ? interfaces[n] : NULL;
-
-  if (nl_socket_modify_cb(nlsk, NL_CB_VALID, NL_CB_CUSTOM,
-                          cb, (void *)ifname)) {
-    fprintf(stderr, "nl_socket_modify_cb failed\n");
-    exit(1);
-  }
-
-  if ((msg = nlmsg_alloc()) == NULL) {
-    fprintf(stderr, "nlmsg_alloc failed\n");
-    exit(1);
-  }
-  if (genlmsg_put(msg, 0, 0, nl80211_id, 0, flag,
-                  cmd, 0) == NULL) {
-    fprintf(stderr, "genlmsg_put failed\n");
-    exit(1);
-  }
-
-  if (ifindex >= 0 && nla_put_u32(msg, NL80211_ATTR_IFINDEX, ifindex)) {
-    fprintf(stderr, "NL80211_CMD_GET_STATION put IFINDEX failed\n");
-    exit(1);
-  }
-
-  if (nl_send_auto(nlsk, msg) < 0) {
-    fprintf(stderr, "nl_send_auto failed\n");
-    exit(1);
-  }
-  nlmsg_free(msg);
-}
-
-
-void RequestInterfaceList(struct nl_sock *nlsk, int nl80211_id)
-{
-  HandleNLCommand(nlsk, nl80211_id, -1, InterfaceListCallback,
-                  NL80211_CMD_GET_INTERFACE, NLM_F_DUMP);
-}  /* RequestInterfaceList */
-
-
-int NlFinish(struct nl_msg *msg, void *arg)
-{
-  int *ret = arg;
-  *ret = 1;
-  return NL_OK;
-}
-
-
-struct nl_sock *InitNetlinkSocket()
-{
-  struct nl_sock *nlsk;
-  if ((nlsk = nl_socket_alloc()) == NULL) {
-    fprintf(stderr, "socket allocation failed\n");
-    exit(1);
-  }
-
-  if (genl_connect(nlsk) != 0) {
-    fprintf(stderr, "genl_connect failed\n");
-    exit(1);
-  }
-
-  if (nl_socket_set_nonblocking(nlsk)) {
-    fprintf(stderr, "nl_socket_set_nonblocking failed\n");
-    exit(1);
-  }
-
-  return nlsk;
-}  /* InitNetlinkSocket */
-
-
-static void ProcessNetlinkMessages(struct nl_sock *nlsk, int *done)
-{
-  for (;;) {
-    int s = nl_socket_get_fd(nlsk);
-    fd_set rfds;
-    struct timeval timeout = { .tv_sec = 1, .tv_usec = 0 };
-
-    FD_ZERO(&rfds);
-    FD_SET(s, &rfds);
-
-    if (select(s + 1, &rfds, NULL, NULL, &timeout) <= 0) {
-      break;
-    }
-
-    if (FD_ISSET(s, &rfds)) {
-      nl_recvmsgs_default(nlsk);
-    }
-
-    if (*done) {
-      break;
-    }
-  }
-}
-
-
-static uint32_t GetBitrate(struct nlattr *attr)
-{
-  int rate = 0;
-  struct nlattr *ri[NL80211_RATE_INFO_MAX + 1];
-  static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = {
-    [NL80211_RATE_INFO_BITRATE] = { .type = NLA_U16 },
-  };
-
-  if (nla_parse_nested(ri, NL80211_RATE_INFO_MAX, attr, rate_policy)) {
-    fprintf(stderr, "nla_parse_nested NL80211_RATE_INFO_MAX failed");
-    return 0;
-  }
-
-  if (ri[NL80211_RATE_INFO_BITRATE]) {
-    rate = nla_get_u16(ri[NL80211_RATE_INFO_BITRATE]);
-  }
-
-  return rate;
-}
-
-
-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;
-  struct nlattr *ri[NL80211_RATE_INFO_MAX + 1];
-  static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1] = {
-    [NL80211_RATE_INFO_MCS] = { .type = NLA_U8 },
-    [NL80211_RATE_INFO_VHT_MCS] = { .type = NLA_U8 },
-    [NL80211_RATE_INFO_VHT_NSS] = { .type = NLA_U8 },
-    [NL80211_RATE_INFO_40_MHZ_WIDTH] = { .type = NLA_FLAG },
-    [NL80211_RATE_INFO_80_MHZ_WIDTH] = { .type = NLA_FLAG },
-    [NL80211_RATE_INFO_80P80_MHZ_WIDTH] = { .type = NLA_FLAG },
-    [NL80211_RATE_INFO_160_MHZ_WIDTH] = { .type = NLA_FLAG },
-    [NL80211_RATE_INFO_SHORT_GI] = { .type = NLA_FLAG },
-  };
-
-  if (nla_parse_nested(ri, NL80211_RATE_INFO_MAX, attr, rate_policy)) {
-    fprintf(stderr, "nla_parse_nested NL80211_RATE_INFO_MAX failed");
-    return;
-  }
-
-  if (ri[NL80211_RATE_INFO_MCS]) {
-    *mcs = nla_get_u8(ri[NL80211_RATE_INFO_MCS]);
-  }
-  if (ri[NL80211_RATE_INFO_VHT_MCS]) {
-    *vht_mcs = nla_get_u8(ri[NL80211_RATE_INFO_VHT_MCS]);
-  }
-  if (ri[NL80211_RATE_INFO_VHT_NSS]) {
-    *vht_nss = nla_get_u8(ri[NL80211_RATE_INFO_VHT_NSS]);
-  }
-  if (ri[NL80211_RATE_INFO_160_MHZ_WIDTH])   w160 = 1;
-  if (ri[NL80211_RATE_INFO_80P80_MHZ_WIDTH]) w80_80 = 1;
-  if (ri[NL80211_RATE_INFO_80_MHZ_WIDTH])    w80 = 1;
-  if (ri[NL80211_RATE_INFO_40_MHZ_WIDTH])    w40 = 1;
-  if (ri[NL80211_RATE_INFO_SHORT_GI])        *short_gi = 1;
-
-  if (w160 || w80_80) {
-    *width = 160;
-  } else if (w80) {
-    *width = 80;
-  } else if (w40) {
-    *width = 40;
-  } else {
-    *width = 20;
-  }
-}
-
-
-static client_state_t *FindClientState(const uint8_t mac[6])
-{
-  client_state_t *s;
-  char macstr[MAC_STR_LEN];
-
-  snprintf(macstr, sizeof(macstr), "%02x:%02x:%02x:%02x:%02x:%02x",
-      mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
-
-  /* Find any existing state for this STA, or allocate new. */
-  if ((s = (client_state_t *)g_hash_table_lookup(clients, macstr)) == NULL) {
-    s = (client_state_t *)malloc(sizeof(*s));
-    memset(s, 0, sizeof(*s));
-    memcpy(s->macstr, macstr, sizeof(s->macstr));
-    s->first_seen = monotime();
-    g_hash_table_insert(clients, strdup(macstr), s);
-  }
-
-  return s;
-}
-
-
-static int HtMcsToNss(int rxmcs)
-{
-  /* https://en.wikipedia.org/wiki/IEEE_802.11n-2009 */
-  switch(rxmcs) {
-    case 0 ... 7:   return 1;
-    case 8 ... 15:  return 2;
-    case 16 ... 23: return 3;
-    case 24 ... 31: return 4;
-    case 32:        return 1;
-    case 33 ... 38: return 2;
-    case 39 ... 52: return 3;
-    case 53 ... 76: return 4;
-    default:        return 0;
-  }
-}
-
-static int StationDumpCallback(struct nl_msg *msg, void *arg)
-{
-  const char *ifname = (const char *)arg;
-  struct genlmsghdr *gh = nlmsg_data(nlmsg_hdr(msg));
-  struct nlattr *tb[NL80211_ATTR_MAX + 1] = {0};
-  struct nlattr *si[NL80211_STA_INFO_MAX + 1] = {0};
-  uint8_t *mac;
-  client_state_t *state;
-  static struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1] = {
-    [NL80211_STA_INFO_INACTIVE_TIME] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_RX_BITRATE] = { .type = NLA_NESTED },
-    [NL80211_STA_INFO_RX_BYTES] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_RX_PACKETS] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_TX_BITRATE] = { .type = NLA_NESTED },
-    [NL80211_STA_INFO_TX_BYTES] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_TX_PACKETS] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_TX_RETRIES] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_TX_FAILED] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_CONNECTED_TIME] = { .type = NLA_U32 },
-    [NL80211_STA_INFO_SIGNAL] = { .type = NLA_U8 },
-    [NL80211_STA_INFO_SIGNAL_AVG] = { .type = NLA_U8 },
-    [NL80211_STA_INFO_STA_FLAGS] = {
-      .minlen = sizeof(struct nl80211_sta_flag_update) },
-
-#ifdef NL80211_RECENT_FIELDS
-    [NL80211_STA_INFO_RX_DROP_MISC] = { .type = NLA_U64 },
-    [NL80211_STA_INFO_EXPECTED_THROUGHPUT] = { .type = NLA_U32 },
-#endif
-  };
-
-  if (nla_parse(tb, NL80211_ATTR_MAX,
-                genlmsg_attrdata(gh, 0), genlmsg_attrlen(gh, 0), NULL)) {
-    fprintf(stderr, "nla_parse failed.\n");
-    return NL_SKIP;
-  }
-
-  if (!tb[NL80211_ATTR_STA_INFO]) {
-    return NL_SKIP;
-  }
-
-  if (nla_parse_nested(si, NL80211_STA_INFO_MAX,
-                       tb[NL80211_ATTR_STA_INFO],
-                       stats_policy)) {
-    fprintf(stderr, "nla_parse_nested failed\n");
-    return NL_SKIP;
-  }
-
-  if (!tb[NL80211_ATTR_MAC]) {
-    fprintf(stderr, "No NL80211_ATTR_MAC\n");
-    return NL_SKIP;
-  }
-
-  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);
-
-  if (si[NL80211_STA_INFO_INACTIVE_TIME]) {
-    uint32_t inactive_msec = nla_get_u32(si[NL80211_STA_INFO_INACTIVE_TIME]);
-    double inactive_since = time(NULL) - ((double)inactive_msec / 1000.0);
-
-    state->inactive_msec = inactive_msec;
-    if ((fabs(inactive_since - state->inactive_since)) > 2.0) {
-      state->inactive_since = inactive_since;
-    }
-  }
-
-  if (si[NL80211_STA_INFO_RX_BITRATE]) {
-    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->rx_sample_index + 1;
-
-    if (n >= MAX_SAMPLE_INDEX) n = 0;
-
-    state->rx_bitrate = GetBitrate(si[NL80211_STA_INFO_RX_BITRATE]);
-    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 = 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;
-
-    state->rx_vht_mcs_samples[n] = rx_vht_mcs;
-    if (rx_vht_mcs > state->rx_max_vht_mcs) state->rx_max_vht_mcs = rx_vht_mcs;
-
-    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->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->rx_sample_index = n;
-  }
-  if (si[NL80211_STA_INFO_RX_BYTES]) {
-    uint32_t last_rx_bytes = state->rx_bytes;
-    state->rx_bytes = nla_get_u32(si[NL80211_STA_INFO_RX_BYTES]);
-    state->rx_bytes64 += (state->rx_bytes - last_rx_bytes);
-  }
-  if (si[NL80211_STA_INFO_RX_PACKETS]) {
-    uint32_t last_rx_packets = state->rx_packets;
-    state->rx_packets = nla_get_u32(si[NL80211_STA_INFO_RX_PACKETS]);
-    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;
-    state->tx_bytes = nla_get_u32(si[NL80211_STA_INFO_TX_BYTES]);
-    state->tx_bytes64 += (state->tx_bytes - last_tx_bytes);
-  }
-  if (si[NL80211_STA_INFO_TX_PACKETS]) {
-    uint32_t last_tx_packets = state->tx_packets;
-    state->tx_packets = nla_get_u32(si[NL80211_STA_INFO_TX_PACKETS]);
-    state->tx_packets64 += (state->tx_packets - last_tx_packets);
-  }
-  if (si[NL80211_STA_INFO_TX_RETRIES]) {
-    uint32_t last_tx_retries = state->tx_retries;
-    state->tx_retries = nla_get_u32(si[NL80211_STA_INFO_TX_RETRIES]);
-    state->tx_retries64 += (state->tx_retries - last_tx_retries);
-  }
-  if (si[NL80211_STA_INFO_TX_FAILED]) {
-    uint32_t last_tx_failed = state->tx_failed;
-    state->tx_failed = nla_get_u32(si[NL80211_STA_INFO_TX_FAILED]);
-    state->tx_failed64 += (state->tx_failed - last_tx_failed);
-  }
-  if (si[NL80211_STA_INFO_CONNECTED_TIME]) {
-    state->connected_secs = nla_get_u32(si[NL80211_STA_INFO_CONNECTED_TIME]);
-  }
-  if (si[NL80211_STA_INFO_SIGNAL]) {
-    state->signal = (int8_t)nla_get_u8(si[NL80211_STA_INFO_SIGNAL]);
-  }
-  if (si[NL80211_STA_INFO_SIGNAL_AVG]) {
-    state->signal_avg = (int8_t)nla_get_u8(si[NL80211_STA_INFO_SIGNAL_AVG]);
-  }
-
-  if (si[NL80211_STA_INFO_STA_FLAGS]) {
-    struct nl80211_sta_flag_update *sta_flags;
-    sta_flags = (struct nl80211_sta_flag_update *)nla_data(
-        si[NL80211_STA_INFO_STA_FLAGS]);
-
-    #define BIT(x) ((sta_flags->mask & (1ULL<<(x))) ? 1 : 0)
-    state->authorized = BIT(NL80211_STA_FLAG_AUTHORIZED);
-    state->authenticated = BIT(NL80211_STA_FLAG_AUTHENTICATED);
-    state->preamble = BIT(NL80211_STA_FLAG_SHORT_PREAMBLE);
-    state->wmm_wme = BIT(NL80211_STA_FLAG_WME);
-    state->mfp = BIT(NL80211_STA_FLAG_MFP);
-    state->tdls_peer = BIT(NL80211_STA_FLAG_TDLS_PEER);
-    state->preamble_length = BIT(NL80211_STA_FLAG_SHORT_PREAMBLE);
-    #undef BIT
-  }
-
-#ifdef NL80211_RECENT_FIELDS
-  if (si[NL80211_STA_INFO_RX_DROP_MISC]) {
-    state->rx_drop64 = nla_get_u64(si[NL80211_STA_INFO_RX_DROP_MISC]);
-  }
-  if (si[NL80211_STA_INFO_EXPECTED_THROUGHPUT]) {
-    state->expected_mbps =
-      nla_get_u32(si[NL80211_STA_INFO_EXPECTED_THROUGHPUT]);
-  }
-#endif
-
-  return NL_OK;
-}  /* StationDumpCallback */
-
-
-void RequestAssociatedDevices(struct nl_sock *nlsk,
-    int nl80211_id, int n)
-{
-  HandleNLCommand(nlsk, nl80211_id, n, StationDumpCallback,
-                  NL80211_CMD_GET_STATION, NLM_F_DUMP);
-}  /* RequestAssociatedDevices */
-
-
-static void ClearClientCounters(client_state_t *state)
-{
-  /* Kernel cleared its counters when client re-joined the WLAN,
-   * clear out previous state as well. */
-  state->rx_bytes = 0;
-  state->rx_packets = 0;
-  state->tx_bytes = 0;
-  state->tx_packets = 0;
-  state->tx_retries = 0;
-  state->tx_failed = 0;
-}
-
-
-static gboolean AgeOutClient(gpointer key, gpointer value, gpointer user_data)
-{
-  client_state_t *state = (client_state_t *)value;
-  time_t mono_now = monotime();
-
-  if ((mono_now - state->last_seen) > MAX_CLIENT_AGE_SECS) {
-    char filename[PATH_MAX];
-    snprintf(filename, sizeof(filename), "%s/%s", STATIONS_DIR, state->macstr);
-    unlink(filename);
-    return TRUE;
-  }
-
-  if (state->connected_secs < 60) {
-    /* If the client recently dropped off and came back, clear any counters
-     * we've been maintaining. */
-    ClearClientCounters(state);
-  }
-
-  return FALSE;
-}
-
-
-static void ConsolidateClientSamples(gpointer key, gpointer value,
-    gpointer user_data)
-{
-  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, 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) {
-      rx_ht_mcs = state->rx_ht_mcs_samples[i];
-    }
-    if (state->rx_vht_mcs_samples[i] > rx_vht_mcs) {
-      rx_vht_mcs = state->rx_vht_mcs_samples[i];
-    }
-    if (state->rx_width_samples[i] > rx_width) {
-      rx_width = state->rx_width_samples[i];
-    }
-    if (state->rx_ht_nss_samples[i] > rx_ht_nss) {
-      rx_ht_nss = state->rx_ht_nss_samples[i];
-    }
-    if (state->rx_vht_nss_samples[i] > rx_vht_nss) {
-      rx_vht_nss = state->rx_vht_nss_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];
-    }
-  }
-
-  state->rx_ht_mcs = rx_ht_mcs;
-  state->rx_vht_mcs = rx_vht_mcs;
-  state->rx_width = rx_width;
-  state->rx_ht_nss = rx_ht_nss;
-  state->rx_vht_nss = rx_vht_nss;
-  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;
-}
-
-
-static void ClientStateToJson(gpointer key, gpointer value, gpointer user_data)
-{
-  const client_state_t *state = (const client_state_t *)value;
-  char tmpfile[PATH_MAX];
-  char filename[PATH_MAX];
-  time_t mono_now = monotime();
-  FILE *f;
-
-  snprintf(tmpfile, sizeof(tmpfile), "%s/%s.new", STATIONS_DIR, state->macstr);
-  snprintf(filename, sizeof(filename), "%s/%s", STATIONS_DIR, state->macstr);
-
-  if ((f = fopen(tmpfile, "w+")) == NULL) {
-    char errbuf[80];
-    snprintf(errbuf, sizeof(errbuf), "fopen %s", tmpfile);
-    perror(errbuf);
-    return;
-  }
-
-  fprintf(f, "{\n");
-
-  fprintf(f, "  \"addr\": \"%s\",\n", state->macstr);
-  fprintf(f, "  \"inactive since\": %.3f,\n", state->inactive_since);
-  fprintf(f, "  \"inactive msec\": %u,\n", state->inactive_msec);
-
-  fprintf(f, "  \"active\": %s,\n",
-      ((mono_now - state->last_seen) < 600) ? "true" : "false");
-
-  fprintf(f, "  \"rx bitrate\": %u.%u,\n",
-      (state->rx_bitrate / 10), (state->rx_bitrate % 10));
-  fprintf(f, "  \"rx bytes\": %u,\n", state->rx_bytes);
-  fprintf(f, "  \"rx packets\": %u,\n", state->rx_packets);
-
-  fprintf(f, "  \"tx bitrate\": %u.%u,\n",
-      (state->tx_bitrate / 10), (state->tx_bitrate % 10));
-  fprintf(f, "  \"tx bytes\": %u,\n", state->tx_bytes);
-  fprintf(f, "  \"tx packets\": %u,\n", state->tx_packets);
-  fprintf(f, "  \"tx retries\": %u,\n", state->tx_retries);
-  fprintf(f, "  \"tx failed\": %u,\n", state->tx_failed);
-
-  fprintf(f, "  \"rx mcs\": %u,\n", state->rx_ht_mcs);
-  fprintf(f, "  \"rx max mcs\": %u,\n", state->rx_max_ht_mcs);
-  fprintf(f, "  \"rx vht mcs\": %u,\n", state->rx_vht_mcs);
-  fprintf(f, "  \"rx max vht mcs\": %u,\n", state->rx_max_vht_mcs);
-  fprintf(f, "  \"rx width\": %u,\n", state->rx_width);
-  fprintf(f, "  \"rx max width\": %u,\n", state->rx_max_width);
-  fprintf(f, "  \"rx ht_nss\": %u,\n", state->rx_ht_nss);
-  fprintf(f, "  \"rx max ht_nss\": %u,\n", state->rx_max_ht_nss);
-  fprintf(f, "  \"rx vht_nss\": %u,\n", state->rx_vht_nss);
-  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->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);
-  fprintf(f, "  \"signal_avg\": %hhd,\n", state->signal_avg);
-
-  #define BOOL(x) (x ? "yes" : "no")
-  fprintf(f, "  \"authorized\": \"%s\",\n", BOOL(state->authorized));
-  fprintf(f, "  \"authenticated\": \"%s\",\n", BOOL(state->authenticated));
-  fprintf(f, "  \"preamble\": \"%s\",\n", BOOL(state->preamble));
-  fprintf(f, "  \"wmm_wme\": \"%s\",\n", BOOL(state->wmm_wme));
-  fprintf(f, "  \"mfp\": \"%s\",\n", BOOL(state->mfp));
-  fprintf(f, "  \"tdls_peer\": \"%s\",\n", BOOL(state->tdls_peer));
-  #undef BOOL
-
-  fprintf(f, "  \"preamble length\": \"%s\",\n",
-      (state->preamble_length ? "short" : "long"));
-
-  fprintf(f, "  \"rx bytes64\": %" PRIu64 ",\n", state->rx_bytes64);
-  fprintf(f, "  \"rx drop64\": %" PRIu64 ",\n", state->rx_drop64);
-  fprintf(f, "  \"tx bytes64\": %" PRIu64 ",\n", state->tx_bytes64);
-  fprintf(f, "  \"tx retries64\": %" PRIu64 ",\n", state->tx_retries64);
-  fprintf(f, "  \"expected Mbps\": %u.%03u,\n",
-          (state->expected_mbps / 1000), (state->expected_mbps % 1000));
-
-  fprintf(f, "  \"ifname\": \"%s\"\n", state->ifname);
-  fprintf(f, "}\n");
-
-  fclose(f);
-  if (rename(tmpfile, filename)) {
-    char errstr[160];
-    snprintf(errstr, sizeof(errstr), "%s: rename %s to %s",
-        __FUNCTION__, tmpfile, filename);
-    perror(errstr);
-  }
-}
-
-
-static void ClientStateToLog(gpointer key, gpointer value, gpointer user_data)
-{
-  const client_state_t *state = (const client_state_t *)value;
-  const callback_data_t *cb_data = (const callback_data_t *)user_data;
-  time_t mono_now = cb_data->mono_now;
-
-  if (!state->authorized || !state->authenticated) {
-    /* Don't log about non-associated clients */
-    return;
-  }
-
-  if ((mono_now - state->first_seen) < 120) {
-    /* Allow data to accumulate before beginning to log it. */
-    return;
-  }
-
-  printf(
-      "%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)),
-
-      /* L2 traffic stats */
-      state->rx_bytes64, state->rx_drop64, state->tx_bytes64,
-      state->tx_retries64, state->tx_failed64,
-
-      /* L1 information */
-      (state->preamble_length ? 'S' : 'L'),
-      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->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_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);
-}
-
-
-void ConsolidateAssociatedDevices()
-{
-  g_hash_table_foreach_remove(clients, AgeOutClient, NULL);
-  g_hash_table_foreach(clients, ConsolidateClientSamples, NULL);
-}
-
-
-/* Walk through all Wifi clients, printing their info to JSON files. */
-void UpdateAssociatedDevices()
-{
-  g_hash_table_foreach(clients, ClientStateToJson, NULL);
-}
-
-
-void LogAssociatedDevices()
-{
-  callback_data_t cb_data;
-
-  memset(&cb_data, 0, sizeof(cb_data));
-  cb_data.mono_now = monotime();
-  g_hash_table_foreach(clients, ClientStateToLog, &cb_data);
-}
-
-
-static int ieee80211_frequency_to_channel(int freq)
-{
-  /* see 802.11-2007 17.3.8.3.2 and Annex J */
-  if (freq == 2484)
-    return 14;
-  else if (freq < 2484)
-    return (freq - 2407) / 5;
-  else if (freq >= 4910 && freq <= 4980)
-    return (freq - 4000) / 5;
-  else if (freq <= 45000) /* DMG band lower limit */
-    return (freq - 5000) / 5;
-  else if (freq >= 58320 && freq <= 64800)
-    return (freq - 56160) / 2160;
-  else
-    return 0;
-}
-
-
-static void print_ssid_escaped(FILE *f, int len, const uint8_t *data)
-{
-  int i;
-
-  for (i = 0; i < len; 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;
-    }
-  }
-}
-
-
-static int WlanInfoCallback(struct nl_msg *msg, void *arg)
-{
-  struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
-  struct nlattr *tb_msg[NL80211_ATTR_MAX + 1];
-
-  nla_parse(tb_msg, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
-            genlmsg_attrlen(gnlh, 0), NULL);
-
-  if (tb_msg[NL80211_ATTR_MAC]) {
-    unsigned char *mac_addr = nla_data(tb_msg[NL80211_ATTR_MAC]);
-    fprintf(wifi_info_handle,
-            "  \"BSSID\": \"%02x:%02x:%02x:%02x:%02x:%02x\",\n",
-            mac_addr[0], mac_addr[1], mac_addr[2],
-            mac_addr[3], mac_addr[4], mac_addr[5]);
-  }
-  if (tb_msg[NL80211_ATTR_SSID]) {
-    fprintf(wifi_info_handle, "  \"SSID\": \"");
-    print_ssid_escaped(wifi_info_handle, nla_len(tb_msg[NL80211_ATTR_SSID]),
-                       nla_data(tb_msg[NL80211_ATTR_SSID]));
-    fprintf(wifi_info_handle, "\",\n");
-  }
-  if (tb_msg[NL80211_ATTR_WIPHY_FREQ]) {
-    uint32_t freq = nla_get_u32(tb_msg[NL80211_ATTR_WIPHY_FREQ]);
-    fprintf(wifi_info_handle, "  \"Channel\": %d,\n",
-            ieee80211_frequency_to_channel(freq));
-  }
-
-  return NL_SKIP;
-}
-
-
-void RequestWifiInfo(struct nl_sock *nlsk, int nl80211_id, int n)
-{
-  HandleNLCommand(nlsk, nl80211_id, n, WlanInfoCallback,
-                  NL80211_CMD_GET_INTERFACE, 0);
-}
-
-
-static int RegdomainCallback(struct nl_msg *msg, void *arg)
-{
-  struct nlattr *tb_msg[NL80211_ATTR_MAX + 1];
-  struct genlmsghdr *gnlh = nlmsg_data(nlmsg_hdr(msg));
-  char *reg;
-
-  nla_parse(tb_msg, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
-            genlmsg_attrlen(gnlh, 0), NULL);
-
-  if (!tb_msg[NL80211_ATTR_REG_ALPHA2]) {
-    return NL_SKIP;
-  }
-
-  if (!tb_msg[NL80211_ATTR_REG_RULES]) {
-    return NL_SKIP;
-  }
-
-  reg = nla_data(tb_msg[NL80211_ATTR_REG_ALPHA2]);
-  fprintf(wifi_info_handle, "  \"RegDomain\": \"%c%c\",\n", reg[0], reg[1]);
-
-  return NL_SKIP;
-}
-
-
-void RequestRegdomain(struct nl_sock *nlsk, int nl80211_id)
-{
-  HandleNLCommand(nlsk, nl80211_id, -1, RegdomainCallback,
-                  NL80211_CMD_GET_REG, 0);
-}
-
-
-void UpdateWifiShow(struct nl_sock *nlsk, int nl80211_id, int n)
-{
-  char tmpfile[PATH_MAX];
-  char filename[PATH_MAX];
-  char autofile[PATH_MAX];
-  const char *ifname = interfaces[n];
-  int done = 0;
-  struct stat buffer;
-  FILE *fptr;
-
-  if (!ifname || !ifname[0]) {
-    return;
-  }
-
-  snprintf(tmpfile, sizeof(tmpfile), "%s/%s.new", WIFIINFO_DIR, ifname);
-  snprintf(filename, sizeof(filename), "%s/%s", WIFIINFO_DIR, ifname);
-
-  if ((wifi_info_handle = fopen(tmpfile, "w+")) == NULL) {
-    perror("fopen");
-    return;
-  }
-
-  fprintf(wifi_info_handle, "{\n");
-  done = 0;
-  RequestWifiInfo(nlsk, nl80211_id, n);
-  ProcessNetlinkMessages(nlsk, &done);
-
-  done = 0;
-  RequestRegdomain(nlsk, nl80211_id);
-  ProcessNetlinkMessages(nlsk, &done);
-
-  snprintf(autofile, sizeof(autofile), "/tmp/autochan.%s", ifname);
-  if (stat(autofile, &buffer) == 0) {
-    fprintf(wifi_info_handle, "  \"AutoChannel\": true,\n");
-  } else {
-    fprintf(wifi_info_handle, "  \"AutoChannel\": false,\n");
-  }
-  snprintf(autofile, sizeof(autofile), "/tmp/autotype.%s", ifname);
-  if ((fptr = fopen(autofile, "r")) == NULL) {
-    fprintf(wifi_info_handle, "  \"AutoType\": \"LOW\"\n");
-  } else {
-    char buf[24];
-    if (fgets(buf, sizeof(buf), fptr) != NULL) {
-      fprintf(wifi_info_handle, "  \"AutoType\": \"%s\"\n", buf);
-    }
-    fclose(fptr);
-    fptr = NULL;
-  }
-  fprintf(wifi_info_handle, "}\n");
-
-  fclose(wifi_info_handle);
-  wifi_info_handle = NULL;
-  if (rename(tmpfile, filename)) {
-    char errbuf[256];
-    snprintf(errbuf, sizeof(errbuf), "%s: rename %s to %s : errno=%d",
-        __FUNCTION__, tmpfile, filename, errno);
-    perror(errbuf);
-  }
-}
-
-#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;
-  int nl80211_id = -1;
-  struct nl_sock *nlsk = NULL;
-  struct rlimit rlim;
-
-  memset(&rlim, 0, sizeof(rlim));
-  if (getrlimit(RLIMIT_AS, &rlim)) {
-    perror("getrlimit RLIMIT_AS failed");
-    exit(1);
-  }
-  rlim.rlim_cur = 6 * 1024 * 1024;
-  if (setrlimit(RLIMIT_AS, &rlim)) {
-    perror("getrlimit RLIMIT_AS failed");
-    exit(1);
-  }
-
-  setlinebuf(stdout);
-
-  clients = g_hash_table_new_full(g_str_hash, g_str_equal, free, free);
-
-  nlsk = InitNetlinkSocket();
-  if (nl_socket_modify_cb(nlsk, NL_CB_FINISH, NL_CB_CUSTOM, NlFinish, &done)) {
-    fprintf(stderr, "nl_socket_modify_cb failed\n");
-    exit(1);
-  }
-  if ((nl80211_id = genl_ctrl_resolve(nlsk, "nl80211")) < 0) {
-    fprintf(stderr, "genl_ctrl_resolve failed\n");
-    exit(1);
-  }
-
-  while (1) {
-    int i, j;
-
-    /* Check if new interfaces have appeared */
-    ninterfaces = 0;
-    RequestInterfaceList(nlsk, nl80211_id);
-    ProcessNetlinkMessages(nlsk, &done);
-    for (i = 0; i < ninterfaces; ++i) {
-      UpdateWifiShow(nlsk, nl80211_id, i);
-    }
-
-    /* Accumulate MAX_SAMPLE_INDEX samples between calls to
-     * LogAssociatedDevices() */
-    for (i = 0; i < MAX_SAMPLE_INDEX; ++i) {
-      sleep(2);
-      for (j = 0; j < ninterfaces; ++j) {
-        done = 0;
-        RequestAssociatedDevices(nlsk, nl80211_id, j);
-        ProcessNetlinkMessages(nlsk, &done);
-        ConsolidateAssociatedDevices();
-        UpdateAssociatedDevices();
-      }
-      TouchUpdateFile();
-    }
-    LogAssociatedDevices();
-  }
-
-  exit(0);
-}
-#endif  /* UNIT_TESTS */
diff --git a/cmds/wifi_files.cc b/cmds/wifi_files.cc
new file mode 100644
index 0000000..95ca209
--- /dev/null
+++ b/cmds/wifi_files.cc
@@ -0,0 +1,1471 @@
+/*
+ * Portions of this code are derived from iw-3.17.
+ *
+ * Copyright (c) 2007, 2008 Johannes Berg
+ * Copyright (c) 2007   Andy Lutomirski
+ * Copyright (c) 2007   Mike Kershaw
+ * Copyright (c) 2008-2009    Luis R. Rodriguez
+ * Copyright (c) 2015   Google, Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* for inttypes.h */
+#define __STDC_FORMAT_MACROS
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <linux/if_ether.h>
+#include <linux/nl80211.h>
+#include <math.h>
+#include <net/if.h>
+#include <net/if_arp.h>
+#include <netlink/attr.h>
+#include <netlink/genl/genl.h>
+#include <netlink/genl/ctrl.h>
+#include <netlink/msg.h>
+#include <netlink/netlink.h>
+#include <netlink/socket.h>
+#include <stdio.h>
+#include <sys/ioctl.h>
+#include <sys/resource.h>
+#include <sys/select.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <cinttypes>
+#include <string>
+#include <tr1/unordered_map>
+
+#ifndef UNIT_TESTS
+#define STATIONS_DIR "/tmp/stations"
+#define WIFIINFO_DIR "/tmp/wifi/wifiinfo"
+#endif
+
+
+#define MAX_CLIENT_AGE_SECS  (4 * 60 * 60)
+
+
+#ifndef UNIT_TESTS
+static time_t monotime(void) {
+  struct timespec ts;
+  if (clock_gettime(CLOCK_MONOTONIC, &ts) < 0) {
+    return time(NULL);
+  } else {
+    return ts.tv_sec;
+  }
+}
+#endif
+
+
+typedef struct wifi_stats {
+  uint64_t rx_drop64;
+
+  // Accumulated values from the 32 bit counters.
+  uint64_t rx_bytes64;
+  uint64_t tx_bytes64;
+  uint64_t rx_packets64;
+  uint64_t tx_packets64;
+  uint64_t tx_retries64;
+  uint64_t tx_failed64;
+
+  uint32_t rx_bitrate;
+  uint32_t rx_bytes;
+  uint32_t rx_packets;
+
+  uint32_t tx_bitrate;
+  uint32_t tx_bytes;
+  uint32_t tx_packets;
+  uint32_t tx_retries;
+  uint32_t tx_failed;
+  uint32_t expected_mbps;
+
+#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 rx_short_gi_samples[MAX_SAMPLE_INDEX];
+  int8_t signal_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
+   * are only sending management frames and ACKs. These
+   * tend to be sent at much lower MCS rates than bulk data;
+   * if we report that MCS rate it gives a misleading
+   * picture of what the client is capable of getting.
+   *
+   * Instead, we choose the largest sample over the reporting
+   * interval. This is more likely to report a meaningful
+   * MCS rate.
+   */
+  uint8_t rx_ht_mcs;
+  uint8_t rx_vht_mcs;
+  uint8_t rx_width;
+  uint8_t rx_ht_nss;
+  uint8_t rx_vht_nss;
+  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
+   * conditions don't allow it to use its full capability. */
+  uint8_t rx_max_ht_mcs;
+  uint8_t rx_max_vht_mcs;
+  uint8_t rx_max_width;
+  uint8_t rx_max_ht_nss;
+  uint8_t rx_max_vht_nss;
+  uint8_t ever_rx_short_gi;
+
+  double max_signal;
+  double min_signal;
+  double avg_signal;
+
+  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;
+
+  uint8_t authorized:1;
+  uint8_t authenticated:1;
+  uint8_t preamble:1;
+  uint8_t wmm_wme:1;
+  uint8_t mfp:1;
+  uint8_t tdls_peer:1;
+  uint8_t preamble_length:1;
+
+  double inactive_since;
+  uint32_t inactive_msec;
+  uint32_t connected_secs;
+} wifi_stats_t;
+
+
+#define MAC_STR_LEN 18
+#define IFNAME_STR_LEN 16
+
+
+/*
+ * Saved state for each associated Wifi device. Wifi clients drop out
+ * after 5 minutes inactive, we want to export information about the
+ * client for a while longer than that.
+ */
+typedef struct client_state {
+  char macstr[MAC_STR_LEN];
+  char ifname[IFNAME_STR_LEN];
+
+  time_t first_seen;  // CLOCK_MONOTONIC
+  time_t last_seen;  // CLOCK_MONOTONIC
+
+  wifi_stats_t s;
+} client_state_t;
+
+
+// Hash table of known Wifi clients.
+typedef std::tr1::unordered_map<std::string, client_state_t *> ClientMapType;
+ClientMapType clients;
+
+
+/* Data about each wifi interface. */
+typedef struct wifi_interface {
+  int ifindex;
+  char ifname[IFNAME_STR_LEN];
+  uint8_t bssid[ETH_ALEN];
+
+  int is_client:1;
+  uint32_t freq;
+
+  wifi_stats_t s;
+} wifi_interface_t;
+
+
+/* List of wifi interfaces in the system. */
+#define NINTERFACES 16
+wifi_interface_t interfaces[NINTERFACES];
+int ninterfaces = 0;
+
+
+typedef struct callback_data {
+  time_t mono_now;
+} callback_data_t;
+
+
+/* FILE handle to /tmp/wifi/wifiinfo, while open. */
+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;
+  struct ifreq ifr;
+
+  if (strlen(ifname) >= sizeof(ifr.ifr_name)) {
+    fprintf(stderr, "interface name %s is too long\n", ifname);
+    exit(1);
+  }
+
+  if ((fd = socket(AF_PACKET, SOCK_DGRAM, 0)) < 0) {
+    perror("socket");
+    exit(1);
+  }
+
+  memset(&ifr, 0, sizeof(ifr));
+  snprintf(ifr.ifr_name, sizeof(ifr.ifr_name), "%s", ifname);
+
+  if (ioctl(fd, SIOCGIFINDEX, &ifr) < 0) {
+    char errbuf[128];
+    snprintf(errbuf, sizeof(errbuf), "SIOCGIFINDEX %s", ifname);
+    perror(errbuf);
+    close(fd);
+    exit(1);
+  }
+
+  close(fd);
+  return ifr.ifr_ifindex;
+}  /* GetIfIndex */
+
+
+static void ProcessNetlinkMessages(struct nl_sock *nlsk, int *done)
+{
+  for (;;) {
+    int s = nl_socket_get_fd(nlsk);
+    fd_set rfds;
+    struct timeval timeout = { .tv_sec = 1, .tv_usec = 0 };
+
+    FD_ZERO(&rfds);
+    FD_SET(s, &rfds);
+
+    if (select(s + 1, &rfds, NULL, NULL, &timeout) <= 0) {
+      break;
+    }
+
+    if (FD_ISSET(s, &rfds)) {
+      nl_recvmsgs_default(nlsk);
+    }
+
+    if (*done) {
+      break;
+    }
+  }
+}
+
+
+static uint32_t GetBitrate(struct nlattr *attr)
+{
+  int rate = 0;
+  struct nlattr *ri[NL80211_RATE_INFO_MAX + 1];
+  static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1];
+
+  memset(&rate_policy, 0, sizeof(rate_policy));
+  rate_policy[NL80211_RATE_INFO_BITRATE].type = NLA_U16;
+
+  if (nla_parse_nested(ri, NL80211_RATE_INFO_MAX, attr, rate_policy)) {
+    fprintf(stderr, "nla_parse_nested NL80211_RATE_INFO_MAX failed");
+    return 0;
+  }
+
+  if (ri[NL80211_RATE_INFO_BITRATE]) {
+    rate = nla_get_u16(ri[NL80211_RATE_INFO_BITRATE]);
+  }
+
+  return rate;
+}
+
+
+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;
+  struct nlattr *ri[NL80211_RATE_INFO_MAX + 1];
+  static struct nla_policy rate_policy[NL80211_RATE_INFO_MAX + 1];
+
+  memset(&rate_policy, 0, sizeof(rate_policy));
+  rate_policy[NL80211_RATE_INFO_MCS].type = NLA_U8;
+  rate_policy[NL80211_RATE_INFO_VHT_MCS].type = NLA_U8;
+  rate_policy[NL80211_RATE_INFO_VHT_NSS].type = NLA_U8;
+  rate_policy[NL80211_RATE_INFO_40_MHZ_WIDTH].type = NLA_FLAG;
+  rate_policy[NL80211_RATE_INFO_80_MHZ_WIDTH].type = NLA_FLAG;
+  rate_policy[NL80211_RATE_INFO_80P80_MHZ_WIDTH].type = NLA_FLAG;
+  rate_policy[NL80211_RATE_INFO_160_MHZ_WIDTH].type = NLA_FLAG;
+  rate_policy[NL80211_RATE_INFO_SHORT_GI].type = NLA_FLAG;
+
+  if (nla_parse_nested(ri, NL80211_RATE_INFO_MAX, attr, rate_policy)) {
+    fprintf(stderr, "nla_parse_nested NL80211_RATE_INFO_MAX failed");
+    return;
+  }
+
+  if (ri[NL80211_RATE_INFO_MCS]) {
+    *mcs = nla_get_u8(ri[NL80211_RATE_INFO_MCS]);
+  }
+  if (ri[NL80211_RATE_INFO_VHT_MCS]) {
+    *vht_mcs = nla_get_u8(ri[NL80211_RATE_INFO_VHT_MCS]);
+  }
+  if (ri[NL80211_RATE_INFO_VHT_NSS]) {
+    *vht_nss = nla_get_u8(ri[NL80211_RATE_INFO_VHT_NSS]);
+  }
+  if (ri[NL80211_RATE_INFO_160_MHZ_WIDTH])   w160 = 1;
+  if (ri[NL80211_RATE_INFO_80P80_MHZ_WIDTH]) w80_80 = 1;
+  if (ri[NL80211_RATE_INFO_80_MHZ_WIDTH])    w80 = 1;
+  if (ri[NL80211_RATE_INFO_40_MHZ_WIDTH])    w40 = 1;
+  if (ri[NL80211_RATE_INFO_SHORT_GI])        *short_gi = 1;
+
+  if (w160 || w80_80) {
+    *width = 160;
+  } else if (w80) {
+    *width = 80;
+  } else if (w40) {
+    *width = 40;
+  } else {
+    *width = 20;
+  }
+}
+
+
+static int HtMcsToNss(int rxmcs)
+{
+  /* https://en.wikipedia.org/wiki/IEEE_802.11n-2009 */
+  switch(rxmcs) {
+    case 0 ... 7:   return 1;
+    case 8 ... 15:  return 2;
+    case 16 ... 23: return 3;
+    case 24 ... 31: return 4;
+    case 32:        return 1;
+    case 33 ... 38: return 2;
+    case 39 ... 52: return 3;
+    case 53 ... 76: return 4;
+    default:        return 0;
+  }
+}
+
+
+static int ParseWifiStats(struct nlattr *sta_info, wifi_stats_t *stats)
+{
+  struct nlattr *si[NL80211_STA_INFO_MAX + 1] = {0};
+  static struct nla_policy stats_policy[NL80211_STA_INFO_MAX + 1];
+
+  memset(&stats_policy, 0, sizeof(stats_policy));
+  stats_policy[NL80211_STA_INFO_INACTIVE_TIME].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_RX_BITRATE].type = NLA_NESTED;
+  stats_policy[NL80211_STA_INFO_RX_BYTES].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_RX_PACKETS].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_TX_BITRATE].type = NLA_NESTED;
+  stats_policy[NL80211_STA_INFO_TX_BYTES].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_TX_PACKETS].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_TX_RETRIES].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_TX_FAILED].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_CONNECTED_TIME].type = NLA_U32;
+  stats_policy[NL80211_STA_INFO_SIGNAL].type = NLA_U8;
+  stats_policy[NL80211_STA_INFO_SIGNAL_AVG].type = NLA_U8;
+  stats_policy[NL80211_STA_INFO_STA_FLAGS].minlen = sizeof(struct nl80211_sta_flag_update);
+
+#ifdef NL80211_RECENT_FIELDS
+  stats_policy[NL80211_STA_INFO_RX_DROP_MISC].type = NLA_U64;
+  stats_policy[NL80211_STA_INFO_EXPECTED_THROUGHPUT].type = NLA_U32;
+#endif
+
+  if (nla_parse_nested(si, NL80211_STA_INFO_MAX, sta_info, stats_policy)) {
+    fprintf(stderr, "nla_parse_nested failed\n");
+    return NL_SKIP;
+  }
+
+  if (si[NL80211_STA_INFO_INACTIVE_TIME]) {
+    uint32_t inactive_msec = nla_get_u32(si[NL80211_STA_INFO_INACTIVE_TIME]);
+    double inactive_since = time(NULL) - ((double)inactive_msec / 1000.0);
+
+    stats->inactive_msec = inactive_msec;
+    if ((fabs(inactive_since - stats->inactive_since)) > 2.0) {
+      stats->inactive_since = inactive_since;
+    }
+  }
+
+  if (si[NL80211_STA_INFO_RX_BITRATE]) {
+    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 = stats->rx_sample_index + 1;
+
+    if (n >= MAX_SAMPLE_INDEX) n = 0;
+
+    stats->rx_bitrate = GetBitrate(si[NL80211_STA_INFO_RX_BITRATE]);
+    GetMCS(si[NL80211_STA_INFO_RX_BITRATE], &rx_ht_mcs, &rx_vht_mcs,
+        &rx_width, &rx_short_gi, &rx_vht_nss);
+
+    stats->rx_ht_mcs_samples[n] = rx_ht_mcs;
+    if (rx_ht_mcs > stats->rx_max_ht_mcs) stats->rx_max_ht_mcs = rx_ht_mcs;
+
+    ht_nss = HtMcsToNss(rx_ht_mcs);
+    stats->rx_ht_nss_samples[n] = ht_nss;
+    if (ht_nss > stats->rx_max_ht_nss) stats->rx_max_ht_nss = ht_nss;
+
+    stats->rx_vht_mcs_samples[n] = rx_vht_mcs;
+    if (rx_vht_mcs > stats->rx_max_vht_mcs) stats->rx_max_vht_mcs = rx_vht_mcs;
+
+    stats->rx_vht_nss_samples[n] = rx_vht_nss;
+    if (rx_vht_nss > stats->rx_max_vht_nss) stats->rx_max_vht_nss = rx_vht_nss;
+
+    stats->rx_short_gi_samples[n] = rx_short_gi;
+    if (rx_short_gi) stats->ever_rx_short_gi = 1;
+
+    stats->rx_width_samples[n] = rx_width;
+    if (rx_width > stats->rx_max_width) stats->rx_max_width = rx_width;
+
+    if (si[NL80211_STA_INFO_SIGNAL]) {
+      int8_t signal = (int8_t)nla_get_u8(si[NL80211_STA_INFO_SIGNAL]);
+      stats->signal_samples[n] = signal;
+    }
+
+    stats->rx_sample_index = n;
+  }
+  if (si[NL80211_STA_INFO_RX_BYTES]) {
+    uint32_t last_rx_bytes = stats->rx_bytes;
+    stats->rx_bytes = nla_get_u32(si[NL80211_STA_INFO_RX_BYTES]);
+    stats->rx_bytes64 += (stats->rx_bytes - last_rx_bytes);
+  }
+  if (si[NL80211_STA_INFO_RX_PACKETS]) {
+    uint32_t last_rx_packets = stats->rx_packets;
+    stats->rx_packets = nla_get_u32(si[NL80211_STA_INFO_RX_PACKETS]);
+    stats->rx_packets64 += (stats->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 = stats->tx_sample_index + 1;
+
+    if (n >= MAX_SAMPLE_INDEX) n = 0;
+
+    stats->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);
+
+    stats->tx_ht_mcs_samples[n] = tx_ht_mcs;
+    if (tx_ht_mcs > stats->tx_max_ht_mcs) stats->tx_max_ht_mcs = tx_ht_mcs;
+
+    ht_nss = HtMcsToNss(tx_ht_mcs);
+    stats->tx_ht_nss_samples[n] = ht_nss;
+    if (ht_nss > stats->tx_max_ht_nss) stats->tx_max_ht_nss = ht_nss;
+
+    stats->tx_vht_mcs_samples[n] = tx_vht_mcs;
+    if (tx_vht_mcs > stats->tx_max_vht_mcs) stats->tx_max_vht_mcs = tx_vht_mcs;
+
+    stats->tx_vht_nss_samples[n] = tx_vht_nss;
+    if (tx_vht_nss > stats->tx_max_vht_nss) stats->tx_max_vht_nss = tx_vht_nss;
+
+    stats->tx_short_gi_samples[n] = tx_short_gi;
+    if (tx_short_gi) stats->ever_tx_short_gi = 1;
+
+    stats->tx_width_samples[n] = tx_width;
+    if (tx_width > stats->tx_max_width) stats->tx_max_width = tx_width;
+
+    stats->tx_sample_index = n;
+  }
+  if (si[NL80211_STA_INFO_TX_BYTES]) {
+    uint32_t last_tx_bytes = stats->tx_bytes;
+    stats->tx_bytes = nla_get_u32(si[NL80211_STA_INFO_TX_BYTES]);
+    stats->tx_bytes64 += (stats->tx_bytes - last_tx_bytes);
+  }
+  if (si[NL80211_STA_INFO_TX_PACKETS]) {
+    uint32_t last_tx_packets = stats->tx_packets;
+    stats->tx_packets = nla_get_u32(si[NL80211_STA_INFO_TX_PACKETS]);
+    stats->tx_packets64 += (stats->tx_packets - last_tx_packets);
+  }
+  if (si[NL80211_STA_INFO_TX_RETRIES]) {
+    uint32_t last_tx_retries = stats->tx_retries;
+    stats->tx_retries = nla_get_u32(si[NL80211_STA_INFO_TX_RETRIES]);
+    stats->tx_retries64 += (stats->tx_retries - last_tx_retries);
+  }
+  if (si[NL80211_STA_INFO_TX_FAILED]) {
+    uint32_t last_tx_failed = stats->tx_failed;
+    stats->tx_failed = nla_get_u32(si[NL80211_STA_INFO_TX_FAILED]);
+    stats->tx_failed64 += (stats->tx_failed - last_tx_failed);
+  }
+  if (si[NL80211_STA_INFO_CONNECTED_TIME]) {
+    stats->connected_secs = nla_get_u32(si[NL80211_STA_INFO_CONNECTED_TIME]);
+  }
+  if (si[NL80211_STA_INFO_SIGNAL]) {
+    stats->signal = (int8_t)nla_get_u8(si[NL80211_STA_INFO_SIGNAL]);
+  }
+  if (si[NL80211_STA_INFO_SIGNAL_AVG]) {
+    stats->signal_avg = (int8_t)nla_get_u8(si[NL80211_STA_INFO_SIGNAL_AVG]);
+  }
+
+  if (si[NL80211_STA_INFO_STA_FLAGS]) {
+    struct nl80211_sta_flag_update *sta_flags;
+    sta_flags = (struct nl80211_sta_flag_update *)nla_data(
+        si[NL80211_STA_INFO_STA_FLAGS]);
+
+    #define BIT(x) ((sta_flags->mask & (1ULL<<(x))) ? 1 : 0)
+    stats->authorized = BIT(NL80211_STA_FLAG_AUTHORIZED);
+    stats->authenticated = BIT(NL80211_STA_FLAG_AUTHENTICATED);
+    stats->preamble = BIT(NL80211_STA_FLAG_SHORT_PREAMBLE);
+    stats->wmm_wme = BIT(NL80211_STA_FLAG_WME);
+    stats->mfp = BIT(NL80211_STA_FLAG_MFP);
+    stats->tdls_peer = BIT(NL80211_STA_FLAG_TDLS_PEER);
+    stats->preamble_length = BIT(NL80211_STA_FLAG_SHORT_PREAMBLE);
+    #undef BIT
+  }
+
+#ifdef NL80211_RECENT_FIELDS
+  if (si[NL80211_STA_INFO_RX_DROP_MISC]) {
+    stats->rx_drop64 = nla_get_u64(si[NL80211_STA_INFO_RX_DROP_MISC]);
+  }
+  if (si[NL80211_STA_INFO_EXPECTED_THROUGHPUT]) {
+    stats->expected_mbps =
+      nla_get_u32(si[NL80211_STA_INFO_EXPECTED_THROUGHPUT]);
+  }
+#endif
+
+  return NL_OK;
+}  /* ParseWifiStats */
+
+
+static int InterfaceListCallback(struct nl_msg *msg, void *arg)
+{
+  struct nlattr *il[NL80211_ATTR_MAX + 1];
+  struct genlmsghdr *gh = (struct genlmsghdr *)nlmsg_data(nlmsg_hdr(msg));
+  wifi_interface_t *wif = &interfaces[ninterfaces];
+  const char *name;
+
+  nla_parse(il, NL80211_ATTR_MAX, genlmsg_attrdata(gh, 0),
+      genlmsg_attrlen(gh, 0), NULL);
+
+  if (!il[NL80211_ATTR_IFNAME]) {
+    return NL_SKIP;
+  }
+
+  name = nla_get_string(il[NL80211_ATTR_IFNAME]);
+  snprintf(wif->ifname, sizeof(wif->ifname), "%s", name);
+  wif->ifindex = GetIfIndex(name);
+  ninterfaces++;
+
+  if (il[NL80211_ATTR_STA_INFO]) {
+    ParseWifiStats(il[NL80211_ATTR_STA_INFO], &wif->s);
+  }
+
+  return NL_OK;
+}
+
+
+static int BssInfoCallback(struct nl_msg *msg, void *arg)
+{
+  struct nlattr *nl[NL80211_ATTR_MAX + 1];
+  struct nlattr *bi[NL80211_BSS_MAX + 1];
+  struct genlmsghdr *gh = (struct genlmsghdr *)nlmsg_data(nlmsg_hdr(msg));
+  wifi_interface_t *wif = NULL;
+  uint32_t ifindex;
+  int i;
+  static struct nla_policy bss_policy[NL80211_BSS_MAX + 1];
+
+  memset(&bss_policy, 0, sizeof(bss_policy));
+  bss_policy[NL80211_BSS_TSF].type = NLA_U64;
+  bss_policy[NL80211_BSS_FREQUENCY].type = NLA_U32;
+  bss_policy[NL80211_BSS_BSSID].type = NLA_UNSPEC;
+  bss_policy[NL80211_BSS_BEACON_INTERVAL].type = NLA_U16;
+  bss_policy[NL80211_BSS_CAPABILITY].type = NLA_U16;
+  bss_policy[NL80211_BSS_INFORMATION_ELEMENTS].type = NLA_UNSPEC;
+  bss_policy[NL80211_BSS_SIGNAL_MBM].type = NLA_U32;
+  bss_policy[NL80211_BSS_SIGNAL_UNSPEC].type = NLA_U8;
+  bss_policy[NL80211_BSS_STATUS].type = NLA_U32;
+
+  nla_parse(nl, NL80211_ATTR_MAX, genlmsg_attrdata(gh, 0),
+      genlmsg_attrlen(gh, 0), NULL);
+
+  if (!nl[NL80211_ATTR_IFINDEX]) {
+    return NL_SKIP;
+  }
+
+  ifindex = nla_get_u32(nl[NL80211_ATTR_IFINDEX]);
+  for (i = 0; i < ninterfaces; ++i) {
+    if (interfaces[i].ifindex == (int)ifindex) {
+      wif = &interfaces[i];
+      break;
+    }
+  }
+  if (wif == NULL) {
+    return NL_SKIP;
+  }
+
+  if (nla_parse_nested(bi, NL80211_BSS_MAX, nl[NL80211_ATTR_BSS],
+        bss_policy)) {
+    return NL_SKIP;
+  }
+
+  if (!bi[NL80211_BSS_BSSID] || !bi[NL80211_BSS_STATUS]) {
+    return NL_SKIP;
+  }
+
+  wif->is_client = 0;
+  if (bi[NL80211_BSS_STATUS]) {
+    uint32_t status = nla_get_u32(bi[NL80211_BSS_STATUS]);
+    wif->is_client = (status == NL80211_BSS_STATUS_ASSOCIATED) ? 1 : 0;
+  }
+  if (bi[NL80211_BSS_BSSID]) {
+    memcpy(wif->bssid, nla_data(bi[NL80211_BSS_BSSID]), sizeof(wif->bssid));
+  }
+  if (bi[NL80211_BSS_FREQUENCY]) {
+    wif->freq = nla_get_u32(bi[NL80211_BSS_FREQUENCY]);
+  }
+
+  return NL_OK;
+}
+
+
+static int InterfaceInfoCallback(struct nl_msg *msg, void *arg)
+{
+  struct nlattr *si[NL80211_ATTR_MAX + 1];
+  struct genlmsghdr *gh = (struct genlmsghdr *)nlmsg_data(nlmsg_hdr(msg));
+  wifi_interface_t *wif = NULL;
+  uint32_t ifindex;
+  int i;
+
+  nla_parse(si, NL80211_ATTR_MAX, genlmsg_attrdata(gh, 0),
+      genlmsg_attrlen(gh, 0), NULL);
+
+  if (!si[NL80211_ATTR_IFINDEX]) {
+    return NL_SKIP;
+  }
+
+  ifindex = nla_get_u32(si[NL80211_ATTR_IFINDEX]);
+  for (i = 0; i < ninterfaces; ++i) {
+    if (interfaces[i].ifindex == (int)ifindex) {
+      wif = &interfaces[i];
+      break;
+    }
+  }
+
+  if (wif == NULL) {
+    return NL_SKIP;
+  }
+
+  if (si[NL80211_ATTR_STA_INFO]) {
+    ParseWifiStats(si[NL80211_ATTR_STA_INFO], &wif->s);
+  }
+
+  return NL_OK;
+}
+
+
+static void HandleNLCommand(struct nl_sock *nlsk, int nl80211_id,
+                            int n, const uint8_t *bssid,
+                            int cb(struct nl_msg *, void *),
+                            int cmd, int flag)
+{
+  struct nl_msg *msg;
+  int ifindex = n >= 0 ? interfaces[n].ifindex : -1;
+  const char *ifname = n >= 0 ? interfaces[n].ifname : NULL;
+
+  if (nl_socket_modify_cb(nlsk, NL_CB_VALID, NL_CB_CUSTOM,
+                          cb, (void *)ifname)) {
+    fprintf(stderr, "nl_socket_modify_cb failed\n");
+    exit(1);
+  }
+
+  if ((msg = nlmsg_alloc()) == NULL) {
+    fprintf(stderr, "nlmsg_alloc failed\n");
+    exit(1);
+  }
+  if (genlmsg_put(msg, 0, 0, nl80211_id, 0, flag,
+                  cmd, 0) == NULL) {
+    fprintf(stderr, "genlmsg_put failed\n");
+    exit(1);
+  }
+
+  if (ifindex >= 0 && nla_put_u32(msg, NL80211_ATTR_IFINDEX, ifindex)) {
+    fprintf(stderr, "NL80211_CMD_GET_STATION put IFINDEX failed\n");
+    exit(1);
+  }
+
+  if (bssid && nla_put(msg, NL80211_ATTR_MAC, ETH_ALEN, bssid)) {
+    fprintf(stderr, "NL80211_CMD_GET_STATION put MAC_ADDR failed\n");
+    exit(1);
+  }
+
+  if (nl_send_auto(nlsk, msg) < 0) {
+    fprintf(stderr, "nl_send_auto failed\n");
+    exit(1);
+  }
+  nlmsg_free(msg);
+}
+
+
+void RequestInterfaceList(struct nl_sock *nlsk, int nl80211_id)
+{
+  HandleNLCommand(nlsk, nl80211_id, -1, NULL, InterfaceListCallback,
+                  NL80211_CMD_GET_INTERFACE, NLM_F_DUMP);
+}
+
+
+void RequestInterfaceInfo(struct nl_sock *nlsk, int nl80211_id, int n)
+{
+  int done = 0;
+  wifi_interface_t *wif = &interfaces[n];
+
+  HandleNLCommand(nlsk, nl80211_id, n, NULL, BssInfoCallback,
+                  NL80211_CMD_GET_SCAN, NLM_F_DUMP);
+  ProcessNetlinkMessages(nlsk, &done);
+
+  if (wif->is_client) {
+    done = 0;
+    HandleNLCommand(nlsk, nl80211_id, n, wif->bssid, InterfaceInfoCallback,
+                    NL80211_CMD_GET_STATION, 0);
+    ProcessNetlinkMessages(nlsk, &done);
+  }
+}
+
+
+int NlFinish(struct nl_msg *msg, void *arg)
+{
+  int *ret = (int *)arg;
+  *ret = 1;
+  return NL_OK;
+}
+
+
+struct nl_sock *InitNetlinkSocket()
+{
+  struct nl_sock *nlsk;
+  if ((nlsk = nl_socket_alloc()) == NULL) {
+    fprintf(stderr, "socket allocation failed\n");
+    exit(1);
+  }
+
+  if (genl_connect(nlsk) != 0) {
+    fprintf(stderr, "genl_connect failed\n");
+    exit(1);
+  }
+
+  if (nl_socket_set_nonblocking(nlsk)) {
+    fprintf(stderr, "nl_socket_set_nonblocking failed\n");
+    exit(1);
+  }
+
+  return nlsk;
+}  /* InitNetlinkSocket */
+
+
+
+static client_state_t *FindClientState(const uint8_t mac[6])
+{
+  ClientMapType::iterator it;
+  client_state_t *s;
+  char macstr[MAC_STR_LEN];
+
+  snprintf(macstr, sizeof(macstr), "%02x:%02x:%02x:%02x:%02x:%02x",
+      mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
+
+  /* Find any existing state for this STA, or allocate new. */
+  if ((it = clients.find(macstr)) == clients.end()) {
+    s = (client_state_t *)malloc(sizeof(*s));
+    memset(s, 0, sizeof(*s));
+    memcpy(s->macstr, macstr, sizeof(s->macstr));
+    s->first_seen = monotime();
+    clients[std::string(macstr)] = s;
+  } else {
+    s = it->second;
+  }
+
+  return s;
+}
+
+
+static int StationDumpCallback(struct nl_msg *msg, void *arg)
+{
+  const char *ifname = (const char *)arg;
+  struct genlmsghdr *gh = (struct genlmsghdr *)nlmsg_data(nlmsg_hdr(msg));
+  struct nlattr *tb[NL80211_ATTR_MAX + 1] = {0};
+  uint8_t *mac;
+  client_state_t *state;
+
+  if (nla_parse(tb, NL80211_ATTR_MAX,
+                genlmsg_attrdata(gh, 0), genlmsg_attrlen(gh, 0), NULL)) {
+    fprintf(stderr, "nla_parse failed.\n");
+    return NL_SKIP;
+  }
+
+  if (!tb[NL80211_ATTR_STA_INFO]) {
+    return NL_SKIP;
+  }
+
+  if (!tb[NL80211_ATTR_MAC]) {
+    fprintf(stderr, "No NL80211_ATTR_MAC\n");
+    return NL_SKIP;
+  }
+
+  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);
+
+  if (ParseWifiStats(tb[NL80211_ATTR_STA_INFO], &state->s) != NL_OK) {
+    return NL_SKIP;
+  }
+
+  return NL_OK;
+}  /* StationDumpCallback */
+
+
+void RequestAssociatedDevices(struct nl_sock *nlsk,
+    int nl80211_id, int n)
+{
+  HandleNLCommand(nlsk, nl80211_id, n, NULL, StationDumpCallback,
+                  NL80211_CMD_GET_STATION, NLM_F_DUMP);
+}  /* RequestAssociatedDevices */
+
+
+static void ClearClientCounters(client_state_t *state)
+{
+  /* Kernel cleared its counters when client re-joined the WLAN,
+   * clear out previous state as well. */
+  state->s.rx_bytes = 0;
+  state->s.rx_packets = 0;
+  state->s.tx_bytes = 0;
+  state->s.tx_packets = 0;
+  state->s.tx_retries = 0;
+  state->s.tx_failed = 0;
+}
+
+
+static int AgeOutClient(client_state_t *state)
+{
+  time_t mono_now = monotime();
+
+  if ((mono_now - state->last_seen) > MAX_CLIENT_AGE_SECS) {
+    char filename[PATH_MAX];
+    snprintf(filename, sizeof(filename), "%s/%s", STATIONS_DIR, state->macstr);
+    unlink(filename);
+    return 1;
+  }
+
+  if (state->s.connected_secs < 60) {
+    /* If the client recently dropped off and came back, clear any counters
+     * we've been maintaining. */
+    ClearClientCounters(state);
+  }
+
+  return 0;
+}
+
+
+static void ConsolidateSamples(wifi_stats_t *stats)
+{
+  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, 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;
+  double max_signal = -1000.0;
+  double min_signal = 0.0;
+  double sum_signal = 0.0;
+
+  for (i = 0; i < MAX_SAMPLE_INDEX; ++i) {
+    if (stats->rx_ht_mcs_samples[i] > rx_ht_mcs) {
+      rx_ht_mcs = stats->rx_ht_mcs_samples[i];
+    }
+    if (stats->rx_vht_mcs_samples[i] > rx_vht_mcs) {
+      rx_vht_mcs = stats->rx_vht_mcs_samples[i];
+    }
+    if (stats->rx_width_samples[i] > rx_width) {
+      rx_width = stats->rx_width_samples[i];
+    }
+    if (stats->rx_ht_nss_samples[i] > rx_ht_nss) {
+      rx_ht_nss = stats->rx_ht_nss_samples[i];
+    }
+    if (stats->rx_vht_nss_samples[i] > rx_vht_nss) {
+      rx_vht_nss = stats->rx_vht_nss_samples[i];
+    }
+    if (stats->rx_short_gi_samples[i] > rx_short_gi) {
+      rx_short_gi = stats->rx_short_gi_samples[i];
+    }
+
+    if (stats->signal_samples[i] > max_signal) {
+      max_signal = stats->signal_samples[i];
+    }
+    if (stats->signal_samples[i] < min_signal) {
+      min_signal = stats->signal_samples[i];
+    }
+    sum_signal += stats->signal_samples[i];
+
+    if (stats->tx_ht_mcs_samples[i] > tx_ht_mcs) {
+      tx_ht_mcs = stats->tx_ht_mcs_samples[i];
+    }
+    if (stats->tx_vht_mcs_samples[i] > tx_vht_mcs) {
+      tx_vht_mcs = stats->tx_vht_mcs_samples[i];
+    }
+    if (stats->tx_width_samples[i] > tx_width) {
+      tx_width = stats->tx_width_samples[i];
+    }
+    if (stats->tx_ht_nss_samples[i] > tx_ht_nss) {
+      tx_ht_nss = stats->tx_ht_nss_samples[i];
+    }
+    if (stats->tx_vht_nss_samples[i] > tx_vht_nss) {
+      tx_vht_nss = stats->tx_vht_nss_samples[i];
+    }
+    if (stats->tx_short_gi_samples[i] > tx_short_gi) {
+      tx_short_gi = stats->tx_short_gi_samples[i];
+    }
+  }
+
+  stats->rx_ht_mcs = rx_ht_mcs;
+  stats->rx_vht_mcs = rx_vht_mcs;
+  stats->rx_width = rx_width;
+  stats->rx_ht_nss = rx_ht_nss;
+  stats->rx_vht_nss = rx_vht_nss;
+  stats->rx_short_gi = rx_short_gi;
+
+  stats->max_signal = max_signal;
+  stats->min_signal = min_signal;
+  stats->avg_signal = sum_signal / (double)MAX_SAMPLE_INDEX;
+
+  stats->tx_ht_mcs = tx_ht_mcs;
+  stats->tx_vht_mcs = tx_vht_mcs;
+  stats->tx_width = tx_width;
+  stats->tx_ht_nss = tx_ht_nss;
+  stats->tx_vht_nss = tx_vht_nss;
+  stats->tx_short_gi = tx_short_gi;
+}
+
+
+static void ClientStateToJson(const client_state_t *state)
+{
+  char tmpfile[PATH_MAX];
+  char filename[PATH_MAX];
+  time_t mono_now = monotime();
+  FILE *f;
+
+  snprintf(tmpfile, sizeof(tmpfile), "%s/%s.new", STATIONS_DIR, state->macstr);
+  snprintf(filename, sizeof(filename), "%s/%s", STATIONS_DIR, state->macstr);
+
+  if ((f = fopen(tmpfile, "w+")) == NULL) {
+    char errbuf[80];
+    snprintf(errbuf, sizeof(errbuf), "fopen %s", tmpfile);
+    perror(errbuf);
+    return;
+  }
+
+  fprintf(f, "{\n");
+
+  fprintf(f, "  \"addr\": \"%s\",\n", state->macstr);
+  fprintf(f, "  \"inactive since\": %.3f,\n", state->s.inactive_since);
+  fprintf(f, "  \"inactive msec\": %u,\n", state->s.inactive_msec);
+
+  fprintf(f, "  \"active\": %s,\n",
+      ((mono_now - state->last_seen) < 600) ? "true" : "false");
+
+  fprintf(f, "  \"rx bitrate\": %u.%u,\n",
+      (state->s.rx_bitrate / 10), (state->s.rx_bitrate % 10));
+  fprintf(f, "  \"rx bytes\": %u,\n", state->s.rx_bytes);
+  fprintf(f, "  \"rx packets\": %u,\n", state->s.rx_packets);
+
+  fprintf(f, "  \"tx bitrate\": %u.%u,\n",
+      (state->s.tx_bitrate / 10), (state->s.tx_bitrate % 10));
+  fprintf(f, "  \"tx bytes\": %u,\n", state->s.tx_bytes);
+  fprintf(f, "  \"tx packets\": %u,\n", state->s.tx_packets);
+  fprintf(f, "  \"tx retries\": %u,\n", state->s.tx_retries);
+  fprintf(f, "  \"tx failed\": %u,\n", state->s.tx_failed);
+
+  fprintf(f, "  \"rx mcs\": %u,\n", state->s.rx_ht_mcs);
+  fprintf(f, "  \"rx max mcs\": %u,\n", state->s.rx_max_ht_mcs);
+  fprintf(f, "  \"rx vht mcs\": %u,\n", state->s.rx_vht_mcs);
+  fprintf(f, "  \"rx max vht mcs\": %u,\n", state->s.rx_max_vht_mcs);
+  fprintf(f, "  \"rx width\": %u,\n", state->s.rx_width);
+  fprintf(f, "  \"rx max width\": %u,\n", state->s.rx_max_width);
+  fprintf(f, "  \"rx ht_nss\": %u,\n", state->s.rx_ht_nss);
+  fprintf(f, "  \"rx max ht_nss\": %u,\n", state->s.rx_max_ht_nss);
+  fprintf(f, "  \"rx vht_nss\": %u,\n", state->s.rx_vht_nss);
+  fprintf(f, "  \"rx max vht_nss\": %u,\n", state->s.rx_max_vht_nss);
+
+  #define BOOL(x) (x ? "true" : "false")
+  fprintf(f, "  \"rx SHORT_GI\": %s,\n", BOOL(state->s.rx_short_gi));
+  fprintf(f, "  \"rx SHORT_GI seen\": %s,\n", BOOL(state->s.ever_rx_short_gi));
+  #undef BOOL
+
+  fprintf(f, "  \"signal\": %hhd,\n", state->s.signal);
+  fprintf(f, "  \"signal_avg\": %hhd,\n", state->s.signal_avg);
+
+  #define BOOL(x) (x ? "yes" : "no")
+  fprintf(f, "  \"authorized\": \"%s\",\n", BOOL(state->s.authorized));
+  fprintf(f, "  \"authenticated\": \"%s\",\n", BOOL(state->s.authenticated));
+  fprintf(f, "  \"preamble\": \"%s\",\n", BOOL(state->s.preamble));
+  fprintf(f, "  \"wmm_wme\": \"%s\",\n", BOOL(state->s.wmm_wme));
+  fprintf(f, "  \"mfp\": \"%s\",\n", BOOL(state->s.mfp));
+  fprintf(f, "  \"tdls_peer\": \"%s\",\n", BOOL(state->s.tdls_peer));
+  #undef BOOL
+
+  fprintf(f, "  \"preamble length\": \"%s\",\n",
+      (state->s.preamble_length ? "short" : "long"));
+
+  fprintf(f, "  \"rx bytes64\": %" PRIu64 ",\n", state->s.rx_bytes64);
+  fprintf(f, "  \"rx drop64\": %" PRIu64 ",\n", state->s.rx_drop64);
+  fprintf(f, "  \"tx bytes64\": %" PRIu64 ",\n", state->s.tx_bytes64);
+  fprintf(f, "  \"tx retries64\": %" PRIu64 ",\n", state->s.tx_retries64);
+  fprintf(f, "  \"expected Mbps\": %u.%03u,\n",
+          (state->s.expected_mbps / 1000), (state->s.expected_mbps % 1000));
+
+  fprintf(f, "  \"ifname\": \"%s\"\n", state->ifname);
+  fprintf(f, "}\n");
+
+  fclose(f);
+  if (rename(tmpfile, filename)) {
+    char errstr[160];
+    snprintf(errstr, sizeof(errstr), "%s: rename %s to %s",
+        __FUNCTION__, tmpfile, filename);
+    perror(errstr);
+  }
+}
+
+
+static void ClientStateToLog(const client_state_t *state, time_t mono_now)
+{
+  if (!state->s.authorized || !state->s.authenticated) {
+    /* Don't log about non-associated clients */
+    return;
+  }
+
+  if ((mono_now - state->first_seen) < 120) {
+    /* Allow data to accumulate before beginning to log it. */
+    return;
+  }
+
+  printf(
+      "%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"
+      " rssi:%0.2f/%0.2f/%0.2f"
+      "\n",
+      state->macstr, state->ifname,
+      ((mono_now - state->last_seen) + (state->s.inactive_msec / 1000)),
+
+      /* L2 traffic stats */
+      state->s.rx_bytes64, state->s.rx_drop64, state->s.tx_bytes64,
+      state->s.tx_retries64, state->s.tx_failed64,
+
+      /* L1 information */
+      (state->s.preamble_length ? 'S' : 'L'),
+      state->s.signal, state->s.signal_avg,
+      state->s.rx_ht_mcs, state->s.rx_ht_nss,
+      state->s.rx_vht_mcs, state->s.rx_vht_nss,
+      state->s.rx_width, state->s.rx_short_gi,
+
+      /* information about the maximum we've ever seen from this client. */
+      state->s.rx_max_ht_mcs, state->s.rx_max_ht_nss,
+      state->s.rx_max_vht_mcs, state->s.rx_max_vht_nss,
+      state->s.rx_max_width, state->s.ever_rx_short_gi,
+
+      state->s.tx_ht_mcs, state->s.tx_ht_nss,
+      state->s.tx_vht_mcs, state->s.tx_vht_nss,
+      state->s.tx_width, state->s.tx_short_gi,
+
+      /* information about the maximum we've ever seen from this client. */
+      state->s.tx_max_ht_mcs, state->s.tx_max_ht_nss,
+      state->s.tx_max_vht_mcs, state->s.tx_max_vht_nss,
+      state->s.tx_max_width, state->s.ever_tx_short_gi,
+
+      state->s.min_signal, state->s.avg_signal, state->s.max_signal);
+}
+
+
+void ConsolidateAssociatedDevices()
+{
+  ClientMapType::iterator it = clients.begin();
+
+  while (it != clients.end()) {
+    client_state_t *state = it->second;
+    ConsolidateSamples(&state->s);
+    if (AgeOutClient(state)) {
+      clients.erase(it++);
+    } else {
+      ++it;
+    }
+  }
+}
+
+
+/* Walk through all Wifi clients, printing their info to JSON files. */
+void UpdateAssociatedDevices()
+{
+  ClientMapType::iterator it = clients.begin();
+
+  while (it != clients.end()) {
+    client_state_t *state = it->second;
+    ClientStateToJson(state);
+    ++it;
+  }
+}
+
+
+void LogAssociatedDevices()
+{
+  ClientMapType::iterator it = clients.begin();
+  time_t mono_now = monotime();
+
+  while (it != clients.end()) {
+    client_state_t *state = it->second;
+    ClientStateToLog(state, mono_now);
+    ++it;
+  }
+}
+
+
+void LogInterfaces()
+{
+  int i;
+  for (i = 0; i < ninterfaces; ++i) {
+    wifi_interface_t *wif = &interfaces[i];
+
+    if (!wif->is_client) {
+      continue;
+    }
+
+    ConsolidateSamples(&wif->s);
+
+    printf(
+        "C %s %d %" 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"
+        " %0.2f %0.2f %0.2f"
+        "\n",
+        wif->ifname, wif->freq,
+
+        /* L2 traffic stats */
+        wif->s.rx_bytes64, wif->s.rx_drop64, wif->s.tx_bytes64,
+        wif->s.tx_retries64, wif->s.tx_failed64,
+
+        /* L1 information */
+        (wif->s.preamble_length ? 'S' : 'L'),
+        wif->s.signal, wif->s.signal_avg,
+        wif->s.rx_ht_mcs, wif->s.rx_ht_nss,
+        wif->s.rx_vht_mcs, wif->s.rx_vht_nss,
+        wif->s.rx_width, wif->s.rx_short_gi,
+
+        /* information about the maximum we've ever received. */
+        wif->s.rx_max_ht_mcs, wif->s.rx_max_ht_nss,
+        wif->s.rx_max_vht_mcs, wif->s.rx_max_vht_nss,
+        wif->s.rx_max_width, wif->s.ever_rx_short_gi,
+
+        wif->s.tx_ht_mcs, wif->s.tx_ht_nss,
+        wif->s.tx_vht_mcs, wif->s.tx_vht_nss,
+        wif->s.tx_width, wif->s.tx_short_gi,
+
+        /* information about the maximum we've ever achieved. */
+        wif->s.tx_max_ht_mcs, wif->s.tx_max_ht_nss,
+        wif->s.tx_max_vht_mcs, wif->s.tx_max_vht_nss,
+        wif->s.tx_max_width, wif->s.ever_tx_short_gi,
+        wif->s.min_signal, wif->s.avg_signal, wif->s.max_signal);
+  }
+}
+
+
+static int ieee80211_frequency_to_channel(int freq)
+{
+  /* see 802.11-2007 17.3.8.3.2 and Annex J */
+  if (freq == 2484)
+    return 14;
+  else if (freq < 2484)
+    return (freq - 2407) / 5;
+  else if (freq >= 4910 && freq <= 4980)
+    return (freq - 4000) / 5;
+  else if (freq <= 45000) /* DMG band lower limit */
+    return (freq - 5000) / 5;
+  else if (freq >= 58320 && freq <= 64800)
+    return (freq - 56160) / 2160;
+  else
+    return 0;
+}
+
+
+static void print_ssid_escaped(FILE *f, int len, const uint8_t *data)
+{
+  int i;
+
+  for (i = 0; i < len; 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;
+    }
+  }
+}
+
+
+static int WlanInfoCallback(struct nl_msg *msg, void *arg)
+{
+  struct genlmsghdr *gnlh = (struct genlmsghdr *)nlmsg_data(nlmsg_hdr(msg));
+  struct nlattr *tb_msg[NL80211_ATTR_MAX + 1];
+
+  nla_parse(tb_msg, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+            genlmsg_attrlen(gnlh, 0), NULL);
+
+  if (tb_msg[NL80211_ATTR_MAC]) {
+    unsigned char *mac_addr = (unsigned char *)nla_data(tb_msg[NL80211_ATTR_MAC]);
+    fprintf(wifi_info_handle,
+            "  \"BSSID\": \"%02x:%02x:%02x:%02x:%02x:%02x\",\n",
+            mac_addr[0], mac_addr[1], mac_addr[2],
+            mac_addr[3], mac_addr[4], mac_addr[5]);
+  }
+  if (tb_msg[NL80211_ATTR_SSID]) {
+    fprintf(wifi_info_handle, "  \"SSID\": \"");
+    print_ssid_escaped(wifi_info_handle, nla_len(tb_msg[NL80211_ATTR_SSID]),
+                       (const uint8_t *)nla_data(tb_msg[NL80211_ATTR_SSID]));
+    fprintf(wifi_info_handle, "\",\n");
+  }
+  if (tb_msg[NL80211_ATTR_WIPHY_FREQ]) {
+    uint32_t freq = nla_get_u32(tb_msg[NL80211_ATTR_WIPHY_FREQ]);
+    fprintf(wifi_info_handle, "  \"Channel\": %d,\n",
+            ieee80211_frequency_to_channel(freq));
+  }
+
+  return NL_SKIP;
+}
+
+
+void RequestWifiInfo(struct nl_sock *nlsk, int nl80211_id, int n)
+{
+  HandleNLCommand(nlsk, nl80211_id, n, NULL, WlanInfoCallback,
+                  NL80211_CMD_GET_INTERFACE, 0);
+}
+
+
+static int RegdomainCallback(struct nl_msg *msg, void *arg)
+{
+  struct nlattr *tb_msg[NL80211_ATTR_MAX + 1];
+  struct genlmsghdr *gnlh = (struct genlmsghdr *)nlmsg_data(nlmsg_hdr(msg));
+  char *reg;
+
+  nla_parse(tb_msg, NL80211_ATTR_MAX, genlmsg_attrdata(gnlh, 0),
+            genlmsg_attrlen(gnlh, 0), NULL);
+
+  if (!tb_msg[NL80211_ATTR_REG_ALPHA2]) {
+    return NL_SKIP;
+  }
+
+  if (!tb_msg[NL80211_ATTR_REG_RULES]) {
+    return NL_SKIP;
+  }
+
+  reg = (char *)nla_data(tb_msg[NL80211_ATTR_REG_ALPHA2]);
+  fprintf(wifi_info_handle, "  \"RegDomain\": \"%c%c\",\n", reg[0], reg[1]);
+
+  return NL_SKIP;
+}
+
+
+void RequestRegdomain(struct nl_sock *nlsk, int nl80211_id)
+{
+  HandleNLCommand(nlsk, nl80211_id, -1, NULL, RegdomainCallback,
+                  NL80211_CMD_GET_REG, 0);
+}
+
+
+void UpdateWifiShow(struct nl_sock *nlsk, int nl80211_id, int n)
+{
+  char tmpfile[PATH_MAX];
+  char filename[PATH_MAX];
+  char autofile[PATH_MAX];
+  const char *ifname = interfaces[n].ifname;
+  int done = 0;
+  struct stat buffer;
+  FILE *fptr;
+
+  if (!ifname || !ifname[0]) {
+    return;
+  }
+
+  snprintf(tmpfile, sizeof(tmpfile), "%s/%s.new", WIFIINFO_DIR, ifname);
+  snprintf(filename, sizeof(filename), "%s/%s", WIFIINFO_DIR, ifname);
+
+  if ((wifi_info_handle = fopen(tmpfile, "w+")) == NULL) {
+    perror("fopen");
+    return;
+  }
+
+  fprintf(wifi_info_handle, "{\n");
+  done = 0;
+  RequestWifiInfo(nlsk, nl80211_id, n);
+  ProcessNetlinkMessages(nlsk, &done);
+
+  done = 0;
+  RequestRegdomain(nlsk, nl80211_id);
+  ProcessNetlinkMessages(nlsk, &done);
+
+  snprintf(autofile, sizeof(autofile), "/tmp/autochan.%s", ifname);
+  if (stat(autofile, &buffer) == 0) {
+    fprintf(wifi_info_handle, "  \"AutoChannel\": true,\n");
+  } else {
+    fprintf(wifi_info_handle, "  \"AutoChannel\": false,\n");
+  }
+  snprintf(autofile, sizeof(autofile), "/tmp/autotype.%s", ifname);
+  if ((fptr = fopen(autofile, "r")) == NULL) {
+    fprintf(wifi_info_handle, "  \"AutoType\": \"LOW\"\n");
+  } else {
+    char buf[24];
+    if (fgets(buf, sizeof(buf), fptr) != NULL) {
+      fprintf(wifi_info_handle, "  \"AutoType\": \"%s\"\n", buf);
+    }
+    fclose(fptr);
+    fptr = NULL;
+  }
+  fprintf(wifi_info_handle, "}\n");
+
+  fclose(wifi_info_handle);
+  wifi_info_handle = NULL;
+  if (rename(tmpfile, filename)) {
+    char errbuf[256];
+    snprintf(errbuf, sizeof(errbuf), "%s: rename %s to %s : errno=%d",
+        __FUNCTION__, tmpfile, filename, errno);
+    perror(errbuf);
+  }
+}
+
+#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;
+  int nl80211_id = -1;
+  struct nl_sock *nlsk = NULL;
+  struct rlimit rlim;
+
+  memset(&rlim, 0, sizeof(rlim));
+  if (getrlimit(RLIMIT_AS, &rlim)) {
+    perror("getrlimit RLIMIT_AS failed");
+    exit(1);
+  }
+  rlim.rlim_cur = 6 * 1024 * 1024;
+  if (setrlimit(RLIMIT_AS, &rlim)) {
+    perror("getrlimit RLIMIT_AS failed");
+    exit(1);
+  }
+
+  setlinebuf(stdout);
+
+  nlsk = InitNetlinkSocket();
+  if (nl_socket_modify_cb(nlsk, NL_CB_FINISH, NL_CB_CUSTOM, NlFinish, &done)) {
+    fprintf(stderr, "nl_socket_modify_cb failed\n");
+    exit(1);
+  }
+  if ((nl80211_id = genl_ctrl_resolve(nlsk, "nl80211")) < 0) {
+    fprintf(stderr, "genl_ctrl_resolve failed\n");
+    exit(1);
+  }
+
+  while (1) {
+    int i, j;
+
+    /* Check if new interfaces have appeared */
+    ninterfaces = 0;
+    memset(interfaces, 0, sizeof(interfaces));
+    RequestInterfaceList(nlsk, nl80211_id);
+    ProcessNetlinkMessages(nlsk, &done);
+    for (i = 0; i < ninterfaces; ++i) {
+      UpdateWifiShow(nlsk, nl80211_id, i);
+    }
+
+    /* Accumulate MAX_SAMPLE_INDEX samples between calls to
+     * LogAssociatedDevices() */
+    for (i = 0; i < MAX_SAMPLE_INDEX; ++i) {
+      sleep(2);
+      for (j = 0; j < ninterfaces; ++j) {
+        done = 0;
+        RequestAssociatedDevices(nlsk, nl80211_id, j);
+        ProcessNetlinkMessages(nlsk, &done);
+        ConsolidateAssociatedDevices();
+        UpdateAssociatedDevices();
+
+        done = 0;
+        RequestInterfaceInfo(nlsk, nl80211_id, j);
+        ProcessNetlinkMessages(nlsk, &done);
+      }
+      TouchUpdateFile();
+    }
+    LogAssociatedDevices();
+    LogInterfaces();
+  }
+
+  exit(0);
+}
+#endif  /* UNIT_TESTS */
diff --git a/cmds/wifi_files_test.c b/cmds/wifi_files_test.cc
similarity index 89%
rename from cmds/wifi_files_test.c
rename to cmds/wifi_files_test.cc
index 902cd73..4880fad 100644
--- a/cmds/wifi_files_test.c
+++ b/cmds/wifi_files_test.cc
@@ -16,7 +16,7 @@
 const char *stations_dir;
 #define STATIONS_DIR stations_dir
 #define WIFIINFO_DIR STATIONS_DIR
-#include "wifi_files.c"
+#include "wifi_files.cc"
 
 
 int exit_code = 0;
@@ -80,7 +80,7 @@
 }
 
 
-static char *expected_json = "{\n"
+static const char *expected_json = "{\n"
 "  \"addr\": \"00:11:22:33:44:55\",\n"
 "  \"inactive since\": 0.000,\n"
 "  \"inactive msec\": 0,\n"
@@ -134,17 +134,17 @@
   TEST_ASSERT(ieee80211_frequency_to_channel(2484) == 14);
   memset(&state, 0, sizeof(client_state_t));
   snprintf(state.macstr, sizeof(state.macstr), "00:11:22:33:44:55");
-  state.rx_bytes64 = 1ULL;
-  state.rx_drop64 = 2ULL;
-  state.rx_bitrate = 47;
-  state.authorized = 0;
-  state.authenticated = 1;
-  state.preamble_length = 0;
-  state.expected_mbps = 7009;
-  ClientStateToJson((gpointer)(&state.macstr), (gpointer)&state, NULL);
+  state.s.rx_bytes64 = 1ULL;
+  state.s.rx_drop64 = 2ULL;
+  state.s.rx_bitrate = 47;
+  state.s.authorized = 0;
+  state.s.authenticated = 1;
+  state.s.preamble_length = 0;
+  state.s.expected_mbps = 7009;
+  ClientStateToJson(&state);
 
   #define SIZ 65536
-  TEST_ASSERT((buf = malloc(SIZ)) != NULL);
+  TEST_ASSERT((buf = (char *)malloc(SIZ)) != NULL);
   memset(buf, 0, SIZ);
   snprintf(filename, sizeof(filename), "%s/%s", STATIONS_DIR, state.macstr);
   TEST_ASSERT((fd = open(filename, O_RDONLY)) >= 0);
@@ -170,15 +170,15 @@
   mac[5] = 0x02;
   state = FindClientState(mac);
   state->last_seen = 10000;
-  TEST_ASSERT(g_hash_table_size(clients) == 2);
+  TEST_ASSERT(clients.size() == 2);
 
   now = 1000 + MAX_CLIENT_AGE_SECS + 1;
   ConsolidateAssociatedDevices();
-  TEST_ASSERT(g_hash_table_size(clients) == 1);
+  TEST_ASSERT(clients.size() == 1);
 
   now = 10000 + MAX_CLIENT_AGE_SECS + 1;
   ConsolidateAssociatedDevices();
-  TEST_ASSERT(g_hash_table_size(clients) == 0);
+  TEST_ASSERT(clients.size() == 0);
   printf("! %s:%d\t%s\tok\n", __FILE__, __LINE__, __FUNCTION__);
 }
 
@@ -189,7 +189,6 @@
 
   stations_dir = mkdtemp(strdup("/tmp/wifi_files_test_XXXXXX"));
   printf("stations_dir = %s\n", stations_dir);
-  clients = g_hash_table_new(g_str_hash, g_str_equal);
 
   testPrintSsidEscaped();
   testPrintSsidEscapedQuoteBackslash();