Reading temperature sensors with Perl

How Cool is Perl?


Linux lets tinkerers connect their hardware creations to computers. We'll show you how you can use a simple Perl interface to check the temperature of your home or office while you're away.

By Michael Schilli

It's not so long ago that users had to write their own device drivers to add hardware. But with the standardization of USB, and with fully-functional hot-plugging support in kernel 2.6, things have become far easier.

The temperature sensor used in this article, a Dallas Semiconductor DS18S20 ([3]), can be controlled via the one-wire bus, which is driven by a USB dongle attached to the computer. The free owfs control software at [2] can request data via a Perl interface. Instead of one-wire, the bus should really be called two-wire, as two thin copper wires (typically in a single sheathing) connect the sensor to the USB dongle (see Figure 1). At the other end there is a western connector (RJ11), which plugs into the USB dongle.

Figure 1: The temperature sensors connect to the USB dongle via the one-wire bus.

The DS18S20 temperature sensor is available from most electronics stores for about US$5 (for example from digikey.com). It can handle a temperature range between -55°C and +125°C. The DS9490R one-wire USB dongle, to which multiple sensors can be connected using western plugs, costs about US$15 to US$25 (at hobby-boards.com for example).

The owfs project ([2]) at Sourceforge offers a number of interfaces for reading temperature values from the sensors. One uses the FUSE user filesystem and maps the sensor data on the filesystem, much like the /proc hierarchy in Linux. Figure 2 shows what kind of data gets transferred from a dongle with two attached sensors. You can read the temperature values and also the unique sensor IDs, type designations, and other things. The sensor housing contains a micro-controller with surprising capabilities.

Figure 2: Querying the one-wire USB dongle and attached temperature sensors at the command line.

Measured values can be read simply by running cat against the FUSE-controlled files, although I used perl -ple1 to add a newline to the output in Figure 2. Below the 10.B2A7C7000800/temperature entry, you can see the value measured by the first sensor, 22.8125 degrees Celsius. The second sensor with the ID of 10.E0E3C7000800, which I left outside overnight in San Francisco, returns a cooler value of 14.4375 degrees (it never gets really cold in California). The type entry gives us the sensor type designation ("DS18S20"); this allows you to query the type of sensor connected to the one-wire bus via the interface. The vendor, Dallas, has all kinds of equipment on sale, including switches, and voltage and current meters, which can all be attached to the same one-wire bus.

Fire Up Your Soldering Iron

The USB dongle has a western socket (RJ11). To connect the three-legged sensors to the socket, I first had to solder a long wire with a western plug onto the temperature sensor pins.

The easiest way of doing this is to buy a normal phone extension lead with plugs at both ends, and just cut off one end with wire cutters. Then strip the insulation and you should see either two or four thin wires.

We will just be using the red and green wires, so we can just cut the remaining ones. The temperature sensor has three pins, one of which is not needed - this is the pin on the right (when the flat side of the sensor case is facing towards you with the pins pointing down). This pin is used to provide a power supply to the sensor, but the sensor is quite happy to use the current flowing through the data line ([8]). You can use wire cutters to remove the superfluous pin. Then go on to prepare the telephone wire by adding heat shrinkable tubing, as shown in Figure 4. We will be heating this tubing later to shrink it, making the appearance of the sensors easier to live with.

Figure 4: Before soldering: green connects to the left pin and red to the right pin on the DS18S20.

Now solder the green inner wire of the telephone lead onto the left pin of the DS18S20, and solder the red wire onto the pin in the center. Hold your soldering iron near the two red inner heat shrinkable tubes to shrink them over the stripped wire ends. If the tubing does not shrink as much as you would like, you can always add some insulating tape. This is just to prevent the bare wires touching and causing a short circuit. Then adjust the position of the thicker (yellow) heat shrinkable tubing until you can just see the tip of the sensor, and follow the same procedure to heat-shrink the tubing. Figure 5 shows you the finished sensor; you can either plug the telephone connector directly into the dongle, or you can use an extension, if you have more than one sensor (Figure 3).

Figure 3: Two sensors connected to a one-wire USB dongle via a phone line splitter; the dongle plugs into the Linux system.

