Arduino ethernet temperature sensor

Motivation

Create cheap rock solid ethernet temperature sensor. It should work years without touching and recover after power or network problems. Full watchdog and reset capabilities should be used.

Bill of materials

Total price

As said before, you can get rid of voltage regulator and DS18B20 doesn't nessesary be waterproof. You can get these items on ebay cheeper also.

4$+ Arduino clone
4$ Waterproof DS18B20
5$ ENC28J60 Ethernet
2$ AMS1117 Voltage regulator
? 5V power source
15$

Requirements

Most of small arduinos (clones) doesn't come with watchdog. I don't know why but we need one, without one code bellow will not work or will not work reliably. Ethernet module works mostly ok but sometimes when network problems or server responds slow module got stuck. I strongly recomend Optiboot - you can find lot of tutorials how to burn new bootloader to arduino or even plain Atmega chip.

Then you will need driver for ethernet module. There is no official support from Arduino itself so we use EtherCard from JeeLabs.

Wiring

Fritzing doesn't have image for ethernet module, but you should get idea from this image. If there is something unclear please look at datasheets or other tutorials.

Output example

Reliability

Internal watchdog can be helpful here when code stuck but it seems its not enough to fully restart ethernet module. Watchdog restart and LOWing ethernet RST pin seems to work almost every time, but in rare cases ethernet stays in some weird state that these software reboots will not work at all as long as arduino is powered. So i connected D4 and Arduino RST pin and used hack from http://weblog.jos.ph/development/arduino-reset-hack/. I am not sure why this works, but at end of the day it works! Small caveat you need RST pin to have disconnected while flashing Arduino.

Commented Arduino code

