Mit einem kleinen GPS-Empfänger am Arm joggt der rasende Perl-Reporter diesen Monat durch das Umland von San Francisco und bereitet die gesammelten Laufdaten mit Perl-Skripts grafisch auf.
Ein tragbares GPS-Gerät von der Größe einer Armbanduhr wie der Garmin Forerunner 10 eignet sich dazu, während eines Jogginglaufs laufend Ortskoordinaten aufzuzeichnen. So weiß der Läufer jederzeit, wie schnell er gerade unterwegs ist, und ob er zulegen muss oder nachlassen darf, um ein gestecktes Zeitziel zu erreichen. Nach abgeschlossener Sportaktivität kann er dann eventuell genußvoll neue Geschwindigkeitsrekorde verbuchen, auf einer Landkarte die abgespulten Kilometer Revue passieren lassen oder ein Höhenprofil der Strecke bestaunen.
Bis vor wenigen Jahren sahen tragbare GPS-Geräte noch aus wie Mobiltelefone
aus den frühen Neunzigern, der kleine Garmin ist aber bereits auf die Größe
einer LED-Digitaluhr aus den Siebzigern geschrumpft (Abbildung 1).
Ein unter Ubuntu in den USB-Eingang eingestöpseltes Gerät erkennt
der Linux-Kernel sofort als Speichereinheit und mounted die auf
dem Gerät gespeicherten Dateien unter /media/GARMIN
.
Dazu ist allerdings ein spezielles Adapterkabel notwendig, das sich wie
die Kreatur aus dem Film Alien an die GPS-Uhr anschmiegt und dem
Gerät beim Kauf beliegt.
Im Verzeichnis GARMIN/ACTIVITY
stehen dann proprietäre
.FIT-Dateien, in denen die Firma Garmin gespeicherte Bewegungsaktivitäten in
einem Binärformat ablegt.
Diese Dateien kann der Sportler nun anschließend unter einem neuangelegten Account auf die Garmin-Website http://connect.garmin.com hochladen und bekommt dort die Laufdaten schön angezeigt (Abbildung 2). Das Beispiel zeigt eine etwa 7 Kilometer lange Route rund um den Lake Merced, einem Binnensee nahe des pazifischen Ozeans etwas südlich von San Francisco, die ich für den Perl-Snapshot in etwa 40 Minuten gerannt bin. Zu bestaunen ist an der Grafik nicht nur die Gesamtzeit, sondern auch die durchschnittliche Zeitspanne pro Meile (8:45 Minuten) und die überwundenen Höhenmeter (300 Fuß). Wer keine amerikanischen Einheiten mag, kann auf der Website natürlich auch Kilometer und Höhenmeter einstellen.
Abbildung 1: Der armbanduhrgroße GPS-Empfänger zeichnet beim Joggen die Geo-Koordinaten der zurückgelegten Strecke mit Zeitstempeln auf. |
Abbildung 2: Auf der Webseite http://connect.garmin.com kann der Sportler seine .FIT-Daten hochladen und grafisch anzeigen. |
Das kleine GPS-Gerät ermittelt etwa alle sechs Sekunden die Position des
Läufers mit Hilfe von erdumkreisenden Navigationssatelliten und speichert
die Datenpunkte in geografischer Breite und Länge ab. Außerdem misst es die
aktuelle Geschwindigkeit und die zurückgelegte Strecke. Das unter
[3] erhältliche Perl-Modul Garmin::FIT kann die .FIT-Datei lesen und
Abbildung 3 zeigt die von der dem Modul beiliegenden
Utiliy fitdump
ausgespuckten Daten.
Der Garmin 10 zeichnet übrigens (anders als andere GPS-Geräte) keine Schwankungen der Meereshöhe während des Laufs auf. Anscheinend ist das satellitentechnisch aufwändiger, aber die Garmin-Webseite kann die Lücken im Bewegungsprofil basierend auf den Geo-Koordinaten mühelos füllen, denn sie nutzt ein serverseitig statisch vorliegenden Höhenprofil, das wohl für alle bewohnten Punkte der Erde deren Höhe über dem Meeresspiegel weiß. Wer nicht gerade in einem Hochhaus die Treppen hochläuft, sollte also auch präzise Höheninformationen zur Laufstrecke erhalten.
Abbildung 3: Die aus dem Fit-Format ausgelesenen Rohdaten zeigen, dass das Gerät etwa alle sechs Sekunden geografische Breite, Länge, die aktuelle Geschwindigkeit, und die zurückgelegte Strecke misst. |
Aber selbst die schönste Webseite lässt sich noch verbessern und manch einer möchte die Daten gar für esoterisch anmutende Zwecke ummodeln. Damit Open-Source-Freunde das Format nicht mühselig entschlüsseln müssen, liegt unter [2] das Garmin-SDK vor, das zwar keine Perl-API definiert, aber doch alle verwendeten Felder dokumentiert. Der Entwickler Kiyokazu Suto hat daraus ein Perl-Modul gebaut, es allerdings (noch) nicht aufs CPAN hochgeladen. Er stellt es statt dessen auf seiner Webseite ([3]) unter einer Public-Domain-artigen Lizenz zur Verfügung.
01 #!/usr/bin/perl 02 use warnings; 03 use strict; 04 use Garmin::FIT; 05 06 my $fit = Garmin::FIT->new(); 07 $fit->file( "354I2029.FIT" ); 08 $fit->data_message_callback_by_name('', 09 \&dump_it); 10 11 $fit->open(); 12 $fit->fetch_header(); 13 1 while $fit->fetch(); 14 15 ########################################### 16 sub dump_it { 17 ########################################### 18 my ($self, $desc, $v) = @_; 19 20 if( $desc->{message_name} ) { 21 print "$desc->{message_name} "; 22 } else { 23 print "Unknown "; 24 } 25 26 print "($desc->{message_number}):\n"; 27 28 $self->print_all_fields($desc, $v, 29 indent => ' '); 30 }
Trotz offengelegtem Format ist das Herausfieseln der Daten
aus dem Binärwust allerdings eine Sisyphus-Arbeit. Das Modul Garmin::FIT
bietet eine Methode print_all_fields()
an, mit der man schnell einen
Dumper wie Listing 1 zusammenklopfen kann, der die Ausgabe in Abbildung 3
erzeugt.
Zur Weiterverarbeitung der Daten
müssen Entwickler allerdings tiefer bohren und eigene Funktionen schreiben.
Listing 2 schickt sich deshalb an, die .FIT-Datei in eine leichter lesbare
YAML-Datei umzuformen.
Dazu lädt Zeile 12 eine auf der Kommandozeile übergebene .FIT-Datei ins Modul
Garmin::FIT. Ziel des dann
folgenden Verfahrens ist es, einen Perl-Array mit den
Daten der "record"
-Einträge der .FIT-Datei anzulegen und ihn dann mit
der Funktion DumpFile()
aus dem YAML-Modul als YAML-Daten in eine
gleichnamige Datei mit der Endung .yaml
hineinzupusten.
Zum Durchforsten der FIT-Einträge definiert Zeile 16 mit der Methode
data_message_callback_by_name()
einen Callback, den der Parser bei
jedem gefundenen Eintrag anspringt.
Die ab Zeile 30 definierte Funktion
message
erledigt die Fieselarbeit und setzt die zahlreichen Einzelfelder
eines Eintrags zu einem sinnvollen Ganzen zusammen. Wie in Abbildung 4
zu sehen ist, liegt der Wert für die zurückgelegte Strecke nicht etwa im
Eintrag distance
der dem Callback überreichten Variable $v
,
sondern es existieren eine ganze Reihe von Werten in einer zweiten
Variablen, $desc
, die der Parser umständlich kombinieren muss, um zum
gesuchten Wert zu gelangen. So steht in $desc
unter dem Schlüssel
i_distance
ein Indexwert (4
), unter dem der numerische Wert
für die gesuchte Entfernung in $v
steht (also $v[4]
).
Dieser wird dann
mit einer Einheit ("m" für Meter) kombiniert und das Resultat
mittels der Methode
value_cooked()
des Moduls Garmin::FIT erzeugt. Die Methode
benötigt allerdings noch die Werte für a_distance
(Skalierung/Einheit)
und I_distance
(Gültigkeitsbereich), damit sie endlich den gesuchten
Wert zusammenbauen kann.
Da das Format alle möglichen Daten auf kleinstem Raum speichern kann, ist
dieses an Buchbinder Wanninger erinnernde Verfahren wohl notwendig.
01 #!/usr/bin/perl 02 use warnings; 03 use strict; 04 use Garmin::FIT; 05 use YAML qw( DumpFile ); 06 07 my( $file ) = @ARGV; 08 die "usage: $0 fit-file" if !defined $file; 09 ( my $yaml_file = $file ) =~ s/.fit$/.yaml/i; 10 11 my $fit = Garmin::FIT->new(); 12 $fit->file( $file ); 13 14 my $messages = []; 15 16 $fit->data_message_callback_by_name( '', 17 sub { 18 my $msg = message( @_ ); 19 push @$messages, $msg if $msg; 20 return 1; 21 } ); 22 23 $fit->open(); 24 $fit->fetch_header(); 25 1 while $fit->fetch(); 26 27 print DumpFile( $yaml_file, $messages ); 28 29 ########################################### 30 sub message { 31 ########################################### 32 my ( $fit, $desc, $v ) = @_; 33 34 if( !$desc->{ message_name } or 35 $desc->{ message_name } ne 36 "record" ) { 37 return undef; 38 } 39 40 my $m = {}; 41 42 foreach my $i_name ( keys %$desc ) { 43 44 next if $i_name !~ /^i_/; 45 (my $name = $i_name ) =~ s/^i_//g; 46 47 my $pname = $name; 48 my $attr = $desc->{'a_' . $name}; 49 my $i = $desc->{$i_name}; 50 my $invalid = $desc->{'I_' . $name}; 51 52 $m->{ $pname } = 53 $fit->value_cooked( 54 "", $attr, "", $v->[$i] ); 55 } 56 57 return $m; 58 }
Abbildung 4: Das eigenwillige Garmin-Format. |
Der Einfachheit halber müht sich Listing 2 erst gar nicht mit allen
erlaubten Datenformaten ab, sondern konzentriert sich auf die record
-Einträge,
die die alle sechs Sekunden gemessenen Laufdaten enthalten. Das Gerät speichert
auch weitere Events, wie zum Beispiel wann und wo der "Start"-Knopf gedrückt
wurde, aber das soll hier nicht weiter interessieren. Abbildung 5 zeigt die
fertigen YAML-Daten, eine Liste von Records, die jeweils die Felder
distance
(seit Start zurųckgelegte Strecke in km),
position_lat
(geografische Breite), position_long
(geografische Länge),
speed
(Geschwindigkeit in m/s) und timestamp
(aktuelle Uhrzeit) führen.
Abbildung 5: Die GPS-Daten des Garmin im YAML-Format. |
Mit den YAML-Daten lassen sich nun ganz einfach Applikationen in Perl und anderen Sprachen schreiben, die die gespeicherten GPS-Daten interpretieren. Auf der Garmin-Webseite vermisste ich zum Beispiel eine Funktion eines älteren GPS-Geräts. Meine bislang verwendete GPS-Joghilfe (ein Garmin Forerunner 101) verfügte über eine Funktion, die ich beim neuen und leichteren Garmin 10 schmerzlich vermisste: den "Virtual Runner". Vor dem Lauf programmiert man dazu die Geschwindigkeit eines virtuellen Laufkumpans ein, der mit konstanter Geschwindigkeit läuft und per Definition exakt zur gewünschten Zeit die Ziellinie überschreitet. Während des Laufs zeigt dann das GPS-Gerät an, wo der virtuelle Läufer sich gerade befindet. Ist er 100m voraus, gilt es, Gas zu geben, denn nur wer auf gleicher Höhe mit dem virtuellen Kumpan trabt, wird zur gewünschen Zeit durch die Ziellinie spurten. Ist der Virtual Runner hingegen zurückgefallen, ist der Läufer so schnell unterwegs, dass er getrost die Laufgeschwindigkeit reduzieren und das hart erarbeitete Zeitpolster abschmelzen kann.
01 #!/usr/bin/perl 02 use strict; 03 use warnings; 04 use Imager; 05 use Imager::Plot; 06 use YAML qw( LoadFile ); 07 08 my $plot = Imager::Plot->new( 09 Width => 600, 10 Height => 400, 11 GlobalFont => '/usr/share/fonts/' . 12 'truetype/freefont/FreeSans.ttf'); 13 14 my( $first, $second ) = @ARGV; 15 die "usage: $0 1.yaml 2.yaml" if 16 !defined $second; 17 18 my @first_pairs = data_extract( $first ); 19 my @second_pairs = data_extract( $second ); 20 21 data_set_add( 22 $plot, \@first_pairs, "green" ); 23 data_set_add( 24 $plot, \@second_pairs, "red" ); 25 26 my $img = Imager->new(xsize => 650, 27 ysize => 450); 28 29 $img->box(filled => 1, color => 'white'); 30 31 # Add text 32 $plot->{'Ylabel'} = 'Distance'; 33 $plot->{'Xlabel'} = 'Time'; 34 $plot->{'Title'} = 'Fast Pace vs. Slow'; 35 36 $plot->Render(Image => $img, 37 Xoff => 40, Yoff => 420); 38 39 $img->write(file => "vrunner.png"); 40 41 ########################################### 42 sub data_extract { 43 ########################################### 44 my( $file ) = @_; 45 46 my $base; 47 my @pairs = (); 48 49 my $data = LoadFile( $file ); 50 for my $record ( @$data ) { 51 my $time = $record->{ timestamp }; 52 my $dist = $record->{ distance }; 53 $base = $time if !defined $base; 54 $dist =~ s/\D+$//; 55 push @pairs, [ $time - $base, $dist ]; 56 } 57 58 return @pairs; 59 } 60 61 ########################################### 62 sub data_set_add { 63 ########################################### 64 my( $plot, $data, $color ) = @_; 65 66 $plot->AddDataSet( 67 X => [ map { $_->[ 0 ] } @$data ], 68 Y => [ map { $_->[ 1 ] } @$data ], 69 style => { 70 marker => { 71 size => 2, 72 symbol => "circle", 73 color => 74 Imager::Color->new($color), 75 } 76 } 77 ); 78 }
Abbildung 6: Der gemütliche Jogger (rot) verliert stetig gegen den Läufer, der alles gibt. |
Listing 3 implementiert eine daran angelehnte Funktion mittels zweier ins
YAML-Format umgewandelter Fit-Dateien von zwei verschiedenen Läufen.
Sie liest die distance
und timestamp
-Einträge aus und
zeichnet ein Diagramm mit den von beiden Läufern zu bestimmten
Zeitpunkten zurückgelegten Wegstrecken. Die X-Achse zeigt die vergangene Laufzeit
in Sekunden, während die Y-Achse die von den Läufern zurückgelegte Wegstrecke
in Metern anzeigt. Beide im Beispiel verwendeten
Fit-Dateien beruhen auf wahren Daten, nach
dem Lauf in Abbildung 2 absolvierte der Perlmeister einen weiteren Trainingslauf.
Er wollte herausfinden, ob ein etwas langsameres Tempo auf den ersten paar
Kilometern mehr Kraft für den Endspurt und einige kleinere Hügel
(Höhenunterschied etwa 30m) lassen würde.
Das CPAN-Modul Imager::Plot zeichnet die als Arrays übergebenen Daten
ins Koordinatensystem und beschriftet auch schön die Achsen. Die ab Zeile
42 definierte Funktion data_extract()
nimmt eine .yaml-Datei mit den
.FIT-Daten entgegen und liefert einen Array zurück, der X-Y-Wertepaare
mit gespeicherten Kombinationen aus Zeitstempel und zurückgelegter Wegstrecke
enthält. Zeile 54 entfernt die Einheit "m" für "Meter" aus dem Wert
für distance
, sodass nur der Zahlenwert übrig bleibt. Die Variable
$base
wird zu Anfang auf den Zeitstempel des ersten Eintrags gesetzt,
so dass folgende Zeitstempel nur noch relativ zu diesem Nullpunkt in den
Ergebnisarray eingespeist werden können.
Der grüne Graph in Abbildung 6 zeigt den ersten, schnellen Lauf, der rote den zweiten, langsameren. Es stellte sich heraus, dass die sich durch das langsamere Starttempo gesammelten Kraftreserven nicht in eine schnellere zweite Hälfte umsetzen ließen. Einmal in Gang gesetzt, scheine ich zu laufen wie ein Uhrwerk, ohne vorgesetzte Karotte allerdings niemals schneller als notwendig, wie ein gleichgültiger Gaul.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2013/07/Perl
Das Perlmodul Garmin::FIT auf der Webseite des Autors Kiyokazu Suto: http://pub.ks-and-ks.ne.jp/cycling/GarminFIT.shtml