Figure 5: The finished sensor in the heat-shrunk tubing.

Finding the Sensor

For test purposes, I will now leave one sensor in the room, while routing the other through the window outdoors. The owfs project provides a generic Perl interface in the form of the OW module. The module in Listing 1 customizes the module to match the type of temperature sensor we will be using.

Listing 1: OWTemp.pm
001 #############################
002 package OWTemp;
003 # Mike Schilli, 2005
004 # (m@perlmeister.com)
005 #############################
006
007 use Log::Log4perl qw(:easy);
008 use OW;
009
010 #############################
011 sub new {
012 #############################
013  my ($class, @options) = @_;
014
015  my $self =
016    { type => "DS18S20", };
017
018  bless $self, $class;
019
020  OW::init('u');
021
022  $self->{devices} =
023    [ $self->discover() ];
024
025  return $self;
026 }
027
028 #############################
029 sub temperatures {
030 #############################
031  my ($self) = @_;
032
033  my @temperatures = ();
034
035  for my $dev (
036   @{ $self->{devices} }) {
037
038   my ($val) = owread(
039    "$dev/temperature");
040   $val =~ s/\s//g;
041   push @temperatures,
042     [ $dev, $val ];
043  }
044
045  return @temperatures;
046 }
047
048 #############################
049 sub DESTROY {
050 #############################
051  OW::finish();
052 }
053
054 #############################
055 sub discover {
056 #############################
057  my ($self) = @_;
058
059  my @found = ();
060
061  for my $entry (owread("")) {
062   DEBUG
063   "Found top entry '$entry'";
064   next if $entry !~ /^\d/;
065
066   my ($type) =
067     owread("$entry/type");
068
069   DEBUG "Found type $type";
070   next
071     if defined $type
072     and $type ne
073     $self->{type};
074   push @found, $entry;
075  }
076  return @found;
077 }
078
079 #############################
080 sub owread {
081 #############################
082  my ($entry) = @_;
083
084  my @found = ();
085
086  my $result = OW::get($entry)
087    or LOGDIE
088    "Failed to read $entry";
089
090  DEBUG
091    "owread result='$result'";
092
093  for my $entry (
094    split /,/, $result) {
095   $entry =~ s#/$##;
096   push @found, $entry;
097  }
098
099  return @found;
100 }
101
102 1;

