Hitzefühler (Linux-Magazin, März 2006)

Mit Linux ist es relativ leicht, selbstgebaute Hardware einzuhängen und kreativ einzusetzen. Heute wird der Lökolben ausgepackt, denn das Bastelfieber ist ausgebrochen!

Es ist noch gar nicht so lange her, da musste man noch Device-Treiber schreiben, um exotische Selbstbau-Hardware anzusteuern. Seit aber USB zum Standard erwachsen ist und Hotplugging im 2.6-Kernel anstandslos funktioniert, geht es viel einfacher.

Der heute vorgestellte Temperaturfühler DS18S20 ([3]) von der Firma Dallas Semiconductor lässt sich über einen sogenannten One-Wire-Bus ansteuern, den wiederum ein im Rechner steckender USB-Dongle betreibt. Die unter [2] frei erhältliche owfs-Steuerungssoftware fragt die Daten dann unter anderem über eine Perl-Schnittstelle ab. Statt One-Wire sollte der Bus allerdings Two-Wire heißen, denn zwei dünne Kupferkabel (meist in einer einzigen Umhüllung) gehen vom Sensor zum USB-Dongle (siehe Abbildung 1). Am anderen Ende des Kabels findet sich ein Telefonstecker (RJ11), der wiederum in den USB-Dongle eingeklickt wird.

Abbildung 1: Diagramm: Die Temperatursensoren hängen über den One-Wire-Bus am USB-Dongle.

Den Temperatursensor DS18S20 gibt es im einschlägigen Elektronikfachhandel für etwa $5 zu kaufen (zum Beispiel bei digikey.com). Er ist zwischen -55C und +125C einsetzbar. Der One-Wire-USB-Dongle DS9490R, in den man mit handelsüblichen Telefon-Mehrfachsteckern viele Sensoren hängen kann, schlägt mit etwa $15 - $25 zu Buche (zum Beispiel bei hobby-boards.com).

Abbildung 2: Abfrage des Onewire-USB-Dongle mit den angeschlossenen Temperatursensoren von der Kommandozeile.

Das owfs-Projekt ([2]) auf Sourceforge bietet eine Reihe von Schnittstellen an, um die Temperaturwerte der Sensoren auszulesen. Eine davon nutzt das User-Filesystem FUSE und bildet die Sensor-Daten auf dem File-System ab, ähnlich der /proc-Hierarchie in Linux. Abbildung 2 zeigt, wie ein Dongle mit zwei Sensoren sich dem Benutzer präsentiert: Nicht nur die ausgelesenen Temperaturwerte sind verfügbar, sondern auch noch eindeutige IDs der Sensoren, deren Typbezeichnung und vieles andere mehr. In den kleinen Transistor-ähnlichen Gehäusen steckt ein kleiner Microcontroller, der einiges auf dem Kasten hat.

Werte der als Dateien erscheinenden Messstationen liest ein einfacher cat-Befehl aus, in Abbildung 2 wurde allerdings perl -ple1 verwendet, um ein Newline anzuhängen. Unter dem Eintrag 10B2A7C7000800/temperature findet sich die im ersten Sensor gemessene Wert, 22.8125 Grad Celsius Zimmertemperatur. Der zweite Sensor mit der ID 10.E0E3C7000800, der in einer kühlen Winternacht in San Francisco draußen hing, misst hingegen kühlere 14.4375 Grad (in Kalifornien wird es selten richtig kalt). Unter dem Eintrag type steht die Typbezeichnung des Fühlers ("DS18S20"), damit man über die Programmierschnittstelle herausfinden kann, ob es sich um einen Temperatursensor oder um ein anderes Teil mit One-Wire-Bus handelt. Der Hersteller Dalles bietet alles mögliche an, darunter auch Schalter, Spannungs- und Strommesser.

Lötkolben anheizen

Abbildung 3: Zwei Sensoren stecken über einen Telefonkabel-Splitter im One-Wired-USB-Dongle, der wiederum an einem Linux-System angeschlossen ist.

