This project started with two goals. First, to demonstrate the capability of the WiFly module, and second, to interact with hardware from a browser in a unique way. We already had the Tweeting Kegerator tutorial, which allows the hardware to post metrics over a wired network connection, so for this project I instead wanted the network side of the project to affect the hardware in some interesting (and of course wireless) way.
With that in mind, let me say up front that up until now I had no experience with either the SpeakJet chip or any sort of embedded network/wifi devices. In this tutorial I'll lead you through my thought process as this project developed. I also include four, count-em four, example Arduino sketches. Each one is a different milestone in the project, so you can pick-and-choose examples without unnecessary code. The sketch for the whole project is at the bottom of the page.
The WiFly GSX Serial Module from Roving Networks has a very simple serial command interface. To access most of the features of the device, all one needs to connect is TX, RX, VCC, and ground. For this project I started with a WiFly Shield and an Arduino Duemilanove. The WiFly shield does not actually communicate with the Duemilanove using the UART, but has a SC16IS750 SPI-UART bridge that is more complicated to deal with than simple UART communication, but allows for a much higher baud rate if one wants to communicate with the WiFly at higher-than-115200 baud speeds.
The first task was to initialize the SC16IS750 SPI-UART bridge chip in order to transparently communicate with the WiFly through a terminal. This is a short series of SPI register writes:
This snippet of code configures the SC16IS750 to 9600 baud (the default rate of the WiFly), enables the communication FIFO, and then tests the SPI communication by writing to and then reading from the chip's "Scratch-Pad Register" (SPR). If this test fails, the code will hang the Arduino in a while(1).
With the communication rates set, I wrote a simple polling loop to check the line status register (LSR) of the SC16IS750 and print any characters it receives. If no characters are available, it checks the Arduino Serial buffer for new characters from the terminal and sends them to the SC16IS750. An Arduino sketch of this can be found here:
Download: WiFly Transparent Terminal Sketch
Then it was time to start digging into the WiFly User Guide (more recent version). My first task was to get the WiFly to associate with our wireless LAN at SparkFun. All commands to the WiFly are executed in command mode, which is entered by sending the characters "$$$" to the device. So I fired up the Arduino with the wifly-basic sketch loaded and typed "$$$" into TeraTerm. The output was:
Now the device is in command mode, and it looks like the WiFly is automatically attempting to associate to an open network. I need it to associate to the SparkFun network, but all I have is our WPA password which I got from our IT guys, so I use the "scan" command to see information about the available wireless networks:
Now I know the security type and name of our wireless LAN. By default, the WiFly is not configured for WPA encryption, so I change that with the command
This changes the Authentication Mode of the device to Mixed WPA1 & WPA2-PSK. I use a similar command to set the password:
Now, since I know the name of the network, I can associate using the join command:
Great! Now I have the device associated and listening on port 2000 so I can test it using Telnet. Using a laptop that is also on the SparkFunWireless network, I open a command prompt and type
This will attempt to connect to the device on port 2000. After pressing enter, the command prompt displays the message "*HELLO*", the default string sent to a connecting client by the WiFly. On the local terminal, I see the message "*OPEN*" indicating that a connection has been opened. Now I can type in either window and have it appear on the other end like some over-engineered instant messaging program. It's a good start, but now I want to be able to host HTML on the Arduino, which means communicating on the HTTP port (port 80). So I disconnect from telnet and set the local port with the command
Now I telnet once again using the command
Now I'm connected to the device over the network on port 80 and communicating over telnet as before. I know that an HTTP connection from a browser will attempt to communicate over port 80, so in Firefox I type
and press enter. The terminal that the WiFly is connected to displays this:
This is great! Now, on the Arduino, I can wait for an open connection and attempt to respond properly. I'd like to see if the WiFly is responding at all, so I download a Firefox add-on called HTTPFox which catches incoming and outgoing packets and displays their data. I fire up HTTPFox and attempt to connect to the WiFly again through the browser.
In HTTPFox I see the same "GET" request as the browser attempts to connect, but now I see that the Response Header from the WiFly is
meaning that the WiFly is responding properly, but the browser is waiting for the connection to close before displaying anything. The activity icon on Firefox is spinning waiting for data. With this in mind, I can attempt to send the browser some characters and then enter the command mode and close the connection. In the terminal window connected to the Arduino, I type "hello world". This doesn't echo in the terminal, but every time I hit a character I can see the red LED on the WiFly light up, indicating UART communication. Next, I type "$$$" again to enter command mode, and then type "close" to close the connection. The Firefox browser then displays a page that says
and states "Done" in the status bar. This means that everything that is sent to the browser after the connection is opened is interpreted and then displayed when the connection is closed.
Now instead of sending "Hello World", I'd like to send some HTML to be interpreted as a web page. However, I do not want to display the "*HELLO*" message automatically sent by the WiFly, and I'd also like to get the device to connect to the SparkFun Wifi automatically. Now that I know all of the commands I need to send to configure the device, I can send them to the WiFly at startup so it autoconnects after boot. Also, the user guide specifies that WiFly commands can be shortened, so
This autoconnect routine simply sends all of the commands I would have typed anyway, without worrying about a printed response. For example, to set the authorization level to three, the code is
with "auth_level" delcared as a global variable.
After the join command, the device attempts to associate with the specified network. I need to be able to detect if the device is associated. To do this, I send the join command, delay for a set amount of time (5 seconds) to give the device time to associate, and then send the command
which is short for "show connection". This command responds with 8130 if connected and 8103 if no IP address has been assigned. So by reading in and checking the one's digit of this response I can see if the device has associated. If it has not, I send the "reboot" command and attempt to autoconnect again. This goes on until the device is associated. Once that happens, the device goes back into the polling loop from the previous example. The Arduino sketch that auto-connects is found here:
Download: WiFly Autoconnect Example Sketch
As I mentioned before, I do not want the WiFly sending connection messages that will get in the way of HTML, so in the autoconnect routine I disable them using the commands
c is short for comm, and r, o, and the other c are short for "remote", "open", and "close" respectively. This tells the device not to send anything when a connection is opened or closed.
With the autoconnect in place, I can now attempt to send an HTML page when a connection is opened. Right now, to detect a client request, all the device has to do is wait for characters to be sent after autoconnecting. So in the main loop, the device continuously queries the SC16IS750 until characters are received, and then sends the HTML lines to the WiFly, and closes the connection with the "close" command. Here's a code snippet.
I connect to the device with Firefox, and it works! I now have a web page that says "Hello World". Now I can send a page that actually does something. The first thing I try is displaying an image with a reference link. The title of the page comes up, but the image wont load. I take a look at the source code that the browser is acually receiving, and I can see that the connection closes before the full page is sent. So I try adding a short delay after every line of HTML I send, and this allows the whole page to be received proplerly. I make a function called HTML_print that is essentially SPI_Uart_println with a short delay (30ms) immediately following. Is it a lazy-kluge-jury-rig-thereIfixedit solution? You betcha. But it works, so I stick with it. Once the HTML is done, I now have a device that wirelessly serves up an image and a button. The sketch for this can be found here:
Download: WiFly Example Page Host Sketch
With the Arduino serving up a page over the WiFly, it's time to start interacting with the hardware over the LAN. At this point I have tons of ideas running around in my head, but then I remember that the Voice Box Shield just came out, so how about making this server talk? The Voice Box Shield operates by accepting phoneme commands and "saying" the corresponing phoneme. SpeakJet includes a dictionary applet called Phrase-A-Lator with about 1400 words spelled out in phoneme form, so I have a good start on a basic lexicon for this server. Before any words come into play, however, I have to figure out how to get a phrase from the web page onto the Arduino in an understandable form.
I already have a page that hosts a button, so I need to turn it into a submit field. My HTML is rusty/nonexistent, so I get on W3Schools and find some example HTML for a submit field. I code it into my page, type "hello world", press submit, and see this pop up in my terminal.
This HTML code is looking to submit this data to the file "html_form_submit.asp", and it is a "get" request. Since I don't have that file on the Arduino, and since this just needs to be a one-way transmission, I change "html_form_submit.asp" to simply "/" and "get" to "post". When I type in "Hello World" and submit again, I see this in the terminal
Well this makes things easier. The typed message is sent at the end, right after the word user. If I change "user" in the HTML to some unique character, I can easily parse the message to be used with the Voice Box. I change the word "user" to "%" (a symbol not found in either the GET or POST messages) and write a parser, creatively called Parse_Request to put the sent message in an array and split it into separate words.
Now that I have all the data I need coming from the web page, it's time to get the SpeakJet talking. All I have right now is the dictionary applet provided by the SpeakJet Phrasealator. This has 1400 words, but it lists them as phoneme strings like so
This means nothing to the SpeakJet, so I'm going to have to translate the strings into SpeakJet codes. I dig around in the PhraseALator folder and manage to find the file PhraseALator.dic which is the plain-text file for the dictionary. Using VIM and some creative find-and-replace, I replace all of the phonemes with their SpeakJet codes and format each string to be hosted in program memory. So the old "airplane" string is now
I also put all of the string addresses and words into a lookup table for easy access. The reason for the "sj_" in "sj_airplane" is that the dictionary includes words like "long" and "while" which are interpreted as keywords and not string names, so I added a general prefix to avoid confusion. The library and it's lookup table can be found here.
Even though this data is hosted in program memory, 1400 strings is still too much data to fit into the Arduino memory, so I'm going to have to pick some choice words and build an abridged dictionary for this project. To choose these words, I used a "Refrigerator Poetry Magnet" example wordlist that I found. I went through and copied the first hundred-or-so words from this list into my Arduino sketch, and built a symbol table and a lookup table. Now when I receive a phrase from the web page, I can compare the individual words against the words in the lookup table, use that index for the symbol table, and copy the referenced SpeakJet command string from program space into a local buffer. Once there, I can just send the string to the SpeakJet and the device should say the word. I put the lookup code into my Parse_Request function (an O(n) linear search, because I'm lazy and I need the code space), fire up the device, and surf to the IP. I type in a phrase that includes several words in the abridged dictionary, and the SpeakJet says them! Unfortunately, when the SpeakJet gets to the last word, it keeps talking. And talking. And talking. It seems as if it's continuing on through the memory, grabbing the next word and saying it. I check my parsing function and SpeakJet strings, but I can't get the SpeakJet to stop talking when I want it to, after RTFM (again) and some guess-and-check work with the device, I find that if I end all the word strings with the commands "0,0,255", the SpeakJet will stop speaking at the desired time. The code "0" translates to a pause of 0ms in the SpeakJet, and the code "255" translates to "end of phrase". I find that the desired behavior is only achieved when two "0's" and a "255" are included, not one or the other. So, back to VIM. I format the dictionary so that the strings all end in these commands, so the original "airplane" string goes from
I test the device again, and now it works great! The only thing left to do is to get the device to display the dictionary when the page is loaded, which is easily accomplished with a loop in the HTML send section. Now the device waits for a client, serves up the page, and waits for a GET or POST request. If a POST request is received, it parses the sent data, "says" the phrase with the SpeakJet, and then refreshes the page and closes the connection.
Everything I do after this point is just gravy. I add a AA battery pack so I can walk around with the device. I also get the SpeakJet to say its IP address when the device associates in case the DNS decides to give me a different IP on association. At this point the project is done. The final code can be found here:
Download: WiFly SpeakJet Server Sketch
And here is a video of the device in action: