header image

Raspberry Pi 1-Wire Temperature Sensor Project

Background

This project was inspired when some pipes froze in the crawlspace over the winter when we were not around. The idea is to pick up some 1-wire temperature sensors and drive them with a raspberry pi (RPi), log the readings, make a webpage tracking them, and maybe trigger an incandescent light in the crawlspace if the moving average of the temperature gets below a threshold to heat it up a little bit, safely.

Materials

Stuff I ended up using:

  • Raspberry Pi (RPi Model-B),
  • A little acrylic case for the RPi,
  • A SD-card to boot the RPi (I used 8Gb because it lying around unused),
  • 26-pin ribbon cable,
  • Two 26-pin crimp on ribbon cable ends,
  • Some headers that fit a ribbon cable end,
  • A little proto board,
  • a 2k ohm resistor,
  • An RJ45 to D-sub adapters without the D-sub part,
  • Some cat5e twisted pair cable,
  • RJ45 crimp on ends for the twisted pair cable,
  • Cat5e twisted pair splitters,
  • Some surface mount RJ45 female jacks,
  • Some twisted pair keystone punchdown jacks,
  • A few DS18S20 1-Wire thermometers,
  • (optional) DS18B20 waterproof 1-Wire thermometer(s),
  • (optional) Realtek RTL8188CUS usb wifi adapter (Edimax EW7811)
  • A power switch tail,
  • Heat shrink tubing,
  • Solder.

10k Foot View

A Raspberry Pi is a credit-card sized board that runs Linux and has some useful headers for making stuff. My plan with this project was to use the GPIO headers of the RPi to read the DS18S20s at regular intervals then log the readings into some kind of time series database (after research I used the rrdool and a python interface to it).

A cronjob polls the temperature readings every minute. A different cronjob computes a 10-minute moving average and may trigger the incandescent lights. Yet a third cronjob wakes up hourly and drives some graphs on a webpage that are used as a display.

Click on most images below for larger views.

Selecting the DS18S20s

I did some research on the 1-Wire thermometers and settled on the DS18S20 mainly because of the temperature range and accuracy. They are relatively expensive, unfortunately. The ones I got ran about $4 each(!) (Edit: they can be had much less expensively these days, I saw 10 for $6 on amazon recently). They can be daisy-chained and are meant to be run via twisted pair.

Another option is to buy the waterproof 1-wire thermometer (link above) which comes already encased in a waterproof metal casing with a ~3ft black cord running into it. This thing was ~$5 from Amazon when I bought it and it works on the same 1-wire network as the others, no problems. It has three wires: red=power, black=ground, white=data.

The datasheet is linked above. Additionally I found a helpful document about how to make the 1-Wire network reliable and this detailed PDF paper about 1-wire protocol and applications.

Setting up the RPi

I'm not going to go into a ton of detail here because you can find all of this information elsewhere online. The RPi wants to boot from an SD card. There are several images you can dd onto the SD card, I chose "wheezy" which seems to have native support for 1-wire bus off the GPIO header. Once I booted up the RPi, I made sure sshd was running, hooked up an Ethernet cable and did the rest of the setup and coding from a more comfortable console.

RPi GPIO -> proto board -> twisted pair bridge

The RPi has a 26-pin header called GPIO (General Purpose I/O). I read that these pins are basically hooked right to the microprocessor so be really careful about shorting them by accident. It's easy to do when you have dangling wires and might ruin your RPi. An alternative to using the GPIO header is that you can get a little USB bridge dongle that brings your 1-wire network into the USB port on the RPi. I decided to use the GPIO stuff though because I'm brave like that. Here's the GPIO pinout:

I used a 26-pin ribbon cable with a little 26-pin connector crimped onto it to pick up the GPIO header. Attaching the connector was done in my woodworking vice, carefully.

