RP2040-GEEK GPX Datenlogging und Auswertung mit GPX-Editor am PC
Anleitung für die Firmware CircuitPython
RP2040-GEEK GPX Datenlogging und Auswertung mit GPX-Editor am PC
Anleitung für die Firmware CircuitPython
Nachdem die Versuche mit dem RP2040-GEEK und dem GPS-Modul (Air530) sehr gut funktionieren, habe ich beschlossen ein 'GPS-Gerät' zu
entwerfen, welches nicht nur die aktuelle GPS-Position anzeigt und aufzeichnet. Es soll die Entfernung zu einem vorgegebenen Ziel bestimmen
und die GPS-Daten in einer gpx-Datei aufzeichnen, die sich direkt mit einem GPX-Editor am PC auswerten läßt.
Wer den Pico-GEEK noch nicht kennt, findet
hier einige grundlegende Hinweise
.
Bildbox 1 (klick hier)
Hardware
- RP2040-GEEK incl. mitgelieferter Kabel (z.B. bei Waveshare)
- seeed Grove - GPS Modul (Air530) incl. mitgelieferter Antenne (z.B. bei Botland)
- (Powerbank zur Stromversorgung, für den Einsatz autark im Freien)
Vorbemerkungen
Bei dem vorgestellten Gerät handelt es sich um einen 'GPS-Logger' der die Daten auf einem internen Speicher im Gerät ablegt. Diese können nicht
direkt in Echtzeit eingesehen werden, sondern im Anschluss mit Hilfe eines PCs sowie entsprechender Software. Bei einem
'GPS-Tracker' hingegen
werden die Standortpositionen direkt durch eine eingebaute SIM-Karte über die mobile Datenübertragung weitergeleitet. So kann er
beispielsweise als Ortungsgerät den aktuellen Standort z.B. eines Autos oder der Kinder nach der Schule angeben.
Im vorliegenden Fall werden die Bauteile in einem kleinen Gehäuse untergebracht, so dass man z.B. in Verbindung mit einer Powerbank vom Netz
unabhängig ist und damit ins Gelände gehen kann. Vor der Benutzung des 'Pico-Geek-Navis' kann man zu Hause die Koordinaten eines Wanderziels
aus einer Karte entnehmen und in der entsprechenden Datei auf der SD-karte eintragen. Dazu legt man auf der SD-Karte ein Verzeichnis
'gps' an und speichert darin eine Textdatei 'ziel.txt', mit dem Namen eines (von Ihnen gewünschten) Ziels
und dessen gps-Koordinaten Attitude und Longitude.
München dahoam 48.142044 11.583694
Unterwegs zeigt das Gerät dann ständig die Entfernung
in 'ost/west' bzw. 'nord/süd'- Richtung und die Entfernung 'Luftlinie' von diesem Ziel an.
Noch eine weitere Textdatei mit dem Namen 'gps_logger.txt' wird im Hauptverzeichnis der SD-Karte abgelegt. Die Datei enthält
nur eine Zahl, die bei der Nummerierung der Log-Dateien mit den gps-Koordinaten gebraucht wird. Tragen Sie als Startwert die Zahl '0' ein.
0
Wenn das Gerät eingeschaltet wird, erhöht sich dieser Wert nach jeweils 10 Minuten und entsprechende log-Dateien
'gps_daten1.gpx', 'gps_daten2.gpx' usw. werden erzeugt (siehe weiter unten). Ich habe entsprechende Tests zu Fuß, mit
dem Rad und im Auto gemacht und bin mit der Genauigkeit der aufgezeichneten Routen sehr zufrieden. Bevor es mit der Programmierung los
geht, ein paar Vorüberlegungen.
Entfernungsberechnung
Mit Hilfe der Koordinaten der aktuellen Position und der Zielkoordinaten läßt sich auf der Erdoberfläche recht leicht der Abstand
zwischen diesen beiden Punkten bestimmen (also Luftlinie). Dazu wird im einfachsten Fall und bei relativ kurzen Entfernungen der Satz des Pythagoras genutzt. Da die Erde aber
keine Scheibe sondern kugelförmig ist, wird es dann bei größeren Entfernungen mathematisch doch etwas komplizierter. Wen die genauen
Hintergründe dazu interessieren, der findet
hier bei Martin Kompf eine sehr gute Beschreibung für die
Berechnung.
Graphische Darstellung im GPX-Editor
Ich habe das Programm für den Pico-Geek so angelegt, dass immer 10 Minuten pro Log-Datei, die fortlaufend nummeriert werden, enthalten sind.
So kann sichergestellt werden, dass die GPX-Dateien syntaktisch abgeschlossen sind und den 'modifizierten xml-Code' korrekt enthalten. Gäbe
es nur eine große Log-Datei ,was bei der SD-Karte ja möglich wäre, könnte der Editor diese nicht ohne 'Nacharbeit' lesen.
Der Pico-Geek hat ja keinen Button, um das Programm zu beenden. Folglich wird beim 'Einsatz im Freien' einfach irgend wann die Stromzufuhr
getrennt. Damit ist die letzte Datei im Editor nicht lesbar. Alle anderen Dateien sind korrekt abgeschlossen und erzeugen keinen Anzeigefehler.
Deshalb lasse ich mir am Display die verbleibenden Sekunden bis zum nächsten Speichervorgang anzeigen, um beim Erreichen des Ziels erst
dann 'auszuschalten', wenn die Speicherung mit den relevanten Daten abgeschlossen ist.
Mit dem ersten 'Update 2.0' wird es hier eine grundlegende Veränderung geben.
Als Anzeigesoftware nutze ich den
GPX-Editor 1.8.0
für Windows am PC. Der ist sehr einfach zu bedienen und enthält die wichtigsten Funktionen zum 'Auswerten' der Route. Man kann sogar ein
'jpg-Bild' der Route auf dem Rechner speichern.
Da immer nur 10 min in der GPX-Datei enthalten sind, wird nicht die gesamte Strecke angezeigt. Die weiteren Streckenabschnitte werden mit Hilfe
des 'Dateimenüs' unter 'GPX hinzufügen' ergänzt, so dass die komplette 'Route' angezeigt wird. Das Foto stellt eine kleine 'Rundfahrt'
als Demo dar.
Genauigkeit der Routendarstellung
Bekanntlich ist die zurückgelegte Strecke bei konstanten Zeitintervallen um so größer, je größer die Geschwindigkeit
ist. Dadurch weichen die Wegstrecken in der graphischen Darstellung stärker vom tatsächlichen Weg ab. Es kommt vor, dass man scheinbar eine Ecke schräg
abkürzt und mitten durch ein Gebäude fährt. Um dies zu vermeiden, oder den Effekt zumindest zu reduzieren, habe ich
die Zeitintervalle zum Speichern in Abhängigkeit von der Geschwindigkeit gewählt. Um etwa 50 Meter Wegstrecke pro gespeichertem Wert
zu erhalten, folgen aus der Geschwindigkeit in Knoten vom GPS folgende Zeiten:
50 kn = 92 km/h = 25.7 m/s . . . . . 2 s entsprechen rund 51 m (schnelle Autofahrt)
27 kn = 50 km/h = 13.8 m/s . . . . . 4 s entsprechen rund 55 m (Stadtverkehr)
10 kn = 18.5 km/h = 5.1 m/s . . . . 10 s entsprechen rund 51 m (Radgeschwindigkeit)
1 kn = 1.85 km/h = 0.5 m/s . . . .100 s entsprechen rund 50 m (Wandergeschwindigkeit)
Daraus und aus einigen Tests folgt im Programm:
145 if gps.speed_knots is not None: 146 speed = gps.speed_knots 147 if speed < 1: 148 wait = 60 149 if speed >= 1 and speed < 10: 150 wait = 15 151 if speed >= 10 and speed < 27: 152 wait = 10 153 if speed >= 27 and speed < 50: 154 wait = 4 155 if speed >= 50: 156 wait = 2 157 else: 158 speed = 0 159 wait = 60
Im Ergebnis werden so recht gute Werte erreicht.
Ein Gehäuse aus dem 3D Drucker
Um das Gerät im Freien zu benutzen, braucht es ein Gehäuse. Die Bilder in der Bildbox 3 zeigen die zwei mit dem 3D Drucker hergestellten Teile
und die Anordnung. Die Lötarbeiten am GPS-Modul und die Anschlüsse am Pico-Geek werden in der
Anleitung GPS-Logger
beschrieben und in
entsprechenden Bildern gezeigt.
Bildbox 3 (klick hier)
Bei Interesse können Sie die beiden stl-Dateien
hier herunterladen und das Gehäuse selbst
ausdrucken.
Los gehts mit dem Programm
Bildbox 3 (klick hier)
1 # SPDX-FileCopyrightText : Detlef Gebhardt, written for CircuitPython 2 # SPDX-FileCopyrightText : Copyright (c) 2024 Detlef Gebhardt 3 # SPDX-Filename : RP2040_GEEK GPS-Logger (12.06.2024) 4 # SPDX-License-Identifier: GEBMEDIA 5 import os 6 import time 7 import board 8 import busio 9 import sdcardio 10 import digitalio 11 import storage 12 from fourwire import FourWire 13 from adafruit_display_text import label 14 import terminalio 15 import displayio 16 from adafruit_st7789 import ST7789 17 import adafruit_gps 18 import math 19
Zunächst werden alle benötigten Bibliotheken importiert. Im nächsten Kasten ist zu sehen, wie die SD-Karte eingebunden wird.
Dabei wird geprüft, ob überhaupt ein Karte eingelegt ist. Auch die Existenz der Datei 'gps_logger.txt' wird geprüft. In ihr wird eine
Zahl für die fortlaufende Nummerierung der 'gpx-Dateien' geschrieben.
20 # SD Karte mounten 21 SD_SCK = board.GP18 22 SD_MOSI = board.GP19 23 SD_MISO = board.GP20 24 cs = board.GP23 25 26 # pruefen, ob SD-Karte vorhanden und ob Datei "name_logger.txt" existiert 27 try: 28 card = busio.SPI(SD_SCK, SD_MOSI, SD_MISO) 29 sdcard = sdcardio.SDCard(card, cs) 30 vfs = storage.VfsFat(sdcard) 31 storage.mount(vfs, "/sd") 32 error = "SD-Karte gemountet" 33 # Zahl fuer Dateiname auslesen und erhoehen 34 with open("/sd/gps_logger.txt", "r") as f: 35 string = f.readline() 36 f.close() 37 except OSError as exc: 38 error = exc.errno 39 if error == "keine SD-Karte": 40 print(error) 41 if error == 2: 42 print("Datei: gps_logger.txt fehlt!") 43 44 zahl = int(string) 45 zahl += 1 46 # neue Zahl fuer Dateiname schreiben 47 with open("/sd/gps_logger.txt", "w") as f: 48 f.write(str(zahl)) 49 f.close() 50 51 # Koordinaten vom Ziel aus Datei lesen 52 datei = open("/sd/gps/ziel.txt", "r") 53 ziel = datei.readline() 54 dest_lat = datei.readline() 55 dest_lon = datei.readline() 56 datei.close() 57 # String in float-Zahl umwandeln 58 ziel = ziel.rstrip('\n') 59 ziel_lat = float(dest_lat) 60 ziel_lon = float(dest_lon) 61 print("Ziel:\n",ziel) 62 print("Latitude, Longitude") 63 print("{0:.6f}".format(ziel_lat),"{0:.6f}".format(ziel_lon)) 64
In den Zeilen 51 bis 63 werden die Koordinaten und der Name des Ziels gelesen und entsprechenden Variablen zugeordnet.
65 # Display initialisieren 66 lcd_cs=board.GP9 67 lcd_dc=board.GP8 68 lcd_reset=board.GP12 69 # Release any resources currently in use for the displays 70 displayio.release_displays() 71 spi = busio.SPI(board.GP10, board.GP11) 72 display_bus = FourWire(spi, command=lcd_dc, chip_select=lcd_cs, reset=lcd_reset) 73 display = ST7789(display_bus, rotation=90, width=240, height=135, rowstart=40, colstart=53) 74 # Make the display context 75 splash = displayio.Group() 76 display.root_group = splash 77 78 # Make a background color fill 79 color_bitmap = displayio.Bitmap(display.width, display.height, 3) 80 color_palette = displayio.Palette(3) 81 color_palette[0] = 0x660088 82 color_palette[1] = 0x000000 83 color_palette[2] = 0xffffff 84 bg_sprite = displayio.TileGrid(color_bitmap, pixel_shader=color_palette, x=0, y=0) 85 splash.append(bg_sprite) 86 87 # Draw a label 88 text_area = label.Label(font=terminalio.FONT, text="" , color=0xFFFF00, scale=2, line_spacing=1 ) 89 text_group = displayio.Group(x=15, y=20) 90 text_group.append(text_area) # Subgroup for text scaling 91 splash.append(text_group) 92
Die Zeilen 65 bis 92 initialiseren wie bei allen vorangegangenen Beispielen das Display, definieren verschiedenen
Hintergrundfarben und bereiten die Textausgabe am Display vor. Es folgen ab Zeile 93 die Definition des GPS-Moduls
und ab Zeile 102 und 109 zwei Funktionen zur Nutzung der UTC-Daten.
93 uart = busio.UART(board.GP4, board.GP5, baudrate=9600, timeout=10) 94 # i2c = busio.I2C(board.SCL, board.SDA) 95 96 gps = adafruit_gps.GPS(uart, debug=False) 97 # gps = adafruit_gps.GPS_GtopI2C(i2c, debug=False) # Use I2C interface 98 99 gps.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") 100 gps.send_command(b"PMTK220,1000") 101 102 def _format_date(datum): 103 return "{:02}.{:02}.{}".format( 104 gps.timestamp_utc.tm_mday, 105 gps.timestamp_utc.tm_mon, 106 gps.timestamp_utc.tm_year, 107 ) 108 109 def _format_time(zeit): 110 return "{:02}:{:02}:{:02}".format( 111 gps.timestamp_utc.tm_hour, 112 gps.timestamp_utc.tm_min, 113 gps.timestamp_utc.tm_sec, 114 ) 115
Ab Zeile 116 folgt die Funktion 'new_log_file()', welche alle 10 Minuten die nächste gpx-Datei anlegt und alle Einträge
'vorbereitet'. Unter anderem werden die Zielkoordinaten und der Name des Ziels als Wegpunkt eingetragen,
damit sie später in der Karte angezeigt werden. Für den Dateinamen wird die Variable 'Zahl' übergeben.
116 def new_log_file(zahl): 117 # Logdatei anlegen 118 filename = "/sd/gps_daten" + str(zahl) + ".gpx" 119 with open(filename, "w") as f: 120 f.write("\n") 121 f.write("\n") 122 f.write(" \n") 123 f.write(" \n\n") 127 128 f.write("GPX Daten mit RP2040_Geek \n") 124 f.write("Validiertes GPX-Beispiel ohne Sonderzeichen \n") 125 f.write("\n") 126 f.write(" Detlef Gebhardt \n".format(ziel_lon)) 129 f.write(" \n\n") 131 132 f.write("" + ziel + " \n") 130 f.write("Flag, Red \n\n") 133 f.write(" \n") 134 f.close() 135 136 last_print = time.monotonic() 137 last_save = time.monotonic() 138 # neue Logdatei anlegen 139 new_log_file(zahl) 140
In Zeile 136 und 137 werden zwei Variable mit der aktuellen Zeit belegt, um daraus später zu ermitteln, wie viel Zeit
vergangen ist. In Zeile 139 wird die Funktion zum Anlegen der gpx-Datei bei Programmstart erstmals ausgeführt. Es
folgt die 'while'-Schleife mit der Programmabarbeitung.
In Zeile 142 werden bei jedem Schleifendurchlauf die gps-Daten aktualisiert. Die Funktion der Zeilen 145 bis 159 für die
Datenspeicherung in Abhänigkeit von der Geschwindigkeit ist bereit ganz oben erläutert worden.
141 while True: 142 gps.update() 143 # 144 # speichern in Abhaengigkeit von speed 145 if gps.speed_knots is not None: 146 speed = gps.speed_knots 147 if speed < 1: 148 wait = 60 149 if speed >= 1 and speed < 10: 150 wait = 15 151 if speed >= 10 and speed < 27: 152 wait = 10 153 if speed >= 27 and speed < 50: 154 wait = 4 155 if speed >= 50: 156 wait = 2 157 else: 158 speed = 0 159 wait = 60 160 # 161 # gpx-Datei abschliessen und neue anlegen 162 # nach 10 Minuten 163 # 164 if time.monotonic() - last_save >= 600: 165 #color_bitmap.fill(1) 166 #text_area.text = "\n Datei\n wird\n gespeichert" 167 with open("/sd/gps_daten" + str(zahl) + ".gpx", "a") as f: 168 f.write("\n\n\n\n") 169 f.close() 170 #print("Datei abgeschlossen") 171 zahl += 1 172 # neue Zahl fuer Dateiname schreiben 173 with open("/sd/gps_logger.txt", "w") as f: 174 f.write(str(zahl)) 175 f.close() 176 new_log_file(zahl) 177 last_save = time.monotonic() 178 #color_bitmap.fill(0) 179 # 180 # Anzeige von Bildschirm 1 mit GPS-Koordinaten 181 # 182 # Anzeige jede Sekunde erneuern 183 if time.monotonic() - last_print >= 1: 184 last_print = time.monotonic() 185 if not gps.has_fix: 186 # Try again if we don't have a fix yet. 187 print("Waiting for fix...") 188 text_area.text = "\n Waiting\n for a\n satellite" 189 continue 190 else: 191 datum = _format_date(gps.timestamp_utc) 192 zeit = _format_time(gps.timestamp_utc) 193 lat = gps.latitude 194 lon = gps.longitude 195 dy = (lat - ziel_lat)*111.3 196 # einfache Methode 197 #dx = (lon - ziel_lon)*71.5 198 # verbesserte Methode, da Longitude vom Breitengrad anhaengt 199 # Formel: dx = 111.3 * cos((lat1 + lat2) / 2 * 0.01745) * (lon1 - lon2) 200 dx = 111.3 * math.cos((lat + ziel_lat) / 2 * 0.01745) * (lon - ziel_lon) 201 distance = math.sqrt(dx * dx + dy * dy) 202 if dx > 0: 203 dir_x = " km nach West" 204 else: 205 dir_x = " km nach Ost" 206 dx = abs(dx) 207 if dy > 0: 208 dir_y = " km nach Sued" 209 else: 210 dir_y = " km nach Nord" 211 dy = abs(dy) 212 text_area.text = (ziel + "\n{0:.3f}".format(dx) + dir_x + "\n{0:.3f}".format(dy) + dir_y + 213 "\nLuftl.: {0:.3f} km".format(distance) + 214 "\nspeed: {} ".format(gps.speed_knots) + 215 "(" + str(600 - int(time.monotonic() - last_save))+ ")") 216 # entspr. der Geschw. speichern 217 if gps.timestamp_utc.tm_sec % wait == 0: 218 with open("/sd/gps_daten" + str(zahl) + ".gpx", "a") as f: 219 f.write("\n") 221 f.close()
Danach wird in Zeile 164 geprüft, ob schon 10 Minuten (600 Sekunden) vergangen sind. Wenn ja wird die gpx-Datei abgeschlossen und
eine neue angelegt. Dieser Teil ist in 178 abgeschlossen. Ab Zeile 183 wird die Displayanzeige einmal pro Minute aktualisiert, falls ein
Satellit gefunden wurde (Zeile 190). Sonst wird gewartet. Der Hersteller gibt für das GPS-Modul Air530 an, dass dies beim Warmstart
ca. 4 s und beim Kaltstart bis zu 30 s dauert.
In den Zeilen 191 bis 211 erfolgt die Entfernungsberechnung vom Ziel. Dabei habe ich die einfache Methode, welche ich zuerst verwendet habe,
zum Anschauen in auskommentierter
Form stehen lassen. Zur Anwendung kommt jetzt die verbesserte Methode, welche die Veränderung der Longitude in Abhänigkeit von der
Latitude berücksichtigt. Die Zeilen 212 bis 215 stellen die Displayausgabe dar.
Besonders hinweisen möchte ich noch auf die Zeile 217. Wenn die ganzzahlige Division in Python % der aus UTC bestimmten
'Sekunden' gps.timestamp_utc.tm_sec durch den Wert von wait gleich Null ergibt, wird
geschwindigkeitsabhänig gespeichert. Ist z.B. wait = 4 heißt das, speichern bei Sekunde 4 od. 8 od. 12 usw. Ist wait = 10
bei Sekunde 10 od. 20 od. 30 usw. Also max. 6 mal pro Minute, während im vorhergehenden Fall 15 mal pro Minute gespeichert wird. Je kleiner also
der Wert von 'wait' ist, um so häufiger wird gespeichert, was bei höherer Geschwindigkeit die angezeigte Genauigkeit verbessert.
viel Spass und Erfolg beim Ausprobieren.
viel Spass und Erfolg beim Ausprobieren.