Aus Linux-Magazin 07/2013

Daten aus GPS-Geräten auslesen

© lama-photography, Photocase.com

Michael Schilli ist gut zu Fuß: Mit einem kleinen GPS-Empfänger am Arm durchfegt der Perl-Athlet (nicht nur) in diesem Monat das Umland von San Francisco. Wieder zu Hause bereitet er keuchend die gesammelten Laufdaten mit Perl-Skripten grafisch auf.

Vor wenigen Jahren glichen tragbare GPS-Geräte noch Mobiltelefonen der frühen 90er. Heute brauchen Sportler keinen Ballast mehr mit sich zu schleppen, denn Geräte wie der Garmin Forerunner 10 [2] sind auf die Größe einer LED-Digitaluhr aus den 70ern geschrumpft (Abbildung 1). Das Sportleraccessoire zeichnet während eines Jogginglaufs die Ortskoordinaten auf.

So erfährt der Läufer, wie schnell er momentan unterwegs ist und ob er zulegen muss oder nachlassen darf, um ein gestecktes Zeitziel zu erreichen. Nach abgeschlossener Muskelaktivität kann er dann hoffentlich genussvoll neue Geschwindigkeitsrekorde verbuchen, auf einer Landkarte die abgespulten Kilometer Revue passieren lassen oder ein Höhenprofil der Strecke bestaunen.

Mein unter Ubuntu in den USB-Eingang eingestöpseltes Gerät erkennt der Linux-Kernel sofort als Speichereinheit und mountet die auf dem Gerät gespeicherten Dateien unter »/media/GARMIN« . Dazu benötige ich allerdings ein spezielles Adapterkabel, das sich wie eine Kreatur aus dem Film “Alien” an die GPS-Uhr anschmiegt und dem Gerät beim Kauf beliegt. Im Verzeichnis »GARMIN/ACTIVITY« findet der Linuxer FIT-Dateien, in denen die Herstellerfirma Bewegungsaktivitäten in einem proprietären Binärformat speichert.

Solche Dateien kann der Sportler unter einem neu angelegten Account auf die Garmin-Website http://connect.garmin.com hochladen und bekommt dort die Laufdaten schön präsentiert (Abbildung 2). Das Beispiel zeigt eine etwa 7 Kilometer lange Route rund um den Lake Merced, einen 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 sind an der Grafik nicht nur die Gesamtzeit, sondern auch die durchschnittliche Zeitdauer 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 Geokoordinaten der zurückgelegten Strecke mit Zeitstempeln auf.

Abbildung 1: Der armbanduhrgroße GPS-Empfänger zeichnet beim Joggen die Geokoordinaten der zurückgelegten Strecke mit Zeitstempeln auf.

Abbildung 2: Auf die Webseite Connect.garmin.com darf der Sportler seine FIT-Daten hochladen und grafisch aufbereiten lassen.

Abbildung 2: Auf die Webseite Connect.garmin.com darf der Sportler seine FIT-Daten hochladen und grafisch aufbereiten lassen.

Der 6-Sekunden-Takt

Das GPS-Gerät ermittelt etwa alle 6 Sekunden die Position des Läufers mit Hilfe erdumkreisender 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 Garmin 10 zeichnet übrigens – anders als andere GPS-Geräte – während des Laufs nicht die aktuelle Höhe über Normalnull auf, anscheinend ist deren Bestimmung recht aufwändig. Aber die Garmin-Webseite füllt diese topografische Lücke im Bewegungsprofil anhand der Geokoordinaten mühelos auf, denn sie nutzt ein Server-seitig 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, erhält präzise Höheninformationen zur Laufstrecke.

Selbst ist der Mann

Aber selbst die schönste Webseite lässt sich noch verbessern, 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 [3] das Garmin-SDK vor. Es definiert zwar kein Perl-API, dokumentiert aber doch alle verwendeten Felder.

Der Entwickler Kiyokazu Suto hat anhand der Informationen ein Perl-Modul [4] unter einer Public-Domain-artigen Lizenz gebaut, es bislang allerdings nicht aufs CPAN hochgeladen. Garmin::FIT kann jedenfalls FIT-Datei lesen, die Abbildung 3 zeigt beispielhaft, wie das dem Modul beiliegenden Utiliy »fitdump« Daten ausspuckt.

Trotz offengelegten Formats erweist sich das Herausfieseln der Daten aus dem Binärwust als Sisyphusarbeit. Das Modul Garmin::FIT bietet die Methode »print_all_fields()« an, mit der ich schnell den Dumper in Listing 1 zusammengeklopft habe, der die Ausgabe in Abbildung 3 erzeugt. Um die Daten weiterzuverarbeiten, müssen Entwickler aber tiefer bohren und eigene Funktionen schreiben.

Abbildung 3: Die aus dem FIT-Format ausgelesenen Rohdaten zeigen, dass das Gerät etwa alle 6 Sekunden geografische Breite und Länge, die aktuelle Geschwindigkeit und die zurückgelegte Strecke bestimmt.

Abbildung 3: Die aus dem FIT-Format ausgelesenen Rohdaten zeigen, dass das Gerät etwa alle 6 Sekunden geografische Breite und Länge, die aktuelle Geschwindigkeit und die zurückgelegte Strecke bestimmt.

Listing 1

fittest

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 }

Fieseln aus FIT

Listing 2 schickt sich deshalb an, die FIT- 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 mit der Funktion »DumpFile()« aus dem Yaml-Modul als Yaml-Daten in eine gleichnamige ».yaml« -Datei zu pusten.

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« fieselt wichtige Werte aus den übergebenen Parametern heraus und setzt sie zu einem neuen Ganzen zusammen.

Wie Abbildung 4 zeigt, liegt die absolvierte Strecke nicht in »distance« der dem Callback überreichten Variablen »$v« , sondern in »$desc« . Diese zweite Variable speichert sehr viele Werte, die der Parser umständlich kombinieren muss. So firmiert in »$desc« unter dem Schlüssel »i_distance« der Indexwert (»4« ), unter dem der numerische Wert für die gesuchte Entfernung in »$v« steht, also »$v[4]« . »fit2yaml« kombiniert diesen mit der Einheit »m« für Meter und bestimmt das Resultat mit der Garmin::FIT-Methode »value_cooked()« .

Abbildung 4: Das Garmin-Format erweist sich als eigenwillig. Die zurückgelegte Strecke beispielsweise ist in »$v[4]« kodiert.

Abbildung 4: Das Garmin-Format erweist sich als eigenwillig. Die zurückgelegte Strecke beispielsweise ist in »$v[4]« kodiert.

Die Methode benötigt noch die Werte für »a_distance« (Skalierung und Einheit) und »I_distance« (Gültigkeitsbereich), damit sie endlich den gesuchten Wert zusammenbauen kann. Da das Format alle möglichen Daten auf kleinstem Raum zu speichern in der Lage ist, bleibt dies an Buchbinder Wanninger erinnernde Verfahren wohl notwendig.

Der Einfachheit halber müht sich Listing 2 erst gar nicht mit allen erlaubten Datenformaten ab, sondern konzentriert sich auf die »record« -Einträge mit den alle 6 Sekunden gemessenen Laufdaten. Andere protokollierte Events, etwa wann und wo der Läufer den [Start]-Knopf gedrückt hat, interessieren hier nicht weiter.

Abbildung 5 zeigt die fertigen Yaml-Daten, eine Liste von Records, die jeweils die Felder »distance« (ab Start zurückgelegte Strecke in Kilometern), »position_lat« (geografische Breite), »position_long« (geografische Länge), »speed« (Geschwindigkeit in Metern pro Sekunde) und »timestamp« (aktuelle Uhrzeit) führen.

Abbildung 5: Dies sind jene GPS-Daten des Garmin in einem Yaml-Format, die das Skript in <link href="#article_l2" class="listing" srcset=

Listing 2 ermittelt hat.” width=”289″ height=”300″ /> Abbildung 5: Dies sind jene GPS-Daten des Garmin in einem Yaml-Format, die das Skript in Listing 2 ermittelt hat.

Listing 2

fit2yaml

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 }

Unsichtbarer Laufkumpel

Für die Yaml-Daten kann ich nun Applikationen in Perl und anderen Sprachen schreiben, die die gespeicherten GPS-Daten interpretieren. Auf der Hersteller-Webseite vermisse ich zum Beispiel für mein Garmin 10 schmerzlich eine Funktion, die meine vorige GPS-Joghilfe, ein Garmin Forerunner 101, noch besaß: den Virtual Partner.

Vor dem Lauf programmiert der Sportler die Geschwindigkeit eines virtuellen Laufkumpans ein, der mit konstanter Geschwindigkeit läuft und per Definition exakt zur erhofften Zeit die Ziellinie überquert. Während des Laufs zeigt das GPS-Gerät an, wie weit unsichtbare Beine den virtuellen Läufer schon getragen haben. Ist er 100 Meter voraus, muss ich einen Zahn zulegen, fällt er zurück, nehme ich Tempo raus, joggen wir nebeneinander, komme ich pünktlich ins Ziel.

Listing 3 implementiert eine daran angelehnte Funktion für mein neues GPS-Gerät. Sie nutzt zwei ins Yaml-Format umgewandelte FIT-Dateien, 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 visualisiert.

Beide FIT-Dateien sind bei echten Läufen entstanden, nach meinem sportlichen Ausflug (siehe Abbildung 2) habe ich einen weiteren Trainingslauf auf derselben Strecke absolviert. Ich wollte nämlich herausfinden, ob mir ein etwas langsameres Tempo auf den ersten paar Kilometern mehr Kraft für den Endspurt und einige kleinere Hügel (Höhenunterschied etwa 30 Meter) übriglassen würde.

Das CPAN-Modul Imager::Plot zeichnet die als Arrays übergebenen Daten ins Koordinatensystem und beschriftet die Achsen schön. Die Funktion »data_extract()« ab Zeile 42 nimmt eine Yaml-Datei mit den FIT-Daten entgegen und liefert einen Array zurück. Der enthält x-y-Wertepaare mit gespeicherten Kombinationen aus Zeitstempel und zurückgelegter Wegstrecke.

Zeile 54 entfernt die Einheit »m« für “Meter” aus dem Wert für »distance« – übrig bleibt der Zahlenwert. Die Variable »$base« bekommt zu Anfang den Zeitstempel des ersten Eintrags verpasst, sodass »vrunner« spätere Zeitstempel nur noch relativ zu diesem Nullpunkt in den Ergebnisarray einspeisen kann.

Listing 3

vrunner

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 }

Online PLUS

In einem Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/plus/2013/07

Uhrwerk und Gaul

Der grüne Graph in Abbildung 6 zeigt den ersten, schnellen Lauf, der rote den zweiten, langsameren. Der Vergleich zeigt, dass ich die durch das langsamere Starttempo gesammelten Kraftreserven nicht in eine schnellere zweite Hälfte umsetzen konnte. Einmal in Gang gesetzt, laufe ich wie ein Uhrwerk – ohne vorgehaltene Karotte aber nie schneller als notwendig. Im Laufschritt bin ich wohl ein gleichgültiger Gaul.

Abbildung 6: Der gemütliche Jogger in Rot verliert stetig gegenüber dem Läufer im kleidsamen Grün, der offenbar alles gibt.

Abbildung 6: Der gemütliche Jogger in Rot verliert stetig gegenüber dem Läufer im kleidsamen Grün, der offenbar alles gibt.

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er nach praktischen Anwendungen der Skriptsprache Perl. Unter mailto:mschilli@perlmeister.com beantwortet er gerne Fragen.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben