Diskreter Lauscher (Linux-Magazin, Dezember 2006)

Ist auf der heimischen Telefonnummer mal wieder kein Durchkommen, verfolgt ein per Fernsteuerung aktiviertes Skript dort den Gesprächsverlauf und zeigt dessen Ende auf einer Webseite an.

Was tun, wenn man mal wieder zuhause anrufen will, dort aber jemand mit einem Marathongespräch die Leitung blockiert? In den USA gibt es die Serviceleistung des ``Call Waiting'' [2], das dem Dauertelefonierer durch ein Piepzeichen anzeigt, dass ein Anrufer versucht durchzukommen. Doch dieser Service kostet extra und nervt leicht, deshalb habe ich neulich mit Hilfe eines kleinen Telefonverstärkers von Radio Shack (Abbildung 1 und Anhang [3]) eine kleine Applikation gebastelt.

Abbildung 1: Die "Smart Phone Recorder Control" von der Firma "Radio Shack" leitet das Signal aus der Telefonleitung an die Soundkarte des Linuxrechners weiter.

Die sogenannte ``Smart Phone Recorder Control'' greift das Signal aus der Telefonleitung ab und leitet es per Klinkenstecker an den Mikrofoneingang der Soundkarte meines Linux-Rechners weiter. Das Tonsignal ist dann unter Linux über das Device /dev/dsp verfügbar und das Perl-Modul Audio::DSP vom CPAN liest es ein. Mit ein paar heuristischen Tricks bestimmt das Skript dann, ob auf der Telefonleitung gesprochen wird. Falls ja, bleibt es am Ball, bis sich nichts mehr regt und gibt dies anschließend über ein verstecktes CGI-Skript auf einer Webseite bekannt.

Abbildung 2: Über eine Webseite wird das Skript auf dem Linuxrechner aktiviert und der Status des Telefongesprächs angezeigt.

Fernaktivierter Schläfer

Im Ruhezustand schläft das Skript phonewatch auf dem heimischen Linuxrechner und sieht alle 60 Sekunden auf der Webseite nach, ob jemand auf dem CGI-Skript per Mausklick den Status ``Check'' eingestellt hat (Abbildung 3). Hierauf erwacht das Skript, und fängt an, Daten aus der Telefonleitung zu sammeln. Das CGI-Skript auf der Webseite zeigt nun den Status ``Busy'' an und frischt seine Seitendarstellung alle 60 Sekunden im Browser auf.

Abbildung 3: Das CGI-Skript ist im 'idle'-Modus, der Lauscher wartet auf seinen Einsatz.

Abbildung 4: Der Lauscher hat bestätigt, dass die Telefonleitung aktiv ist und das CGI-Skript auf 'busy' geschaltet. Wird der Hörer aufgelegt, springt der Status wieder auf 'idle' zurück.

Wird der Hörer aufgelegt, bekommt phonewatch dies mit und stellt die Anzeige des CGI-Skripts auf ``idle'' zurück.

