Introduction

A few months ago I started working on Arduino to build my project, a weather station that offered data through the Internet.

The objectives were mainly these:

After several tests, I decided to build a station that served a simple web page with temperature, humidity and barometric pressure from one or several sensors. Some time later, I started investigating Pachube and how I could take advantage of it to get graphs and historic data. I adapted my code to work with Pachube and the results are here


Why Dalton?

John Dalton (1766-1844) was an English chemist and physicist best known for his work in the development of atomic theory and his research in colour blindness. But he was also one of the first meteorologists, and kept for 57 years a meteorological diary with more than 200.000 observations. Naming Dalton this weather station is my humble homage to a great scientist.


Hardware

This is the hardware needed for this project:

1 Wishield 2.0 by Asynclabs. As far as I know, you can only buy it at their site. I chose this shield because of its very easy to use library. Take into account that it uses 802.11b, so depending on your Wifi AP, it could work for you or not.
1 Arduino UNO or Duemilanove. Sorry, no Diecimila for this. The WiServer add-on of the Wishield library I'm using requires an ATMEGA328. Besides, the sketch is too big for a Diecimila. On the other hand, maybe it could possible be to make the sketch more compact and to change it to not use WiServer.
1 temperature sensor. I chose the SHT-15 sensor because it includes temperature and humidity in just one sensor, comes in a nice breakout board (so, no resistors needed) and there was already-proven code available on the Internet (yes, I'm a lazy programmer :) ).
1 barometric pressure sensor. I've used a BMP085 sensor. Once again, it came in a breakout board and it has an extremely simple library. It also gives temperature data, so it's redundant, but this works on our favor, since it allows, as I've done, to get a temperature measure from each sensor and calculate the average, giving a more precise measure.
8 wires. Use whatever suits you, of course, but I like very much this kind of breadboard wires, easy to connect to Arduino pins.
Power supply. You need 9v, 500 mA for this project, in the usual Arduino range. I have a power socket right at the place where I wanted to put the station, so I'm using a power adapter. You can user other power sources, but I don't know if the Wifi connection would make batteries drain out soon.
A project case. Use whatever you want. The one on the image fits the Arduino and Wishield, but you have to cut shorter the center plastic peg, since it doesn't fit the central Wishield hole.

Building the weather station

This is the circuit to build with the components (WiShield not shown)

click to enlarge

Image made with Fritzing


The BMP085 is i2c, so SDA goes to analog input 4 and SCL to analog input 5. The SHT15 is digital, so we need two of the digital inputs. The WiShield uses the pins from 13 to 8 (although you can change 8 to 2 with a jumper), so we're using 5 and 6.

Take into account that the BMP085 uses 3.3V. NEVER connect it to 5V. The good thing is that since these two sensors use different voltages, we have to GND pins, one sensor is i2c and the other is digital, and they both come in a breakout board, we need no breadboard.

If you prefer to see a few photos of the real thing (click the photos to enlarge):

A general view of Dalton. Since the case wasn't tall enough, I had to bend the cables plastic terminations. One didn't like it and broke, hence the hot glue blob.

A closer look...

And the finished project. Both sensors have wide female connectors, so I soldered the cables to keep them in place.



Making Dalton serve a simple webpage

In my first version of the weather station, I wrote a sketch to make Dalton serve a webpage that showed the sensors data. Acknowledgments to Wayne, of Wayne's Raging Reality and commenters to this post, that gave all I needed to make the SHT15 work with Arduino.

Acknowledgments also to the people of Ardupilot, who made the nice and easy to use library for the BMP085. Speaking of which, you can get that library here. I couldn't find a zip file on this site with all the correct structure for the Arduino environment to be able to understand it, so maybe you will prefer to get this zip file of the library I made. As usual, uncompress it, copy the folder to your Arduino libraries folder, and restart the Arduino environment.

You can get the WiShield library here. The sketch is based in a WiServer sketch. That means that in the WiShield library, you must open the file apps-conf.h and modify the #define entries to look like this:

//#define APP_WEBSERVER
//#define APP_WEBCLIENT
//#define APP_SOCKAPP
//#define APP_UDPAPP
#define APP_WISERVER

Check the Asynclabs wiki for more information about setup and configuration of WiShield.

This is the full sketch. We will comment it below.

First, we include the libraries needed: APM_BMP085 for the BMP085 sensor, Wire (used by the former), and WiServer for the WiShield. We also define two constants for making the code more readable.

#include <WiServer.h>
#include <Wire.h>
#include <APM_BMP085.h> // ArduPilot Mega BMP085 Library
#define WIRELESS_MODE_INFRA	1
#define WIRELESS_MODE_ADHOC	2

Next, wireless configuration. You should replace the entries here with your network configuration: IP addresses, security, etc. Finally, we define the wireless mode. Since we're connecting to an access point, we use the previously defined constant WIRELESS_MODE_INFRA

// Wireless configuration parameters ----------------------------------------
unsigned char local_ip[] = {xxx,xxx,xxx,xxx};	// IP address of WiShield
unsigned char gateway_ip[] = {yyy,yyy,yyy,yyy};	// router or gateway IP address
unsigned char subnet_mask[] = {zzz,zzz,zzz,zzz};	// subnet mask for the local network
const prog_char ssid[] PROGMEM = {"YourWifiSSIDhere"};		// max 32 bytes

unsigned char security_type = n;	// 0 - open; 1 - WEP; 2 - WPA; 3 - WPA2

// WPA/WPA2 passphrase
const prog_char security_passphrase[] PROGMEM = {"YourWPApassphrasehere"};	// max 64 characters

// WEP 128-bit keys
// sample HEX keys
prog_uchar wep_keys[] PROGMEM = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,	// Key 0
				  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	// Key 1
				  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,	// Key 2
				  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00	// Key 3
				};

