USB DSL Traffic Meter / Alarm Thingy

USB Bandwidth Alarm image
USB Bandwidth Alarm Prototype

Recently, something went rogue on our network and happily consumed over 3GB every hour, for 16 hours — over 85GB in less than a day. :-/ I was not impressed.

So, this afternoon I thought, “Wouldn’t it be nice to have some simple, visual / audible display or alarm that would alert me to problems like this, without having to remember to check on my traffic accounting system.” So I made one.

I soldered up this little circuit board today, based on a previous project for which I already had a number of blank PCBs handy.

The board contains an ATmega88p MCU and runs V-USB based firmware, which I cobbled together to control the simple LED display and alarm buzzer.

Finally, I wrote the host C program (employing libusb, based on a V-USB example project) on the Linux gateway server, along with a cron job to run an awk script to collect the traffic data and … well yes, it is, “nice to have” 😛

There are four LEDs [Green Yellow Yellow* Red ] (*yellow because I couldn’t find an orange one in the junk drawer – grr!) and one rather noisy buzzer thingy.

The LED’s (had they been the colours I wanted) represent previous-hour bandwidth usage of …

Green   — up to 500MB
Yellow  — up to 1GB
Orange  — up to 2GB
Red     — more than 2GB

These are updated every five minutes by our Ubuntu Linux gateway server, into which the interface board is plugged.

I chose the rather high value for the red/alarm condition, because we quite often rent Apple TV videos (movies), which are around a Gig’ a shot and often not much over an hour in viewing time. Plus we have three other home boarders using the ‘net, through the same gateway. Excess traffic over and above our 150GB/mth plan is actually only $0.50 a Gig’. So it should be fine. (With two boarders and myself home during the week most the time, we struggle to stay under 150GB a month, actually! Crazy.)

The alarm sounds whenever the red LED is on (pulsating on/off, so as not to kill my ears!) and can also be independently shut off from the host interface program.

The green LED will go off if no data update has been received over the USB wire for more than ten minutes. This lets me know that data updates aren’t happening and that I need to check the system. (The other lights remain as they were in this case — just because.)

Every five minutes, 10 seconds after traffic data is logged into rrdtool, a cron job on the gateway server updates the LED/alarm unit. Following is the awk script for that …

#!/usr/bin/awk -f
BEGIN {
  cmd = "rrdtool fetch traffic.rrd AVERAGE -e now -s end-1h";
  while ((cmd | getline) > 0) {
    if ($2 ~ /^[0-9.e+]+$/) {
      hourlyTraffic += ($2 * 300);
    }
  }
  close(cmd);

  oneGig = 1073741824;
  if (hourlyTraffic  > 2 * oneGig) level = 3; # 2.0GB
  else if (hourlyTraffic > oneGig) level = 2; # 1.0GB 
  else if (hourlyTraffic > oneGig / 2) level = 1; # 0.5GB
  else level = 0;

  printf "Traffic: %4.2f MB over last hour\n", hourlyTraffic / 1048576;
  printf "Level: %d\n", level;
  system("tMeterControl " level);

  exit;
}

I was already logging traffic into an RRD-Tool database, at 5-minute intervals. (That also gives me all the traffic graphing ability I need.) So, at the top of the awk script, I use rrdtool fetch to extract the last hour’s worth of samples (12 of them), filter that into just the valid inbound traffic values, multiply by 300 seconds (5 minutes — since rrdtool stores everything as per-second rates) and sum those up to arrive at the total bytes of ingress bandwidth for the hour.

The rest of the code is just about telling the LED/alarm unit what to do. The actual USB communication for that is handled by the C program tMeterControl …

 

/* Name: powerSwitch.c
 * Project: tMeterControl based on PowerSwitch based on AVR USB driver
 * Author: Gruvin
 * Original Author: Christian Starkjohann
 * Creation Date: 2005-01-16
 * Tabsize: 4
 * Copyright: (c) 2013 by Bryan J. Rentoul (aka Gruvin)
 * Copyright: (c) 2005 by OBJECTIVE DEVELOPMENT Software GmbH
 * License: GNU GPL v2 (see License.txt) or proprietary (CommercialLicense.txt)
 * This Revision: $Id$
 */

/*
General Description:
This program controls the PowerSwitch USB device from the command line.
It must be linked with libusb, a library for accessing the USB bus from
Linux, FreeBSD, Mac OS X and other Unix operating systems. Libusb can be
obtained from http://libusb.sourceforge.net/.
*/

#include 
#include 
#include 
#include 
#include     /* this is libusb, see http://libusb.sourceforge.net/ */

