home
erste Version am 19.06.2016
letzte Änderung am 06.06.2018

Raspberry Pi als GPS-Tracker

und Wardriving-Device


Bei diesem Projekt soll ein Raspberry Pi so konfiguriert werden, dass er GPS-Tracks aufzeichnet.
Die Tracks sollen auf einem USB-Stick gespeichert werden und der System-Status soll über WLAN von einem Smartphone abfrag- bzw. anzeigbar sein.
Das ganze Gedöns soll dann idealerweise in eine Fahrradtasche passen - notfalls landet es im Fahrradkoffer.

Natürlich kann ein Smartphone sowas auch ohne RasPi leisten - zumindest, wenn z.B. OsmAnd installiert ist.
Praktisch nutze ich OsmAnd bei Rad-Touren auch genau dafür.
Weil die real gefahrenen Tracks bei der Ansicht auf GPSies jedoch gerne mal ein paar Meter neben dem Weg liegen, möchte ich gern eine zweite Meinung dazu haben.

Ab der dritten Seite geht es primär ums Wardriving bzw. Warbiking mit dem Rasperry Pi 3 und dessen Auswertung.
Dazu wird der RasPi dann schließlich auch insofern umgestellt, selbst Funkstille zu halten und stattdessen USB-Tethering zur Kommunikation mit dem Smartphone zu nutzen.


Inhaltsverzeichnis

Planung / Design-Entscheidungen
Den Automount des USB-Sticks einrichten
Den USB-WLAN-Adapter als AccessPoint einrichten
Einrichtung des GPS-Moduls
Den Web-Server einrichten
So weit...so schön...aber jetzt gibt es ein Problemchen
Python-Scripte
   Systemzeit gemäß GPS einstellen
Der RasPi v3 ist da
Erster outdoor-Test

Erweiterungen
    WPA2 aufs WLAN
    Serialisieren von set_time und gps-Tracker-Dateinamen-Init
    Die Button im WebGUI brauchen noch eine Bestätigungs-Abfrage
    Geräte-Verfügbarkeit determiniert Track-schreiben
    Mehr try/except-Blöcke
    Vielleicht noch eine Art Wardriving-Funktion zufügen
Tests zur Genauigkeit
Zweiter outdoor-Test

Track-Visualisierung mit Anzeige von Zusatzdaten
Wardriving einrichten
mein erstes Wardriving in Wees
Analyse der beim Wardriving ermittelten Daten
Das Zentrum mehrerer Koordinaten bestimmen

eine Datenbank für den kismet_server
Weitere Erkenntnisse
Anzeige aktuell gesehener AccessPoints
Alternative Komponenten
Erweiterungen bzgl. Wardriving
Nachtrag 2017
RasPi-GUI v1.6a
USB-Tethering
Alfa AWUS036H

Berechnung des AccessPoint-Standortes
eine Messreihe

drei Fotos (von den Warbiking-Komponenten)
Nachtrag 2018


Planung / Design-Entscheidungen

Zunächst habe ich mir dazu einen USB-GPS-Empfänger und eine PowerBank als Stromversorgung bestellt.
Einen Raspberry Pi (Modell B Rev 2.0) hatte ich noch rumliegen, einen alten USB-Stick und einen USB-WLAN-Adapter ebenfalls.
Der RasPi bekommt ein aktuelles Raspbian Jessie lite (2016-05-27-raspbian-jessie-lite.img ), seine Grund-Konfiguration via raspi-config und danach den ganzen Kram wie update, upgrade, dist-upgrade, user anlegen...

Die Tracks sollen deswegen auf einem USB-Stick (statt auf der SD-Karte) abgelegt werden, um sie
  1. einfach durch Abziehen vom RasPi an einem anderen Rechner verfügbar zu haben und
  2. um die (das Betriebssystem bereitstellende) SD-Karte nicht mit unnötigen Schreibzugriffen zu belasten (bzgl. Alterung - nicht Performance!).