Listing 1: phonewatch

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use Audio::DSP;
    04 use Log::Log4perl qw(:easy);
    05 use SoundActivity;
    06 use LWP::Simple;
    07 
    08 Log::Log4perl->easy_init({
    09   file => "/tmp/phonewatch.log",
    10   level => $INFO,
    11 });
    12 
    13 my $IN_USE_POLL =  10;
    14 my $IDLE_POLL   =  60;
    15 my $STATUS_URL  = 
    16      'http://u:p@_foo.com/phonewatch.cgi';
    17 my $SAMPLE_RATE = 1024;
    18 
    19 INFO "Starting up";
    20 
    21 while(1) {
    22   my $state = state();
    23 
    24   if(! defined $state) {
    25     DEBUG "Fetch failed";
    26     sleep $IDLE_POLL;
    27     next;
    28   }
    29 
    30   DEBUG "web site state: $state";
    31 
    32   if($state eq "idle") {
    33     DEBUG "Staying idle";
    34     sleep $IDLE_POLL;
    35     next;
    36   }
    37 
    38   INFO "Monitor requested";
    39   state("busy");
    40   poll_busy();
    41   state("idle");
    42 }
    43 
    44 ###########################################
    45 sub poll_busy{
    46 ###########################################
    47 
    48   my $dsp = new Audio::DSP(
    49     buffer   => 1024,
    50     channels => 1,
    51     format   => 8,
    52     rate     => $SAMPLE_RATE,
    53   );
    54 
    55   $dsp->init() or die $dsp->errstr();
    56 
    57   my $act = SoundActivity->new();
    58 
    59   while(1) {
    60     DEBUG "Reading DSP";
    61     $dsp->read() or die $dsp->errstr();
    62 
    63     $act->sample_add( $dsp->data() );
    64     $dsp->clear();
    65 
    66     if(! $act->is_active()) {
    67         INFO "Hangup detected";
    68         $dsp->close();
    69         return 1;
    70     }
    71     sleep $IN_USE_POLL;
    72   }
    73 }
    74 
    75 ###########################################
    76 sub state {
    77 ###########################################
    78   my($value) = @_;
    79 
    80   my $url = $STATUS_URL;
    81   $url .= "?state=$value" if $value;
    82   DEBUG "Fetching $url";
    83   my $content = get $url;
    84   if($content =~ m#<b>(.*?)</b>#) {
    85       return $1;
    86   }
    87 }

Die Endlosschleife ab Zeile 21 holt den Status auf der Webseite ein und schläft $IDLE_POLL Sekunden, falls dieser auf idle eingestellt ist. Die Funktion state() dient zur Abfrage des aktuell eingestellten Status, kann aber auch (wenn ihr ein Parameter überreicht wird) einen neuen Status auf der Webseite einstellen. Beides erledigt sie mit einem get-Aufruf aus dem Modul LWP::Simple, das eine Webseite per URL abruft. Aus dem zurückkommenden Seiteninhalt filtert sie den Skriptstatus aus dem <b>...<b>-Tag heraus.

Fischen im Audio

Der Konstruktor der Klasse Audio::DSP erwartet vier Parameter: die Länge des zu füllenden Datenpuffers (1024), die Anzahl der Kanäle (hier 1, da Mono), das Format der abgegriffenen Datenpunkte (unsigned 8bit), und die Sampling-Rate (1024 Samples pro Sekunde).

Digitale Audiodaten liegen als Zahlenwerte vor, die ein Wandler N mal pro Sekunde aus dem analogen Tonsignal abgreift. Diese Samplingrate N muss doppelt so hoch sein wie die höchste abzugreifende Audiofrequenz, also bei HiFi-Qualität (bis knapp über 20kHz) mehr als 40.000 Mal pro Sekunde. Da wir keinen Lauschangriff starten, sondern nur Aktivität messen wollen, reichen 1024 Samples pro Sekunde völlig aus. Mehr zum Thema ``Digitales Audio'' findet sich in [3].

Binnen einer Sekunde füllt phonewatch so den Datenpuffer bis zum Rand und füttert ihn an die Methode sample_add() des Moduls in Listing SoundActivity.pm. Diese entpackt die 8-bit-Werte mit unpack("C") aus dem überreichten Datenblock und ermittelt deren statistische Standardabweichung. Aus einer toten Telefonleitung kann der Verstärker kein Signal auslesen, und nur etwas Rauschen kommt am Mikrofoneingang der Soundkarte an. Die Sample-Werte, deren Wertebereich sich von 0 bis 255 erstreckt, nehmen im Ruhezustand alle etwa den Wert 127 an und schwanken manchmal um 1 nach oben oder unten. Die Standardabweichung lag im Experiment typischerweise bei etwa 0.5.

In SoundActivity.pm errechent die Methode sdev ab Zeile 63 die auf zwei Nachkommastellen gerundete Standardabweichung der Elemente eines als Referenz übergebenen Arrays. sdev() nutzt für die hierzu notwendige einfache Arithmetik einfach das CPAN-Modul Statistics::Basic::StdDev.