#include <OneWire.h>
#include <EtherCard.h>
#include <avr/wdt.h>
// milliseconds to wait for data send
#define REQUEST_RATE 60000
// ethernet module RST pin
#define ETHERNET_RST 6
// arduino RST pin
#define ARDUINO_RST 4
// do you want debug through Serial?
#define SERIAL true
// ethernet interface mac address, must be unique on the LAN
static byte mymac[] = { 0x74,0x69,0x69,0x2D,0x30,0x31 };
static byte myip[] = { 192,168,88,111 };
static byte mygw[] = { 192,168,88,1 };
static byte mydns[] = { 8,8,8,8 };
static byte mask[] = { 255,255,255,0 };
// we will test every minut DNS(restart if unavailable)
// and send data to that host
const char website[] PROGMEM = "your.server.com";
// we need to send request right after start
static long timer = -REQUEST_RATE;
OneWire ds(3);
byte Ethernet::buffer[500];
BufferFiller bfill;
char zxc[7];
byte addr[8]; // temp sensor address
uint8_t errata;
void setup () {
// http://weblog.jos.ph/development/arduino-reset-hack/
digitalWrite(ARDUINO_RST, HIGH); //We need to set it HIGH immediately on boot
pinMode(ARDUINO_RST,OUTPUT); //We can declare it an output ONLY AFTER it's HIGH
// if there is watchdog running stop it!
wdt_disable();
// Starting watchdog with 8s timeout
wdt_enable(WDTO_8S);
#if SERIAL
// starting Serial debug
Serial.begin(9600);
debugln('Hello world!');
#endif
// Starting ethernet
// will keep restarting whole arduino while it got started properly
while(restartEthernet()) {
digitalWrite(ARDUINO_RST, LOW); // Force restart
}
// Find temp sensor, some attempts can fail so we will run it until
// address with proper CRC is found
while(restartDS18B20()) {
digitalWrite(ARDUINO_RST, LOW); // Force restart
}
}
bool restartEthernet() {
debugln(F("Restarting ethernet"));
pinMode(ETHERNET_RST, OUTPUT);
digitalWrite(ETHERNET_RST, HIGH);
delay(200);
// ENC28J60 resets on holding RST pin LOW
digitalWrite(ETHERNET_RST, LOW);
delay(200);
digitalWrite(ETHERNET_RST, HIGH);
delay(200);
ether.doBIST();
debugln(F("...done"));
errata = ether.begin(sizeof Ethernet::buffer, mymac);
if (errata == 0) {
debugln(F("Failed to access Ethernet controller"));
return 1;
}
Serial.println(errata);
debugln(F("Ethernet begin() success"));
debugln(F("Setting static IP"));
ether.staticSetup(myip, mygw, mydns, mask);
// dynamic/static setup can sometimes take long time
// so we rather get another 8s to DNS resolve test
wdt_reset();
#if SERIAL
debugln(F("...done"));
ether.printIp("IP: ", ether.myip);
ether.printIp("GW: ", ether.gwip);
ether.printIp("DNS: ", ether.dnsip);
#endif
if (!ether.dnsLookup(website)) {
debugln(F("Failed to resolve domain name via DNS"));
return 1;
}
#if SERIAL
ether.printIp("SRV: ", ether.hisip);
#endif
wdt_reset();
debugln(F("...OK"));
return 0;
}
bool restartDS18B20() {
debugln(F("Searching temp sensor"));
byte i;
ds.reset_search();
if ( !ds.search(addr)) {
debugln(F("No more addresses."));
ds.reset_search();
return 1;
}
debug(F("ROM ="));
for( i = 0; i < 8; i++) {
debug(F(" "));
debug(addr[i], HEX);
}
debugln();
debugln(F("CRC check"));
if (OneWire::crc8(addr, 7) != addr[7]) {
debugln(F("CRC is not valid!"));
return 1;
}
debugln(F("...OK"));
return 0;
}
static void response (byte status, word off, word len)
{
debugln(F(">>>"));
Ethernet::buffer[off+300] = 0;
debugln((const char*) Ethernet::buffer + off);
debugln(F("..."));
}
void loop ()
{
ether.packetLoop(ether.packetReceive());
if (millis() > timer + REQUEST_RATE) {
timer = millis();
delay(500);
if (!ether.dnsLookup(website)) {
debugln(F("Failed to resolve domain name via DNS, forcing restart"));
digitalWrite(ARDUINO_RST, LOW); // Force restart
}
char data[50];
sprintf(data, "?temp=%s&uptime=%ld", dtostrf(temperature(),3,2,zxc), timer/1000);
debug(F("<<< REQ "));
debugln(data);
wdt_reset();
ether.browseUrl(PSTR("/therm.php"), data, website, response);
}
if(timer > 86400000) {
digitalWrite(ARDUINO_RST, LOW); // Force restart
}
wdt_reset();
}
float temperature()
{
byte i;
byte present = 0;
byte data[12];
float celsius, fahrenheit;
ds.reset();
ds.select(addr);
ds.write(0x44, 1); // start conversion, with parasite power on at the end
delay(1000); // maybe 750ms is enough, maybe not
// we might do a ds.depower() here, but the reset will take care of it.
present = ds.reset();
ds.select(addr);
ds.write(0xBE); // Read Scratchpad
debug(F(" Data = "));
debug(present, HEX);
debug(F(" "));
for ( i = 0; i < 9; i++) { // we need 9 bytes
data[i] = ds.read();
debug(data[i], HEX);
debug(F(" "));
}
debug(F(" CRC="));
debug(OneWire::crc8(data, 8), HEX);
debugln();
// Convert the data to actual temperature
// because the result is a 16 bit signed integer, it should
// be stored to an "int16_t" type, which is always 16 bits
// even when compiled on a 32 bit processor.
int16_t raw = (data[1] << 8) | data[0];
byte cfg = (data[4] & 0x60);
// at lower res, the low bits are undefined, so let's zero them
if (cfg == 0x00) raw = raw & ~7; // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
//// default is 12 bit resolution, 750 ms conversion time
celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;
debug(F(" Temperature = "));
debug(celsius);
debugln(F(" Celsius"));
return celsius;
}
/********************* DEBUG SUGAR ****************************/
void debug(const __FlashStringHelper *ifsh) {
#if SERIAL
Serial.print(ifsh);
#endif
}
void debug(const char *message) {
#if SERIAL
Serial.print(message);
#endif
}
void debug(int num) {
#if SERIAL
Serial.print(num);
#endif
}
void debug(uint8_t b, int s) {
#if SERIAL
Serial.print(b, s);
#endif
}
void debugln(const __FlashStringHelper *ifsh) {
#if SERIAL
Serial.println(ifsh);
#endif
}
void debugln(const char *message) {
#if SERIAL
Serial.println(message);
#endif
}
void debugln(int num) {
#if SERIAL
Serial.println(num);
#endif
}
void debugln() {
#if SERIAL
Serial.println();
#endif
}
view raw therm.ino hosted with ❤ by GitHub

