measuring temperature from Linux

measuring temperature with a linux system and cheap serial temperature probes using the perl programming language and displaying a webpage graph with rrdtool

One of the things i wanted for a long time is measuring temperature (room and outside) and display this via a graph on my website. I didn’t want to pay a huge amount of money for the sensors so that stalled things for a while. A couple of weeks ago a collegue of mine found these nifty RS232 thermometers from a company called Papouch for a very reasonable price (the 14 euro is a bit deceptive since the total costs incl. BTW (VAT) and shipping will be around 20 euro per sensor). The fun thing about these sensors is that the are “intelligent”, they present temperature directly on the serial line, so you don’t have to do any fancy calculations. Also, since they are RS232 based it will work on any operating system. I ordered two probes for my computer at home, one to measure the room temperature and one for the outside temperature. The arrived after a week or two and i quickly plugged them in the serial ports, on Linux they are represented by:
/dev/ttyS0
/dev/ttyS1

There is a small program which comes with the sensors, tm.c, which is supposed to read from a sensor and show the results on screen, but that does not seem to work on my Fedora Core 3 machines. So i made a small kermit init script:

set modem type none   ; There is no modem
set carrier-watch off ; If DTR CD are not cross-connected
set line /dev/ttyS0   ; Device name
set speed 9600        ; Desired speed
set parity none       ; No parity
set stop-bits 2       ; 2 stop bits
set flow none         ; no flow-control
connect               ; Enter Connect (terminal) state

and saved this under .kermrc in my home directory, also i made sure i had all the necessary rights on the serial port devices (as root: chown my-userid /dev/ttyS[01]).
When i fired up kermit, i immediatly got some output, so the sensors worked fine.

I decided to write a program in perl which would be flexible enough to cater for all my future temperature needs. After a short quest on google i found that serial port communcations from perl are best done using: Device::SerialPort.
Since this module was not on my system yet, i had perl fetch it from CPAN and install it for me:

perl -MCPAN -e 'install Device::SerialPort'

This didn’t complete since there was a problem with one of the tests and perl reported it wouldn’t install without force. Well since i am the lazy type i thought i would try the module anyway, so:

cd ~/.cpan/build/Device-SerialPort-1.002/
make install

And that did the job. The first thing you always should do after installing a new module is read the documentation, it most often has ample examples in there to help you getting started:

perldoc Device::SerialPort

Since perl reads like a novel, i’ll just give the code straight away:
[perl]
#!/usr/bin/perl
#
# (c) 20060318 by Ewald
# https://www.oiepoie.nl

use Device::SerialPort; # to read serial port data
use Getopt::Std; # to handle cmdline options

my $V=”0.9 20060318″; # version and date of latest modification
my $DEBUG = 0; # do we want debugging output?
my $WAIT = 300; # how many seconds to wait between reads
my $ONCE = 0; # run only once?
my $UT = 0; # display timestamp in Unix format?
my $DONE = 0;
my $now;
my $TS=1; # do we want timestamps at all? (yes)
my $C=1; # display C to denote Celsius? (yes)
my $P = “/dev/ttyS0”; # default serial port to monitor

my %Options;
$ok = getopts(‘t:w:1uhdnc’, \%Options);

if (defined $Options{h}) {
print “logger.pl version: $V\n”;
print “Program to read temperature data from Papouch serial temp probe\n”;
print “Possible options:\n”;
print “-h Help (this output)\n”;
print “-d print debugging output\n”;
print “-c Don’t print C for celsius after values?\n”;
print “-n Don’t display timestamps\n”;
print “-1 Output temperature and exit\n”;
print “-u show timestamp in Unix format (seconds since epoch)\n”;
print “-t x read data from /dev/ttySx (default: $P) \n”;
print “-w N wait N seconds between readouts (default $WAIT)\n”;
exit 0;
}

$ONCE = 1 if defined $Options{1};
$DEBUG = 1 if defined $Options{d};
$TS = 0 if defined $Options{n};
$C = 0 if defined $Options{c};

$WAIT = $Options{w} if defined $Options{w};
$UT = $Options{u} if defined $Options{u};