// setup the wireless mode
// infrastructure - connect to AP
// adhoc - connect to another WiFi device
unsigned char wireless_mode = WIRELESS_MODE_INFRA;

unsigned char ssid_len;
unsigned char security_passphrase_len;
// End of wireless configuration parameters ----------------------------------------

Next, we define the pins used by the SHT15. You can change them here, but remember to check first, as we said before, the pins used by WiShield. After this, we define some constants used in the functions that will get values from the SHT15.

//SHT15 pins

int dataPin = 5;
int sckPin = 6;

// SHT15 Sensor Coefficients
const float C1=-4.0; // for 12 Bit
const float C2= 0.0405; // for 12 Bit
const float C3=-0.0000028; // for 12 Bit
const float D1=-40.0; // for 14 Bit @ 5V
const float D2=0.01; // for 14 Bit DEGC
const float T1=0.01; // for 14 Bit @ 5V
const float T2=0.00008; // for 14 Bit @ 5V

Next come the different functions used to get values from the SHT15. I can't comment too much here, since it's not my code, but just two short notes. First, I'm using s version of Wayne's code made to show Celsius degrees. If you live in Fahrenheit lands, you could use the original version of the code. Check Wayne's blog for more information.

The second important thing is that, after the first freezing temperatures here, I noticed that the serialPrintFloat function doesn't work well with below zero temperatures. I just removed it and I use the print function to write SHT-15 values.


void resetSHT()
{
  pinMode(dataPin,OUTPUT);
  pinMode(sckPin,OUTPUT);
  
  shiftOut(dataPin, sckPin, LSBFIRST, 255);
  shiftOut(dataPin, sckPin, LSBFIRST, 255);
  
  digitalWrite(dataPin,HIGH);
  for(int i = 0; i < 15; i++){
     digitalWrite(sckPin, LOW);
     digitalWrite(sckPin, HIGH);
  }
}

//Specific SHT start command
void startSHT()
{
  pinMode(sckPin,OUTPUT);
  pinMode(dataPin,OUTPUT);
  digitalWrite(dataPin,HIGH);
  digitalWrite(sckPin,HIGH);
  digitalWrite(dataPin,LOW);
  digitalWrite(sckPin,LOW);
  digitalWrite(sckPin,HIGH);
  digitalWrite(dataPin,HIGH);
  digitalWrite(sckPin,LOW);
}

void writeByteSHT(byte data)
{ 
  pinMode(sckPin,OUTPUT);
  pinMode(dataPin,OUTPUT);  
  
//  digitalWrite(dataPin,LOW);
  shiftOut(dataPin,sckPin,MSBFIRST,data);
  
  pinMode(dataPin,INPUT);

  //Wait for SHT15 to acknowledge by pulling line low
  while(digitalRead(dataPin) == 1);
  
  digitalWrite(sckPin,HIGH);
  digitalWrite(sckPin,LOW);  //Falling edge of 9th clock
  
  //wait for SHT to release line
  while(digitalRead(dataPin) == 0 );
 
  //wait for SHT to pull data line low to signal measurement completion
  //This can take up to 210ms for 14 bit measurments
  int i = 0;
  while(digitalRead(dataPin) == 1 )
  {
    i++;
    if (i == 255) break;
    
    delay(10);
  } 
  
  //debug
  i *= 10;
  //Serial.print("Response time = ");
  //Serial.println(i);
}

