The intensity of natural light in my basement.

Arduino: Super Graphing Data Logger

The intensity of natural light in my basement.
Sections:

  1. Introduction
  2. The Results
  3. How to Make One For Yourself

Introduction

What is the Super Graphing Data Logger (SGDL)? It is an Arduino project that integrates data logging and the graphing of this data online using little more than an Arduino with the appropriate shields and sensors.   It differs from similar projects in that it doesn’t require a separate server or system to collect the data or to run script for the actual plot. Between the Arduino and the user’s browser, everything is taken care of.

Some time back I came across this neat javaScript based library for plotting and graphing called Highcharts JS. It didn’t take long for me to realize that charting with javaScript is very convenient for projects in which the server is limited in it’s capabilities, such as when using an Arduino with the Ethernet shield. Since the user’s browser does all the heavy lifting, the Arduino only needs to serve the files which is something it is perfectly capable of. This is especially true now that the Ethernet and SD libraries included in 1.0 support opening of multiple files simultaneously amongst other things. Thus the use of Highcharts allows us to create beautiful interactive charts based on data logged by the Arduino using nothing but the Arduino (and your browser, and a public javaScript CDN).

The Results

The best way to appreciate the final product is to actually play with it. While I’m not going to open up my home network and Arduino to the big wide internet, I have mirrored the pages and datafiles it produces on the webhost I used for my Has the World Ended Yet? project. You can find them here. These won’t be updated with new datapoints like the actual Arduino version will be, but they should at least give a fair impression of how the project looks and feels without the need to actually implement it.

For those who are unsure what they are looking at, I’ll offer a quick interpretation:

The list of data files available for graphing.
The list of data files available for graphing.

Going to the above page, we see that we are presented with a very basic list of the data files that can be selected from. Clicking any of them will cause  the graph for that datafile to be loaded (much more quickly than the Arduino can manage).

A graph for the first week of data collected.
A graph for the first week of data collected.

This chart for the 25-12-12.CSV file is already complete, and won’t have any new data added to it in the future, because the files for subsequent weeks have already been made. There is a lot to see though. The two data points that are at 1000 on the y-axis are from when I pointed a bright flashlight directly at the photo sensor. All of the data points between 300 and 400 on the y-axis are the result of the basement lights being on. The abnormally large gaps in the data are periods when the Arduino was powered off because I was still tweaking and developing it. Finally, the short humps that occur everyday are the result of natural light coming through one of the basement windows. By zooming in on one of them, we can see even more detail:

The intensity of natural light in my basement.
The intensity of natural light in my basement.

The first thing we notice is that the levels rise from zero to about 65 before falling and levelling out at close to 35 for two hours. This is followed by a another small increase before it ultimately decreases down to a value of ~10 where it levels out. That middle valley where the light levels are equal to 35 is due to the shadow cast  on the basement window by our neighbour’s house to the south of us. The levelling out of the light intensity at 10 after all the daylight has disappeared is because a light out in the hallway is usually on in the evening. It is eventually turned off for the night, causing the light levels to drop to zero where they will usually remain until the next morning. I must admit, I’m impressed that the cheap $1.00 photoresistor is capable of capturing this level of detail, and that these trends are so easily interpreted from the graphs.

How to Make One For Yourself

To replicate this project, a few things are necessary. You’ll obviously need an Arduino capable of connecting over Ethernet and storing files on an SD card. In my case, this is achieved through the use of an Uno with the Ethernet shield. Presumably an Arduino Ethernet model will also work fine, though I have not personally tested it. Other non official Ethernet shields and SD card adapters may also work if they use the same libraries, though I make no guarantees. For the more adventurous, it may be possible to adapt my code to achieve the same functionality using a Wifi shield. You will also need a data source of some sort. For my project I chose to use a very cheap photoresistor, which I rigged up on a small perf board to plug directly into the 5v, gnd, and A0 pins of my Arduino (or more precisely,  the headers on the Ethernet shield). It is set up in such a way that the minimum recordable light intensity is zero, while the maximum is 1024.

The photo sensor board fits like a charm.
The photo sensor board fits like a charm.

DSCF2931
One header is bent to reach A0.

The pins on the male headers don’t quite line up, so I intentionally used extra long ones and added a slight S-curve to the one that goes to A0. This can be seen in better detail above. For those who are interested, the circuit is very simple:

The circuit.
The circuit.

Before we get started, we need to make sure our SD card is good to go. It should be formatted as a FAT16 or FAT32 filesystem, the details of which are available on the official Arduino website. Once that is done, we need to ensure two things are present in the root directory of the card: the HC.htm file, and a data/ directory for our datafiles. The data directory is easily made with the same computer that was used to format the card provided one has an SD card reader of some sort. The HC.htm simply consists of the following code:

<!DOCTYPE HTML>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Super Graphing Data Logger!</title>

        <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
        <script type="text/javascript">
function getDataFilename(str){
    point = str.lastIndexOf("file=")+4;

    tempString = str.substring(point+1,str.length)
    if (tempString.indexOf("&") == -1){
    return(tempString);
    }
    else{
        return tempString.substring(0,tempString.indexOf("&"));
    }
        
}

query  = window.location.search;

var dataFilePath = "/data/"+getDataFilename(query);