Der USB-Dongle hat am Ausgang eine Telefonbuchse. Um die Sensoren dort einzuhängen, muss vorher noch jeweils ein langes Kabel mit einem abschließenden Telefonstecker an die Beinchen der Temperaturfühler angelötet werden.

Am einfachsten geht das, indem man ein normales Telefonverlängerungskabel mit Steckern an beiden Enden einkauft und einen davon brutal mit einer Zange abzwackt. Dann wird die äußere Hülle des Kabels abisoliert und es kommen entweder zwei oder vier dünne Kabel zum Vorschein.

Abbildung 4: Vor dem Anlöten: Grün ans linke Bein, Rot ans Mittlere Bein des DS18S20.

Zum Einsatz kommen nur das rote und das grüne Kabel, die restlichen können abgezwickt werden. Der Temperaturfühler hat drei Beinchen, von denen das rechteste (wenn man das Gehäuse mit der abgeflachten Seite nach vorne ansieht und die Beinchen nach unten zeigen lässt) überflüssig ist. Es dient dazu, dem Fühler extra Spannung zuzuführen, aber der begnügt sich auch damit, Strom aus der Datenleitung zu stehlen ([8]). Mit einer Zange wird also das rechteste Fühlerbeinchen abgezwickt und das Telefonkabel mit drei Schrumpfschlauchstücken vorbereitet (Abbildung 4). Später werden die Schlauchstücke leicht erhitzt, was sie elegant zusammenschmurgeln lässt, um dem Fühler ein einigermaßen Wohnzimmer-kompatibles Aussehen zu verleihen.

Abbildung 5: Der Sensor in der Klemme mit einem angelöeteten Kabel.

Das grüne innere Telefonkabel wird anschließend ans linke Bein des DS18S20 angelötet, das rote kommt ans mittlere dran (Abbildung 5). Dann fährt man mit dem Lötkolben nahe an den zwei roten inneren Schrumpfschläuchen entlang, worauf diese einschrumpfen und so die abisolierten Drahtstücke umschließen. Falls sie nicht weit genug schrumpfen, hilft ein Stück Isolierband, sie so zu befestigen, dass sie sich nicht berühren und einen Kurzschluss erzeugen. Anschließend führt man das dickere (gelbe) Schrumpfschlauchstück vor, bis der Sensor nur noch leicht rausspitzelt und lässt den Schlauch unter der Lötkolbenhitze zusammenschmurgeln. Abbildung 6 zeigt den fertigen Fühler, dessen Telefonstecker entweder direkt im Dongle Platz findet oder aber -- falls mehrere Sensoren zum Einsatz kommen -- über einen Mehrfachstecker (Abbildung 3).

Abbildung 6: Der fertige Sensor in dem zusammengeschrumpften Schrumpfschlauch.

Such den Sensor

Zu Testzwecken wird nun ein Sensor im Zimmer belassen und der andere durchs Fenster ins Freie geführt. Das owfs-Projekt liefert mit dem Modul OW eine generische Perl-Schnittstelle mit, die das Modul in Listing OWTemp.pm auf die verwendeten Temperaturfühler zuschneidert. Zunächst ist nicht bekannt, wieviele Geräte am Bus hängen, welche davon Temperaturfühler sind, und was ihre eindeutigen IDs sind. Die vom Konstruktor new aufgerufene discover-Methode findet dies heraus, indem sie einfach die type-Einträge aller am Bus hängenden Geräte öffnet und nachsieht, ob sich dort ein DS18S20 zu erkennen gibt. Mit OW::init('u') nimmt das Modul dann Kontakt zum USB-Dongle auf und nachfolgende Aufrufe der Methode temperatures() liefern Paare aus Fühler-Nummer und dem ausgelesenen Temperaturwert. Der Destruktor in Zeile 44 ruft OW::finish() auf, um die Verbindung zum USB-Dongle aufzulösen.