Listing 2: SoundActivity.pm

    01 package SoundActivity;
    02 ###########################################
    03 use strict;
    04 use warnings;
    05 use Statistics::Basic::StdDev;
    06 use Log::Log4perl qw(:easy);
    07 
    08 ###########################################
    09 sub new {
    10 ###########################################
    11   my($class, %options) = @_;
    12 
    13   my $self = {
    14       min_hist       => 5,
    15       max_hist       => 5,
    16       history        => [],
    17       sdev_threshold => 0.01,
    18       %options,
    19   };
    20 
    21   bless $self, $class;
    22 }
    23 
    24 ###########################################
    25 sub sample_add {
    26 ###########################################
    27   my($self, $data) = @_;
    28 
    29   my $len     = length($data);
    30   my @samples = unpack("C$len", $data);
    31 
    32   my $sdev = $self->sdev(\@samples);
    33 
    34   my $h = $self->{history};
    35   push @$h, $sdev;
    36   shift @$h if @$h > $self->{max_hist};
    37   DEBUG "History: [", join(', ', @$h), "]";
    38 }
    39 
    40 ###########################################
    41 sub is_active {
    42 ###########################################
    43   my($self) = @_;
    44 
    45   if(@{$self->{history}} < 
    46      $self->{min_hist}) {
    47       DEBUG "Not enough samples yet";
    48       return 1;
    49   }
    50 
    51   my $sdev = $self->sdev($self->{history});
    52   DEBUG "sdev=$sdev";
    53 
    54   if($sdev < $self->{sdev_threshold}) {
    55       DEBUG "sdev too low ($sdev)";
    56       return 0;
    57   }
    58 
    59   return 1;
    60 }
    61 
    62 ###########################################
    63 sub sdev {
    64 ###########################################
    65   my($self, $aref) = @_;
    66 
    67   return sprintf "%.2f", 
    68          Statistics::Basic::StdDev->
    69               new($aref)->query;
    70 }
    71 
    72 1;

Aufwachen und Messen

phonewatch werkelt im aktiven Modus in der Funktion poll_busy() herum. Es führt jeweils eine Sekundenmessung an der Soundkarte durch, wartet 10 Sekunden und ermittelt dann den nächsten Messpunkt. Die gesammelten Standardabweichungen aus fünf Messpunkten sehen bei einem Telefongespräch etwa folgendermaßen aus:

     [0.64, 0.78, 0.73, 0.89, 0.86]

Die maximale Anzahl gespeicherter historischer Standardabweichungen bestimmt der Parameter max_hist in SoundActivity.pm. min_hist hingegen ist die Mindestanzahl von Messpunkten, die das Modul verlangt, um ein fundiertes Urteil über den Zustand der Leitung abzugeben. Wird der Hörer aufgelegt, breitet sich almählich Stille in der Leitung aus und die historischen Abweichungen pendeln sich alle auf dem gleichen Wert ein:

     [0.51, 0.51, 0.51, 0.51, 0.51]

Um diese zwei Zustände zu unterscheiden, ermittelt SoundActivity.pm einfach erneut die Standardabweichung dieser fünf historischen Werte. Ist sie kleiner als 0.01, gilt die Leitung als tot und is_active() liefert einen falschen Wert zurück.

Das CGI-Skript auf dem Webserver speichert seinen Zustand aufrufübergreifend in der Datei phonewatch.dat, an die es den persistenten Hash %store koppelt. Über den CGI-Parameter state setzt es neue Zustandswerte. Das Skript muss dann nur noch die passende Zustandsfarbe aus dem Hash %states abrufen und die Methode process des Template-Toolkits mit dem im DATA-Anhang definierten HTML-Template aufrufen. Diese generiert die HTML-Darstellung der Seite, indem es die simple aber effektive Template-Sprache abarbeitet. Außerdem pflanzt es das Reload-Meta-Tag in die Seite ein, die den Browser dazu bewegt, die Seite alle 30 Sekunden mit einem erneuten Aufruf des CGI-Skripts aufzufrischen. So wird der irgendwann vom Linuxrechner asyncron mit state() veränderte Zustand bei aufgelegten Telefonhörer im Browser des ungeduldig Wartenden sichtbar.