$(function () {
    var chart;
    $(document).ready(function() {
    
        // define the options
        var options = {
    
            chart: {
                renderTo: 'container',
                zoomType: 'x',
                spacingRight: 20
            },
    
            title: {
                text: 'Light levels recorded by the Arduino'
            },
    
            subtitle: {
                text: 'Click and drag in the plot area to zoom in'
            },
    
            xAxis: {
                type: 'datetime',
                maxZoom: 2 * 3600000
            },
    
            yAxis: {
                title: {
                    text: 'Light Levels (0 - 1024)'
                },
                min: 0,
                startOnTick: false,
                showFirstLabel: false
            },
    
            legend: {
                enabled: false
            },
    
            tooltip: {
                formatter: function() {
                        return '<b>'+ this.series.name +'</b><br/>'+
                        Highcharts.dateFormat('%H:%M - %b %e, %Y', this.x) +': '+ this.y;
                }
            },
    
            plotOptions: {
                series: {
                    cursor: 'pointer',
                    lineWidth: 1.0,
                    point: {
                        events: {
                            click: function() {
                                hs.htmlExpand(null, {
                                    pageOrigin: {
                                        x: this.pageX,
                                        y: this.pageY
                                    },
                                    headingText: this.series.name,
                                    maincontentText: Highcharts.dateFormat('%H:%M - %b %e, %Y', this.x) +':<br/> '+
                                        this.y,
                                    width: 200
                                });
                            }
                        }
                    },
                }
            },
    
            series: [{
                name: 'Light Levels',
                marker: {
                    radius: 2
                }
            }]
        };
    
    
        // Load data asynchronously using jQuery. On success, add the data
        // to the options and initiate the chart.
        // http://api.jquery.com/jQuery.get/
        jQuery.get(dataFilePath, null, function(csv, state, xhr) {
            var lines = [],
                date,
    
                // set up the two data series
                lightLevels = [];
    
            // inconsistency
            if (typeof csv !== 'string') {
                csv = xhr.responseText;
            }
    
            // split the data return into lines and parse them
            csv = csv.split(/\n/g);
            jQuery.each(csv, function(i, line) {
    
                // all data lines start with a double quote
                line = line.split(',');
                date = parseInt(line[0], 10)*1000;
    
                lightLevels.push([
                    date,
                    parseInt(line[1], 10)
                ]);
                
            });
    
            options.series[0].data = lightLevels;
    
            chart = new Highcharts.Chart(options);
        });
    });
    
});
        </script>
    </head>
    <body>
        <p style="text-align:center;">Please allow the chart to load, it may take up to 30 seconds </p>
        <hr/>
<script src="http://cdnjs.cloudflare.com/ajax/libs/highcharts/2.3.5/highcharts.js"></script>

<!-- Additional files for the Highslide popup effect -->
<script type="text/javascript" src="http://www.highcharts.com/highslide/highslide-full.min.js"></script>
<script type="text/javascript" src="http://www.highcharts.com/highslide/highslide.config.js" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="http://www.highcharts.com/highslide/highslide.css" />

<div id="container" style="min-width: 400px; height: 400px; margin: 0 auto"></div>

    </body>
</html>

You will need to edit this file first to make sure it points towards the preferred  location of your highcharts.js files. You can leave this as the public CDN: http://cdnjs.cloudflare.com/ajax/libs/highcharts/2.3.5/highcharts.js, change it to point towards your own webhost, or it can even be on the Arduino’s SD card (this will be slow). It is not necessary to create a datafile before hand, the SGDL sketch will take care of that when it decides to record its first data point. Before we get that far though, it is necessary to make sure we have configured the EEPROM memory for the SGDL sketch. This is very easily accomplished using a separate sketch, which I have called EEPROM_config. This sketch (along with SGDL itself) requires an extra library called EEPROMAnything, which needs to be added to the Arduino’s libraries folder wherever one’s sketchbook folder is. While you’re at it, you should also add the Time library which we need for SGDL.

/* ************************************************************************
 * ***            Super Graphing Data Logger - EEPROM config            ***
 * ************************************************************************
 * Everett Robinson, December 2012.
 *
 * The following extra non standard libraries were used, and will need to be
 * added to the libraries folder:
 * - EEPROMAnything: http://playground.arduino.cc/Code/EEPROMWriteAnything
 *
 * This sketch helps you set the values in EEPROM which are necessary for
 * Super Graphing Data Logger. It should only need the be run once before
 * the first time you set up SGDL, or in the unlikely event that the EEPROM
 * becomes corrupted.
 *
 * Please ensure that the values in configuration config are appropriate for
 * your project before uncommenting the EEPROM_writeAnything(0, config); line.
 *
 */

#include <EEPROM.h>
#include <EEPROMAnything.h>

typedef struct{
    unsigned long newFileTime;
    char workingFilename[19];
  } configuration;

//This is a one off thing, so everything is in setup
void setup(){
  Serial.begin(9600);
  
  //Create the config struct to write to EEPROM, change values as appropriate
  //Make sure your filename is not too long for the workingFilename char array 
  configuration config = {1356912000L,"/data/25-12-12.csv"};
  //Write the values to the EEPROM
  //EEPROM_writeAnything(0, config);       //Uncomment when you're sure everything is correct
  configuration config2;                   //Create a second config struct for verification
  EEPROM_readAnything(0,config2);
  Serial.print("The value read from EEPROM for newFileTime is: ");
  Serial.println(config2.newFileTime);
  Serial.print("The value read from EEPROM for workingFilename is: ");
  Serial.println(config2.workingFilename);
  Serial.println("If those values are correct then everything went as planned. Otherwise,");
  Serial.println("please double check that the values declared for the struct config are");
  Serial.println("correct and that that EEPROM_writeAnything line is uncommented.");
}


void loop(){
}

I have intentionally commented out the write line so that no one writes junk to the EEPROM by accident. While the EEPROM has a life of ~100,000 write cycles, I’d rather not waste any of them. Please review the sketch carefully and ensure you’ve adjusted it accordingly before uploading it to the Arduino. The most important thing is to ensure that your newFileTime is something sensible (in the near future most of all).

Now that that’s all taken care of, we’re ready to get SGDL all set up! The code will need a few adjustments for your own specific setup, mostly in regards to the Ethernet MAC and IP addresses. I trust that anyone making use of this code already knows how to configure their router to work with the Arduino, and that they can find the appropriate local IP address to update this sketch with. You may also wish to change the timeserver IP address to one that is geographically closer to yourself.

I currently have my code set up to make a measurement every 10 minutes, and to create a new data file every week. You are welcome to change those parameters, just be aware that the current data file management names files using a dd-mm-yy.csv date format, so the new file interval should be at least 24 hours. Another concern, is that the shorter the measurement interval and the longer the new data file interval is, the larger the files will be. Because the Arduino is not especially powerful, this will have consequences for the loading times of each chart.

/* ************************************************************************
 * ***                    Super Graphing Data Logger                    ***
 * ************************************************************************
 * Everett Robinson, December 2012. More at: http://everettsprojects.com
 *
 * This sketch relies on the SD and ethernet libraries in arduino 1.0 or newer.
 * The following extra non standard libraries were also used, and will need to
 * be added to the libraries folder:
 * - Time: http://playground.arduino.cc/Code/Time
 * - EEPROMAnything: http://playground.arduino.cc/Code/EEPROMWriteAnything
 *
 * If this is your first time setting up this project, please go get the
 * EEPROM_config sketch from http://everettsprojects.com so that you can 
 * configure the config struct in the EEPROM memory. Usage of the EEPROM 
 * is needed to make the project resiliant against a temporary loss of power.
 *
 * You must also ensure that you have the HC.htm file in the root directory
 * of your SD card, as well as a data directory where the datafiles will be
 * stored.
 *
 * This sketch combines the functionality of an existing fileserver example
 * which can be found at http://www.ladyada.net/learn/arduino/ethfiles.html
 * with the Datalogger example that comes with the new SD library from 1.0,
 * as well as some code from the UdpNtpClient example that cones with the
 * ethernet library. 
 *
 * Added to all of these are some tricks to make it manage and serve up the
 * datafiles in conjunction with a page which uses highcharts JS to graph it.
 * This is basically accomplished using the arduino by itself. Because I
 * actually host the highcharts.js files externally, this is true more in
 * theory than in actual practice, but oh well. It should work just fine to
 * have the highcharts.js file on the arduino's SD card, though loading the 
 * page will be painfully slow.
 *
 * Some of the code this was derived from may or may not be under a GPL
 * licence; I'm not entirely sure. I suppose anyone using this should treat 
 * it like it is too, but I don't really care too much.
 * Also if one intends to use this for commercial applications, it may be
 * necessary to purchase a license for Highcharts.
 *
 * Changes:   -------------------------------------------------------------
 * January 2013: Updated so that the dd-mm-yy.csv file format is properly 
 * followed, all single digit days, months, and years will have a leading 
 * zero now. 
 *
 */

#include <SD.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <SPI.h>
#include <string.h>
#include <Time.h>
#include <EEPROM.h>
#include <EEPROMAnything.h>
#include <avr/pgmspace.h>

/************ ETHERNET STUFF ************/
byte mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x4C, 0x64 };
byte ip[] = { 192,168,1, 100 };
EthernetServer server(80);

/************** NTP STUFF ***************/
unsigned int localPort = 8888;          // local port to listen for UDP packets
IPAddress timeServer(132, 163, 4, 101); //NIST time server IP address: for more info
                                        //see http://tf.nist.gov/tf-cgi/servers.cgi

const int NTP_PACKET_SIZE= 48; //NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets 
EthernetUDP Udp;

/*** DATA LOGGER AND TIMER CONTROLS ****/
const int analogPin = 0;
unsigned long lastIntervalTime = 0; //The time the last measurement occured.
#define MEASURE_INTERVAL 600000     //10 minute intervals between measurements (in ms)
unsigned long newFileTime;          //The time at which we should create a new week's file
#define FILE_INTERVAL 604800        //One week worth of seconds

//A structure that stores file config variables from EEPROM
typedef struct{                     
    unsigned long newFileTime;      //Keeps track of when a newfile should be made.
    char workingFilename[19];       //The path and filename of the current week's file
} configuration;
  
configuration config;               //Actually make our config struct


// Strings stored in flash mem for the Html Header (saves ram)
prog_char HeaderOK_0[] PROGMEM = "HTTP/1.1 200 OK";            //
prog_char HeaderOK_1[] PROGMEM = "Content-Type: text/html";    //
prog_char HeaderOK_2[] PROGMEM = "";                           //

// A table of pointers to the flash memory strings for the header
PROGMEM const char *HeaderOK_table[] = {   
  HeaderOK_0,
  HeaderOK_1,
  HeaderOK_2
};

// A function for reasy printing of the headers  
void HtmlHeaderOK(EthernetClient client) {
  
    char buffer[30]; //A character array to hold the strings from the flash mem
    
    for (int i = 0; i < 3; i++) {
      strcpy_P(buffer, (char*)pgm_read_word(&(HeaderOK_table[i]))); 
      client.println( buffer );
    }
} 
  
  
// Strings stored in flash mem for the Html 404 Header
prog_char Header404_0[] PROGMEM = "HTTP/1.1 404 Not Found";     //
prog_char Header404_1[] PROGMEM = "Content-Type: text/html";    //
prog_char Header404_2[] PROGMEM = "";                           //
prog_char Header404_3[] PROGMEM = "<h2>File Not Found!</h2>"; 

// A table of pointers to the flash memory strings for the header
PROGMEM const char *Header404_table[] = {   
  Header404_0,
  Header404_1,
  Header404_2,
  Header404_3
};

// Easy peasy 404 header function
void HtmlHeader404(EthernetClient client) {
  
    char buffer[30]; //A character array to hold the strings from the flash mem
    
    for (int i = 0; i < 4; i++) {
      strcpy_P(buffer, (char*)pgm_read_word(&(Header404_table[i]))); 
      client.println( buffer );
    }
} 


void setup() {
  Serial.begin(9600);
  
  pinMode(10, OUTPUT);          // set the SS pin as an output (necessary!)
  digitalWrite(10, HIGH);       // but turn off the W5100 chip! 
  
  // see if the card is present and can be initialized:
  if (!SD.begin(4)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    return;
  }
  Serial.println("card initialized.");
  
  // The SD card is working, start the server and ethernet related stuff!
  Ethernet.begin(mac, ip);
  server.begin();
  Udp.begin(localPort);
  EEPROM_readAnything(0,config); // make sure our config struct is syncd with EEPROM
}


// A function that takes care of the listing of files for the
// main page one sees when they first connect to the arduino.
// it only lists the files in the /data/ folder. Make sure this
// exists on your SD card.
void ListFiles(EthernetClient client) {
  
  File workingDir = SD.open("/data");
  
  client.println("<ul>");
  
    while(true) {
      File entry =  workingDir.openNextFile();
       if (! entry) {
         break;
       }
       client.print("<li><a href=\"/HC.htm?file=");
       client.print(entry.name());
       client.print("\">");
       client.print(entry.name());
       client.println("</a></li>");
       entry.close();
    }
  client.println("</ul>");
  workingDir.close();
}