Der Zugang zum RasPi per WLAN erschien mir nötig, um
  1. mobil prüfen zu können, ob die Track-Aufzeichnung aktiv und funktionsfähig ist, um
  2. die Track-Aufzeichnung nicht durch brutales Strom vom RasPi abziehen stoppen zu müssen und um
  3. bereits ein mobiles GUI für mögliche zukünftige Funktionen zu haben.

Den Automount des USB-Sticks einrichten

Basierend auf dieser Quelle.

Anlegen einer Datei /etc/udev/rules.d/99-usbmount.rules mit folgendem Inhalt:
ACTION=="add", SUBSYSTEMS=="usb", KERNEL=="sd*", RUN+="/bin/bash /usr/local/bin/mountusb mount /dev/%k"
ACTION=="remove", SUBSYSTEMS=="usb", KERNEL=="sd*", RUN+="/bin/bash /usr/local/bin/mountusb umount /dev/%k"
Anlegen einer Datei /usr/local/bin/mountusb mit folgendem Inhalt:
#!/bin/bash
# Verzeichnis fuer den USB-Stick definieren
MountPoint=/home/dede/usbstick

# Kommandozeilen-Parameter
ACTION=$1
DEVICE=$2

if [ "$ACTION" = "mount" ]; then
  # falls nicht vorhanden -> Mountpoint anlegen
  [ ! -d $MountPoint ] && mkdir -p $MountPoint
  # USB-Stick einhaengen 
  /bin/mount $DEVICE $MountPoint
elif [ "$ACTION" = "umount" ]; then
  # USB-Stick aushaengen
  /bin/umount -f $DEVICE
fi
Ausführbar machen:
sudo chmod +x /usr/local/bin/mountusb

Und (bei angeschlossenem USB-Stick mit ext4-Dateisystem) mit User-Rechten schreibbar machen:
sudo chmod 777 /home/dede/usbstick


Den USB-WLAN-Adapter als AccessPoint einrichten

Nachtrag: die beiden in diesem Abschnitt installierten Programme/Pakete sind in der finalen Konfiguration wieder abgeschaltet worden.

Der alte WLAN-Adapter ist ein EDIMAX EW-7811UN Wireless USB Adapter.
Dazu passend habe ich diese Quelle und diese Quelle als Anleitung gesichtet.
In beiden Quellen heißt es, man müsse den hostapd von der Hersteller-Webseite runterladen und per make selbst übersetzen.
Das habe ich dann auch so gemacht und es hat danach sogar funktioniert, sich per Smartphone zu verbinden und mittels AndFTP ins Dateisystem des RasPi zu schauen.
Erstmal jedenfalls.
Am nächsten Tag kam beim RasPi ein Schwung Updates rein und ich habe auch noch einen Webserver nachinstalliert.
Danach war es vorbei mit dem WLAN-Zugang und ich habe es auch nicht wieder zum laufen gebracht.
Der hostapd war laut Datum und md5sum eindeutig immer noch der von mir compilierte!?   ...how auch ever...

Weil beide o.g. Quellen schon mehrere Jahre alt sind, habe ich beim zweiten Versuch einfach per Gut-Glück darauf gesetzt, dass der Standard-hostapd den EW-7811UN mittlerweile direkt unterstützt.
Und siehe da...mit den folgenden Schritten klappt es direkt mit der Version aus den Repos.

Erstmal das Paket für den Betrieb als Access-Point installieren:
sudo apt-get install hostapd

Nun die /etc/network/interfaces anpassen:
source-directory /etc/network/interfaces.d

auto lo
iface lo inet loopback

iface eth0 inet manual

allow-hotplug wlan0
iface wlan0 inet static
   address 192.168.2.1
   netmask 255.255.255.0
Dann in /etc/default/hostapd das conf-File eintragen:
DAEMON_CONF="/etc/hostapd/hostapd.conf"
Und es anlegen als /etc/hostapd/hostapd.conf mit dem Inhalt:
interface=wlan0
ssid=RaspAP
hw_mode=g
channel=6
beacon_int=100
auth_algs=3
wmm_enabled=1
Danach Installation des DHCP-Servers mit:
sudo apt-get install dnsmasq

