XBee Library graphing and logging application

Here’s a program that uses Rob Faludi and Dan Shiffman’s XBee library for processing to read three analog sensors from multiple remote XBee radios and graph them. It also saves the data to a comma-delimited file. It also makes sounds when the value exceeds a given threshold. For this application, you need two or more XBee series 1 radios. One is attached to the serial port of the computer, and the other is remote, broadcasting three analog values.

This also uses the ControlP5 library by Andreas Schlegel and the Ess library by Krister Olsson.

The settings for the radios are as follows:

Base station radio:

  • Personal Area Network (PAN) ID: AAAA
  • Source address: ATMY 0
  • Baud rate 115200 bits per second: ATBD 7

Remote radios:

  • Personal Area Network (PAN) ID: AAAA
  • Source address: ATMY 1 (MY2, MY3, etc. for other remote radios)
  • Destination address: ATDL 0
  • Analog inputs activated:
    • ATD0 2
    • ATD1 2
    • ATD2 2
  • Sample rate 80 milliseconds: ATIR 50 ( modify this by 5ms or so per radio for best results)
  • 1 sample per transmission: ATIT 1
  • Baud rate 115200 bits per second: ATBD 7

The code is separated into several pages for portability: The main methods, the Radio class, the graphing methods, the file IO methods, the GUI methods, the XBee methods, and the sound methods.

The main methods:

/*
  XBee sensor data graphing and logging sketch
 
 This sketch takes data in from multiple XBee radios and graphs the analog 
 input values.  It's for XBee series 1 radios. It also plays a sound when
 the value exceeds a given threshold
 
 Xbee methods based on code from  Rob Faludi and Daniel Shiffman
 http://www.faludi.com
 http://www.shiffman.net 
 
 ControlP5 methods based on code from Andreas Schlegel
 http://www.sojamo.de/libraries/controlP5/
 
 Ess methods based on code from Krister Olsson
 http://www.tree-axis.com/Ess/
 
 created 2 Feb 2008
 by Tom Igoe  
 */

//import the xbee, serial, and GUI libraries:
import xbee.*;
import processing.serial.*;
import controlP5.*;

// Your Serial Port
Serial port;
// Your XBee Reader object
XBeeReader xbee;

ControlP5 gui;
// set up a font for displaying text:
PFont myFont;
int fontSize = 12;

// ArrayList to hold instances of Radio objects:
ArrayList radios = new ArrayList();  

// data file name for saving to a file:
String dataFileName = "Untitled";

// set up Xbee parameters:
int numSensors = 3;                 // number of sensors you actually plan to graph

int xpos = 0;                       // graph x position
int oldYPos = 0;                    // graph y position
int yPos = 0;                       // previous y position

int hitThreshold = 250;             // peak value to check hits against
int hits = 0;                       // hit count
int lastHitValue = 0;               // value of last hit

int graphMin;                       // minimum value for graph (top of graph)
int graphMax;                       // maximium value for graph (bottom of graph)
boolean newData = false;            // new data flag   

// which radio radio you're graphing, chosen by keystroke:
Radio chosenRadio = new Radio(0); 

void setup() {
  size(640, 480);                // window size
  frameRate(60);                 // frame rate
  smooth();                      // clean the jagged edges

  // create a font with the third font available to the system:
  myFont = createFont(PFont.list()[2], fontSize);
  textFont(myFont);

  // you might need a list of the serial ports to find yours:
  println("Available serial ports:");
  println(Serial.list());
  // open the first serial port.  Change this to match 
  // your serial port number in the list:
  port = new Serial(this, Serial.list()[0], 115200);

  // initialize the xbee library:
  xbee = new XBeeReader(this,port);
  xbee.startXBee();

  // initialize sound:
  startSound();
  // black screen:

  background(0);
  // set graph minima and maxima:
  graphMin = 160;
  graphMax = height - 10;

  // initialize the GUI controls:
  guiSetup();
}

void draw() {
  // if there's new data, graph it and write the status text:
  if (newData) {
    drawGraph(chosenRadio);
    writeStatusText(chosenRadio);
    newData = false;  
  }
}

void keyReleased() {
  // convert numerical ASCII values to actual values:
  int thisKey = keyCode - 48;

  // if the number typed is less than the number of radios
  // you've heard from, use it to choose which one to listen to:
  if (thisKey <= radios.size() && thisKey > 0) {
    chosenRadio = (Radio)radios.get(thisKey-1);
  }
}

String getTimeStamp() {
  // make a timestamp string:
  String thisTime = hour() + ":" + minute() + ":" + second();
  return thisTime;
}

void stop() {
  // close any open file:
  if (fileIsOpen) {
    closeFile();
  } 
  stopSound();
} 

