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(); } }