#define USBDEV_SHARED_VENDOR    0x16C0  /* VOTI */
#define USBDEV_SHARED_PRODUCT   0x05DC  /* Obdev's free shared PID */
/* Use obdev's generic shared VID/PID pair and follow the rules outlined
 * in firmware/usbdrv/USBID-License.txt.
 */

/* These are the vendor specific SETUP commands implemented by our USB device */
#define PSCMD_ECHO  0
#define PSCMD_GET   1
#define PSCMD_SET   2
#define PSCMD_ALARM 3
#define PSCMD_RES   4

static void 
usage(char *name)
{
    fprintf(stderr, "usage:\n");
    fprintf(stderr, "  %s test -- runs a communications an LED/alarm test.\n", name);
    fprintf(stderr, "  %s 0:1:2:3:4:5:6:7:8 [alarm] -- set LED level. Optionally turn on alarm.\n", name);
    fprintf(stderr, "  %s alarm on:off -- set audible alarm.\n", name);
}


static int
usbGetStringAscii(usb_dev_handle *dev, int index, int langid, char *buf, int buflen)
{
    char    buffer[256];
    int     rval, i;

    if ((rval = usb_control_msg(dev, USB_ENDPOINT_IN, USB_REQ_GET_DESCRIPTOR, (USB_DT_STRING << 8) + index, 
                                  langid, buffer, sizeof(buffer), 1000)) < 0)
        return rval;
    if (buffer[1] != USB_DT_STRING)
        return 0;
    if ((unsigned char)buffer[0] < rval)
        rval = (unsigned char)buffer[0];
    rval /= 2;
    /* lossy conversion to ISO Latin1 */
    for (i=1;i<rval;i++){ if (i > buflen)  /* destination buffer overflow */
            break;
        buf[i-1] = buffer[2 * i];
        if(buffer[2 * i + 1] != 0)  /* outside of ISO Latin1 range */
            buf[i-1] = '?';
    }
    buf[i-1] = 0;
    return i-1;
}


/* PowerSwitch uses the free shared default VID/PID. If you want to see an
 * example device lookup where an individually reserved PID is used, see our
 * RemoteSensor reference implementation.
 */

#define USB_ERROR_NOTFOUND  1
#define USB_ERROR_ACCESS    2
#define USB_ERROR_IO        3

static int 
usbOpenDevice(usb_dev_handle **device, int vendor, char *vendorName, int product, char *productName)
{
    struct usb_bus      *bus;
    struct usb_device   *dev;
    usb_dev_handle      *handle = NULL;
    int                 errorCode = USB_ERROR_NOTFOUND;
    static int          didUsbInit = 0;

    if (!didUsbInit){
        didUsbInit = 1;
        usb_init();
    }
    usb_find_busses();
    usb_find_devices();
    for (bus=usb_get_busses(); bus; bus=bus->next){
        for (dev=bus->devices; dev; dev=dev->next){
            if (dev->descriptor.idVendor == vendor && dev->descriptor.idProduct == product){
                char    string[256];
                int     len;
                handle = usb_open(dev); /* we need to open the device in order to query strings */
                if (!handle){
                    errorCode = USB_ERROR_ACCESS;
                    fprintf(stderr, "Warning: cannot open USB device: %s\n", usb_strerror());
                    continue;
                }
                if (vendorName == NULL && productName == NULL){  /* name does not matter */
                    break;
                }
                /* now check whether the names match: */
                len = usbGetStringAscii(handle, dev->descriptor.iManufacturer, 0x0409, string, sizeof(string));
                if (len < 0){ errorCode = USB_ERROR_IO; fprintf(stderr, "Warning: cannot query manufacturer for device: %s\n", usb_strerror()); } else { errorCode = USB_ERROR_NOTFOUND; /* fprintf(stderr, "seen device from vendor ->%s<-\n", string); */ if (strcmp(string, vendorName) == 0){ len = usbGetStringAscii(handle, dev->descriptor.iProduct, 0x0409, string, sizeof(string));
                        if (len < 0){ errorCode = USB_ERROR_IO; fprintf(stderr, "Warning: cannot query product for device: %s\n", usb_strerror()); } else { errorCode = USB_ERROR_NOTFOUND; /* fprintf(stderr, "seen product ->%s<-\n", string); */
                            if (strcmp(string, productName) == 0)
                                break;
                        }
                    }
                }
                usb_close(handle);
                handle = NULL;
            }
        }
        if (handle)
            break;
    }
    if (handle != NULL){
        errorCode = 0;
        *device = handle;
    }
    return errorCode;
}


int
main(int argc, char **argv)
{
    usb_dev_handle      *handle = NULL;
    unsigned char       buffer[8];
    int                 nBytes;

    if (argc < 2){
        usage(argv[0]);
        exit(1);
    }
    usb_init();
    if (usbOpenDevice(&handle, USBDEV_SHARED_VENDOR, "www.gruvin9x.com", USBDEV_SHARED_PRODUCT, "gMeter-std") != 0){
        fprintf(stderr, "Could not find USB device \"TrafficMeter\" with vid=0x%x pid=0x%x\n", USBDEV_SHARED_VENDOR, USBDEV_SHARED_PRODUCT);
        exit(1);
    }
/* We have searched all devices on all busses for our USB device above, and found it.
 * The USB device is already "opened", as referenced by file handle, 'handle'.
 * Let's now perform our vendor specific control operations for the
 * function requested by the user.
 */
    if (strcmp(argv[1], "test") == 0){
        int i, v, r;
/* The test consists of writing 1000 random numbers to the device and checking
 * the echo. This should discover systematic bit errors (e.g. in bit stuffing).
 */
        for (i=0; i<1000; i++){
            v = rand() & 0xffff;
            nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, PSCMD_ECHO, 
                                      v, 0, (char *)buffer, sizeof(buffer), 5000);
            if (nBytes < 2){
                if (nBytes < 0)
                    fprintf(stderr, "USB error: %s\n", usb_strerror());
                fprintf(stderr, "only %d bytes received in iteration %d\n", nBytes, i);
                fprintf(stderr, "value sent = 0x%x\n", v);
                exit(1);
            }
            r = buffer[0] | (buffer[1] << 8);
            if (r != v){
                fprintf(stderr, "data error: received 0x%x instead of 0x%x in iteration %d\n", r, v, i);
                exit(1);
            }
        }
        printf("communications test succeeded\n");

        printf("running LED and beeper test ...");

        int level;
        for(level=0; level < 10; level++)
        {
          if (level < 9) { printf(" %d", level); fflush(stdout); nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, PSCMD_SET, 0/*wValue*/, level/*wIndex*/, (char *)buffer, sizeof(buffer), 5000); } else { printf(" beep\n"); fflush(stdout); nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, PSCMD_ALARM, 0/*wValue*/, 1 /*wIndex*/, (char *)buffer, sizeof(buffer), 5000); } usleep(250000); } nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, PSCMD_SET, 0/*wValue*/, 0/*wIndex*/, (char *)buffer, sizeof(buffer), 5000); nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, PSCMD_ALARM, 0/*wValue*/, 0 /*wIndex*/, (char *)buffer, sizeof(buffer), 5000); printf("test completed\n\n"); } else if (strcmp(argv[1], "alarm") == 0) { unsigned char alarmOn; if (argc > 2)
      {
        if (strcmp(argv[2], "off") == 0) alarmOn = 0;
        else if (strcmp(argv[2], "on") == 0) alarmOn = 1;
        else
        {
          nBytes = 0;
          usage(argv[0]);
          exit(1);
        }

        nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, PSCMD_ALARM, 
                              0/*wValue*/, alarmOn/*wIndex*/, (char *)buffer, sizeof(buffer), 5000);
      }

    } else { // assume an integer meter level has been provided

        int level;
        if (sscanf(argv[1], "%d", &level))
        {
          nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_OUT, PSCMD_SET, 
                                      0/*wValue*/, level/*wIndex*/, (char *)buffer, sizeof(buffer), 5000);
          if (argc > 2 && strcmp(argv[2], "alarm") == 0)
            nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, PSCMD_ALARM, 
                                0/*wValue*/, 1 /*wIndex*/, (char *)buffer, sizeof(buffer), 5000);
          else
            nBytes = usb_control_msg(handle, USB_TYPE_VENDOR | USB_RECIP_DEVICE | USB_ENDPOINT_IN, PSCMD_ALARM, 
                                0/*wValue*/, 0 /*wIndex*/, (char *)buffer, sizeof(buffer), 5000);
        } else {
            nBytes = 0;
            usage(argv[0]);
            exit(1);
        }
        if (nBytes < 0)
            fprintf(stderr, "USB error: %s\n", usb_strerror());
    }
    usb_close(handle);
    return 0;
}

All I need now is nice little box and some labels stuck on it. Then again, it looks kinda cool, just Blu-Tack™’d to the wall. 😀 (Except with the cable hanging down and I should really swap the red and green LEDs.)

Author: gruvin

Born anatomically male, of a largely uninteresting bipedal, carbon based species, during the ninth moon orbit of the one thousand, nine hundred and seventy first meaningfully recorded solar orbital cycle (according to Gregory) of a small blue planet, situated near the outer edge of a seemingly long forgotten galaxy, located at the precise centre of its universe. In short; — I am. Therefore, I am.

Leave a Reply

Your email address will not be published.