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. |
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.
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.
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
.
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;
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.
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...
.
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 |
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.
Michael Schilliarbeitet 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. |