Listing 1: OWTemp.pm

    01 ###########################################
    02 package OWTemp;
    03 # Mike Schilli, 2005 (m@perlmeister.com)
    04 ###########################################
    05 
    06 use Log::Log4perl qw(:easy);
    07 use OW;
    08 
    09 ###########################################
    10 sub new {
    11 ###########################################
    12     my($class, @options) = @_;
    13 
    14     my $self = {
    15         type => "DS18S20",
    16     };
    17 
    18     bless $self, $class;
    19 
    20     OW::init('u');
    21 
    22     $self->{devices} = [$self->discover()];
    23 
    24     return $self;
    25 }
    26 
    27 ###########################################
    28 sub temperatures {
    29 ###########################################
    30     my($self) = @_;
    31 
    32     my @temperatures = ();
    33 
    34     for my $dev ( @{ $self->{devices} } ) {
    35         my($val) = owread("$dev/temperature");
    36         $val =~ s/\s//g;
    37         push @temperatures, [$dev, $val];
    38     }
    39 
    40     return @temperatures;
    41 }
    42 
    43 ###########################################
    44 sub DESTROY {
    45 ###########################################
    46     OW::finish();
    47 }
    48 
    49 ###########################################
    50 sub discover {
    51 ###########################################
    52     my($self) = @_;
    53 
    54     my @found = ();
    55 
    56     for my $entry (owread("")) {
    57         DEBUG "Found top entry '$entry'";
    58         next if $entry !~ /^\d/;
    59 
    60         my($type) = owread("$entry/type");
    61         
    62         DEBUG "Found type '$type'";
    63         next if defined $type and 
    64            $type ne $self->{type};
    65         push @found, $entry;
    66     }
    67     return @found;
    68 }
    69 
    70 ###########################################
    71 sub owread {
    72 ###########################################
    73     my($entry) = @_;
    74 
    75     my @found = ();
    76 
    77     my $result = OW::get($entry) or 
    78         LOGDIE "Failed to read $entry";
    79 
    80     DEBUG "owread result='$result'";
    81 
    82     for my $entry (split /,/, $result) {
    83         $entry =~ s#/$##;
    84         push @found, $entry;
    85     }
    86 
    87     return @found;
    88 }
    89 
    90 1;

Eine typische Sensor-Anwendung zeigt das Skript in Listing rrdmon. Es nutzt das Modul RRDTool::OO vom CPAN, das eine objektorientierte Schnittstelle zu Tobi Oetikers Werkzeug rrdtool bereitstellt.

Ohne Optionen aufgerufen, liest es alle Sensoren aus und legt ihre aktuellen Werte in der Round-Robin-Datenbank ab. Falls diese noch nicht existiert, legt die create-Methode in Zeile 26 sie mit zwei Datensourcen an, ``Inside'' und ``Outside'', für den Zimmer- und den Außentemperaturfühler. Die Zeilen 15 und 16 ordnen den Sensoren-IDs diese einfacher zu merkenden Bezeichnungen zu. Diese IDs sind weltweit eindeutig, neu gekaufte Sensoren haben andere IDs.

Der Parameter step in Zeile 27 gibt mit 300 ein Auffrischintervall von 300 Sekunden (5 Minuten) vor und 5000 Werte speichert die Datenbank bevor sie alte Werte überschreibt. Ab Zeile 56 findet dann der Auslese- und Auffrischungsvorgang statt, mit den von OWTemp bereitgestellten Methoden und update() von RRDTool::OO. Die Zeilen 15 und 16 weisen den wenig aussagekräftigen Fühler-IDs lesbare Bezeichnungen zu: ``Outside'' und ``Inside''. Welcher Sensor welche ID hat, lässt sich einfach herausfinden, indem man nur einen Sensor anschliesst und dann wie in Abbildung 2 gezeigt, die Verzeichnisstruktur anzeigen lässt.

Wird rrdmon aber mit dem Parameter -g aufgerufen, erzeugt es aus den RRD-Daten eine grafische Darstellung der Temperaturverläufe beider Fühler und legt sie in der PNG-Datei /tmp/temperature.png ab (Abbildung 7). Der Innensensor wird rot, der Aussensensor blau dargstellt.

Abbildung 7: Der mit RRDTool gezeichnete Graph veranschaulicht den Temperaturverlauf des Aussen- und des Innensensors.