Then I jammed some wires into the other connector end and hooked it all to a breadboard to see if I could see/read a DS18S20. Everything worked and the RPi had an entry at /sys/bus/w1/devices for the thermometer which I could read as a file (via cat or a python script). Here's what it looks like if it's working (yours will have a different serial number, they are all unique):

$ cat /sys/bus/w1/devices/10-0008021eb5ed/w1_slave
2a 00 4b 46 ff ff 0d 10 d1 : crc=d1 YES
2a 00 4b 46 ff ff 0d 10 d1 t=20937

The exact format of this data is described in the datasheet linked above. The initial 0x2a and 0x00 are the temperature payload as sign extended two's compliment. Fortunately for you and me, someone already converted it to a temperature in the driver. That's the "t=" business n the second line. Note that there's an implicit decimal point so the above means 20.937°C. Just remember to divide by 1000. It works!

Also notice the "crc=d1 YES" bit. If that ends with "NO" then the CRC (cyclical redundancy check, i.e. error-detecting bits) indicate a data error and you should discard the reading and retry. This happens.

I had some proto boards lying around here so I soldered some headers that fit the ribbon cable connectors. Neither the proto board nor the header was large enough to pick up all 26 pins from the cable but that's ok because all you needed on the twisted pair network was a power, ground and signal. I originally used power from GPIO pin1 (3v) but later switched to GPIO pin2 (5v) because the document about reliable 1-wire networks recommended 5v and the datasheet for the DS18S20 says it can handle up to 5v. For ground I used GPIO pin9 and for signal I used GPIO pin7 (GPIO 4/GPCLK0).

 

The DS18S20s have a parasitic power mode where you can run them with only two wires: ground and data. The data has a pullup resistor to power and provides enough current to the chip to run it. If you do this you have to ground the unused power pin on the DS18S20. I didn't have a lot of luck with this, though. The DS18S20s would report back bad temperatures way more often when wired this way. That reliable 1-wire network doc also says that for large networks you should use three pins (run power). So I didn't mess with figuring out why parasitic power mode didn't work very much.

I took a D-sub to RJ45 adapter and removed the D-sub part so that I just had a eight pins hanging off the back.

 

Then I cut some cat5e twisted pair cable, crimped an end onto it, plugged it into the D-sub thingy, and used a multimeter to figure out what colors in the twisted pair cable corresponded to what colors on the D-sub thingy. On my setup it looked like this:

cat5e twisted pair D-sub connector thingy
orange/white blue
orange orange
green/white black
blue red
blue/white green
green yellow
brown/white brown
brown white

You should check your own if you decide to do it this way, though, because I'm sure this is just related to the way I crimp network cables (incidentally, in the same order as the "cat5e twisted pair" colors above).

Based on a diagram at hobby boards about 1-wire networks, I decided to use my twisted pair green wire for 5v power, my green/white wire for ground, my blue wire for data and my blue/white wire for ground. I also hooked orange to GPIO1 and brown to GPIO17 with their pairs (orange/white and brown/white) both to ground also. I did this so that I could control far away light switches via a power switch tail or solid state relay later on.

Twisting a signal or power wire with ground helps remove interference on the line and will help increase the length of network you can run.

I used some hot glue and a zip tie to firmly anchor the D-sub thingy to the proto board and soldered the right pins to the right spots on the board to implement the connections described above. I added a 2k ohm resistor between power and signal and tested it out using a long Ethernet cable with one end cut off and jammed into a breadboard to see if I could still read the thermometer. Here's the final circuit and what it looked like:

     

As you can see I ended up using a 2k ohm resistor instead of the recommended 4.7k ohm resistor, again on the advice of that reliable 1-wire network doc and some threads I had read online about long distance 1-wire networks. That doc defines "network weight" as the total amount of load (wire, devices) on the network and "network radius" as the distance between the master (RPi) and the most distant slave (DS18S20). I wanted several DS18S20s and had an idea that my network would need at least 20 meters of cable to do what I wanted.