An /etc/dnsmasq.conf folgendes anhängen:
interface=wlan0
dhcp-range=192.168.2.20,192.168.2.50,255.255.255.0,12h
address=/#/192.168.2.1  #redirect all DNS requests to 192.168.2.1

dhcp-host=d0:5b:a8:e1:1f:d7,   ZTEBlade,   192.168.2.10,    3h

Damit klappt es (nach Reboot) bereits, sich per WLAN mit dem RasPi zu verbinden.

Allerdings steht der nameserver nun auf localhost. Somit funktioniert z.B. ein apt-get install nicht mehr.
Um das bei Bedarf zu fixen, habe ich zwei Scripte unter ~/bin abgelegt:
root@rp3:/home/dede# cat bin/startAP.sh
#!/bin/bash
sudo service hostapd start
sudo service dnsmasq start
cat /etc/resolv.conf
root@rp3:/home/dede# cat bin/stopAP.sh
#!/bin/bash
sudo service hostapd stop
sudo service dnsmasq stop
cat /etc/resolv.conf

Mit stopAP.sh wird der AccessPoint deaktiviert und damit automagisch die Fritzbox für die Namens-Auflösung eingestellt.
Um den AccessPoint wieder zu aktivieren, ist startAP.sh aufzurufen. Damit wird der nameserver wieder zu 127.0.0.1.


Einrichtung des GPS-Moduls

Die nötige Software ist schnell installiert mit:
sudo apt-get install gpsd python-gps

Zur Entwicklung und für Debug-Zwecke macht es durchaus Sinn, auch noch
apt-get install gpsd-clients
zu installieren. Dabei kommt dann allerdings ein ziemlicher Rattenschwanz an abhängigen Paketen mit rein.
Aber um schnell einen ersten Funktions-Test mit
cgps -s
machen zu können, wird das Paket benötigt.
Liefert dann z.B. folgende Anzeige, wenn das GPS-Modul am Fenster meines Büros liegt:
┌───────────────────────────────────────────┐┌─────────────────────────────────┐
│    Time:       2016-06-23T12:52:49.000Z   ││PRN:   Elev:  Azim:  SNR:  Used: │
│    Latitude:    54.805109 N               ││   2    24    047    21      Y   │
│    Longitude:    9.524913 E               ││   4    46    290    34      Y   │
│    Altitude:   44.0 m                     ││   5    12    087    00      Y   │
│    Speed:      0.0 kph                    ││   9    01    000    00      Y   │
│    Heading:    54.6 deg (true)            ││  12    14    112    10      Y   │
│    Climb:      0.0 m/min                  ││  14    01    231    00      Y   │
│    Status:     3D FIX (25 secs)           ││                                 │
│    Longitude Err:   +/- 13 m              ││                                 │
│    Latitude Err:    +/- 11 m              ││                                 │
│    Altitude Err:    +/- 75 m              ││                                 │
│    Course Err:      n/a                   ││                                 │
│    Speed Err:       +/- 96 kph            ││                                 │
│    Time offset:     0.204                 ││                                 │
│    Grid Square:     JO44st                ││                                 │
└───────────────────────────────────────────┘└─────────────────────────────────┘

Sechs genutzte Satelliten sind für diese Position schon ziemlich gut.
Mein TomTom hat von dieser Position aus noch nie seinen Standort bestimmen können.
Und wenn ich mir die Koordinaten in Google-Maps anzeigen lasse, dann passt das auf eins/zwei Meter genau :-)


Den Web-Server einrichten

Installation mit:
sudo apt-get install lighttpd

Danach gleich cgi aktivieren:
sudo bash
lighty-enable-mod cgi
mkdir /var/www/cgi-bin