Listing 2: rrdmon

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Getopt::Std;
    04 use Log::Log4perl qw(:easy);
    05 use Sysadm::Install qw(:all);
    06 use RRDTool::OO;
    07 use OWTemp;
    08 
    09 Log::Log4perl->easy_init($DEBUG);
    10 
    11 my  $RRDDB   = "/tmp/temperature.rrd";
    12 my  $GRAPH   = "/tmp/temperature.png";
    13 
    14 my %sensors = (
    15     "10.E0E3C7000800" => "Outside",
    16     "10.B2A7C7000800" => "Inside",
    17 );
    18 
    19 getopts("g", \my %o);
    20 
    21     # Constructor     
    22 my $rrd = RRDTool::OO->new(
    23              file => $RRDDB);
    24 
    25     # Create a round-robin database
    26 $rrd->create(
    27      step        => 300,
    28      data_source => { name      => "Outside",
    29                       type      => "GAUGE" },
    30      data_source => { name      => "Inside",
    31                       type      => "GAUGE" },
    32      archive     => { rows      => 5000 }) unless -f $RRDDB;
    33 
    34 if($o{g}) {
    35         # Draw a graph in a PNG image
    36     $rrd->graph(
    37       start          => time() - 24*3600*3,
    38       image          => $GRAPH,
    39       vertical_label => 'Temperatures',
    40       draw           => {
    41           color  => '00FF00',
    42           type   => "line",
    43           dsname => 'Outside',
    44           legend => 'Outside',
    45       },
    46       draw           => {
    47           type   => "line",
    48           color  => 'FF0000',
    49           dsname => 'Inside',
    50           legend => 'Inside',
    51       },
    52       width          => 300,
    53       height         => 75,
    54       lower_limit    => 0,
    55     );
    56 } else {
    57     my $ow = OWTemp->new();
    58     my %values = ();
    59     for my $station ($ow->temperatures()) {
    60         my($dev, $temp) = @$station;
    61         $values{$sensors{$dev}} = $temp;
    62     }
    63     $rrd->update(time => time(), values => \%values);
    64 }

Hinausposaunt

Wer die Temperaturausgabe lieber als Text und global zugreifbar möchte (zum Beispiel, um im Urlaub nachzuprüfen, ob man vergessen hat, daheim den Herd auszuschalten), schreibt sich einfach einen IRC-Bot wie in Listing tempbot. Der Bot verbindet sich mit dem IRC-Server auf irc.freenode.org und macht den Chatroom #sftemp auf.

Der besorgte Urlauber nutzt dann einfach einen IRC-Client oder auch dem IM-Client gaim, um dem Bot im Chatroom einen Besuch abzustatten. Abbildung 8 zeigt die Gaim-Konfiguration und Abbildung 9 das Kommando, um in den Chatroom einzusteigen, wo der Bot schon auf das Stichwort "temp" wartet. Fällt es, extrahiert er die letzten gemessenen Fühlerwerte aus dem RRD-Archiv und sendet sie zurück in den Chatroom (Abbildung 10).

Abbildung 8: Der IM-Client gaim beherrscht auch das IRC-Protokoll. Einfach einen Account anlegen, auf "Online" klicken und ...

Abbildung 9: ... das Kommando zum Eintreten in den Channel #sftemp geben.

Abbildung 10: Der IRC-Bot antwortet mit den gerade herrschenden Temperaturen.

Listing 3: tempbot

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

Bot::BasicBot ist ein gutes Beispiel, wie man mit einem CPAN-Modul und denkbar wenig Code hochkomplizierte Funktionen erledigen kann. Man erzeugt einfach eine von Bot::BasicBot abgeleitete Klasse und definiert dort die Methode said(), die dann aufgerufen wird, falls jemand im Chatroom etwas sagt. said() erhält die Nachricht als Parameter und kann dann überprüfen, ob der Bot etwas erwidern will und entweder eine Nachricht oder undef zurückgeben. Mit Bot::BasicBot in der Version 0.65 kommt beim Starten die Warnung ``Use of ->new() is deprecated, please use spawn()'' hoch, sie darf aber ignoriert werden.

Installation

