Dokumentation zum GPSPi-Sourcecode

Du möchtest tiefer in den Sourcecode einsteigen und verstehst etwas von Programmierung in einer Hochsprache (das muss nicht zwingend Python sein)? Gut. Hier ein paar Hinweise, die ich zum Code mitgeben möchte.

main()

Die Musik spielt – wie nicht anders zu erwarten – in main(). Dort werden zuerst in init() alle Variablen und Schnittstellen gesetzt (s.u.), und dann werden ein bis zwei Hintergrundthreads angelegt – abhängig davon, ob 1wire aktiv ist oder nicht. Hintergrundthreads haben den Vorteil, dass sie unabhängig von Wartezeiten (bspw. zwischen zwei Ansichten) oder Benutzereingaben (später einmal) durchlaufen. In meinem Fall lesen sie die Daten aus den beiden Quellen – USB-GPS und 1wire-Bus – aus und legen sie in den Datenstrukturen (s.u.) ab.

In der Folge wird eine (Quasiendlos-)Schleife durchlaufen, in der nacheinander die Ansichten 1 bis 5 oder, sofern 1wire deaktiviert ist, 1 bis 4 durchlaufen werden (viewindex). Zwischen den Ansichten wird displaydelay Sekunden gewartet, dieser Wert kommt aus der gpspi.conf. Nur für den Fall, dass derzeit kein GPS-Empfang besteht, wird ausschließlich die Ansicht 0 angezeigt (die den Spezialfall “kein Empfang” wiedergibt), im Wechsel mit Ansicht 5 (1wire), sofern 1wire aktiviert ist. Zur Ausgabe einer Ansicht wird outputNMEA() aufgerufen.

Ablauf in main()

Ablauf in main()

Die Schleife kann nur verlassen werden, indem einer der Hintergrundprozesse die exitcondition auf TRUE setzt. Da ich aktuell noch keine Benutzerinteraktion geplant habe, weil ich auf dem GPIO keinen Platz für einen oder zwei Taster habe, gibt es derzeit nur zwei Möglichkeiten, die exitcondition zu erfüllen: die nmea.txt ist zu Ende (und das funktioniert nur im ziemlich sinnfreien Modus FILE, in dem GPS-Daten aus einer Datei und nicht vom GPS-Empfänger kommen), oder ein konfigurierter Temperatursensor DS18B20 wird nicht (mehr) gefunden. Mit einem Taster als Öffner an der Datenleitung an einem der Temperatursensoren lässt sich die Abbruchbedingung also provozieren.

Dann wird loopindicator auf FALSE gesetzt, was den Hintergrundthreads sagt, dass sie beim nächsten Schleifendurchlauf abbrechen sollen. Mit join() werden die beiden Hintergrundthreads dann eingefangen, dass heißt, main() endet erst, wenn die beiden ihren letzten Schleifendurchlauf ordnungsgemäß abgeschlossen haben und damit selbst enden – loopindicator ist quasi die “Fernbedienung” von main() auf die beiden Hintergrundthreads. Wirklich wichtig ist das nicht, weil sie ja beide nur lesend auf Schnittstellen zugreifen (also nichts zerstören, wenn sie einfach abgeschossen würden), aber es soll ja ordentlich sein.

init() – Initialisierung und Konfigurationsdaten lesen

Die init() kapselt alle wesentlichen Definitionen, die zum Start einmalig erfolgen müssen und global Bedeutung haben:

Zuerst liest die getconfig() die Datei gpspi.conf aus und legt alle dort konfigurierten Parameter in dem assoziativem Array cfg[] ab. Mit cfg[“KEY”] erhält man also den VALUE als String, den der Nutzer konfiguriert hat. Einer davon ist LANGUAGE.

Aus LANGUAGE wird die Sprachdatei bestimmt und mit getlanguage() ausgelesen. Diese Routine überträgt die Sprachinformationen wiederum in ein assoziatives Array lng[]. Sämtlicher weiterer Code kann also auf lng[“KEY”] zurückgreifen, wenn sprachspezifische Ausgaben gemacht werden.

Dann werden die Schnittstellen initialisiert: initdisplay() ruft den Ausgabekanal initial auf (wobei dort nichts weiter als eine Startnachricht ausgegeben wird; für zukünftige exotische Ausgabekanäle wie ein grafisches Display mag da mehr erforderlich sein), initNMEAsource() öffnet ttyUSBx für den GPS-Empfänger, und ggfs. liest init1wire() noch die Seriennummern der tatsächlich angeschlossenen 1wire-Temperatursensoren aus.