The 1-Wire Network

Making the 1-wire network is basically as easy as laying twisted pair cable around wherever you want temperature readings. I chose to use unshielded cat5e twisted pair because I had a roll of it sitting in the closet.

First, I took the DS1820s, a short distance of twisted pair cat5e cable, and a RJ45 end and made a little plug-in thermometer. Crimp the end onto the cable then solder the DS1820's three pins to the appropriate color wires on the other side. For me that is DS1820 pin1 (ground) to blue/white, DS1820 pin2 (data) to blue and DS1820 pin3 (power) to green. I used two sizes of heat-shrink tubing to shield the wires from each other and to encase all the wires in a sheath. Be careful when you're doing this not to pre-shrink your heat shrink tubing by getting the soldering iron too close. Also look at your wires carefully; I managed to nick a couple of them when stripping one of the cat5e cables and one of my thermometers would not work until I cut it open and fixed it with some electrical tape...

Once you have your plug in thermometers you can run a normal twisted pair network around the area you want to take temperature readings from. I ordered some twisted pair RJ45 splitters online to facilitate this. I don't know if you can use an old hub if you have one and would love to hear. But definitely do not use a switch because my understanding is that a hub operates on the physical layer whereas a switch is aware of data. Don't worry about the bad reviews people give to RJ45 splitters: as best I can tell they are from dumb people who try to use them to wire up Ethernet instead of using a hub/switch.

I terminated my runs with modular surface mount RJ45 jacks that I could plug the little thermometers into.

Software

Once everything was setup the fun part could begin: writing the code. After some research I found rrdtool: "round robin database tool". This is a really neat utility that you can use to create and populate files that store a timeseries of data. It can compute derivative values, populate archive timeseries, and even make graphs of the data for you. And, to boot, there is a simple python interface to it. I also found a pretty good tutorial for using it.

To setup the rrdtool on the SD card of the RPi you will need to provide the interval (--step) at which you'll provide readings, designate one or more data sources, and create one or more RRA (round robin archives?). Here's the command I ended up using:

% rrdtool create temps.ttd --start 1371018212 --step 60 \
  DS:inside:GAUGE:900:-50:50 \
  DS:outside:GAUGE:900:-50:50 \
  DS:crawlspace:GAUGE:900:-50:50 \
  RRA:AVERAGE:0.5:1:525600 \
  RRA:AVERAGE:0.25:60:87600

This creates three data sources ("inside", "outside" and "crawlspace") that can store data values between -50 and 50. All are supposed to be provided once every 60 seconds (--step) and will be considered to be "unknown" if more than 900 seconds have passed without being set. Values can be stored anytime after the UNIX timestamp passed to --start. Moreover it creates two RRAs; one that averages just one datum (so takes the value of each update) and stores it for 525600 generations (1 year). The other averages sixty readings together allowing 25% of the 60 (up to 15) to be unknown and stores 87600 generations.

Next, I wrote a simple python utility to poll the temperature of a sensor values and write them into this rrdtool file. Remember to check whether the CRC was valid on your read; if it's not the data is probably corrupt. This happens sometimes:

#!/usr/bin/python

import rrdtool

databaseFile = "temps.rrd"
MIN_TEMP = -50
ERROR_TEMP = -999.99

rrds_to_filename = {
  "outside" : "/sys/bus/w1/devices/10-0008021eb5ed/w1_slave",
  "inside" : "/sys/bus/w1/devices/10-0008021eb67a/w1_slave",
  "crawlspace" : "/sys/bus/w1/devices/10-0008021ec8b6/w1_slave",
}

def read_temperature(file):
  tfile = open(file)
  text = tfile.read()
  tfile.close()
  lines = text.split("\n")
  if lines[0].find("YES") > 0:
    temp = float((lines[1].split(" ")[9])[2:])  # (get rid of the t=)
    temp /= 1000
    return temp
  return ERROR_TEMP