Simple server code

I used PHP script as thin layer to manage rrdtools. It generates two charts - temperature and controller uptime. Script should have rights to execute system shell, server must have rrdtools installed, and script must have rights to create and write files in directory. Only under these conditions it will work.

<?php
// some settings first
$rrdtool = "/usr/bin/rrdtool";
$rrd = "temps.rrd";
$png = "temps.png";
$pngUptime = "temps.uptime.png";
$last = "temps.last.txt";
// parse data from request
$temp = isset($_GET['temp']) ? floatval($_GET['temp']) : null;
$uptime = isset($_GET['uptime']) ? intval($_GET['uptime']) : null;
// log data to text file for debug
if(!empty($temp)) {
file_put_contents($last, date('Y-m-d H:i:s').' '."$temp $uptime\n", FILE_APPEND);
echo "OK";
}
// if there is no rrd database yet we create it
$start = time();
if (!is_file($rrd)) {
$cmd = "$rrdtool create $rrd --start ".($start-1)." --step 60 ".
"DS:temperature:GAUGE:120:-50:80 ".
"DS:uptime:GAUGE:120:0:U ".
"RRA:AVERAGE:0.5:1:600 ".
"RRA:AVERAGE:0.5:6:700 ".
"RRA:AVERAGE:0.5:24:775 ".
"RRA:AVERAGE:0.5:288:797 ".
"RRA:MIN:0.5:1:600 ".
"RRA:MIN:0.5:6:700 ".
"RRA:MIN:0.5:24:775 ".
"RRA:MIN:0.5:288:797 ".
"RRA:MAX:0.5:1:600 ".
"RRA:MAX:0.5:6:700 ".
"RRA:MAX:0.5:24:775 ".
"RRA:MAX:0.5:288:797 ".
"RRA:AVERAGE:0.5:1:200";
echo shell_exec($cmd);
}
// fill with data if we have them
if(!empty($temp)) {
$cmd = "$rrdtool update $rrd $start:$temp:$uptime";
echo shell_exec($cmd);
}
// if client is not Arduino but some browser we generate and serve two images
if(!empty($_SERVER['HTTP_USER_AGENT'])) {
$cmd = "$rrdtool graph $png ".
"--title 'Temperature' ".
"--start 'now-1d' ".
"--end now ".
"--imgformat PNG ".
"--width=600 ".
"--height=300 ".
"--lower-limit 0 ".
"DEF:a=$rrd:temperature:AVERAGE ".
"'LINE2:a#00b6e4:Temperature in kitchen' ";
//echo $cmd."<br />";
shell_exec($cmd);
echo "<br /><img src='$png'><br /><br />";
$cmd = "$rrdtool graph $pngUptime ".
"--title 'Uptime' ".
"--start 'now-1d' ".
"--end now ".
"--imgformat PNG ".
"--width=600 ".
"--height=300 ".
"--lower-limit 0 ".
"DEF:a=$rrd:uptime:AVERAGE ".
"'LINE2:a#FE6E3A:Uptime' ";
//echo $cmd."<br />";
shell_exec($cmd);
echo "<br /><br /><img src='$pngUptime'><br /><br />";
}
view raw therm.php hosted with ❤ by GitHub

What todo next?

ATMEL has its own internal temperature sensor, we should chart it as well!

http://playground.arduino.cc/Main/InternalTemperatureSensor

Comments