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

  
  

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("GPX Daten mit RP2040_Geek\n")
124         f.write("Validiertes GPX-Beispiel ohne Sonderzeichen\n")
125         f.write("Detlef Gebhardt\n")
126         f.write("\n\n")
127
128         f.write("\n".format(ziel_lon))
129         f.write("" + ziel + "\n")
130         f.write("Flag, Red\n\n\n")
131
132         f.write("\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.