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.
I had clone of Arduino nano 3.0 with mini-usb with 3V level logic. You can buy various variants arduinos, it doesn't really matter what type you will use until there is Atmel atmega 328 and you know how to flash it or burn bootloader.
I choosed DS18B20 because it is well suited for outdoor conditions and meassure very precisely.
There are two modules on the market 3V and 5V with its own regulator. Since we have 3V logic and power supply i just choosed cheapest one. But you can get rid off separate AMS1117 voltage regulator if you buy module with its own regulator.
We need 3V power. A LOT suprisingly. Ethernet module need couple houndreds of miliamps. AMS1117 can do at least 800mA. I have different version of circuit, smaller, but it works same.
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$ |
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.
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.
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.
#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 | |
} |
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 />"; | |
} |
ATMEL has its own internal temperature sensor, we should chart it as well!
http://playground.arduino.cc/Main/InternalTemperatureSensor