Radio class:

 /*
  The Radio object keeps track of all the data from
  a given Radio's radio: the sensor values, the previous values,
  the address, the signal strength.

*/

public class Radio {
  int[] sensorValues;
  int[] previousValues;
  int rssi;
  int address;
  boolean initialized = false;

  public Radio(int thisAddress, int thisRssi, int[] theseValues) {
    address = thisAddress;
    rssi = thisRssi;
    sensorValues = theseValues;
    println("new Radio");
    initialized = true;
  }

  public Radio(int thisAddress) {
    address = thisAddress;
    sensorValues = new int[6];
    previousValues  = new int[6];
    rssi = 0;
    initialized = true;
  }

  public int getAddress() {
    return this.address;
  }

  public void setAddress(int thisAddress) {
    this.address = thisAddress;
  }

  public int getRssi() {
    return rssi;
  }

  public void setRssi(int thisRssi) {
    rssi = thisRssi;
  }

  public int[] getSensors() {
    return sensorValues;
  }

  public void setSensors(int[] theseSensors) {
    arraycopy(sensorValues, previousValues);
    arraycopy(theseSensors,sensorValues);
  }

  public int[] getPrevSensors() {
    return previousValues;
  }

  public boolean isRadio() {  
    return initialized;
  }

  public String getProperties() {
    String dataToSend = getTimeStamp();
    dataToSend += ",";
    dataToSend += address;
    dataToSend += ",";
    dataToSend += rssi;
    dataToSend += ",";  
    for (int i = 0; i < sensorValues.length; i++) {

      dataToSend += sensorValues[i];
      if (i < sensorValues.length-1) {
        dataToSend += ",";
      }
    }
    dataToSend += "\r\n";
    return dataToSend;
  }
}

Graphing methods:

void drawGraph(Radio thisRadio) {
  // if there are any radios to get data from:
  if (radios.size() > 0) {
    // if the given radio object has been initialized:
    if (thisRadio.isRadio()) {
      // iterate over the number of sensors to graph:
      for (int thisSensor = 0; thisSensor < numSensors; thisSensor++) {
        // if you have new data and it's valid (>0), graph it:
        if (thisRadio.getSensors()[thisSensor] > -1) {

          // map the sensor values to the graph rect:
          yPos = int(map(thisRadio.getSensors()[thisSensor], 0, 1023, graphMin, graphMax));
          oldYPos = int(map(thisRadio.getPrevSensors()[thisSensor], 0, 1023, graphMin, graphMax));

          // if we get a big change, increment the hit counter:
          if (abs(yPos - oldYPos) >= hitThreshold) {
            // make a sound: 
            makeSound();
            hits++;
            // not the magnitude of the hit:
            lastHitValue = abs(yPos - oldYPos);
          }
        }
        // draw the graph axis: 
        stroke(255);
        int xAxis = int(map(height/2, 0, height, graphMin, graphMax));
        line(0, xAxis, width, xAxis);

        // use a different the graphing color for each axis:
        switch (thisSensor) {
        case 0:
          stroke(255,0,0);
          break;
        case 1:
          stroke(0,255,0);
          break;
        case 2: 
          stroke(0,0,255);
          break;
        }
        // draw the graph line from last value to current:
        line(xpos, oldYPos, xpos+1,yPos);
      }
      // if you're at the right of the screen, 
      // clear and go back to the left:
      if (xpos >= width) {
        xpos = 0;
        background(0);
      } 
      else {
        xpos++;
      }
    } 
  } 
}

void writeStatusText(Radio thisRadio) {
  // write the text at the top of the screen:
  noStroke();
  fill(0);
  rect(0, 0, width, graphMin);
  fill(255);
  text("From: " + hex(thisRadio.getAddress()), 10, 20);
  text ("RSSI: " + thisRadio.getRssi() + " dBm", 10, 40);  
  text("X: " + thisRadio.getSensors()[0] + "  Y: " + thisRadio.getSensors()[1] + "  Z: " + thisRadio.getSensors()[2], 10, 60);
  text("Last hit value: " + lastHitValue,  10, 80); 
  text("Filename: " + dataFileName, 10, 100);
  // if there's a file open to save data, let the user know:
  if (fileIsOpen) {
    fill(255,0,0);
    text("LOGGING", 200,100);
  }
} 

File IO methods:

 
PrintWriter writer;            // writer to file
boolean fileIsOpen = false;    // whether or not file is open

void openFile(String fileName) {
  // create a file with the given filename
  // in the "data" subdirectory of the sketch's directory:
  writer = createWriter("data/" + fileName + ".csv");
  // put a timestamp at the beginning of the file:
  writer.println(getTimeStamp()); 
  fileIsOpen = true;
}