Die Distribution der owfs-Software, die über die USB-Schnittstelle den One-Wire-Bus anspricht, steht unter [2] bereit. Zur Drucklegung dieses Artikels funktionierte nur die aktuellste Version aus dem CVS-Repository des owfs-Projekts, die mit

    cvs -d :pserver:anonymous@cvs.sourceforge.net:/cvsroot/owfs co owfs

geholt wird. Außerdem steht auf [5] ein Tarball bereit, der erwiesenermaßen mit den heute vorgestellten Skripts funktioniert.

Um owfs zu installieren, wird die aktuellste Version von SWIG ([7]) gebraucht, die Entwicklerversion 1.3.27 funktionierte tadellos. Wer nicht nur die Perl-Schnittstelle installieren will, sondern auch noch mit dem Kommandozeilentool owfs den One-Wire-Bus auf das Dateisystem abbilden will (Abbildung 2), braucht außerdem das User-Filesystem FUSE von [4], wenn es nicht schon der verwendeten Linuxdistribution beiliegt (Testen mit ls -l /usr/local/bin/fusermount). Dann erfolgt mit

    ./bootstrap
    ./configure
    make

der Build-Prozess im explodierten Tarball. Ein anschließendes make install installiert das Kommandozeilentool. Das Perl-Modul OW installiert man mit

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

ebenfalls in der owfs-Distribution.

Ein Cronjob, der alle fünf Minuten aufgerufen wird, füllt stetig das RRD-Archiv:

    */5 * * * * cd /pfad; ./rrdmon; ./rrdmon -g;

Unter /pfad sollte dann nicht nur rrdmon, sondern auch das Perl-Modul OWTemp.pm liegen. Die von rrdmon erzeugten Dateien liegen in /tmp, wem das zu wackelig ist, der sollte die Pfadvariablen in rrdmon (Zeilen 11/12) umsetzen. Und da jeder Sensor eine eigene ID hat, müssen die Zeilen 15 und 16 müssen an die lokalen Gegebenheiten angepasst werden. Die IDs der angeschlossenen Sensoren findet man nach dem in Abbildung 2 gezeigten Verfahren heraus.

Die restlichen Module Sysadm::Install und Log::Log4perl stehen auf dem CPAN. RRDTool::OO erfordert entweder eine funktionierende rrdtool-Installation oder wird versuchen, eine vom Netz zu laden.

Für den Bot wird Bot::BasicBot benötigt, das wiederum automatisch die POE-Distribution installiert.

Sicher ist sicher

Steckt man den USB-Dongle in den Rechner, schaltet sich der Hotplug-Mechanismus ein und erzeugt ein USB-Device:

    root -rw-r--r-- /proc/bus/usb/003/008

Da owfs auch beim Datenlesen schreibend auf den Dongle zugreift, funktioniert das Auslesen der Temperaturwerte nur für root. Es wäre aber schlechter Stil, alle Skripts als root laufen zu lassen. Abhilfe schafft hier ein Hotplug-Skript, das ausführbar in der Datei /etc/hotplug/usb/ds2940 stehen muss:

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

Damit der Hotplugger es beim Einschieben des Dongles ausführt und damit die Berechtigungen des Device-Eintrages korrigiert, muss folgende Zeile ans Ende von /etc/hotplug/usb.usermap:

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

So können alle vorgestellten Skripts unter normalen User-IDs laufen und der Sicherheitsbeauftragte kriegt keinen roten Kopf.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/03/Perl

[2]
Das One-Wire-File-System-Projekt: http://owfs.sourceforge.net

[3]
Temperatursensor DS18S20: http://www.maxim-ic.com/quick_view2.cfm/qv_pk/2815

[4]
Die Website des Fuse-Projekts: http://fuse.sourceforge.net

[5]
CVS-Schnappschuss von owfs: http://perlmeister.com/devel/owfs-2.2p0RC-cvssnap.tgz

[6]
Datenblatt des One-Wire-USB-Dongles DS9490R: http://pdfserv.maxim-ic.com/en/ds/DS9490-DS9490R.pdf

[7]
Die Development-Version von SWIG: http://prdownloads.sourceforge.net/swig/swig-1.3.27.tar.gz

[8]
One-wire Bus: http://en.wikipedia.org/wiki/1-Wire

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.