//Read 16 bits from the SHT sensor
int readByte16SHT()
{
  int cwt = 0;
  unsigned int bitmask = 32768;
  int temp;
  
  pinMode(dataPin,INPUT);
  pinMode(sckPin,OUTPUT);
  
  digitalWrite(sckPin,LOW);
  
  for(int i = 0; i < 17; i++) {
    if(i != 8) {
      digitalWrite(sckPin,HIGH);
      temp = digitalRead(dataPin);
//      Serial.print(temp,BIN);
      cwt = cwt + bitmask * temp;
      digitalWrite(sckPin,LOW);
      bitmask=bitmask/2;
    }
    else {
      pinMode(dataPin,OUTPUT);
      digitalWrite(dataPin,LOW);
      digitalWrite(sckPin,HIGH);
      digitalWrite(sckPin,LOW);
      pinMode(dataPin,INPUT); 
    }
  }
  
  //leave clock high??
  digitalWrite(sckPin,HIGH);
  
  return cwt;
}

int getTempSHT()
{
  startSHT();
  writeByteSHT(B0000011);
  return readByte16SHT();
}

int getHumidSHT()
{
  startSHT();
  writeByteSHT(B00000101);
  return readByte16SHT();
}

Now, the function that will build the web page of the station. The trick here is using the print and println functions of WiServer to build the page. First we have a few lines to make a header, and then we start to call SHT15 functions to get sensor values and put them in the page with print and println.

Finally we get sensor values from the BMP085 with APM_BMP085.Read(); and print the barometric pressure with (APM_BMP085.Press)/100. This function gives pressure values in Pascals, so we divide the result by 100 to get the standard hectoPascals.

You could get the temperature from the BMP085 if you prefer it with (APM_BMP085.Temp)/10.0. We will use it in the second, Pachube, sketch, but in this first one, to keep it simple, we will stick to the SHT-15 data.

You can also use the BMP to get the altitude in feet, with the code on this example.

// This is the page serving function that generates web pages
boolean sendMyPage(char* URL) {
  
    // Check if the requested URL matches "/"
    if (strcmp(URL, "/") == 0) {
        // Use WiServer's print and println functions to write out the page content
        //header
        WiServer.print("<html>");
        WiServer.println("<h2>Welcome to Dalton</h2>");
        WiServer.println("<h3>Location: Sant Cugat del Valles, Barcelona, Spain</h3>");
        WiServer.println("<br>");
            
        //SHT-15
        int temp_raw = getTempSHT(); // get raw temperature value
        WiServer.print("<strong>Temperature: </strong>");
        float temp_degc = (temp_raw * D2) + D1; // Unit Conversion - See datasheet
        WiServer.print(temp_degc);
        WiServer.print(" Celsius");
        WiServer.println("<br>");
        int rh_raw = getHumidSHT(); // get raw Humidity value
        WiServer.print("<strong>Humidity: </strong>");
        float rh_lin = C3 * rh_raw * rh_raw + C2 * rh_raw + C1; // Linear conversion
        float rh_true = (temp_degc * (T1 + T2 * rh_raw) + rh_lin); // Temperature compensated RH
        WiServer.print(rh_true);
        WiServer.print("%");
        WiServer.println("<br>");
        //BMP-085
        APM_BMP085.Read();
        WiServer.print("<strong>Pressure: </strong>");
        WiServer.print((APM_BMP085.Press)/100);
        WiServer.print(" hPa");
        
        WiServer.print("</html>");
        // URL was recognized
        return true;
    }
    // URL not found
    return false;
}

The setup() function initializes the BMP085 (inserting a delay to allow it time to do it), initializes WiServer, configures the SHT15 pins and initializes the SHT15. The Serial.begin is just for debugging purposes, you can remove it in your final code.

void setup() {
  APM_BMP085.Init();   // APM ADC initialization
  delay(1000);
  // Initialize WiServer and have it use the sendMyPage function to serve pages
  WiServer.init(sendMyPage);
  pinMode(dataPin,OUTPUT);
  pinMode(sckPin,OUTPUT);
  // Enable Serial output
  Serial.begin(57600);
  resetSHT();
}

Then, the loop() function, that just makes WiServer run.

void loop(){

  // Run WiServer
  WiServer.server_task();
 
  delay(10);
}

After all this mess, we get a page like this that is constantly updated with values from the sensors (refreshing the browser, of course :) ).