Zum Schluss werden noch eine Reihe von impliziten Konfigurationseinstellungen gesetzt: zum einen werden Maßeinheiten wie bspw. die für die Geschwindigkeit in Abhängigkeit von den Konfigurationseinstellungen und der Sprachdatei gesetzt, ebenso wie die Standardansicht output[0] für den Fall, dass kein GPS-Empfang vorliegt. Zum anderen müssen noch ein paar Werte für ein weiteres assoziatives Array, nämlich NMEAdata[], gesetzt werden. Dies ist erforderlich, da es dank der Multithreadings (s.o.) passieren kann, dass die Ausgabe erstmalig auf das Array zugreifen will, bevor der Thread getNMEA() zum ersten Mal sinnvolle Werte hineinschreiben und die KEYs definieren kann, und dann gibt’ einen Fehler.

getNMEA() – Hintergrundthread für den GPS-Empfänger

Diese Routine liest bitweise die Daten des GPS-Empfängers aus. Zeilenweise würde nicht funktionieren, weil der GPS-Empfänger nicht viele Daten sendet (deswegen reichen im Standard ja auch 4.800 baud), also regelmäßig auf der Schnittstelle nichts los ist und die Routine dann Gefahr laufen würde, halbe NMEA-Sentences einzulesen (die mangels Prüfsumme verworfen würden). So steht in NMEAsentence jedoch ein vollständiger NMEA-Sentence inkl. CRC-Prüfsumme (hinter dem “*”, weswegen apos die Position des Asterisk “*” enthält), die im Folgenden auch überprüft wird.

Stimmt die Prüfsumme, passieren drei Dinge nacheinander:

  • der Thread wird locked, das heißt, er arbeitet jetzt quasi single-threaded. Ziel ist es, dass während der jetzt folgenden Updates des Arrays NMEAdata[] nicht zufällig eine Ausgabe erfolgt, was dazu führen könnte, dass die Anzeige eine Mischung aus alten und neuen Daten enthält. So könnte bspw. die aktuelle Geschwindigkeit aktualisiert werden (NMEAdata[“SPEED”]) und dann eine Ausgabe erfolgen, bevor die maximale Geschwindigkeit auch aktualisiert werden konnte (NMEAdata[“VMAX”]), und in der Ausgabe würden möglicherweise inkonsistente Daten angezeigt.
  • storeNMEAvalues() (s.u.) überträgt die Nutzdaten des NMEA-Sentence in den Array NMEAdata[]
  • Der Thread wird wieder unlocked und geht damit wieder in den Hintergrund. Da die Funktionen während des Locks ausschließlich auf Variablen im Speicher erfolgen und entsprechend rasend schnell sind, kann in den anderen Threads – gettemps() für 1wire und main() für die Ausgabe – in der Zwischenzeit nichts Wichtiges passieren.

Dann ruht die Routine eine Zehntelsekunde – das reduziert die Prozessorlast enorm, und sollten auf der seriellen Schnittstelle inzwischen Daten vom GPS-Empfänger eintreffen, landen sie im Puffer und werden nach der Zwangspause abgeholt. Mit einer Zehntelsekunde je NMEAsentence ist das System in der Lage, GPS-Empfänger mit bis zu neun (= 9x 0.1 Sekunden + Verarbeitungszeit) Sentences pro Sekunde auszulesen – das reicht in der Praxis, zumal die wenigsten NMEA-Sentences sekündlich gesendet werden.

storeNMEAvalues() – NMEA in seine Bedeutung zerlegen

Diese Routine hat eine zentrale Aufgabe – sie überführt den Bytebrei, den NMEA erstmal darstellt, in die fachlich zugeordneten Assoziationen im Array NMEAdata[]. Sie bestimmt anhand des ersten Parameters (NMEA-Sentences sind Komma-separierte Parameterlisten) den Typ des Datensatzes, denn der bestimmt die Semantik der folgenden Parameter. So ist der dritte Parameter (NMEAparams[2]) im GPGGA-Datensatz die geografische Breite, im GPGSA hingegen die Qualität des Satellitenempfangs und im GPGLL wiederum die Nord-Süd-Differenzierung der geografischen Breite.

Was welcher Typ enthält, ist ausführlich im Internet dokumentiert (obwohl die NMEA-Spezifikation eigentlich gar nicht öffentlich ist).

Die Routine bedient sich an verschiedenen Stellen weiterer Subroutinen (storeposition(), storetime(), storespeed()), wenn die semantisch gleichen Informationen in verschiedenen Typen enthalten sind. So wird bspw. die Position bei jedem GPGGA-, jedem GPRMC- und jedem GPGSA-Datensatz aktualisiert, also potenziell mehrmals pro Sekunde. Um das nicht redundant zu implementieren, habe ich diese Funktionen nochmals ausgegliedert.

gettemps() – 1wire-Bus-Temperatursensoren auslesen