cd /etc/lighttpd/conf-enabled
mv 10-cgi.conf 10-cgi.conf.bak
nano 10-cgi.conf
und 10-cgi.conf füllen mit:
# /usr/share/doc/lighttpd/cgi.txt

server.modules += ( "mod_cgi" )

alias.url += ( "/cgi-bin/" => "/var/www/cgi-bin/" )
$HTTP["url"] =~ "/cgi-bin/" {
        cgi.assign = (
               ".pl" => "/usr/bin/perl",
               ".py" => "/usr/bin/python",
               ".py3" => "/usr/bin/python3",
               ".cgi" => "/bin/bash",
        )
}

Danach noch:
sudo chown -R dede:www-data /var/www

Und weil es in der cgi-conf schon vorbereitet wurde, gleich noch:
sudo apt-get install python3

Bezüglich Python3-Scripten in /var/www/cgi-bin ist zu beachten, dass diese auf .py3 enden müssen, um über python3 ausgeführt zu werden.

Legt man in /var/www/cgi-bin nun z.B. eine Datei py.py3 mit folgendem Inhalt an:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

import cgi
import subprocess
import time

print("Content-type: text/html\n")
print('<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">')
print('<title>RasPi GPS Tracker Status</title>')
print('<meta name="viewport" content="width=device-width, initial-scale=1" />')
print('<meta http-equiv="refresh" content="5">')
print('</head><body>')
print('<h1><center>RasPi GPS tracker</center></h1>')

print("<h3>Datum/Zeit: %s</h3>"%(time.strftime("%d.%m.%Y  %H:%M:%S")))

process=subprocess.Popen(["uptime", "-p"], stdin =subprocess.PIPE,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.DEVNULL)
rc=process.communicate()[0]
print("<h3>Uptime: %s</h3><p>"%(rc.decode(),))

process=subprocess.Popen(["df", "-hT"], stdin =subprocess.PIPE,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.DEVNULL)
rc=process.communicate()[0]
if rc.decode().find("/home/dede/usbstick")>=0:
  print("<h3>USB-Stick ist verf&uuml;gbar!</h3>")
else:
  print("<h2>USB-Stick ist <u>nicht</u> verf&uuml;gbar!</h2>")

process=subprocess.Popen(["ps", "aux"], stdin =subprocess.PIPE,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.DEVNULL)
rc=process.communicate()[0]
if rc.decode().find("/usr/sbin/gpsd")>=0:
  print("<h3>GPS-Dienst l&auml;uft!</h3>")
else:
  print("<h2>GPS-Dienst l&auml;uft <u>nicht</u>!</h2>")

print("<h3>Anzahl Satelliten: ??</h3>")
print("<h3>Alter GPS-Signal [Sek.]: %d</h3>"%(5, ))
print("<h3>Aufzeichnung seit [Min.]: %d</h3>"%(25, ))
print("<h3>Anzahl Track-Punkte: %d</h3>"%(205, ))

print('</body></html>')

kann man (bei aktiviertem AccessPoint und damit verbundenem Smartphone) im Webbrowser die Adresse
    192.168.2.1/cgi-bin/py.py3
öffnen und sollte dann folgendes Bild bekommen:
Screenshot

Mindestens der Shutdown-Button fehlt natürlich noch...

Für den Shutdown (und Restart ebenso) braucht der Nutzer www-data noch eine sudo-Berechtigung auf /sbin/init.
Daher ist in /etc/sudoers eine weitere Zeile einzufügen:
www-data ALL=(ALL) NOPASSWD: /sbin/init
Das mag jetzt für einen WebServer, der vom Internet aus erreichbar wäre, etwas unsicher sein...aber hier sehe ich da keine große Gefahr.


So weit...so schön...aber jetzt gibt es ein Problemchen

Mittlerweile sind am RasPi drei USB-Geräte angeschlossen:
  1. USB-Stick
  2. USB-GPS-Empfänger
  3. USB-WLAN-Stick
