Boarduino vivarium temperature monitor

I like snakes. This weekend I will be collecting my new snake, he’s a baby Royal Python. I have been worried about the temperature in the vivarium I will be housing him so I built an ethernet enabled monitoring device to do a science!

Vivarium Temperature Monitor


Its a BoArduino, an HD4470 compatible LCD display, a WIZ810MJ ethernet module (in an adapter board) and some discrete components with a TMP36 for temperature monitoring.

This is the result set for the last day. The live graph is here.

The Generated Graph

The circuit uses nearly all of the digital pins (it would use them all if the ethernet library supported interrupts). The LCD is connected using digital pins 3 to 8. Pins 3 to 6 are the data lines, 7 is the enable pin and 8 is RS. The ethernet module is connected using digital pins 9 to 13 being Reset, SS, MOSI, MISO and SCLK. The temperature sensor is connected to Analog pin 0. It outputs 10mV per degree Celcius with a 500mV offset so that 0C is 500mv, 50C is 1V etc. The code sketch is below.

#include <string.h>
 
// Required for the LCD
#include <LiquidCrystal.h>
 
// Required for ethernet
#include <Client.h>
#include <Ethernet.h>
#include <Server.h>
 
// Required for the IINCHIP macros
#include <utility/spi.h>
 
// The ethernet reset control pin
int resetPin  = 9;
// The temperature analog input pin
int tempAnPin = 0;
 
// String buffer
char buffer[256];
 
// Hardcoded MAC, IP and gateway
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
byte ip[]  = { 192, 168, 1, 140 };
byte gw[]  = { 192, 168, 1, 254 };
 
// Create an LCD control object.  Doesn't use regular pins as
// we need some of them for the Ethernet SPI.
LiquidCrystal lcd(8, 7, 6, 5, 4, 3);
 
// Hardcoded server and port
byte svr[] = { 192, 168, 1, 128 };
int  port  = 80;
 
// A client class for connecting to a remote server to send data
Client client(svr, port);
 
/**
 * Writes a byte to a register in the W5100 chip used by the 
 * WIZ810MJ ethernet board. 
 *
 * @param addr The address in the W5100 controller to write.
 * @param data The byte to write.
 */
void writeByte(int addr, byte data) {
  IINCHIP_ISR_DISABLE();
  IINCHIP_SpiInit();
  IINCHIP_CSoff();
 
  IINCHIP_SpiSendData(0xf0);
  IINCHIP_SpiSendData((addr & 0xff00) >> 8);
  IINCHIP_SpiSendData(addr & 0xff);
  IINCHIP_SpiSendData(data);
 
  IINCHIP_CSon();
  IINCHIP_ISR_ENABLE();
}
 
// The four IP addres bytes (we're not IP6 compatible here)
byte ip0, ip1, ip2, ip3;
 
/**
 * Initialise the hardware
 */
void setup() {
  // Reset the ethernet board
  pinMode(resetPin, OUTPUT);
  digitalWrite(resetPin, HIGH);
  delay(1); // 1ms
  digitalWrite(resetPin, LOW);
  delay(1); // 1ms
  digitalWrite(resetPin, HIGH);  
 
  // Start ethernet
  Ethernet.begin(mac, ip, gw);
 
  // Enable ping.  Useful for debugging.
  writeByte(0x0000, 0x00);
 
  // Read the IP address (Check to see its working right) 
  ip0 = readByte(0x000f);
  ip1 = readByte(0x0010);
  ip2 = readByte(0x0011);
  ip3 = readByte(0x0012);
 
  // Form the output string (Adds about a K to the code size)
  sprintf(buffer, "%d.%d.%d.%d", ip0, ip1, ip2, ip3);
 
  // Display it
  lcd.home();
  lcd.print(buffer);
 
#ifdef DEBUG
  Serial.begin(9600);
#endif
}
 
// The value of the temperature analog pin
int tempAnValue;
 
// The temperature
int temperature;
 
// The temperature components;
int tempInt;
int tempFrac;
 
// A counter for sychronization.  Allows server to work out if a
// 'packet' is missing
int count = 0;
 
/**
 * Main processing thread
 */
