The goal of this project is to log some weather data and be able to access it from anywhere. There is some sensor data (temperature, relative humidity, pressure, and ambient light) and some computed data (dew point). You can see a few reports at my sensors site:
All source code is available from my local download or from github.com/larsi-org/WeatherStation
Like most logging projects this is almost like lego - you choose the sensors you like, connect them to the pins of a microcontroller that the sensor protocol require, and copy and paste all the code that is needed to talk to the individual sensors. I wanted to keep it simple, which means this project requires a host computer that collects the data and sends it out. The host computer can be replaced by an Arduino Ethernet shield or some 802.11 wireless connection, but this would make it slightly more complicated and I would loose the local graphing.
The microcontroller is responsible to setup all the pins to the correct mode (input/output) and to initialize all sensors. The main loop will just read the current value of all sensors and send it out over the serial port. This was relatively easy for me, because I already owned all the sensors and I had figured out the communication before I started this project. Here are the links to the subproject:
The sensors are connected as follows:
Sensor | Sensor Pin | Arduino Pin |
---|---|---|
SHT15 | GND | GND |
SCK | DIGITAL 7 | |
DATA | DIGITAL 6 | |
VCC | 5V | |
BMP085 | VCC | 3V3 |
GND | GND | |
EOC | ||
XCLR | ||
SCL | ANALOG 5 | |
SDA | ANALOG 4 | |
TEMT6000 | VCC | 5V |
GND | GND | |
S | ANALOG 0 |
The software has a few independent pieces and each is not very complicated. If a logging service is used, like www.thingspeak.com, only three pieces are necessary:
If you want to set up your own web logging than you need a few more additional pieces:
All the code can be found in the download section.
The Arduino code is just copied together from my previous projects:
/* * WeatherStation by Lars Schumann (make.larsi.org) * * Uses the following senors: SHT15, BMP085, and TEMT6000 */ #include "Wire.h" #define PIN_SDA 6 #define PIN_SCL 7 #define I2C_ADDRESS 0x77 const unsigned char oversampling_setting = 3; //oversamplig for measurement const unsigned char pressure_waittime[4] = { 5, 8, 14, 26 }; //just taken from the BMP085 datasheet int ac1; int ac2; int ac3; unsigned int ac4; unsigned int ac5; unsigned int ac6; int b1; int b2; int mb; int mc; int md; void bmp085_read_temperature_and_pressure(int* temperature, long* pressure) { long ut = bmp085_read_ut(); long up = bmp085_read_up(); long x1, x2, x3, b3, b5, b6, p; unsigned long b4, b7; //calculate the temperature x1 = ((long)ut - ac6) * ac5 >> 15; x2 = ((long) mc << 11) / (x1 + md); b5 = x1 + x2; *temperature = (b5 + 8) >> 4; //calculate the pressure b6 = b5 - 4000; x1 = (b2 * (b6 * b6 >> 12)) >> 11; x2 = ac2 * b6 >> 11; x3 = x1 + x2; //b3 = (((int32_t) ac1 * 4 + x3)<> 2; if (oversampling_setting == 3) b3 = ((int32_t) ac1 * 4 + x3 + 2) << 1; if (oversampling_setting == 2) b3 = ((int32_t) ac1 * 4 + x3 + 2); if (oversampling_setting == 1) b3 = ((int32_t) ac1 * 4 + x3 + 2) >> 1; if (oversampling_setting == 0) b3 = ((int32_t) ac1 * 4 + x3 + 2) >> 2; x1 = ac3 * b6 >> 13; x2 = (b1 * (b6 * b6 >> 12)) >> 16; x3 = ((x1 + x2) + 2) >> 2; b4 = (ac4 * (uint32_t) (x3 + 32768)) >> 15; b7 = ((uint32_t) up - b3) * (50000 >> oversampling_setting); p = b7 < 0x80000000 ? (b7 * 2) / b4 : (b7 / b4) * 2; x1 = (p >> 8) * (p >> 8); x1 = (x1 * 3038) >> 16; x2 = (-7357 * p) >> 16; *pressure = p + ((x1 + x2 + 3791) >> 4); } unsigned int bmp085_read_ut() { write_register(0xf4,0x2e); delay(5); //longer than 4.5 ms return read_int_register(0xf6); } void bmp085_get_cal_data() { //Serial.println("Reading Calibration Data"); ac1 = read_int_register(0xAA); //Serial.print("AC1: "); //Serial.println(ac1,DEC); ac2 = read_int_register(0xAC); //Serial.print("AC2: "); //Serial.println(ac2,DEC); ac3 = read_int_register(0xAE); //Serial.print("AC3: "); //Serial.println(ac3,DEC); ac4 = read_int_register(0xB0); //Serial.print("AC4: "); //Serial.println(ac4,DEC); ac5 = read_int_register(0xB2); //Serial.print("AC5: "); //Serial.println(ac5,DEC); ac6 = read_int_register(0xB4); //Serial.print("AC6: "); //Serial.println(ac6,DEC); b1 = read_int_register(0xB6); //Serial.print("B1: "); //Serial.println(b1,DEC); b2 = read_int_register(0xB8); //Serial.print("B2: "); //Serial.println(b1,DEC); mb = read_int_register(0xBA); //Serial.print("MB: "); //Serial.println(mb,DEC); mc = read_int_register(0xBC); //Serial.print("MC: "); //Serial.println(mc,DEC); md = read_int_register(0xBE); //Serial.print("MD: "); //Serial.println(md,DEC); } long bmp085_read_up() { write_register(0xf4,0x34+(oversampling_setting<<6)); delay(pressure_waittime[oversampling_setting]); unsigned char msb, lsb, xlsb; Wire.beginTransmission(I2C_ADDRESS); Wire.send(0xf6); // register to read Wire.endTransmission(); Wire.requestFrom(I2C_ADDRESS, 3); // read a byte while(!Wire.available()) { // waiting } msb = Wire.receive(); while(!Wire.available()) { // waiting } lsb |= Wire.receive(); while(!Wire.available()) { // waiting } xlsb |= Wire.receive(); return (((long)msb<<16) | ((long)lsb<<8) | ((long)xlsb)) >>(8-oversampling_setting); } void write_register(unsigned char r, unsigned char v) { Wire.beginTransmission(I2C_ADDRESS); Wire.send(r); Wire.send(v); Wire.endTransmission(); } char read_register(unsigned char r) { unsigned char v; Wire.beginTransmission(I2C_ADDRESS); Wire.send(r); // register to read Wire.endTransmission(); Wire.requestFrom(I2C_ADDRESS, 1); // read a byte while(!Wire.available()) { // waiting } v = Wire.receive(); return v; } int read_int_register(unsigned char r) { unsigned char msb, lsb; Wire.beginTransmission(I2C_ADDRESS); Wire.send(r); // register to read Wire.endTransmission(); Wire.requestFrom(I2C_ADDRESS, 2); // read a byte while(!Wire.available()) { // waiting } msb = Wire.receive(); while(!Wire.available()) { // waiting } lsb = Wire.receive(); return (((int)msb<<8) | ((int)lsb)); } void resetSHT() { pinMode(PIN_SDA, OUTPUT); pinMode(PIN_SCL, OUTPUT); shiftOut(PIN_SDA, PIN_SCL, LSBFIRST, 255); shiftOut(PIN_SDA, PIN_SCL, LSBFIRST, 255); digitalWrite(PIN_SDA, HIGH); for(int i = 0; i < 15; i++) { digitalWrite(PIN_SCL, LOW); digitalWrite(PIN_SCL, HIGH); } } //Specific SHT start command void startSHT() { pinMode(PIN_SCL, OUTPUT); pinMode(PIN_SDA, OUTPUT); digitalWrite(PIN_SDA, HIGH); digitalWrite(PIN_SCL, HIGH); digitalWrite(PIN_SDA, LOW); digitalWrite(PIN_SCL, LOW); digitalWrite(PIN_SCL, HIGH); digitalWrite(PIN_SDA, HIGH); digitalWrite(PIN_SCL, LOW); } void writeByteSHT(byte data) { pinMode(PIN_SCL, OUTPUT); pinMode(PIN_SDA, OUTPUT); // digitalWrite(PIN_SDA,LOW); shiftOut(PIN_SDA,PIN_SCL, MSBFIRST, data); pinMode(PIN_SDA, INPUT); //Wait for SHT15 to acknowledge by pulling line low while(digitalRead(PIN_SDA) == 1); digitalWrite(PIN_SCL, HIGH); digitalWrite(PIN_SCL, LOW); //Falling edge of 9th clock //wait for SHT to release line while(digitalRead(PIN_SDA) == 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(PIN_SDA) == 1 ) { i += 10; if (i >= 1000) break; delay(10); } //debug //Serial.print("Response time = "); //Serial.println(i); } //Read 16 bits from the SHT sensor int readByte16SHT() { int cwt = 0; pinMode(PIN_SDA, INPUT); pinMode(PIN_SCL, OUTPUT); digitalWrite(PIN_SCL, LOW); for(int i = 0; i < 17; i++) { if(i != 8) { digitalWrite(PIN_SCL, HIGH); cwt = cwt << 1 | digitalRead(PIN_SDA); digitalWrite(PIN_SCL, LOW); } else { pinMode(PIN_SDA, OUTPUT); digitalWrite(PIN_SDA, LOW); digitalWrite(PIN_SCL, HIGH); digitalWrite(PIN_SCL, LOW); pinMode(PIN_SDA, INPUT); } } //leave clock high?? digitalWrite(PIN_SCL, HIGH); //Serial.println(); return cwt; } int getTempSHT() { startSHT(); writeByteSHT(B0000011); return readByte16SHT(); } int getHumidSHT() { startSHT(); writeByteSHT(B00000101); return readByte16SHT(); } void setup() { Serial.begin(9600); // start serial for output //Serial.println("Setting up BMP085"); pinMode(PIN_SDA, OUTPUT); pinMode(PIN_SCL, OUTPUT); Wire.begin(); bmp085_get_cal_data(); resetSHT(); delay(2000); } void loop() { int temperature = 0; long pressure = 0; bmp085_read_temperature_and_pressure(&temperature,&pressure); float val; val = (float)getTempSHT(); float tempC = -40.0 + 0.01 * val; val = (float)getHumidSHT(); float humid = -4.0 + 0.0405 * val + -0.0000028 * val * val; // http://en.wikipedia.org/wiki/Dew_point float a = 17.271; float b = 237.7; float gamma = log(humid / 100) + a / (b / tempC + 1); float dewPoint = b / (a / gamma - 1); Serial.print(tempC); Serial.print(","); //Serial.print(temperature / 10, DEC); //Serial.print("."); //Serial.print(temperature % 10, DEC); //Serial.print(","); Serial.print(humid); Serial.print(","); Serial.print(pressure); Serial.print(","); Serial.print(analogRead(0), DEC); // prints the value OF analog input pin 0 Serial.print(","); Serial.println(dewPoint); delay(10000); }
It writes to the serial port every 10 seconds all the sensor value separated by comma (temperature in °C, relative humidity in %, pressure in Pa, ambient light (0-1023), dew point in °C):
19.53,74.03,97493,2,14.77 19.48,74.09,97503,0,14.74 19.51,74.01,97509,0,14.75 19.51,74.01,97502,1,14.75 19.53,73.98,97495,1,14.76
The Processing code uses a modified version of the arduinoscope's channel library:
/* * Channel.java by Lars Schumann (make.larsi.org) * * Draws a float array as a graph * * It is based on Channel.java by David Konsumer <david.konsumer@gmail.com> * but had to be modified */ import processing.core.PApplet; import processing.core.PConstants; public class Channel implements PConstants { PApplet parent; String label; float minval; float maxval; int dimX; // width int dimY; // height int offY; // y start position int COLOR_GRAPH; // color for lines int COLOR_CENTER; // color for center line private float[] values; // all values in the graph public Channel(PApplet parent, String label, float minval, float maxval, int dimX, int dimY, int offY) { this.parent = parent; this.label = label; this.minval = minval; this.maxval = maxval; this.dimX = dimX; this.dimY = dimY; this.offY = offY; // set some defaults COLOR_GRAPH = 0xFFFF0000; // red COLOR_CENTER = 0xFF999999; // gray values = new float[dimX]; for (int i = 0; i < dimX; i++) values[i] = minval; } public void draw() { // draw center line parent.stroke(COLOR_CENTER); parent.line(0, offY + (dimY/2), dimX, offY + (dimY/2)); parent.stroke(COLOR_GRAPH); int yOld = getY(0); for (int x = 1; x < dimX; x++) { int yNew = getY(x); parent.line(x, yOld, x, yNew); yOld = yNew; } } // add a single point public void addData(float val) { for (int i = 0; i < dimX - 1; i++) values[i] = values[i + 1]; values[dimX - 1] = val; if (val < minval) minval = val; if (val > maxval) maxval = val; } // add a single point public float getCurrentData() { return values[dimX - 1]; } private int getY(int index) { return parent.round(parent.map(values[index], minval, maxval, offY + dimY - 2, offY + 1)); } }
The ThingSpeak.pde is the main application. It gets data from the serial port and updates the graphs. Every 5 minutes it send the data to ThingSpeak. It constructs an URL that looks like this (all values in <> need to be replaced by the real values):
http://api.thingspeak.com/update?key=<key>&field1=<value1>&field2=<value2>&field3=<value3>&field4=<value4>&field5=<value5>
Processing does not have any direct way to request an URL from a server, but Processing allows to use any Java code. So, this works:
String url =; try { java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.net.URL(url).openStream())); String line = reader.readLine(); while (line != null) { System.out.println(line); line = reader.readLine(); } } catch (java.io.IOException e) { e.printStackTrace(); }
The complete code is here (you have to adjust your ThingSpeak update key and the port number of your serial connection):
/* * ThingSpeak.pde by Lars Schumann (make.larsi.org) * * Collects weather data from an Arduino and * sends the data to ThingSpeak every 5 minutes. */ import processing.serial.*; // Logging server String SERVER = "http://api.thingspeak.com/update?key=0000000000000000"; // Station channels String labels[] = { "Temperature", "RelativeHumidity", "Pressure", "LightTEMT6000", "DewPoint" }; String fields[] = { "field1", "field2", "field3", "field4", "field5" }; float mins[] = { 30, 0, 950, 0, 30 }; float maxs[] = { 100, 100, 1050, 1023, 100 }; float m[] = { 1.8, 1, 0.01, 1, 1.8 }; float n[] = { 32, 0, 0, 0, 32 }; Channel channels[] = new Channel[labels.length]; Serial port; int LINE_FEED=10; // setup vals from serial float[] vals = new float[labels.length]; long currentID = 0; long lastID = 2; // skip first two values String lastTime = ""; void setup() { size(1080, 800, P2D); frameRate(1); background(0); // set these up under tools/create font, if they are not setup. textFont(loadFont("TrebuchetMS-20.vlw")); int dimX = width - 180; // 180 margin for text int dimY = height / labels.length; for (int i = 0; i < labels.length; i++) channels[i] = new Channel(this, labels[i], mins[i], maxs[i], dimX, dimY, dimY * i); println("Available serial ports:"); println(Serial.list()); port = new Serial(this, Serial.list()[1], 9600); // clear and wait for linefeed port.clear(); port.bufferUntil(LINE_FEED); } void draw() { background(0xFFFFFFFF); // white // update channels if (currentID > lastID) { for (int i = 0; i < labels.length; i++) channels[i].addData(m[i] * vals[i] + n[i]); lastID = currentID; } // al the same int dimX = channels[0].dimX; int dimY = channels[0].dimY; // draw channels for (int i = 0; i < labels.length; i++) { int offY = channels[i].offY; // draw lines stroke(0xFF000000); // black line(0, offY, width, offY); line(0, offY + dimY - 1, width, offY + dimY - 1); channels[i].draw(); // add labels fill(0xFFFF0000); // red text(channels[i].label, dimX + 5, offY + 20); text(channels[i].getCurrentData(), dimX + 60, offY + 50); text(channels[i].minval, dimX + 60, offY + 80); text(channels[i].maxval, dimX + 60, offY + 110); fill(0xFF000000); // black text("now:", dimX + 5, offY + 50); text("min:", dimX + 5, offY + 80); text("max:", dimX + 5, offY + 110); } // draw text seperator, based on first scope stroke(0xFF000000); // black line(0, 0, 0, height); line(dimX, 0, dimX, height); line(width - 1, 0, width - 1, height); } // handle serial data void serialEvent(Serial p) { String data = trim(p.readStringUntil(LINE_FEED)); if (data != null) { String[] data_split = split(data, ','); for (int i = 0; i < labels.length; i++) vals[i] = float(data_split[i]); DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); java.util.Date date = new java.util.Date(); int m = 5 * (date.getMinutes() / 5); int h = date.getHours(); String currentTime = dateFormat.format(date) + " " + twoDigits(h) + ":" + twoDigits(m) + ":00"; String url = SERVER; if (!currentTime.equals(lastTime)) { lastTime = currentTime; for (int i = 0; i < labels.length; i++) url += "&" + fields[i] + "=" + vals[i]; try { java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(new java.net.URL(url).openStream())); String line = reader.readLine(); while (line != null) { System.out.println(line); line = reader.readLine(); } } catch (java.io.IOException e) { e.printStackTrace(); } } } currentID++; } String twoDigits(int v) { return "" + (v < 10 ? "0" : "") + v; }
This is an example of the graphical output:
ThingSpeak makes it easy to access your own data. Here is just an example, but much better graphs are possible:
<html> <head> <title>Weather Condition</title> </head> <body> <h1>Weather Condition <?php print(date('m/d/Y')); ?></h1> <table width="100%"> <tr> <td><div align="center"><iframe width="480" height="250" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/346/charts/1?timescale=10"></iframe></div></td> <td><div align="center"><iframe width="480" height="250" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/346/charts/2?timescale=10"></iframe></div></td> </tr> <tr> <td><div align="center"><iframe width="480" height="250" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/346/charts/3?timescale=10"></iframe></div></td> <td><div align="center"><iframe width="480" height="250" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/346/charts/4?timescale=10"></iframe></div></td> </tr> <tr> <td><div align="center"><iframe width="480" height="250" style="border: 1px solid #cccccc;" src="https://thingspeak.com/channels/346/charts/5?timescale=10"></iframe></div></td> <td> </td> </tr> </table> </body> </html>
This is the more advanced section, if you want to set up your own web logging service and use Google's Chart API to visualize the graphs.
CREATE TABLE `datalogger` ( `DateTime` datetime NOT NULL, `Location` enum('Attic','BathRoom2','Bathroom3','Bedroom2','Bedroom2','Bench','Den','DiningRoom','FamilyRoom','Garage','Garden','GuestRoom','HisOffice','HerOffice','KidsRoom1','KidsRoom2','KidsRoom3','Kitchen','LivingRoom','ManCave','MasterBathRoom','MasterBedRoom','MediaRoom','Office','Porch','Study','UtilityRoom','WorkoutRoom') NOT NULL, `Type` enum('DewPoint','Energy','LightTEMT6000','Power','Pressure','RelativeHumidity','RPM','Temperature') NOT NULL, `Value` float NOT NULL ) ENGINE=MyISAM DEFAULT CHARSET=utf8;