Das verwendete RasPi-Modell bietet aber nur zwei USB-Ports.
Notgedrungen habe ich einen passiven USB-Hub am RasPi angeschlossen und dort zwei der drei USB-Geräte reingesteckt.
Leider klappt das damit immer nur kurz.
Nach ein paar Minuten stirbt entweder der WLAN-Stick oder das GPS-Modul - je nachdem, welches am Hub angeschlossen ist.
Wobei sich das Sterben so auswirkt, dass das Gerät aus der von lsusb angezeigten Liste verschwindet und kurz darauf mit einer um Eins erhöhten Device-Nummer wieder erscheint.
In /var/log/messages erscheinen Meldungs-Blöcke wie diese:
usb 1-1.2: USB disconnect, device number 4
usb 1-1.2: new full-speed USB device number 5 using dwc_otg
usb 1-1.2: New USB device found, idVendor=1546, idProduct=01a6
usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-1.2: Product: u-blox 6  -  GPS Receiver
usb 1-1.2: Manufacturer: u-blox AG - www.u-blox.com
cdc_acm 1-1.2:1.0: ttyACM0: USB ACM device
usb 1-1.2: USB disconnect, device number 5
usb 1-1.2: new full-speed USB device number 6 using dwc_otg
usb 1-1.2: New USB device found, idVendor=1546, idProduct=01a6
usb 1-1.2: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-1.2: Product: u-blox 6  -  GPS Receiver
usb 1-1.2: Manufacturer: u-blox AG - www.u-blox.com

oder auch:

usb 1-1.2.4: USB disconnect, device number 7
rtw_cmd_thread: DriverStopped(0) SurpriseRemoved(1) break at line 482
usb 1-1.2.4: new high-speed USB device number 8 using dwc_otg
usb 1-1.2.4: New USB device found, idVendor=7392, idProduct=7811
usb 1-1.2.4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-1.2.4: Product: 802.11n WLAN Adapter
usb 1-1.2.4: Manufacturer: Realtek
usb 1-1.2.4: SerialNumber: 00e04c000001


Weil eigentlich alles schon ganz zufriedenstellend klappt (ohne den Hub), soll es jetzt nicht an so einem Kleinkram haken.
Daher habe ich mir eben kurzerhand ein Raspberry Pi 3 (Model B) bestellt.
Das Ding hat vier USB-Ports und ein integriertes WLAN-Modul. Das sollte die Probleme beseitigen.


Python-Scripte

Bis der RasPi 3 geliefert wird, kann ich mich ja schon mal um die zwei zu bauenden Komponenten kümmern.
  1. das Python-Script zum Aufzeichnen der Tracks (das dann eben erstmal nur auf SD-Card speichert)
  2. das Python-Script zur Generierung der Status- und Steuer-Web-Seite.
Beide sind als Funktions-Studie zwar schon durchaus fleißig am laufen, von der Praxis-Tauglichkeit aber noch ziemlich weit entfernt.

Auch bedarf es noch einer geeigneten Methode, das Track-Schreib-Script bei PowerOn automatisch zu starten.
Ich könnte mir da was mit der crontab vorstellen.
Also etwa: einmal pro Minute prüfen, ob das Script läuft und der letzte geschriebene Wert maximal xxx Sekunden alt ist.
Wenn nicht: Script ggf. killen und neu (oder auch initial) starten.
Das hätte den Vorteil, dass das Script nicht nur (einmalig) gestartet, sondern auch gleichzeitig überwacht und ggf. neu gestartet würde.

Systemzeit gemäß GPS einstellen

Eine weitere Aufgabe ist das Stellen von Datum und Uhrzeit.
Ein ntp-Server im Internet kann nicht verwendet werden, weil der RasPi nach PowerOn üblicherweise keinen Internet-Zugriff haben wird.
Aber weil im GPS-Signal eine Zeitinformation enthalten ist, sollte sich das darüber realisieren lassen.
Lediglich wegen der Umwandlung von Zulu- bzw. UTC-Zeit nach Orts-Zeit muss ich noch die passende Funktion finden....und wieder mal war stackoverflow sehr hilfreich.