if (defined $Options{t}) {
$P = “/dev/ttyS”;
$P .= $Options{t};
}

$DEBUG && print “monitoring serial port: $P\n”;
$DEBUG && print “wait interval is $WAIT\n”;
$DEBUG && print “run once? $ONCE\n”;
$DEBUG && print “Unix timestamps? $UT\n”;

# Serial Port initialization
my $port=Device::SerialPort->new($P) || die “cannot open $P: $!”;

$port->user_msg(ON);
$port->error_msg(ON);
$DEBUG && $port->debug($DEBUG);

$port->baudrate(9600) || die “failed setting baudrate”;
$port->parity(“none”) || die “failed setting parity”;
$port->databits(8) || die “failed setting databits”;
$port->stopbits(2) || die “failed setting databits”;
$port->handshake(“none”) || die “failed setting handshake”;
$port->write_settings || die “cannot write settings”;

# unset RTS and DTR
$port->rts_active(0);
$port->dtr_active(0);
sleep 1; # just a little wait to be sure we get stable data later on

if (($port->can_modemlines) && $DEBUG) {
$ModemStatus = $port->modemlines;
if ($ModemStatus & $port->MS_RTS_ON) { print “RTS ON\n”; }
else { print “RTS OFF\n”; }
if ($ModemStatus & $port->MS_DTR_ON) { print “DTR ON\n”; }
else { print “DTR OFF\n”; }
print “\n”;
}

# by setting RTS and DTR high we supply power to the device
# and in return it will supply us with data
$port->rts_active(1);
$port->dtr_active(1);

if (($port->can_modemlines) && $DEBUG) {
$ModemStatus = $port->modemlines;
if ($ModemStatus & $port->MS_RTS_ON) { print “RTS ON\n”; }
else { print “RTS OFF\n”; }
if ($ModemStatus & $port->MS_DTR_ON) { print “DTR ON\n”; }
else { print “DTR OFF\n”; }
print “\n”;
}

$port->read_char_time(0); # don’t wait for each character
$port->read_const_time(1000); # 1 second per unfulfilled “read” call

while (!$DONE) {
my ($count,$saw)=$port->read(8); # we should get 8 chars
if ($count > 0) {

$port->rts_active(0); # power-off device for now
$port->dtr_active(0);

$saw =~ s/([\+\-]\S+C)\r/$1/e; # value will be something like: +020.9C^M or -001.3C^M
# so we want to strip of the ^M (\r)
if (!$C) { $saw =~ s/(\S+)C/$1/e; } # strip of the C char
if ($TS) {
if ($UT) {
$now = time; # UNIX timestamp
}
else {
$now = localtime time; # human readable timestamp
}
printf “%s\t%s\n”, $now,$saw;
}
else {
printf “%s\n”, $saw; # output just the data
}

if ($ONCE) { # are we done?
$DONE = 1;
}
else {
sleep $WAIT; # sleep a little while before reading the next value
$port->rts_active(1); # power-on
$port->dtr_active(1);
}
}
else {
print “ERR: we got $count bytes of data\n”;
}
}

$port->rts_active(0); # power-off device
$port->dtr_active(0);
undef $port;

[/perl]

You can download the code from here: logger.pl

So now we got something which displays the output from the temperature sensors, but i wanted a nice graph of the information. Most people with a little Linux experience will see where this is going, rrdtool to the rescue!
Rrdtool is a very handy utility if you want to put anything (and i mean ANYthing) in a graph. It’s name stands for “Round Robin Database Tool” and that is wat it uses. In a round robin database values are stored untill the specified storage is used up and then the oldest data gets overwritten with new entries.
So to get a graph of one day of temperature data which i sample every 5 minutes, i would have to define a round robin archive of 288 (24 x 12) entries. Apart from the day graph you also want week, month and year graphs and thats easilly specified in one go.

On Fedora Core 3 we can install rrdtool using yum like this:

yum install rrdtool

All dependencies are automagically resolved. Debian users of course have theire trusty apt-get to do this, there is emerge for Gentoo users, Yast for Suse, and so on. If you are a die-hard, you can fetch the code and compile it yourself but there is a lot to be said for package management. When you install the software, the perl modules are automatically installed with it.