Listing 3: phonewatch.cgi

    01 #!/usr/bin/perl -w
    02 use strict;
    03 use CGI qw(:all);
    04 use DB_File;
    05 use Template;
    06 
    07 my %states = (
    08   idle  => 'green',
    09   check => 'yellow',
    10   busy  => 'red',
    11 );
    12 
    13 tie my %store, "DB_File", 
    14   "data/phonewatch.dat" or die $!;
    15 
    16 $store{state} = "idle" unless 
    17     defined $store{state};
    18 
    19 print header();
    20 
    21 my $new = param('state');
    22 if($new and exists $states{$new}) {
    23     $store{state} = $new;
    24 }
    25 
    26 my $tpl = Template->new();
    27 $tpl->process(\ join('', <DATA>),
    28   { bgcolor => $states{$store{state}},
    29     state   => $store{state},
    30     self    => url(),
    31   }) or die $tpl->error;
    32 
    33 ###########################################
    34 __DATA__
    35 <HEAD>
    36   <META HTTP-EQUIV="Refresh" 
    37         CONTENT="30; 
    38             URL=[% self %]">
    39 </HEAD>
    40 <BODY>
    41   <H1>Phone Monitor</H1>
    42   <TABLE CELLPADDING=5>
    43     <TR>
    44       <TD BGCOLOR="[% bgcolor %]">
    45         Status: <b>[% state %]</b>
    46       </TD>
    47       [% IF state == "idle" %]
    48       <TD>
    49         <A HREF="[% self %]?state=check">
    50         check</A>
    51       </TD>
    52       [% END %]
    53     </TR>
    54   </TABLE>
    55 </BODY>

Das CGI-Skript liefert, ohne Parameter aufgerufen, einfach den aktuellen Zustand als HTML formatiert zurück. Erhält es allerdings in einem CGI-Parameter status einen neuen Status, idle, busy oder check, verändert es seinen internen Status und speichert ihn permanent ab.

Das CGI-Skript sollte nicht offen im Internet herumstehen, eine leichte Hürde lässt sich mit einer .htaccess-Datei einbauen, die vom Benutzer einen Usernamen und ein gültiges Passwort einfordert. Wenn phonewatch dann das CGI-Skript aufruft, packt es die Credentials einfach mit in den URL hinein: https://user:pass@URL....

Die ewige Leier

Eine Soundkarte unter Linux zum Laufen zu bekommen, ist nicht immer ganz einfach, aber die im Experiment verwendete Audacity lief mit Version 1.0.13 des ALSA-Projekts problemlos. Wichtig ist es außerdem, den Mikrofoneingang gemäß Abbildung 5 mit dem alsamixer von ``Line'' auf ``Mic'' umzustellen, sonst ist die Empfindlichkeit zu gering. Zum Debuggen hat sich der Sound-Recorder und -Editor audicity bewährt.

Abbildung 5: Damit der Mikrofon-Eingang der Soundkarte funktioniert, sollte der rechteste Regler in C "Mic" (und nicht "Line") anzeigen.

Die von den Skripts verwendeten CPAN-Module lassen sich wie immer mit einer CPAN-Shell installieren, das Extra-Modul SoundActivity sollte entweder im gleichen Verzeichnis wie phonewatch installiert werden oder in einem Verzeichnis wo phonewatch es findet. Der Logging-Level von phonewatch ist auf $INFO gestellt, so dass nur wenige wichtige Statusinformationen in der Logdatei /tmp/phonewatch.log landen, für etwas ausführlichere Meldungen lässt sich der Level auf $DEBUG einstellen.

Dann noch einen Eintrag in die inittab setzen, damit das Skript beim Hochfahren des Rechners automatisch in seine Endlosschleife eintritt, und schon lässt der freundliche Helfer jederzeit und von überallher mit einem Webbrowser aktivieren.

Infos

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

[2]
http://en.wikipedia.org/wiki/Call_waiting

[3]
Principles of Digital Audio, Ken C. Pohlmann, McGraw Hill, 5th Edition, 2005

[4]
``Smart Phone Recorder Control'', Katalog der Firma ``Radio Shack'', http://www.radioshack.com/search/index.jsp?kw=43-2208

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.