The SHT15 and the BMP05 run pretty well. I've been able to compare readings with commercial weather stations and other sensors, and the values are very accurate for a home-made project.

The WiShield (or is it maybe the Arduino?) seems to have a problem. Sometimes after a whole day, sometimes after several days, the website stops working. Sometimes you can see the WiShield LED is off (and so, the network connection is lost), and sometimes it's on, but anyway the page doesn't load. A RESET solves the problem in a minute. Maybe it could be interesting to add a way in the sketch to make a reset from time to time, but the horror stories I've read about it on the Internet are not encouraging.

Another important note: the Wishield is unable to restablish the wireless connection if your wireless AP or router is reset or switched off and on. So, if you need to reset your AP, remember to reset Dalton too once you're finished with the AP.


Enter the Pachube

Pachube is a data brokerage platform that allows to store and share data from sensors. It offers an API that allows the connection of many different platforms and systems, and with one of their free accounts, you can store up to a month's data and graph it.

After reading documentation, and specially Pachube forums, I found a way to make it work with the WiShield. Among the different options Pachube offers, I chose to configure an automatic feed (that is, I serve the data and Pachube fetchs them each 15 minutes) and to offer the data in XML format.

The important point here is that while with the first sketch, we could keep Dalton private and access it only on our local network, or through a VPN, now we need to make it public and be able to serve its page through the Internet for Pachube to get it. Considering I have a home DSL connection, for me the easiest choice was using a dynamic DNS service. I used DynDNS. There are other services of this kind, some free, some paid. Configuring a dynamic DNS service to make Dalton available through the Internet is beyond the scope of this guide, but if you need more information, let me know through the email at the bottom of this page, and I will try to help you to the best of my knowledge.

Once we have Dalton connected to the Internet (you can try it while still using the first sketch), we can move to change our sketch to work with Pachube. But first of all, we need to make some changes in several Wishield library files. These changes are intended to make the WiShield serve an XML page that Pachube is able to accept for its feed. So, go to your libraries directory in your Arduino installation, open WiShield folder, and we'll explain the changes file by file. Remember to use a simple text editor to avoid introducing undesired characters or formats.

First of all, open apps-conf.h. This second sketch is also a WiServer sketch, so if you haven't done it yet, change the #define section in apps-conf.h to look like this:

//#define APP_WEBSERVER
//#define APP_WEBCLIENT
//#define APP_SOCKAPP
//#define APP_UDPAPP
#define APP_WISERVER

Next you need to add just this #define, you can put it right after the WISERVER line:

#define CONTENT_XML

Save your changes, and now open the strings.c file, and add this near the top of the file, for example after the two #include:

// Content type for xml data if CONTENT_XML is defined
#ifdef CONTENT_XML
const prog_char contentXML[] = {"Content-Type: application/xml"};
#endif // CONTENT_XML

And finally, open Wiserver.cpp and make these two changes. Near the top of the file, for example after #define LF 10, add the following lines:

#ifdef CONTENT_XML
extern const prog_char contentXML[];
#endif // CONTENT_XML

Next, look for the sendPage() function in this file, and add this right after WiServer.println_P(httpOK);

      // Include xml content type header if CONTENT_XML is defined
#ifdef CONTENT_XML
      WiServer.println_P(contentXML);
#endif // CONTENT_XML

Once we have done this, we can open the Arduino environment, and write the sketch. The complete sketch is here.

The sketch have some differences with the first one. The sendMyPage() function is totally different, and we include a few additional lines to be able to show another data, the dew point. We will explain now the changes.

First, we've added the math.h library and a couple of constants that we will use to calculate the dew point. The #include <math.h> goes will all the rest of libraries, at the top of the sketch, and the constants go right before the wireless configuration parameters. The comments explain the formula we're using.

//constants for dew point formula
//we follow the formula Td = b*gamma(T,RH)/a-gamma(T,RH) where gamma(T,RH)=((a*T)/(b+T)+ln(RH/100))
// Td =dew point, T = temperature in celsius, RH = relative humidity
#define b 237.7
#define a 17.271

Let's move on now to the sendMyPage() function. We need to generate a page written in the Extended Environments Markup Language (EEML), a protocol for sharing sensor data between remote responsive environments written in XML. You can learn more about this in the pachube site, but basically we have just to write some <data> and <value> tags enclosing our sensors data for Pachube to be able to parse and understand them.

The sendMyPage() function starts as usual, and then we write the first line, that says this is an XML document, and the second line, that says we're using EEML.