Erstmal braucht es das dateutil-Modul (und sicherheitshalber gleich für beide Python-Versionen):
sudo apt-get install python-dateutil python3-dateutil

Dann folgendes Script als ~/bin/GPS_set_time.py anlegen:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-

import time
import subprocess
import gps
import datetime
from dateutil import tz
 
# ######################################################################
# Liefert für einen Timestamp im Format "2016-06-25T10:40:31.000Z"
# ein entsprechendes datetime-Objekt in Ortszeit.
def utcToLocal(timestamp):
  utc=datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ')
  utc=utc.replace(tzinfo=tz.tzutc())
  return(utc.astimezone(tz.tzlocal()))

# ######################################################################
# Schreibt "strg" ins syslog.
def writeLog(strg):
  subprocess.call(["logger", "GPS_set_time: %s"%(strg,)])

# ######################################################################
#
if __name__ == '__main__':
  time.sleep(3)
  writeLog("warte auf GPS")
  mygps=gps.gps(mode=gps.WATCH_ENABLE)
  writeLog("Init erfolgt")
  time_set=False
  wcnt=0
  while not time_set:
    try:
      mygps.next()
      if mygps.utc is not None and len(mygps.utc)==24:
        dtl=utcToLocal(mygps.utc)
        subprocess.call(["sudo", "date", "-s", dtl.strftime("%Y-%m-%d %H:%M:%S")])
        time_set=True
        writeLog("Zeit gesetzt")
      else:
        wcnt+=1
        if wcnt%10==0:
          writeLog("%d mal gewartet"%(wcnt,))
    except Exception, e:
      writeLog("Ausnahme aufgetreten %s"%(str(e),))
      pass


Zwecks Autostart wird eine Zeile in /etc/rc.local angelegt (vor der Zeile mit "exit 0"):
/usr/bin/python /home/dede/bin/GPS_set_time.py &


Der RasPi v3 ist da

Eben wurde der große RasPi samt Gehäuse, USB-Stick und microSD-Karte geliefert.
Weiterhin kam noch ein abgewinkeltes USB-Kabel an, mit dem die oben verlinkte PowerBank jetzt auch in die ebenfalls oben verlinkte Fahrradtasche passt.

Wegen des geänderten Formfaktors der SD-Karte habe ich alles neu gemacht und konnte dabei gleich meine Anleitung auf Korrektheit prüfen.
Alles klappt!  :-)
Sogar der integrierte WLAN-Adapter macht genau das, was er soll.
Und das Teil fühlt sich wirklich deutlich geschmeidiger als die 1'er-Version an...also bezüglich Performance.
Wenn sowohl das GPS-Signal abgefragt als auch die Status-Webseite abgerufen wird, meldet top den idle konstant oberhalb von 96%.
Der RasPi v1 ging da gerne mal auf 50% runter.


Erster outdoor-Test

Eben habe ich meinen ersten Funktionstest außer Haus durchgeführt.
Die erzeugte GPX-Datei hatte 36 Trackpunkte und sah nach dem Hochladen auf wegeundpunkte so aus:

Screenshot
Auf GPSies konnte ich sie zwar hochladen und ansehen, jedoch nicht als öffentlich markieren. Dazu hätte sie mindestens 250m lang sein müssen....

Den Autostart des Tracker-Scripts habe ich erstmal über einen simplen Eintrag in der crontab gelöst:
@reboot  /usr/bin/python /home/dede/GPS_tracker.py
Anbei das aktuelle Tracker-Script und das Script für die Erzeugung der WebSeite.
Das Web-GUI sieht derzeit so aus:
Screenshot        Screenshot
Beim rechten Screenshot wurde runter-gescrollt, um die Button erreichen zu können.

Beide Scripte funktionieren soweit und sprechen miteinander, sind aber eindeutig noch nicht final.
Die Fortsetzung wird dann beizeiten auf ihrer eigenen Seite zu finden sein.