void closeFile() {
  // close the file:
  writer.flush(); 
  writer.close();
  fileIsOpen = false;
}

GUI methods

 
int guiX;          // left of the GUI space
int guiY;          // top of the GUI space

void guiSetup() {
  // set the position of the GUI space:
  guiX = width/2;
  guiY = 0;
  // initialize the GUI:
  gui = new ControlP5(this);
  // add the GUI elements:
  gui.addSlider("hit_thresh",0,height/2,hitThreshold, guiX+60,guiY+10,10,150).setId(1);
  gui.addToggle("logData",false, guiX+100,guiY+80,10,10).setId(2);
  gui.addTextfield("filename",guiX+100,guiY+120,150,20).setId(3);
}

void controlEvent(ControlEvent theEvent) {
  // do something different depending on which control
  // generated the event:
  switch(theEvent.controller().id()) {
    // if it was the slider, update the hit threshold:
    case(1):
    hitThreshold = int(theEvent.controller().value());
    break;
    // if it was the toggle, open or close the data file:
    case(2):
    int value = int(theEvent.controller().value());
    boolean logging = boolean(value);
    if (logging) {
      openFile(dataFileName);
    } 
    else {
      closeFile();
    }
    break;  
    // if it's the text box, update the file name
    // with the textbox string:
    case(3): 
    dataFileName = (theEvent.controller().stringValue());
    break;
  }
}

XBee methods:

 
/*  
 This function works just like a "serialEvent()" and is 
 called for you when data is available to be read from your XBee radio.
 */

public void xBeeEvent(XBeeReader xbee) {

  // Grab a frame of data
  XBeeDataFrame data = xbee.getXBeeReading();

  // This version of the library only works with IOPackets
  // For ZNet radios, you would say XBeeDataFrame.ZNET_IOPACKET
  if (data.getApiID() == XBeeDataFrame.SERIES1_IOPACKET) {
    // Get the transmitter address
    Radio myRadio = checkAddress(data.getAddress16());

    // Get the RSSI reading in dBM 
    myRadio.setRssi(data.getRSSI());

    // Get the current state of each analog channel 
    // (-1 indicates channel is not configured);
    myRadio.setSensors(data.getAnalog());

    // if this is the chosenRadio, graph it:
    if (myRadio == chosenRadio) {
      newData = true;   
    } 
    // if there are no radios in the ArrayList yet, make this the first:
    else if (radios.size() == 1) {
      chosenRadio = myRadio; 
    }
    // if logging data, log it:
    if (fileIsOpen) {
      writer.println(myRadio.getProperties());
    }
  } 
  else {
    // this is not an I/O packet:
    println("Not I/O data: " + data.getApiID());
  }
}


Radio checkAddress(int thisAddress) {
  boolean isRadio = false;         // whether the Radio object is initialized
  Radio radioToReturn = null;      // which radio has the given address 

  // iterate over the playerList:
  for (int r = 0; r < radios.size(); r++) {
    // get the next object in the ArrayList and convert it
    // to a Radio:
    Radio thisRadio = (Radio)radios.get(r);

    // if thisPlayer's address matches the one that generated 
    // the serverEvent, then this radio is already in the list:
    if (thisRadio.getAddress() == thisAddress) {
      // we already have this radio
      isRadio = true;
      radioToReturn = thisRadio;
    }

  }
  // if the radio isn't already in the ArrayList, then 
  // make a new Radio and add it to the ArrayList:
  if (!isRadio) {
    Radio newRadio = new Radio(thisAddress);
    radios.add(newRadio);
    radioToReturn = newRadio;
  }
  return radioToReturn;
} 

Sound methods:

 // import the library:
import krister.Ess.*;

// set up an audio channel:
AudioChannel myChannel;

// flag for whether or not a sound is playing
boolean soundPlaying = false;

void startSound() {
  // set up sound:
  Ess.start(this);
  Ess.masterVolume(1);
  // load a file, give the AudioPlayer buffers that are 512 samples long
  // load a sound into a new Channel
  myChannel=new AudioChannel("punch.aif");
  // set the volume to max:
  myChannel.volume(1);
}

void makeSound() {
  // if there's an open sound channel and there's no sound playing:
  if ((myChannel != null) && (!soundPlaying)) {
    myChannel.play();
    soundPlaying = true;
  } 
}

void audioChannelDone(AudioChannel ch) {
  // trip the soundPlaying flag when the sound stops:
  soundPlaying = false;
}

void stopSound() {
  // always close Minim audio classes when you are done with them
  if (myChannel != null) {
    Ess.stop();
    super.stop();
  }
}