In the code below you can see that there are two DataSources (DS) defined, one in-temp for the temperature sensor in the room, and out-temp for the outdoor sensor. For both DataSources we have 4 Round Robin Archives (RRA). The DS is of type GAUGE which is what you would use for temperature readout. Other options are COUNTER, DERIVE, ABSOLUTE and COMPUTE.
If you want to get started yourself with rrdtool, the best thing to do is read the rrdtutorial. Or of course get somebody elses code and adapt it to your own needs.

Here is the perl code to create all the rrd stuff:
[perl]
#!/usr/bin/perl
#
# (c) 20060318 by Ewald
# https://www.oiepoie.nl

use RRDs;

sub CreateGraph
{
# creates the graph
# input: $_[0]: interval (ie, day, week, month, year)
# $_[1]: $rrd
# $_[2]: $img

my $rrd = $_[1];
my $img = $_[2];

RRDs::graph “$img/logger-$_[0].png”,
“–lazy”,
“-s -1$_[0]”,
“-t Leiden temperature “,
“-h”, “200”, “-w”, “600”,
“-a”, “PNG”,
“-v degrees C”,

“DEF:intemp=$rrd/logger.rrd:in-temp:AVERAGE”,
“LINE2:intemp#FF0000:Room Temperature”,
“GPRINT:intemp:MIN: Min\\: %2.lf”,
“GPRINT:intemp:MAX: Max\\: %2.lf”,
“GPRINT:intemp:AVERAGE: Avg\\: %4.1lf”,
“GPRINT:intemp:LAST: Current\\: %2.lf degrees C\\n”,

“DEF:outtemp=$rrd/logger.rrd:out-temp:AVERAGE”,
“LINE3:outtemp#0000FF:Outside Temperature”,
“GPRINT:outtemp:MIN: Min\\: %2.lf”,
“GPRINT:outtemp:MAX: Max\\: %2.lf”,
“GPRINT:outtemp:AVERAGE: Avg\\: %4.1lf”,
“GPRINT:outtemp:LAST: Current\\: %2.lf degrees C\\n”;

if ($ERROR = RRDs::error) { print “$0: unable to generate $_[0] graph: $ERROR\n”; }
}

# location of rrdtool databases
my $rrd = ‘/var/rrd’;
# location where the images should go
my $img = ‘/oiepoie/rrdtool’;

# get temp for inside and outside sensors
my $tempIN = `/usr/local/bin/logger.pl -1 -n -c -t 1`;
chop($tempIN);
my $tempOUT = `/usr/local/bin/logger.pl -1 -n -c -t 0`;
chop($tempOUT);

# if rrdtool database doesn’t exist, create it
if (! -e “$rrd/logger.rrd”)
{
print “creating rrd database …\n”;
RRDs::create “$rrd/logger.rrd”,
“-s 300”,
“DS:in-temp:GAUGE:600:-20:100”,
“DS:out-temp:GAUGE:600:-20:100”,
“RRA:AVERAGE:0.5:1:576”,
“RRA:AVERAGE:0.5:6:672”,
“RRA:AVERAGE:0.5:24:732”,
“RRA:AVERAGE:0.5:144:1460”;
}

# insert value into rrd
RRDs::update “$rrd/logger.rrd”, “-t”, “in-temp:out-temp”, “N:$tempIN:$tempOUT”;
if ($ERROR = RRDs::error) { print “$0: unable to update $rrd/logger.rrd: $ERROR\n”; }

# create graphs
&CreateGraph(“day”,$rrd,$img);
&CreateGraph(“week”,$rrd,$img);
&CreateGraph(“month”,$rrd,$img);
&CreateGraph(“year”,$rrd,$img);
[/perl]

Of course you have to make sure that you have write priviledge in the apropriate directories. Both programs do not need root priviledges (and hence should not be run as root just from a security perspective).

You can download the code from here: rrd_logger.pl

Add an entry in cron to run the stuff every 5 minutes:

*/5 * * * * /usr/local/bin/rrd_logger.pl > /dev/null

If you’re curious what the output looks like,
it’s right on my website