// A function to get the Ntp Time. This is used to make sure that the data
// points recorded by the arduino are referenced to some meaningful time
// which in our case is UTC represented as unix time (choosen because it 
// works simply with highcharts without too much unecessary computation).
unsigned long getTime(){
  sendNTPpacket(timeServer); // send an NTP packet to a time server

  // wait to see if a reply is available
  delay(1000);  
  if ( Udp.parsePacket() ) {  
    // We've received a packet, read the data from it
    Udp.read(packetBuffer,NTP_PACKET_SIZE);  // read the packet into the buffer

    //the timestamp starts at byte 40 of the received packet and is four bytes,
    // or two words, long. First, esxtract the two words:

    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);  
    // combine the four bytes (two words) into a long integer
    // this is NTP time (seconds since Jan 1 1900):
    unsigned long secsSince1900 = highWord << 16 | lowWord;  
    // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
    const unsigned long seventyYears = 2208988800UL;     
    // subtract seventy years:
    unsigned long epoch = secsSince1900 - seventyYears;  
    // return Unix time:
    return epoch;
  }
}

// send an NTP request to the time server at the given address,
// necessary for getTime().
unsigned long sendNTPpacket(IPAddress& address){
  
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE); 
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49; 
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:         
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer,NTP_PACKET_SIZE);
  Udp.endPacket(); 
}


// How big our line buffer should be for sending the files over the ethernet.
// 75 has worked fine for me so far.
#define BUFSIZ 75

void loop(){
  if ((millis() % lastIntervalTime) >= MEASURE_INTERVAL){ //Is it time for a new measurement?
     
    char dataString[20] = "";
    int count = 0;
    unsigned long rawTime;
    rawTime = getTime();

    while((rawTime == 39) && (count < 12)){     //server seems to send 39 as an error code
      delay(5000);                              //we want to retry if this happens. I chose
      rawTime = getTime();                      //12 retries because I'm stubborn/persistent.
      count += 1;                               //NIST considers retry interval of <4s as DoS
    }                                           //attack, so fair warning.
    
    if (rawTime != 39){                         //If that worked, and we have a real time
      
      //Decide if it's time to make a new file or not. Files are broken
      //up like this to keep loading times for each chart bearable.
      //Lots of string stuff happens to make a new filename if necessary.
      if (rawTime >= config.newFileTime){
        int dayInt = day(rawTime);
        int monthInt = month(rawTime);
        int yearInt = year(rawTime);
        char newFilename[18] = "";
        char dayStr[3];
        char monthStr[3];
        char yearStr[5];
        char subYear[3];
        strcat(newFilename,"data/");
        itoa(dayInt,dayStr,10);
        if (dayInt < 10){
          strcat(newFilename,"0");
        }
        strcat(newFilename,dayStr);
        strcat(newFilename,"-");
        itoa(monthInt,monthStr,10);
        if (monthInt < 10){
          strcat(newFilename,"0");
        }
        strcat(newFilename,monthStr);
        strcat(newFilename,"-");
        itoa(yearInt,yearStr,10);
        //we only want the last two digits of the year
        memcpy( subYear, &yearStr[2], 3 );
        strcat(newFilename,subYear);
        strcat(newFilename,".csv");
        
        //make sure we update our config variables:
        config.newFileTime += FILE_INTERVAL;
        strcpy(config.workingFilename,newFilename);
        //Write the changes to EEPROM. Bad things may happen if power is lost midway through,
        //but it's a small risk we take. Manual fix with EEPROM_config sketch can correct it.
        EEPROM_writeAnything(0, config); 
      }
        
      //get the values and setup the string we want to write to the file
      int sensor = analogRead(analogPin);  
      char timeStr[12];
      char sensorStr[6];
      
      ultoa(rawTime,timeStr,10); 
      itoa(sensor,sensorStr,10);
      
      strcat(dataString,timeStr);
      strcat(dataString,",");
      strcat(dataString,sensorStr);
      
      //open the file we'll be writing to.
      File dataFile = SD.open(config.workingFilename, FILE_WRITE);
  
      // if the file is available, write to it:
      if (dataFile) {
        dataFile.println(dataString);
        dataFile.close();
        // print to the serial port too:
        Serial.println(dataString);
      }  
      // if the file isn't open, pop up an error:
      else {
        Serial.println("Error opening datafile for writing");
      }
    }
    else{
      Serial.println("Couldn't resolve a time from the Ntp Server.");
    }
    //Update the time of the last measurment to the current timer value
    lastIntervalTime = millis();
  }
  //No measurements to be made, make sure the webserver is available for connections.
  else{
    char clientline[BUFSIZ];
    int index = 0;
    
    EthernetClient client = server.available();
    if (client) {
      // an http request ends with a blank line
      boolean current_line_is_blank = true;
      
      // reset the input buffer
      index = 0;
      
      while (client.connected()) {
        if (client.available()) {
          char c = client.read();
          
          // If it isn't a new line, add the character to the buffer
          if (c != '\n' && c != '\r') {
            clientline[index] = c;
            index++;
            // are we too big for the buffer? start tossing out data
            if (index >= BUFSIZ) 
              index = BUFSIZ -1;
            
            // continue to read more data!
            continue;
          }
          
          // got a \n or \r new line, which means the string is done
          clientline[index] = 0;
          
          // Print it out for debugging
          Serial.println(clientline);
          
          // Look for substring such as a request to get the root file
          if (strstr(clientline, "GET / ") != 0) {
            // send a standard http response header
            HtmlHeaderOK(client);
            // print all the data files, use a helper to keep it clean
            client.println("<h2>View data for the week of (dd-mm-yy):</h2>");
            ListFiles(client);
          }
          else if (strstr(clientline, "GET /") != 0) {
            // this time no space after the /, so a sub-file!
            char *filename;
            
            filename = strtok(clientline + 5, "?"); // look after the "GET /" (5 chars) but before
            // the "?" if a data file has been specified. A little trick, look for the " HTTP/1.1"
            // string and turn the first character of the substring into a 0 to clear it out.
            (strstr(clientline, " HTTP"))[0] = 0;
            
            // print the file we want
            Serial.println(filename);
            File file = SD.open(filename,FILE_READ);
            if (!file) {
              HtmlHeader404(client);
              break;
            }
            
            Serial.println("Opened!");
                      
            HtmlHeaderOK(client);
            
            int16_t c;
            while ((c = file.read()) > 0) {
                // uncomment the serial to debug (slow!)
                //Serial.print((char)c);
                client.print((char)c);
            }
            file.close();
          }
          else {
            // everything else is a 404
            HtmlHeader404(client);
          }
          break;
        }
      }
      // give the web browser time to receive the data
      delay(1);
      client.stop();
    }
  }
}