At first, we have no way of knowing how many devices are attached to the bus, which of them are temperature sensors, and what their unique IDs are. The discover method, which is called by the new constructor, finds this out for us by opening the type entries for all the devices attached to the bus and checking for a DS18S20. OW::init(`u') then tells the module to contact the USB dongle, and the following call to the temperatures() method returns pairs of sensor IDs and measured temperature values. The destructor in line 49 calls OW::finish() to close the connection to the USB dongle.

The script in Listing 2 shows a typical sensor application. It uses the RRDTool::OO module from CPAN to provide an object-oriented interface to Tobi Oetiker's rrdtool.

Listing 2: rrdmon
01 #!/usr/bin/perl -w
02 #############################
03 # rrdmon -Temp sensor monitor
04 # Mike Schilli, 2005
05 # (m@perlmeister.com)
06 #############################
07 use strict;
08 use Getopt::Std;
09 use Log::Log4perl qw(:easy);
10 use Sysadm::Install qw(:all);
11 use RRDTool::OO;
12 use OWTemp;
13
14 Log::Log4perl->easy_init(
15                      $DEBUG);
16
17 my $RRDDB =
18   "/tmp/temperature.rrd";
19 my $GRAPH =
20   "/tmp/temperature.png";
21
22 my %sensors = (
23  "10.E0E3C7000800" =>
24    "Outside",
25  "10.B2A7C7000800" =>
26    "Inside",
27 );
28
29 getopts("g", \my %o);
30
31 # Constructor
32 my $rrd =
33   RRDTool::OO->new(
34  file => $RRDDB);
35
36 # Create a round-robin DB
37 $rrd->create(
38  step        => 300,
39  data_source => {
40   name => "Outside",
41   type => "GAUGE"
42  },
43  data_source => {
44   name => "Inside",
45   type => "GAUGE"
46  },
47  archive => { rows => 5000 }
48   )
49   unless -f $RRDDB;
50
51 if ($o{g}) {
52
53  # Draw graph in a PNG image
54  $rrd->graph(
55   start => time() - 24 *
56     3600 * 3,
57   image          => $GRAPH,
58   vertical_label =>
59     'Temperatures',
60   draw => {
61    color  => '00FF00',
62    type   => "line",
63    dsname => 'Outside',
64    legend => 'Outside',
65   },
66   draw => {
67    type   => "line",
68    color  => 'FF0000',
69    dsname => 'Inside',
70    legend => 'Inside',
71   },
72   width       => 300,
73   height      => 75,
74   lower_limit => 0,
75  );
76
77 } else {
78
79  my $ow = OWTemp->new();
80
81  my %values = ();
82
83  for my $station (
84   $ow->temperatures())
85  {
86   my ($dev, $temp) =
87     @$station;
88   $values{ $sensors{$dev} } =
89     $temp;
90  }
91
92  $rrd->update(
93   time   => time(),
94   values => \%values
95  );
96 }

Without specifying any options, this script reads all the sensors and stores the current temperature values in a round-robin database. If the database does not exist, the create method in line 37 of the script creates the database with two data sources, "Inside" and "Outside," for the temperature inside the room and the temperature outside the window. Lines 23 and 25 map the sensor IDs to these intuitive names. The IDs are globally unique; any sensors you buy will have different IDs.

The step parameter in Line 38 sets the refresh interval to 300, that is, 300 seconds or 5 minutes; the database stores 5000 values before it starts to overwrite the older values. Reading and refreshing starts in Line 83 using the methods provided by OWTemp, and update() from RRDTool::OO.

Lines 23 and 25 map the non-intuitive sensor IDs to human-readable values: "Outside" and "Inside." It is easy to find out which sensor has which ID by just attaching one sensor, and then viewing the directory structure, as shown in Figure 2.

If you call rrdmon with the -g parameter, it creates a graph of the temperature curve from the RRD data from both sensors and stores it in the /tmp/temperature.png file (Figure 8). The inside sensor is shown in red, and the outside sensor in blue.

Figure 8: The graph drawn by RRDTool visualizes the temperature curve for the inside and outside sensors.

Shouting Out

If you prefer remote, text-based access to the temperature output (say, you are on vacation and want to know whether you have left the oven on), you can write an IRC bot like the one shown in Listing 3. The bot connects to the IRC server at irc.freenode.org and opens a chatroom called #sftemp.

Listing 3: tempbot
01 #!/usr/bin/perl -w
02 #############################
03 # tempbot-Temp sensor IRC bot
04 # Mike Schilli, 2005
05 # (m@perlmeister.com)
06 #############################
07 use strict;
08 use Bot::BasicBot;
09
10 package TempBot;
11 use base qw( Bot::BasicBot );
12 use Log::Log4perl qw(:easy);
13 use RRDTool::OO;
14
15 #############################
16 sub said {
17 #############################
18  my ($self, $mesg) = @_;
19
20  return
21    unless $mesg->{body} eq
22    "temp";
23
24  my $rrd =
25    RRDTool::OO->new(file =>
26     "/tmp/temperature.rrd");
27
28  my $dsnames =
29    $rrd->meta_data(
30      "dsnames");
31
32  $rrd->fetch_start(
33   start => time() - 5 * 60,
34   end   => time()
35  );
36
37  my $string;
38
39  while (my ($time, @values) =
40   $rrd->fetch_next()) {
41
42   for (my $i=0;
43        $i < @$dsnames; $i++){
44    $string .=
45      sprintf "%10s: %.1f\n",
46      $dsnames->[$i],
47      $values[$i];
48   }
49   return $string;
50  }
51 }
52
53 $^W = undef;
54
55 TempBot->new(
56  server =>
57    'irc.freenode.net',
58  channels => ['#sftemp'],
59  nick     => 'tempbot',
60 )->run();

To dispel your worries, so that you can get on with your vacation, just launch an IRC client or the gaim IM client to visit the bot in its chatroom. Figure 6 shows the gaim configuration and Figure 7 gives you the command for entering the chatroom, where the bot is waiting for you to enter the "temp" keyword. The bot listens to the conversation in the chatroom, and if anyone says "temp", it extracts the last temperature values from the RRD archive and sends them back to the chatroom (Figure 9).

Figure 6: The gaim IM client speaks the IRC protocol. Just create an account, click "Online," and ...

Figure 7: ... enter the command to enter the #sftemp channel.

Figure 9: The IRC bot responds with the current temperatures.

Bot::BasicBot is a good example that shows you how to complete highly complex tasks with a CPAN module and just a bit of code. You just derive a class from Bot::BasicBot, and define the said() method for the class; the method is called when someone says something in the chatroom. said() receives the message as a parameter, and can then check if the bot wants to respond, and either return a message or undef. If you have version 0.65 of Bot::BasicBot, you will see a message when you launch that says Use of ->new() is deprecated, please use spawn(), but you can just ignore the warning.

Installation

The owfs software distribution, which uses a USB interface to talk on the one-wire bus, is available at [2]. When this article went to print, only the latest version from the owfs project's CVS repository worked; enter cvs -d:pserver: anonymous @cvs.sourceforge.net:/cvsroot/owfs co owfs to get the version. There is also a tarball at [5] that has been shown to work with the scripts in this issue.

You need the latest version of SWIG ([7]) to install owfs; the developer version 1.3.27 worked fine. If you will be installing the owfs command line tool to access the one-wire bus via the command line (Figure 2), along with the Perl interface, you also require the FUSE user filesystem from [4], unless your distribution happens to provide it (test by entering ls -l /usr/local/bin/fusermount). Then enter the following:

./bootstrap
./configure
make

to start the build. Then run make install to install the command line tool. The following steps install the OW Perl module:

cd module/swig/perl5
perl Makefile.PL
make install

from the owfs distribution. A cronjob which runs every five minutes fills the RRD archive: */5 * * * * cd /path; ./rrdmon; ./rrdmon -g;.

You need both rrdmon and the OWTemp.pm Perl module in your /path. The files created by rrdmon are stored in /tmp, but you can change the path variables in rrdmon (lines 18/20). As each sensor has a unique ID, you need to modify lines 23 and 25 to reflect your environment. Use the approach shown in Figure 2 to discover the sensor IDs.

The remaining modules, Sysadm::Install and Log::Log4perl, are available from CPAN. RRDTool::OO requires either a working rrdtool installation, or it will try to download a working installation. For the bot, you need Bot::BasicBot, which automatically installs the POE distribution.

Be Secure

When you plug the USB dongle into your computer, the hot-plug mechanism creates a USB device, something like root -rw-r--r-- /proc/bus/usb/003/008. As owfs needs write access to the dongle, root privileges are required to read the temperature values. An executable hot-plug script in /etc/hotplug/usb/ds2940 gives you a workaround to avoid running the scripts as root:

#!/bin/bash
# /etc/hotplug/usb/ds2940
chmod a+rwx "${DEVICE}"

To allow the hotplugger to run the script when the dongle is inserted, append the following code to /etc/hotplug/usb.usermap:

# /etc/hotplug/usb.usermap
# DS2940 one-wire USB device
ds2940 0x0003 0x4fa 0x2490 0x0000 0x0000 0x00 0x00 0x00 0x00 0x00 0x000x00000000

This allows all our scripts to run with non-privileged user IDs, which keeps the host security people from jumping on your back.

INFO
[1] Listings for this article: http://www.linux-magazine.com/Magazine/Downloads/65/Perl
[2] The one-wire file system project: http://owfs.sourceforge.net
[3] DS18S20 temperature sensor: http://www.maxim-ic.com/quick_view2.cfm/qv_pk/2815
[4] The Fuse project website: http://fuse.sourceforge.net
[5] CVS snapshot for owfs: http://perlmeister.com/devel/owfs-2.2p0RC-cvssnap.tgz
[6] DS9490R one-wire USB dongle datasheet: http://pdfserv.maxim-ic.com/en/ds/DS9490-DS9490R.pdf
[7] SWIG developer version : http://prdownloads.sourceforge.net/swig/swig-1.3.27.tar.gz
[8] One-wire bus: http://en.wikipedia.org/wiki/1-Wire