Early on I got tired of having my XBee set up wrong because I forgot something when I configured it and created a little piece of code to check the configuration to some degree at boot time on the Arduino. By sending AT commands to the XBee, you can check things like software version, operating mode, etc. Then if something was messed up, you can correct it easily without a long debugging session to discover some minor setting error. I also use this to get the name of the device, and the address that it normally sends to. Using this piece of code makes the entire device more configurable. For example, you need to change to broadcast, instead of a specific address, to monitor the packets, just use XCTU to remotely change the DL, DH attributes and reset the Arduino so it reads the new address. If you decide to change the device name, remotely change the NI attribute, and again, reboot the arduino. I've been using this in one form or another now for a while to help work the kinks out of the latest device I'm working with.
Yes, you can change the configuration of the XBee that you stuck in the corner of the attic remotely, This has been a capability of XCTU for a long time, and the latest version of XCTU makes it even easier. To remotely reset an Arduino, I simply decode a command I send and then call location zero.
These two tiny tricks makes minor changes to the network really easy and can be done from my recliner.
The code that follows (below somewhere) is a simple thing with complete XBee interaction, I have examples of sending formatted strings containing float data through the XBee, parsing a long unsigned integer out of an XBee message and using it, simulating the data portion of an XBee message for debugging, setting up a timer to send status periodically, reading some of the configuration out of the XBee to be sure it's OK before continuing the work. That kind of thing.
Since the barometer code is so easy to understand, I thought that would be a perfect device to put all this stuff in and use as an illustration. Go take a look at the barometer <link> then come back here and I'll step through some of the things that you can use in your own code to make the XBee a valuable device in do it yourself home automation.
First, just to get it out of the way, resetting the Arduino from software. If you haven't hit it yet, you will, that time when you know a reset will cure a malfunction on the Arduino, but its up high, out in the yard, in the dark on the side of the house; somewhere hard or inconvenient to get to. Wouldn't it be nice to just send a command to it and have it reset itself? A software reset is easy to do. If you need a hard reset (similar to pulling the plug) this won't do it; but those are not often needed. Just put this line somewhere at the top of the code:
void(* resetFunc) (void) = 0; //declare reset function @ address 0
and when you need it, call it like this:
resetFunc(); //This will reset the board and start all over.
Yep, that's all there is to it. I use this a lot for overcoming memory leaks in libraries that haven't been tested long enough, memory loss from using Streams in c++, that kind of problem. It also helps overcome my own bugs from time to time. As an example, in the code below I use it twice. I have one software reset that is on a timer to reset the board every day around midnight. This is to recover memory lost to whatever code does a malloc() and then doesn't remember to do a free() to release the memory back. This is a very common problem (called memory leak) and has plagued tiny computers as long as they've existed. When you want to run an Arduino 24/7, you have to code defensively. The other instance is when I send a reset command over the XBee network to cause the board to reset. I use this to test things like power failures, how long it will take to settle down, and to see if a reset will work before I get the ladder out to take a physical look at it.
Since I mentioned remotely resetting the board, I'll describe how I do that, but I need to explain how I use the IDE itself first. Most of you already know that the Arduino IDE supports 'tabs', but the new folk haven't caught up to that yet. Tabs are actually separate source files that the IDE stores in the same directory as the sketch. These are extremely nice for organizing code so you don't have to scroll through a couple of hundred lines of code to get to something you want to work on.
What you're looking at above is the Arduino IDE with three tabs. One is for the bulk of the barometer code, the next is for some utility routines I use a lot, and the one selected is the XBee code I use on a lot of my devices. I opened the window that controls tabs so the new folk can get a look at the tool provided for working with tabs. Remember, each tab is actually a separate file in the sketch directory. When you compile something, the files are all concatenated together and passed to the compiler, this is just a convenience to help you organize your project. There's a couple of tutorials out there that mention tabs, and I've heard of a book or two that do, but most of the instructions are great at telling you how to install it, but not how to use it effectively. So, don't be afraid to poke around and experiment.
No, I'm not going to write a book about it. If I did, they'd change it and I'd have to have a second revision. I've got projects to work on instead.
Anyway, The picture shows how I decode the command and reset the board. Over time I've worked out my own particular method of sending and receiving packets that works well for me. Everything is done in a string; I don't try to send integers, floats or compact the data. For home automation, interactions are measured in a minimum of seconds and plain text traveling between the machines is perfectly adequate. It also makes monitoring what's going on much easier since I don't have to decode anything to read it, it's already text. So my command to reset the barometer is:
Barometer,Reset
I can type that right into an XCTU session from my recliner and the barometer out on the fencepost (where it will eventually reside) will reset itself. Yes, I have to construct a packet in API mode 2 format, but XCTU has a packet generator that works pretty well to help with that.
Now, let's talk about AT commands to the XBee itself. When you look at the various tutorials on the web, they love to type AT into the XBee console and look for the OK that comes back. Then they teach you how to set up the XBee using these commands. This is good, but not very useful if you're going to actually do a project that will run all the time and have to survive a lot of the normal problems such as RF noise, distance, flaky devices, etc. For that you have to go to API mode and send fully constructed packets between machines. I have already covered the various pitfalls and have instructions on how to set this up on my XBee page, so I won't go over it again here, go here <link> for that when you need it. What I want to show you is how to send an AT command, get something from the XBee and then use it. Doing this you can double check the configuration, get the device name, etc. It's a really good feature to understand. As an example, here are the first few lines executed on my barometer:
I know, it's a dumb screenshot. I did this on purpose, to illustrate again how tabs can work for you. I have selected the barometer tab and scrolled down a bit to the setup() routine. See how you can flip between tabs to do things instead of scrolling though tons of lines of code and losing your train of thought? I like this feature.
Anyway, I set up the XBee code, then call getDeviceParameters(). This routine sends a series of AT commands to gather various items and then returns. Then I display the parameters so I can tell if I have the XBee set up the way I want it. I actually store the string 'Barometer' in the NI command; this is the name of the device and I use it to check incoming packets to see if they're for this device and include it in the out going status packets I send. This way I can change the name of the device by simply changing the parameter on the XBee. Here's the code that does this:
void getDeviceParameters(){ uint32_t addrl = 0; uint32_t addrh = 0; uint32_t daddrl = 0; uint32_t daddrh = 0; if(sendAtCommand((uint8_t *)"VR")){ if (atResponse.getValueLength() != 2) Serial.println(F("Wrong length in VR response")); deviceFirmware = atResponse.getValue()[0] << 8; deviceFirmware += atResponse.getValue()[1]; } if(sendAtCommand((uint8_t *)"AP")){ if (atResponse.getValue()[0] != 0x02) Serial.println(F("Double Check the XBee AP setting, should be 2")); } if(sendAtCommand((uint8_t *)"AO")){ if (atResponse.getValue()[0] != 0) Serial.println(F("Double Check the XBee A0 setting, should be 0")); } if(sendAtCommand((uint8_t *)"NI")){ memset(deviceName, 0, sizeof(deviceName)); for (int i = 0; i < atResponse.getValueLength(); i++) { deviceName[i] = (atResponse.getValue()[i]); } if (atResponse.getValueLength() == 0){ Serial.println(F("Couldn't find a device name")); } } if(sendAtCommand((uint8_t *)"SH")){ for (int i = 0; i < atResponse.getValueLength(); i++) { addrh = (addrh << 8) + atResponse.getValue()[i]; } } if(sendAtCommand((uint8_t *)"SL")){ for (int i = 0; i < atResponse.getValueLength(); i++) { addrl = (addrl << 8) + atResponse.getValue()[i]; } } ThisDevice=XBeeAddress64(addrh,addrl); if(sendAtCommand((uint8_t *)"DH")){ for (int i = 0; i < atResponse.getValueLength(); i++) { daddrh = (daddrh << 8) + atResponse.getValue()[i]; } } if(sendAtCommand((uint8_t *)"DL")){ for (int i = 0; i < atResponse.getValueLength(); i++) { daddrl = (daddrl << 8) + atResponse.getValue()[i]; } } Controller=XBeeAddress64(daddrh,daddrl); } uint8_t frameID = 12; boolean sendAtCommand(uint8_t *command) { while(1){ frameID++; atRequest.setFrameId(frameID); atRequest.setCommand(command); // send the command xbee.send(atRequest); //Serial.println("sent command"); // now wait a half second for the status response if (xbee.readPacket(500)) { // got a response! // should be an AT command response if (xbee.getResponse().getApiId() == AT_COMMAND_RESPONSE) { xbee.getResponse().getAtCommandResponse(atResponse); if (atResponse.isOk()) { //Serial.println("response OK"); if (atResponse.getFrameId() == frameID){ //Serial.print("Frame ID matched: "); //Serial.println(atResponse.getFrameId()); return(true); } else { Serial.println("Frame Id did not match"); } } else { Serial.print(F("Command returned error code: ")); Serial.println(atResponse.getStatus(), HEX); } } else { Serial.print(F("Expected AT response but got ")); Serial.println(xbee.getResponse().getApiId(), HEX); } } else { // at command failed if (xbee.getResponse().isError()) { Serial.print(F("Error reading packet. Error code: ")); Serial.println(xbee.getResponse().getErrorCode()); } else { Serial.println(F("No response from radio")); } } } }
On a network like mine where there are a number of XBees sending data at essentially random intervals, traffic can be mixed in with the responses to the AT commands. I send a request for the software version at the same time as a time update is received from my house clock. The XBee will send the packet from the clock first and I could miss the response to the AT command. Also, the receive for the packets is interrupt driven and characters can be missed by simultaneous sends and receives. To overcome these kinds of problems, I wait for the response about a half second, if it doesn't come back, I try it again. This guarantees that I will accumulate the items I need during initialization regardless of traffic conditions, it just may take a couple of tries to do it. It's actually kind of fun to watch it during high traffic conditions; it tries each command as many times as necessary to gather the data and then returns to let setup() print them out. Here's what the output looks like when I run it:
Barometer Version 1 Init... This is device Barometer Device Firmware version 2370 This Device Address is 0x0013A200 0x4089D915 Default Destination Device Address is 0x0013A200 0x406F7F8C Setup Complete Mem = 276 Barometer: temperature: 77.5, uncorrected pressure: 937.8See how the name of the device, long address, version, destination long address can be gathered directly from the XBee? This is nice to guarantee that you set things up correctly when you try your code out.
Now, let's look at sending the actual data that I gathered from the barometer chip. I've learned to like JSON encoding as a method of passing data and am converting all my devices to send data using this format. I haven't gotten very far on that project, but the barometer is new, so I use it here. I use this line to construct the status text that is sent:
void sendStatusXbee(){ float temperature = bmp.readTemperature()* 9/5 +32; float pressure = bmp.readSealevelPressure(altitude)/100.0; sprintf(Dbuf, "{\"%s\":{\"temperature\":\"%s\",\"pressure\":\"%s\",\"utime\":\"%ld\"}}\n", deviceName, // originally read from the XBee dtostrf(temperature, 1, 1, t), // This is a text conversion of a float dtostrf(pressure, 1, 1, p), // same as above now()); // current time in seconds Serial.print(Dbuf); // notice this is only for the serial port sendXbee(Dbuf); // out to the XBee Serial.println("Pressure message sent"); }
I use sprintf() because the documentation is all over the web and there are tons of examples out there for people to follow. The problem is that the Arduino sprintf() doesn't support floating point, but does have a handy routine dtostrf() that will turn a float variable into a nice little string that can be used. Pay particular attention to the format string I used; you don't have to do it this way. You can assemble any string you want to and send it instead. Like I said though, JSON is already documented on the web and I don't have to invent anything this way.
Now, I want to show you how I control where the message is sent. I read the default address from the XBee DH and DL parameters during initialization and put it into the variable 'Destination', now I get to use the variable:
void sendXbee(const char* command){ ZBTxRequest zbtx = ZBTxRequest(Destination, (uint8_t *)command, strlen(command)); xbee.send(zbtx); }
This will take the string I just constructed and put it into an XBee packet, set the address I mentioned and send it. To change the address such that the message is broadcast to all devices, I decode a command and just change the value of Destination:
// The ability to broadcast is sometimes used to see what it going on // using an XBee plugged into a laptop to monitor the data. else if (strcmp(nxtToken, "SendBroadcast") == 0){ Serial.println(F("Switching Address to Broadcast")); Destination = Broadcast; // set XBee destination address to Broadcast for now } else if (strcmp(nxtToken, "SendController") == 0){ Serial.println(F("Switching Address to Controller")); Destination = Controller; // set XBee destination address to Broadcast for now }
This is just like the Reset command above; I send
Barometer,SendBroadcast
and it switches the Destination variable to the broadcast address and from then on every XBee in the network will see the message. To put it back to normal:
Barometer,SendController
And, this brings up another feature I like to have on my remote devices, a nice way to test the commands without having to drag out an XBee breakout board every time I want to add or try something. Wouldn't it be nice to be able to simulate the receipt of a command through the IDE serial display? I worked up this code to allow that very thing:
// This allows emulation of an incoming XBee command from the serial console // It's not a command interpreter, it's limited to the same command string as // the XBee. Can be used for testing if(Serial.available()){ // char c = Serial.read(); if(requestIdx < sizeof(requestBuffer)){ requestBuffer[requestIdx] = c; requestIdx++; } else{ //the buffer filled up with garbage memset(requestBuffer,0,sizeof(requestBuffer)); //start off empty requestIdx = 0; // and at the beginning c = 0; // clear away the just-read character } if (c == '\r'){ requestBuffer[requestIdx] = '\0'; // Serial.println(requestBuffer); // now do something with the request string processXbee(requestBuffer,strlen(requestBuffer)); //start collecting the next request string memset(requestBuffer,0,sizeof(requestBuffer)); //start off empty requestIdx = 0; // and at the beginning } }
Now, normally, I would post the code of the entire module, but I bet you've had enough code samples in windows for one session. Instead, I finally got the Arduino code I'm using for the various things into GitHub. This entire module is in the 'Barometer' directory so you can grab it and modify away. It's been running for a few days and hasn't blown up yet, but I'll certainly be adding to it over time.
If you go looking in the other directories don't expect all of these features to be in each thing. Like I said, I developed these tricks over time and only recently started to go back and update the older devices. Heck, I haven't changed the thermostats at all in months. One of the modules hasn't even been compiled under an IDE version in a couple of years. I'll work through the devices one at a time as I have the chance, and probably come up with another trick that I'll want to fit into the others. It never ends.
Here's the link to GitHub <link>. Have fun.
Hello,
ReplyDeleteI'd just like to say thanks to you for publishing this blog, with all of your information and code. It's been a great inspiration, since I found it, for me to get of my a&* and do something similar, though much less comprehensive.
Regards
You're welcome. It's fun when I occasionally hear about other folk that have used something in their own house.I've also had folk find bugs for me, suggest better ways to do things and point out a new piece of hardware I never heard of.
DeleteSo, when you get something going, let me know.
Hi Dave,
ReplyDeleteCan you send a single JSON string with mixed floats and integers,
or do you need to send two separate JSON strings, one of floats and one of integers?
Yep, you sure can. That's one of the great things about it.
DeleteHi Dave. It's been a while but I saw that there are now Series 3 XBee available that can run as both a S1 and S2 depending on what you load on it. Ever had a chance to try that out?
ReplyDelete