62 thoughts on “Arduino: Super Graphing Data Logger”

  1. Hi,

    i’ve problem to get it work. What about “Super Graphing Data Logger – EEPROM config” ? Is this a.cpp file for the lib. Don’t know. Hope you can solve my issue

    1. No the eeprom config is a file you need to flash to the board and run once before flashing the main file. It does the initial configuration to the eeprom for the main sketch.

      1. Thanks for the reply. But after testing your codes, how can i “undo” the eeprom configuration? May be i have overread this part…..

  2. Hallo. I have problem with main list of data files.. i have .csv files in /data directory, but main web page is empty. can you help me? measuring work fine

    1. Unfortunately I’m on the other side of the world from my arduino at the moment, and will stay that way for about another 10 days. If you can wait until then, I’ll have a look at it.

    2. Okay. Mine is up and running again, and everything seems to be going okay. I’ll try my best to replicate your problem, but I might need some more details about your exact setup. First of all, have you made sure that the file HC.htm is in the root directory of the SD card?

    3. Hi Jozefiel, Is your problem solved?

      I got the same problen.
      The .csv file is made in the data directory but they don’t show up in the web page.

      So I can’t sellect them to graph.

      the data seems to be OK. I also put some other .txt files on the card, but he doesnot see them either.

      Marcel

      1. Sorry replied to the wrong subject below (I can’t remove it). Now on the right place.

        Perhaps good, to give some extra information to help solving the problem.

        changing the pad to the file by hand in the HC.htm gives a good graph.

        I replaced <var dataFilePath = "/data/"+getDataFilename(query);
        for ;

        So the csv file is OK and the HC file itself seems to work.

        The serial monitor gives the next information:
        card initialized.
        1384118384,1023
        1384118391,1023
        1384118398,1023
        GET / HTTP/1.1
        1384118405,1023
        1384118412,1023
        1384118419,1023

        Perhaps this gives an idea where to search
        Marcel

  3. Excelent tip on PROGMEM use.

    I was struggling on memory doing something similar with uno, sd, ethernet, 1 DHT11 & 1 TMP36, DS1307RTC instead 4 timing.

    Without using progmem i wasnt able to debug (had to comment every single serial.print to compile).

    good article, thanx!

  4. Hi, your sketch is awesome, but I have a problem. I copied your main sketch and tryed to upload it, but I get the error that in line 266 ‘day’ was not declared. What must to be changed?

    1. Sorry, it was my fail. I got it uploaded, but there are no csv files on the sd card and no entries in the list… There is only th HC file in the directiory, do you have a tip for me? ;)

      1. You might want to run the eeprom config sketch and make sure that the newFileTime parameter you choose is reasonable. It will then take about 10 minutes for the first data point to be created, along with the data file. Other things to look at are whether the arduino is properly connecting to the NTP time server, since it skipsmaking data points (and the file for the first data point) if it can’t connect. Another possibility is if there is something wrong with the file system on the SD card, and the arduino cannot write anything to it.

        Enabling the serial monitor, and maybe adding your own serial debug comments will really help to pin point exactly where it’s hanging up.

  5. I think this looks great,

    I was looking for something lijke this, to use it for multiple temperature sensors for my wood stove with buffer, to measure the temp at different hights in the buffer.
    I can change the code to measure my one wire temperatures I have something like that running at the moment it writes to SD, but I have to take out the the card to make graphs.

    I would love to see the graphs online.

    I think I can change your sketch to collect the data in the csv file, but I’m not sure how to get the HC.HTM working with more lines. Is this easy for you to awnser?

    Thanks, Marcel

    1. It shouldn’t be too hard, once you change the sketch to add more columns to the .csv file you just need to modify HC.htm to get this bit:

      // Load data asynchronously using jQuery. On success, add the data
      // to the options and initiate the chart.
      // http://api.jquery.com/jQuery.get/
      jQuery.get(dataFilePath, null, function(csv, state, xhr) {
      var lines = [],
      date,

      // set up the two data series
      lightLevels = [];

      // inconsistency
      if (typeof csv !== ‘string’) {
      csv = xhr.responseText;
      }

      // split the data return into lines and parse them
      csv = csv.split(/n/g);
      jQuery.each(csv, function(i, line) {

      // all data lines start with a double quote
      line = line.split(‘,’);
      date = parseInt(line[0], 10)*1000;

      lightLevels.push([
      date,
      parseInt(line[1], 10)
      ]);

      });

      options.series[0].data = lightLevels;

      chart = new Highcharts.Chart(options);

      dealing with the extra columns after the line = line.split(‘,’); as a separate series presumably. So just add your additional series for each column up in the chart initialization, then just add some code to push these extra columns which will be stored in line[2], line[3], etc to those new series that you’ve made.

      1. Okay,

        Thanks for the response.
        I hope I have time to try this at the weekend.
        When I succeed, I will let you know.

        Marcel

    2. Perhaps good, to give some extra information to help solving the problem.

      changing the pad to the file by hand in the HC.htm gives a good graph.

      I replaced <var dataFilePath = "/data/"+getDataFilename(query);
      for ;

      So the csv file is OK and the HC file itself seems to work.

      The serial monitor gives the next information:
      card initialized.
      1384118384,1023
      1384118391,1023
      1384118398,1023
      GET / HTTP/1.1
      1384118405,1023
      1384118412,1023
      1384118419,1023

      Perhaps this gives an idea where to search
      Marcel

      1. Hi Everett,

        My post abouve was on the wrong spot, it was a reaction on the problem of Jozefiel (I have the same problem).

        The problem is, I don’t get a file list on the site.
        changing the pad to the file by hand in the HC.htm gives a good graph.
        I replaced <var dataFilePath = "/data/"+getDataFilename(query);
        to <var dataFilePath = "/data/10-11-13.csv");

        Then everything worked and I get the nice graph.

        So the problem is why does my files not show up on the site?, I only see

        Marcel

  6. In my browser I have “http://192.168.178.34/” without the /data

    In the browser I see “View data for the week of (dd-mm-yy):”
    nothing else.

    after refreshing the page, the serial monitor gives:
    GET / HTTP/1.1

    I’m trying for manny hours now. Something strange hapened this evening.

    To see where the program is, I try to print lines for trouble shooting.

    File workingDir = SD.open(“/data”);
    client.println(“”);
    while(true) {
    File entry = workingDir.openNextFile();
    if (! entry) {
    Serial.println(“no more files”);
    break;
    }

    I added the line ” Serial.println(“no more files”); ”

    In the first run I finaly saw my files.
    After removing the line it didnot work again.
    When I put the line back it did not work again!!

    On the serial monitor I see

    “card initialized.
    1384289043,1023 ”
    after pressing refresh in the web page, the next message shows up:

    “GET / HTTP/1.1
    no more files”

    I thought perhaps it is something with timing, so i tried some delays after the serial.print line.
    doesn’t work…..

    I also tried an other SD card, no results.

    any tips?

    1. Uh oh. Non replicable problems in software are the worst kind. Out of curiosity (and grasping at straws for a source of the problem) what version of the arduino IDE are you using? Also in the same line of thought, what make of arduino are you using. If it’s old enough it may be running into out of memory errors at that point in the code. If there’s any other interesting differences like those that you can think of, then feel free to mention them, and hopefully we can track down the source of your issues.

      1. Hi Everett,

        ypur questions made me think. First of al I have an arduino R3 with an ethernet shield.
        After an upload I get a measage “Binaire sketch-grootte: 26.400 bytes (van een 32.256-byte maximum)” Then I thought about your saying about memory.
        I tried to make it smaller just by removing 2 lines with an serial.print meassage. after this upload I saw it was a bit smaller “Binaire sketch-grootte: 25.928 bytes (van een 32.256-byte maximum)” And now it workes perfect!!

        Thats great, but I don’t think I can use this because I have to make the programm bigger, because I have to ad the onewire measurements to the programm.

        But your programm looks good, So if I can’t find some thing better, perhaps I buy an Arduino Mega.

        Thanks for the help
        Marcel

        1. Yeah, unfortunately the sketch was pushing resource limits to begin with. Perhaps it would be possible to squeeze enough room for your additions by strategic removal of more debug strings, and of other strings to the progmem. Because the problem seems to be one of ram, not flash mem.

      2. With the mega both the ram and flash are bigger I asume.
        Wich type arduino do you use yourself?

        I also have an real time clock I can try to use, in stead of the time server. I just got it and didn’t use it before, perhaps this will also safe data, but i’m not sure.
        Because it is not my own code it is not easy to see exact wich part I can delete in that case. My wife is allready complaining I’m at the computer all the time ;-).

        I will try to see if this will help, in the mean time I think I will order an arduino mega.
        It will take some time, but, to be continued…..

        1. I’m using an original UNO. The RTC may help a lot, and thus allow for data points to be taken even without an active internet connection. That means you wouldn’t need to worry about gaps in your dataset as much. I don’t know the exact mechanics of the rtc, but it should just be a matter of including the right libraries and redoing the whole getTime() function for the rtc instead of a ntp server. You could also dump all of the ntp config variables. It might be all you need to solve your issues, but would involve a non trivial amount of coding.

  7. First of all I would like to thank for sharing such a great project!
    My question is if this type of data logger can stream real time values.

    Thank you in advance
    Great job

    1. Sorry, I never noticed your comment before. It is currently not set up to stream realtime values, though I’m sure it would be possible with highcharts and an arduino if you were willing to rewrite a bit of the code.

      1. Thank you for your reply!
        At the mean time I managed to use highcharts for graphing in real time.
        Although I realized that I also need older data so I have to store them to csv files like you do!
        So the only thing I dont really get is the use of EEPROM in your project. COuld you send me some sources for further studying how this concept works?
        Thank you in advance

  8. I want to say fantastic work, easy to implement too. I’m wanting to capture from multiple sensors and log in separate logs/graphs(temp, humidity, ph, ppm, waterlevel, etc, its for a home hydroponics farm), any thoughts on how to do this?

    1. In theory yes, but as marcel ran into out of memory issues trying to implement multiple readings per line, you may also run into these issues. Using a Mega or other beefier arduino may solve these issues though.

      1. After I received my mega, the memory issues were gone and I managed to do multiple readings. They are all in the CSV file so logging is no problem, and it is running stable.

        I tried to make a graph with more lines, but I didn’t manage so far.

        At the end I have to display about 10 sensors I’m not sure if this is too slow.
        At the mean time I’m looking for an other solution I found and see wich works best for me.

        Again compliments, so many people want to log there data and plot it in a graph without removing the sd card. I was looking for months and did not find a good solution. Bu now I tried yours and for sure with one sensor it works great.

        Marcel

  9. Hi
    I have copied some of your code and am trying to get multiple line plot to work. So far no luck.
    Using Mega with Ethernet card.
    I collected the data to the sd card with my program and the csv file I am plotting looks like
    this.
    This what tdaytmp2.csv looks like
    1389745800,32.1125,70.3625
    1389747600,31.6625,72.1625
    1389749400,31.55,80.0375
    1389751200,31.4375,83.6375
    etc.
    I am using your sketch to serve the data. Modified it to remove the time stuff as I am only using it to serve the data for testing. I will copy what I need from it later when I get the plots to work.
    The data is being sent to the client as I have turned on the serial print for testing and it looks good. In fact I can plot either field 1 or 2, but not both. I’m just changing lines in the hce.htm file select which data to plot and either one looks correct, but can’t seem to get both lines to plot at same time.

    Another thing I noticed, but haven’t spent much time trouble shooting is if a select a file that has 2 digit precision ( like 55.75 instead of 55.6789 the plot is different in that there is no connecting line between data points.

    The HCE.HTM file I am using is below. I put in some comments to show what I have tried changing while troubleshooting. Obviously I am doing something wrong.and would appreciate any help.
    Thanks
    Mike

    Super Graphing Data Logger!

    // this function gets the file names from ardunio .. I think
    function getDataFilename(str){
    point = str.lastIndexOf(“file=”)+4;

    tempString = str.substring(point+1,str.length)
    if (tempString.indexOf(“&”) == -1){
    return(tempString);
    }
    else{
    return tempString.substring(0,tempString.indexOf(“&”));
    }

    }

    query = window.location.search;

    var dataFilePath = “/data/”+getDataFilename(query);

    $(function () {
    var chart;
    $(document).ready(function() {

    // define the options
    var options = {

    chart: {
    renderTo: ‘container’,
    zoomType: ‘x’,
    spacingRight: 20
    },

    title: {
    text: ‘Solar Controller Temperatures’
    },

    subtitle: {
    text: ‘Click and drag in the plot area to zoom in’
    },

    xAxis: {
    type: ‘datetime’,
    maxZoom: 2 * 3600000
    },

    yAxis: {
    title: {
    text: ‘Temperature in F’
    },
    min: 0,
    startOnTick: false,
    showFirstLabel: false
    },

    legend: {
    enabled: false
    },

    tooltip: {
    formatter: function() {
    return ‘‘+ this.series.name +’‘+
    Highcharts.dateFormat(‘%H:%M – %b %e, %Y’, this.x) +': ‘+ this.y;
    }
    },

    plotOptions: {
    series: {
    cursor: ‘pointer’,
    lineWidth: 1.0,
    point: {
    events: {
    click: function() {
    hs.htmlExpand(null, {
    pageOrigin: {
    x: this.pageX,
    y: this.pageY
    },
    headingText: this.series.name,
    maincontentText: Highcharts.dateFormat(‘%H:%M – %b %e, %Y’, this.x) +': ‘+
    this.y,
    width: 200
    });
    }
    }
    },
    }
    },

    series: [{
    name: ‘tsensor2′,
    marker: {
    radius: 2
    }
    }

    ,
    {
    name: ‘tsensor3′,
    marker: {
    radius: 3
    }
    }

    ]
    };

    // Load data asynchronously using jQuery. On success, add the data
    // to the options and initiate the chart.
    // http://api.jquery.com/jQuery.get/
    jQuery.get(dataFilePath, null, function(csv, state, xhr) {
    var lines = [],
    date,

    // set up the two data series
    tsensor2 = []; // bottom
    tsensor3 = []; //top
    // tsensor5 = []; // Wh
    // tsensor6 = []; // Mix
    // tsensor7 = []; // door
    // tsensor8 == [];
    // inconsistency
    if (typeof csv !== ‘string’) {
    csv = xhr.responseText;
    }

    // split the data return into lines and parse them
    csv = csv.split(/n/g);
    jQuery.each(csv, function(i, line) {

    // all data lines start with a double quote
    line = line.split(‘,’);
    date = parseInt(line[0], 10)*1000;

    tsensor2.push([
    date,
    parseInt(line[1], 10)
    ]);
    tsensor3.push ([
    date,
    parseInt(line[2], 10)
    ]);
    });
    // Running with only series[0] plots ok
    options.series[0].data = tsensor2;
    // options.series(1).data = tsensor3;
    // If I enable both of the above commands I get no plot
    // Testing using tsensor3 data for series [0] as below
    // also works. i.e the plot is of tsensor3 data
    // options.series(0).data = tsensor3;

    chart = new Highcharts.Chart(options);
    });
    });

    });

    Please allow the chart to load, it may take up to 30 seconds

    1. Okay, so I went and looked at the new Highcharts 3 ajax example and found it has changed a lot since I made this project.Luckily it looks almost exactly like what you want, and I made a couple modifications to get it working on the data set you posted above:
      http://pastebin.com/UQyixApJ

      The main issue right now is that the CSV file needs to have the first row be the series names for the legend. So for the data file above it would be:

      time,tsensor1,tsensor2
      1389745800,32.1125,70.3625
      1389747600,31.6625,72.1625
      1389749400,31.55,80.0375
      1389751200,31.4375,83.6375

      Luckily this should be an easy fix. Just have the arduino write that label line right after the new file is made at line 298 or so. The code would be something like this:

      File dataFile = SD.open(config.workingFilename, FILE_WRITE);

      // if the file is available, write to it:
      if (dataFile) {
      dataFile.println(“time,tsensor1,tsensor2″);
      dataFile.close();
      // print to the serial port too:
      Serial.println(“time,tsensor1,tsensor2″);
      }
      // if the file isn’t open, pop up an error:
      else {
      Serial.println(“Error opening datafile for writing”);
      }

      Hopefully this fixes the issues you were having. Let me know if you need any clarification on anything.

  10. Thanks very much for the help. It got me back on track. I edited the file with the pc and put in the header and it is plotting 6 lines fine. Also, updated my sketch to put in the header. I edited the titles etc, but will probably make some more changed. I noticed that the year in date is showing 1970 when clicking a data point. Also, the time isn’t correct but haven’t really looked at that. I’m pretty sure the unix time stamp is correct in the file. I trying to learn how to use Firebug so maybe I can do some checking later. My plan is to add some bars to the plot too, but haven’t collected that data yet. Great to be making progress though. I will try to bug you too much. I am making a sketch to gather temperatures from solar system and some home automation. I did a project using basic stamp a couple of years ago but decided to redo it using Ardunio. Staring up learning C ,HTML and JavaScript at 70 is hard for an old brain.

    Thanks again

    Mike

  11. Hey.
    Cool projects, just wondering how to add an temperature sensor to it.
    trying to learn arduino but having some problems understanding how u add the sensor,

      1. I’m thinking Off replacing the photoresistor with and ds18 temprature sensor and a ultrasonic range sensor to view tank level and the temprature off the water.

        1. Okay, getting the DS18 attached should be as simple as attaching the V+ pin to 5V on the arduino, gnd to gnd, and data to A0. Getting the range sensor working is trickier. It depends on how the sensor communicates data. If it is analog, then using the tricks already discussed in the comments here should work. If it’s pulse width or serial based, then it’ll be necessary to write some additional code for your project to get it working.

  12. Hi,

    This is an awesome project, thanks for sharing. Being a newbie at coding in general, it is teaching me a lot. Thanks again.

    I am having trouble getting the graphing stuff to work for my application. I receive a CSV string over serial every X seconds. Ideally, I would like to dynamically update the chart as the data comes in and/or read from the stored CSV as well.

    This is what the first and second lines of my csv file look like:

    Time,Tap 1,Tap 2,Tap 3,Tap 4,Tap 5,Tap 6,Tap 7,Tap 8,Tap 9,Tap 10,Blue,Yellow,White,Green,BattV,SoC,BCMAmps,Disch mAh,Chg mAh,Max,Min,Difference
    1399896413,16.24,16.24,16.23,16.29,16.24,16.24,16.20,16.28,16.23,16.26,80.25,81.74,85.15,79.33,162,18.00,0.00,0.00,0.00,16.29,16.20,0.10

    But there are or will be many lines.

    Right now, I can get the chart to load the legend, but no graph is displayed.

    I was hoping you could point me in the right direction. While I’ve been tinkering with Arduino for a while and can generally figure things out, this javascript stuff has me fairly baffled. Let me know if you need any other information. I’ll continue staring at things in the meantime..

    -Eli

    1. With a datastring that long it’s likely that the chart is failing to load because the arduino is running out of memory; especially if you are using an uno or equivalent with only 2kb. It’s an existing problem that is already identified, so my money is on that. Otherwise it could just be that the high charts code is not configured right for all your datasets.

      1. Interesting. I hadn’t ever considered that. I’m using a Mega though.

        I think its more on the highcharts code side of things. I can get the graphs to load sometimes(?), but never my whole dataset. So maybe that is the problem.. Dynamic loading would probably be better in that regard..

        Maybe this whole idea is just beyond me right now. Ideally, I would like to get to the point where each group of relevant data is sent to its own chart, ie: the voltage taps go to one chart, the temperatures go to another, etc.

        I would still appreciate some input on the charts side of things. I am pretty sure I’m doing something wrong there. If you could get the single data string I posted to graph, I would be forever grateful – I think it would help me understand what is going on better. I spent all day yesterday tinkering and looking at the Highcharts examples, but I didn’t make a whole lot of progress.

  13. just wondering how easy it would be to change the light sensor to a DHT11 or DHT22 to measure temp / humidity instead of light. sorry for the noob question but your code looks to be just what is needed to log and display almost anything.

    1. It should be reasonably easy. From what I understand, the DHTXX sensors are not analog, and need to be wired up differently, then the data needs to be read using the functions in an appropriate library. If you wire it up according to the information here: https://learn.adafruit.com/dht/connecting-to-a-dhtxx-sensor and then replace line 302 (” int sensor = analogRead(analogPin); “) with the appropriate lines like “float t = dht.readTemperature();” and “float h = dht.readHumidity();”. You’ll also need to change line 307 from itoa(…) to ftoa(…).

      Hope this helps!

  14. Thank you a bunch for sharing this with all of us you really recognize what you are speaking about!
    Bookmarked. Please also discuss with my web site =).
    We may have a hyperlink change contract beween us

  15. Part of creating a successful search engine optimization campaign for you is optimizing your website for the
    correct keywords. Many companies also offer to train people
    under their SEO reseller programs to help them get more companies looking for SEO assistance and earn a better commission at the same time but before you join any program make sure to join an SEO firm that is going to compensate you well for the business you will be bringing for the firm.

    SEO and web developer professionals vary
    on the prescribed keyword density, usually ranging from a one to three percent density rate.

  16. Hi, I am trying to use to or more sensors in my code, so used the code for a comment on 19th January. The datapoints ar shown in the chart correctly, but the date ist from 1970. What do I have to change in my code to get the right date. If I use your code for only one sensor, I get the correct date and time in the chart.
    Thanks :)

      1. The time in the CSV file is correct. Do I have to change something in the HC.htm file? Or do I have to change the headline of my CSV file?

      2. I think I have found the reason for this error. I have to multiply the time with the factor 1000 and i have done this manually in the CSV file. I’m not so good in programming, where and how do I have to change my programm?

        1. If you have decided to multiply the values in the CSV file, then you can get the SGDL sketch to do this by replacing line 306 (currently “ultoa(rawTime,timeStr,10);”) with something like ultoa(rawTime*1000,timeStr,10);

          The HC.HTM file is supposed to be multiplying the values by 1000 already though, and for some reason the code: “date = parseInt(line[0], 10)*1000;” that should be doing that is not.

  17. Do you know of any reason this wouldn’t work with with a w5200 instead of a w5100, I have tried the example codes and get dhcp addresses but your code does not respond to pings

    1. The only reasons I can think of are that either the W5200 currently lacks support in the default Ethernet Library, or the Ethernet Library does things a bit differently now and has broken some of the code in my sketch.

      If you are using this http://www.seeedstudio.com/wiki/Ethernet_Shield_V2.0 shield, then it appears to have a separate version 2 library that is different from the one included with the Arduino by default. Maybe manually installing the V2 library will solve these problems?

  18. Hi Everett
    Back in January I contacted you and you got me back on track with my project. .. A SolarController and a bunch of other stuff.
    It displays data about my solar system in addition to running the system. I have several charts that I generate using highcharts.
    My problem is even though they work fine with Firefox and Chrome I can’t seem to figure out why they don’t work with IE or on android tablet which I suppose is some variant of IE.
    If you want to look at it these links (http://pumps-solardata.pw/hilow , http://pumps-solardata.pw/chart, http://pumps-solardata.pw/mth_yr ) show the problem in that no data gets plotted when using IE, but work fine with Firefox or Chrome

    There are others, but this should give you and idea of what it does. By using Firebug on firefox I can see the get request going to the Ardunio.
    GET http://pumps-solardata/pw/data/30dhilow.csv
    That never happens when using IE so I pretty sure there is something in the htm file that IE doesn’t like but since I havn’t figured how to get firefox lite or IE’s debugger to work in IE I can’t figure out what is wrong.

    Any ideas?
    Also, note that not all the sensors readings are valid as they are on my bench instead of on the solar system. Outside temps are pretty close, but sensors are too close to the house to bereally accurate.
    I didn’t include the htm files as you can see them by inspecting element or with firebug.
    Thanks
    Mike

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>