Diese Routine – auch als Hintergrundthread angelegt – bedient sich der Raspberry-Pi-eigenen Standardimplementierung des 1wire-Busses. Angeschlossen am GPIO, steht mit den Modulen w1-gpio und w1-therm der direkte Zugriff auf DS18B20-Temperatursensoren (und vorerst auch nur darauf!) zur Verfügung.

Dies erfolgt durch Auslesen der “Dateien” /sys/devices/w1_bus_master1/w1_master_slaves, in der eine Liste der Seriennummern aller am Bus entdeckten Temperatursensoren steht, und der jeweiligen /sys/bus/w1/devices/[Seriennummer]/w1_slave, in der je Sensor die gemessene Temperatur steht.

gettemps() macht also nichts anderes als für jeden Sensor, der am Bus erkannt wurde, die Temperatur auszulesen (auch für solche, die nicht in der gpspi.conf als Datenquelle konfiguriert sind!) und sie in das assoziative Array temperature[] zu schreiben. Das ist formal unsauber, weil damit a) auch Sensoren ausgelesen und ihre Werte gespeichert werden, die mit GPSPi nichts zu tun haben, und b) nicht sichergestellt wird, dass alle (derzeit drei) in GPSPi konfigurierten Sensoren auch tatsächlich dabei sind. In der Praxis stellt das aber wohl kein Problem dar.

Meldet der Sensor exakt 85.000° Celsius – das ist der Default-Wert, wenn keine Messung vorliegt, und das kann insbesondere bei Start des Busses schon mal vorkommen -, wird der Wert auf -99° C gesetzt: das ist am Display dem Anwender gegenüber sicherer als “ungültig” zu erkennen als 85° C, was ja bspw. bei der Motorüberwachung ein zulässiger Wert sein könnte.

Das Schreiben der Variablen erfolgt wieder – wie in getNMEA() – im locked Zustand, damit keine Inkonsistenzen in den Werten entstehen können. Alle angezeigten Temperaturwerte stammen also immer aus der gleichen Messreihe. Gemessen wird alle drei Sekunden – ein kürzerer Takt ist natürlich möglich, aber durch die Trägheit der Sensoren (die geänderte Temperatur muss ja erstmal das Gehäuse durchdringen) erscheint es mir nicht wirklich sinnvoll, und drei Sekunden sind ja auch nicht gerade eine Ewigkeit.

Datenfluss durch den GPSPi

Datenfluss durch den GPSPi

outputNMEA() – die Ausgabe auf dem Display

Diese Routine ist das andere der Datenkette – hier werden die (derzeit fünf) unterschiedlichen Ansichten aus dynamischen Inhalten (Arrays MNEAdata[] und temperature[]) und statischen Inhalten (Sprach-Array lng[] und Konfigurations-Array cfg[], bspw. für die 1wire-Labels) zusammengesetzt.

Abhängig von der Ausgabeart – LCD oder HDMI-/VideoOut-Display – wird die Ausgabe dann entsprechend ausgelöst.

Dieser Ansatz erlaubt mir derzeit noch nicht, unterschiedliche Sichten für unterschiedliche LCDs zu erzeugen. Das ist nicht gut – ein 20×4-LCD mit HD44780-Treiber liegt bereits zuhause, also möchte ich auch eine Ausgabemöglichkeit, die die immerhin 80 statt 32 Zeichen auch ausnutzt, schaffen. Da muss ich noch mal ran …

die Variablen in der Übersicht

cfg[]
assoziatives Array mit allen Konfigurationsparametern aus gpspi.conf

lng[]
assoziatives String-Array mit allen Sprachdaten (KEY-VALUE-Paare) aus gpspi_xx.lng

NMEAdata[]
assoziatives String-Array mit GPS-Daten (Position, Datum, Uhrzeit, Geschwindigkeit, Kurs, Details zur Empfangssituation)

temperature[]
assoziatives String-Array mit aktuellen Temperaturwerten der 1wire-Sensoren

degree
String der Länge 1, enthält das Gradzeichen (ausgabekanalspezifisch, daher konfigurierbar)

onewireIDs
String, Liste der Seriennummern aller angeschlossenen 1wire-Sensoren

NMEAsentence
String, aktueller NMEA-Sentence 

output
String-Array mit den 5+1 unterschiedlichen Ansichten, derzeit als 16+16-Zeichen-String für 16x2-LCDs optimiert

maxspeed
Float; höchste gemessene Geschwindigkeit seit Start

exitcondition
Boolean; wenn TRUE, führt es zur Beendigung von GPSPi

loopindicator
Boolean; wenn TRUE, beenden sich die Hintergrundthreads nach dem aktuellen Schleifendurchlauf

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.