void loop() {
  // Read the temperature
  tempAnValue = analogRead(tempAnPin);
 
  // Fixed point math to calculate the temperature (multiply then divide
  // to reduce rounding issues) Gives temperature in 100ths of degree C
  // 5000 millivolts is the maximum voltage
  // 1024 is the maximum ADC value
  // 500mv is 0 degrees C
  temperature = ((tempAnValue * 50000) / 1024) - 5000;
 
  // Since we're only going to report to 1 decimal place, add 5 for correct
  // rounding
  tempInt  = (temperature + 5) / 100;
  tempFrac = ((temperature + 5) % 100) / 10;
 
  // Form the output string
  sprintf(buffer, "Temp: %d.%d C", tempInt, tempFrac);
 
  // Display it
  lcd.clear();
  lcd.print(buffer);
 
#ifdef DEBUG
{
    sprintf(buffer, 
            "HEAD /newtemp.php?count=%d&temp=%d.%d HTTP/1.1", 
            count, tempInt, tempFrac);
 
    Serial.println(buffer);
    Serial.println("Host: my.host.org");
    Serial.println("Connection: close");
    Serial.println();
}
#endif
 
#ifndef NO_ETHERNET
  // Send to server
  if (client.connect()) {
    // If connected send the packet count and temperature
    sprintf(buffer, 
            "HEAD /newtemp.php?count=%d&temp=%d.%d HTTP/1.1", 
            count, tempInt, tempFrac);
 
    client.println(buffer);
    client.println("Host: my.host.org");
    client.println("Connection: close");
    client.println();
    client.stop();
  } else {
    // If failed sad face
    lcd.print(" :-(");
  }
#endif
 
  // Update packet counter
  count++;
 
  // Sleep for a minute
  delay(60000);
}

A couple of things to note are:

  • I use integer math. avrgcc, the compiler, supports floats but its just more efficient and requires less memory to use fixed point integer math when its just one or two values you need to represent.
  • I use a count value sent with each data packet so that if one goes missing you can tell, Currently the code at the other end doesn’t use it though.
  • I send a HEAD request with a query string containing the values. My request is not technically valid HTTP but the server doesn’t have a problem. A HEAD request returns the headers for a request but no body. Since I’m ignoring the whole response anyway I don’t need the body!
  • The script on the web server that receives the data is below. It is very simple PHP. Since PHP doesn’t support HEAD requests directly in its super-globals (watch this space :-) I process the query string by hand. As I know the format of the data since I control the client I’m not worried about URL decoding the data. It writes three values (the time in seconds since the UNIX epoch, the packet count and the temperature), colon separated, as a line to a text file. It returns nothing.

    <?php
     
    if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
     
    	$fields = explode("&", $_SERVER['QUERY_STRING']);
    	$values = array();
     
    	foreach ($fields as $field) {
    		$keyval = explode("=", $field);
    		$values[$keyval[0]] = $keyval[1];
    	}
     
    	$file = fopen('/path/to/tempdata.txt','a+');
    	if ($file) {
    		fwrite($file, time() . ':' . $values['count'] . ':' . $values['temp']);
    		fwrite($file, "\n");
    		fclose($file);
    	}
    }
     
    ?>

    Another script generates a graph as a PNG file based on the data. This code is below.

    <?php
     
    define('WIDTH', 1000);
    define('HEIGHT', 520);
    define('FONT', 1);
     
    // Get the temperature data
    $data = array();
    $file = fopen("/path/to/tempdata.txt","r");
    if ($file) {
    	while (!feof($file)) {
    		$line = trim(fgets($file));
    		if (strlen($line)) {
    			$fields = explode(":", $line);
    			$keyval = array();
    			$keyval['time']        = $fields[0];
    			$keyval['count']       = $fields[1];
    			$keyval['temperature'] = $fields[2];
    			$data[] = $keyval;
    		}
    	}
     
    	fclose($file);
    }
     
    // Get the number of data points
    $datapoints = count($data);
     
    // Lines are chronological
    $mintime = $data[0]['time'];
    $maxtime = $data[$datapoints - 1]['time'];
     
    // Temperatures need to be processed.
    $mintemp = $data[0]['temperature'];
    $maxtemp = $data[0]['temperature'];
     
    foreach ($data as $datapoint) {
    	$mintemp = $mintemp < $datapoint['temperature'] ? 
    	           $mintemp : $datapoint['temperature'];
    	$maxtemp = $maxtemp > $datapoint['temperature'] ? 
    	           $maxtemp : $datapoint['temperature'];
    }
     
    // Get the axis dimensions.  Round up and down to the nearest
    // degree C and hour.
    $lowtime  = intval($mintime / 3600) * 3600;
    $hightime = (intval($maxtime / 3600) + 1) * 3600;
    $difftime = $hightime - $lowtime;
     
    $lowtemp  = intval($mintemp);
    $hightemp = intval($maxtemp) + 1;
    $difftemp = $hightemp - $lowtemp;
     
    // Create the image
    $image = imagecreate(WIDTH, HEIGHT);
    if ($image) {
    	$background = imagecolorallocate($image, 255, 255, 255);
    	$black      = imagecolorallocate($image, 0, 0, 0);
    	$red        = imagecolorallocate($image, 255, 0, 0);
    	$blue       = imagecolorallocate($image, 0, 0, 255);
     
    	// Draw the axes
    	imageline($image, 20, 20, 20, 485, $black);
    	imageline($image, 15, 480, 980, 480, $black);
    	imageline($image, 15, 20, 20, 20, $black);
    	imageline($image, 980, 480, 980, 485, $black);
     
    	for ($i = 3600; $i < $difftime; $i += 3600) {
    		$x = 20 + (($i * 960) / $difftime);
    		imageline($image, $x, 480, $x, 483, $black);
    	}
     
    	for ($i = 1; $i < $difftemp; $i++) {
    	    $y = 480 - (($i * 460) / $difftemp);
    		imageline($image, 17, $y, 20, $y, $black);
    	}
     
    	// Draw the labels
    	imagestring($image, FONT, 8, 490, date("H:i", $lowtime), $black);
    	imagestring($image, FONT, 970, 490, date("H:i", $hightime), $black);
    	imagestringup($image, FONT, 2, 485, $lowtemp . 'C', $black);
    	imagestringup($image, FONT, 2, 25, $hightemp . 'C', $black);
     
    	// Draw the points
     
    	// Position of the first point
    	$prevx = (($data[0]['time'] - $lowtime) * 960) / $difftime;
    	$prevy = (($data[0]['temperature'] - $lowtemp) * 460) / $difftemp;
     
    	// Draw line from previous point to current point
    	for ($i = 1; $i < $datapoints; $i++) {
    		$x = (($data[$i]['time'] - $lowtime) * 960) / $difftime;
    		$y = (($data[$i]['temperature'] - $lowtemp) * 460) / $difftemp;
    		imageline($image, $prevx + 20, 480 - $prevy, $x + 20, 480 - $y, $red);
    		$prevx = $x;
    		$prevy = $y;
    	}
     
    	// Finally time and date stamp
    	$generated = 'Generated: ' . date("r");
    	imagestring($image, 
    	            FONT,
    	            WIDTH - 15 - (imagefontwidth(FONT) * strlen($generated)),
    	            HEIGHT - 15, 
    	            $generated,
    	            $blue);
     
    	// Output the image
    	header('Content-Type: image/png');
    	imagepng($image);
     
    	// Destroy it
    	imagedestroy($image);
    }
     
    ?>

    This was a fun little project and as can be seen from the snap shot of the graph changing the bulb significantly changes the maximum temperature of the vivarium. I will be running with the smaller wattage bulb.

    Tags: ,