def read_all():
  template = ""
  update = "N:"
  for rrd in rrds_to_filename:
    template += "%s:" % rrd
    temp = read_temperature(rrds_to_filename[rrd])
    update += "%f:" % temp
  update = update[:-1]
  template = template[:-1]
  rrdtool.update(databaseFile, "--template", template, update)

read_all()

This thing is called every minute from a cronjob and, as you can see, puts some data into the rrdtool file created above.

Since rrdtool can also graph, I created another cronjob that runs once an hour to create a graph image:

#!/bin/sh

rrdtool graph /home/pi/www/one_day.png \
  --title "Temp (C) at the Cabin, 1d" \
  --start now-1d --end now \
  --width=640 --height=480 \
  --step=60 -v degreesC \
  DEF:temp1=/home/pi/temps.rrd:outside:AVERAGE \
  DEF:temp2=/home/pi/temps.rrd:inside:AVERAGE \
  DEF:temp3=/home/pi/temps.rrd:crawlspace:AVERAGE \
  LINE2:temp1#008000:"outside" \
  LINE2:temp2#800000:"inside" \
  LINE2:temp3#800080:"crawlspace" \
  HRULE:0#0000FF:"freezing"
...

Here's a script that can wake up as a cronjob, compute the 10 minute moving average temperature, and toggle a light switch by setting GPIO pin 17 high or low. That pin goes out over the twisted pair network on the brown wire and can be fed into a power switch tail or a solid state relay to flip a switch. Remember that the GPIO pins are very low current (~18ma) so you can't flip a mechanical relay with one of these directly.

#!/usr/bin/python

import time
import datetime
import rrdtool
import RPi.GPIO as GPIO

def toggle_light(n):
  GPIO.output(17, GPIO.HIGH if n else GPIO.LOW)

intervals_to_avg = 10
rrd_file = '/home/pi/temps.rrd'
interval = 60  # this matches the logging interval in the rrdTool
end_time_obj = datetime.datetime.now()
end_time = int(time.mktime(end_time_obj.timetuple())) - interval
start_time = end_time - (intervals_to_avg * interval)
threshold = 2.0  # Turn on at <= 2 degrees C

# Setup pin17 for output
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(17, GPIO.OUT)

# Fetch the last 10 readings in the rrdfile.  They are at 1m intervals.
data = rrdtool.fetch(rrd_file, 'LAST', '-r', "%d" % interval, '-s', "%d" % start
_time, '-e', "%d" % end_time)

# this is the format of the data from the rrdtool return: tuples of
# tuples and lists of tuples... oh my.
# (
#   (1354516740, 1354517400, 60),
#   ('degreesC',),
#   [
#     (20.875,), (20.934400703733335,), (20.937000000000005,),
#     (20.878312342766666,), (20.875,), (20.81502969835,),
#     (20.812,), (20.812,), (20.812,), (20.812,)
#   ]
# )

# Average them.
aggregate = 0.0
num_valid_entries = 0
for x in data[2]:c
    if x != None:
        aggregate += x[0]
        num_valid_entries += 1

if num_valid_entries == 0:
    print "Error - no valid data"
    exit(0)

moving_avg = (aggregate / num_valid_entries);
print "10m moving average temperature: %5.3f" % moving_avg

# Decide about toggling the lights.
if moving_avg <= threshold:
    toggle_light(True)
    print "Turning on the light."
elif moving_avg > threshold:
    toggle_light(False)
    print "Turning off the light."
exit(moving_avg)

The wifi adapter I linked about from Edimax works with the RPi and doesn't need a powered USB hub. So I used it to avoid running Ethernet down to where I wanted the 1-Wire network to live.

Here's another sample of a temperature graph extracted from the RRD (not live):

index.html was last updated 1 January 2017 and is Copyright (C) 2002-2015 by Scott Gasch (scott.gasch@gmail.com).