// This is the page serving function that generates web pages
boolean sendMyPage(char* URL) {
  
    // Check if the requested URL matches "/"
    if (strcmp(URL, "/") == 0) {
        // Use WiServer's print and println functions to write out the page content
        WiServer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
        WiServer.println("<eeml xmlns=\"http://www.eeml.org/xsd/005\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" 
        xsi:schemaLocation=\"http://www.eeml.org/xsd/005 http://www.eeml.org/xsd/005/005.xsd\">");

As you can see, the quotes (") must be escaped with \ to be correctly interpreted. Next, we open the <environment> tag...

WiServer.println("<environment>");

...and we start writing sensor values. For that, we need to write a <data id=> tag, and then a <value> tag. You can use any sequence of numbers for the ids; later on, when defining the feed at the Pachube site, we'll tell Pachube which id is which data.

WiServer.print("<data id=\"0\">\n<value>");

The \n (newline character) is not mandatory, but it will make a more human-readable XML for debugging purposes. Now we write the temperature data, but this time, instead of just getting the data from the SHT-15, we're going to get temperature data from both sensors and calculate the average:

//SHT-15
int temp_raw = getTempSHT(); // get raw temperature value
float temp_degc = (temp_raw * D2) + D1;
// Unit Conversion - See datasheet
//BMP-085
APM_BMP085.Read();
float temp_bmp = (APM_BMP085.Temp)/10.0;
float temp_avrg = (temp_degc+temp_bmp)/2;
WiServer.print(temp_avrg);

And now we close the tags.

WiServer.println("</value>\n</data>");

We do the same for the relative humidity and the barometric pressure...

WiServer.print("<data id=\"1\">\n<value>");
int rh_raw = getHumidSHT(); // get raw Humidity value
float rh_lin = C3 * rh_raw * rh_raw + C2 * rh_raw + C1; // Linear conversion
float rh_true = (temp_degc * (T1 + T2 * rh_raw) + rh_lin); // Temperature compensated RH
WiServer.print(rh_true);
WiServer.println("</value>\n</data>");
WiServer.print("<data id=\"2\">\n<value>");
//BMP-085
APM_BMP085.Read();
WiServer.print((APM_BMP085.Press)/100);
WiServer.println("</value>\n</data>");

...And now we calculate and show the dew point.

WiServer.print("<data id=\"3\">\n<value>");
float gamma = ((a*temp_avrg)/(b+temp_avrg))+log(rh_true/100);
WiServer.print((b*gamma)/(a-gamma));
WiServer.println("</value>\n</data>");

Next, just as a control, we include the temperature values from the SHT-15 and the BMP085.

WiServer.print("<data id=\"4\">\n<value>");
WiServer.print(temp_bmp);
WiServer.println("</value>\n</data>");
WiServer.print("<data id=\"5\">\n<value>");
WiServer.print(temp_degc);
WiServer.println("</value>\n</data>");

...and we close the XML document, and finish the function.

        WiServer.println("</environment>\n</eeml>");    
        // URL was recognized
        return true;
    }
    // URL not found
    return false;
}

If everything goes well, now Dalton will be serving a page that, depending on your browser, will show just the six values, or all the XML document.


Configuring the Pachube feed

Configuring the Pachube feed for Dalton is just a few steps. You will have first to create an account in Pachube. After setting up your account, you have to register a feed following the instructions provided here. You just have to select an automatic feed, give it a name and the URL, use the Verify URL button to check if Pachube can get the data, and fill in the rest of the optional fields if you want, plus the location of the sensors. Click the Save Feed button, and our new feed is created.

Now, in the page feed, we need to select Edit Feed to define the datastreams, that is, to tell Pachube what our sensors data is. These are the Datastreams for Dalton. As you can see, we specify the ids we defined when we generated the XML, and we add the kind of data it is, units, and symbols. Click Save Feed, and after 15 minutes, Pachube will start querying data.

From this moment, you can start working with Pachube in many different ways. You can check the feed page. Or you can use this free iPhone app, Pachubemon, to monitor your feed. Or use any of the many different applications you can find at the output section of the Pachube.apps site, including web gadgets that will allow you to generate graphs like these:


Further development

A few ideas I have for getting this project further:


Contact

If you want to comment something about the project, or if you need help building your own Dalton, or have any ideas to improve the station (I'm very interested in the points commented in the Further Development section), you can get in touch with me: jlmartinmas <---at---> gmail.com. English and Spanish spoken. :)




© 2010 Kawasemi Corp.