10 Responses to “Boarduino vivarium temperature monitor”

  1. Andrew Says:

    great work!

    I have a couple questions regarding the temp sensors.
    I’ve been thinking of doing a similar project, however as a fellow ball python owner, i know that we need to monitor not just the air temp, but also the ground temp on the warm and cool side. Have you thought about surface mounted type sensors?

    What about humidity?

    I am also putting together a vivarium for some dendros (dart frogs) and have similar concerns for them

    thanks!
    great work and thanks for sharing!

  2. wrecks Says:

    You could use the boarduino to control the output of the light. Use a triac and you can clip the ac going into the bulb so you can produce the amount of light needed to stabilize the temp.

  3. john Says:

    very cool, you should check out openflashcharts for the graph generation. they worked pretty well for me for an arduino current monitoring project i did awhile ago (see http://jarv.org/pwrmon_current.shtml )

  4. cyberspice Says:

    @wrecks Actually that’s the next step :-) I need an opto-isolator as a zero point crossing detector and an opto-triac driver. Watch this space :-)

  5. cyberspice Says:

    @John They’re very kewl. Thanks for the tip.

  6. cyberspice Says:

    @Andrew Currently its just the one sensor taking the ambient air temperature in the middle of the tank. However there are several spare ADCs on the AVR so there’s no reason why you can’t have more. The sensor has a flat on the package so you could fix it to the wall of the viv I would think although I’ve never tried it.

    I’m having noise issues with the sensor. I’m thinking a low pass filter and some amplification. I’ve also been looking at some 1 wire sensors (which would transmit the temperature to the AVR as digital and hence not have the noise issue).

    I’ve not looked at humidity at all but this is still very much the early stages.

  7. jan Says:

    hey very nice project!
    i use the same temp sensors, just take a couple of readings (maybee 10 with 20ms or so delay between them) and then average them. should do the trick!
    as the noise is random it will be 0 in average.

  8. cyberspice Says:

    @jan I was discussing something similar not that long ago. Glad to see it will work. Thanks.

  9. Wiley Says:

    I’ve been looking to do something similar with a planted viv that has a water feature. I’m hoping to add a humidity sensor that can kick on a relay with misters and do the same with fans if the temp gets too high. I’m thinking that the humidity sensor that sparkfun sells might not be able to deal with the high humidity that would be in a regularly misted vivarium though..

  10. Arduino temperature and light data-logging and chartplotting webmonitor | codetorment Says:

    [...] started from Cyberspice’s code and changed the arduino code to make use of the DB18S20 and the ethernet [...]