OBD/AWS IoT-Script als Service – Quick and Dirty

Im letzten Beitrag haben wir ein Python-Script vorgestellt, mit dem Daten von einem ELM327-kompatiblen OBD-Interface und einer USB-GPS-Maus über einen Raspberry Pi zum AWS IoT Core hochgeladen werden. Nachteil der Lösung ist, dass das Script noch manuell gestartet werden muss – und im Auto sind häufig weder Tastatur und Monitor verfügbar noch hat man die Zeit, vor dem losfahren erst Scripte zu starten. Praktischer wäre es also, wenn der Raspberry Pi nach dem Starten des Fahrzeugs automatisch hochfährt und sich dem mit AWS IoT Core verbindet. Ein normales raspbian muss man zudem vor dem Ausschalten herunter fahren, da die SD-Karte sonst früher oder später kaputt geht. Mit Hilfe des von Adafruit vorgestellten Read-Only-Raspbian lässt sich auch dies verhindern.

Verwendete Hardware und Software

Vorbereiten des Scripts für die Verwendung als Service

Unter Linux wird das „Init“-System verwendet, um beim Systemstart die Konfiguration des Systems zu übernehmen und im Hintergrund laufende Dienste zu starten [1]. Unter Raspbian wird wie unter vielen aktuellen Linux-Distributionen „Systemd“ als Init-System verwendet [2]. Um das Python-Script als Service zu starten, brauchen wir zwei Dinge: Ein Shell-Script, dass das Python-Script mit den richtigen Argumenten aufruft (Endpunkt, Zertifikate usw.) und eine Systemd-Konfigurationsdatei, mit der das Script beim Systemstart ausgeführt wird. Das Python-Script muss als user, z.B. „pi“, ausgeführt werden damit die benötigen Python-Module geladen werden können. Im vorherigen Beitrag habt ihr alle Dateien von github bzw. AWS heruntergeladen. Zunächst müssen wir die für den Service benötigten Dateien, also das Python-Script, das AmazonRootCa-Zertifikat und die Zertifikate für das „Thing“ an einem Ort ablegen, an dem der User Zugriffsrechte hat. Dafür legen wir z.B. das Verzeichnis /usr/local/lib/simple-car-connect an und kopieren alle Dateien hinein (XXX ist die ID von eurem Thing):

pi@raspberrypi:~ $ ls -l /usr/local/lib/simple-car-connect/
total 36
-rw-r--r-- 1 root root  1220 Oct  4 20:31 XXXX-certificate.pem.crt
-rw-r--r-- 1 root root  1675 Oct  4 20:31 XXXX-private.pem.key
-rw-r--r-- 1 root root   451 Oct  4 20:31 XXXX-public.pem.key
-rw-r--r-- 1 root root  1187 Oct  4 20:31 AmazonRootCA1.pem
-rw-r--r-- 1 root root   655 Oct  4 20:31 AmazonRootCA3.pem
-rwxr-xr-x 1 root root 13006 Oct  4 20:31 simple_car_connect.py

Da wir aus der Systemd-Konfiguration keine Argumente an das Python-Script übergeben können, benötigen wir ein Shell-Script, dass das Python-Script mit den richtigen Argumenten aufruft. Im Prinzip entspricht es dem Aufruf von der Kommandozeile aus dem letzten Beitrage. Zusätzlich ist noch eine Verzögerung von 10 Sekunden eingebaut, um dem System beim Hochfahren Zeit für die Initialisierung (z.B. zum Setzen der Zeit über NTP) zu geben. Das Script liegt unter /usr/sbin/simple-car-connect.sh und hat folgenden Inhalt:

#!/bin/sh
sleep 10
python3 /usr/local/lib/simple-car-connect/simple_car_connect.py --endpoint YYYY-ats.iot.eu-central-1.amazonaws.com --root-ca /usr/local/lib/simple-car-connect/AmazonRootCA1.pem --cert /usr/local/lib/simple-car-connect/XXXX-certificate.pem.crt --key /usr/local/lib/simple-car-connect/XXXX-private.pem.key --topic cars/01 --count=0 > /tmp/simple_car_connect.log 2>&1

Die erste Zeile enthält den „Shebang„, der das Script mit /bin/sh ausführt. In der nächsten Zeile wird 10 s gewartet, bevor in der dritten Zeile das Python-Script mit den notwendigen Argumenten ausgeführt wird. Diese Zeitverzögerung habe ich hier eingebaut, da NTP nicht immer synchronisiert war, wenn die Verbindung zum AWS IoT Core hergestellt wurde. Das hat zu Verbindungsabbrüchen geführt, die hier nicht abgefangen werden. YYYY ist der AWS IoT Core Endpoint, XXXX das Thing. Mittels Ausgabeumleitung werden die Standardausgabe und der Fehlerkanal (2>&1) in die Datei /tmp/simple_car_connect.log umgeleitet, die sich in einem für User schreibbaren Verzeichnis befindet.

Konfigurieren von Systemd

Im Verzeichnis /etc/systemd liegt eine Struktur von Unterverzeichnissen, Dateien und Links, die dafür sorgt, dass beim Start von raspbian auf dem Raspberry PI jede Menge Programme automatisch als Hintergrunddienst ausgeführt werden. Wer mehr dazu erfahren will, kann z.B. im „Kofler“ [2] nachschauen. Hier werde ich nur kurz darauf eingehen, was ihr tun müsst, damit das Python-Script zum Senden der OBD-Daten an den AWS IoT-Core automatisch beim hochfahren gestartet wird. Als erstes müsst ihr dafür eine Konfigurations-Datei unter /etc/systemd/system erstellen (ich habe sie hier cariot.service genannt):

sudo nano /etc/systemd/system/cariot.service

Der Inhalt der Datei ist wie folgt:

[Unit]
Description=Simple Service for sending ELM327 OBD Data to AWS IoT Core
After=network.target ntp.service

[Service]
User=pi
ExecStart=/usr/sbin/simple-car-connect.sh

[Install]
WantedBy=multi-user.target

Im Abschnitt [Unit] wird neben einer kurzen Beschreibung des Services festgelegt, welche Dienste vor unserem Service gestartet werden müssen. Wir brauchen hier „network“, damit „ntp“, damit die Netzwerkverbindung für die Kommunikation mit dem AWS IoT Core steht. Außerdem brauchen wir für den Verbindungsaufbau eine korrekte Systemzeit.

Als nächstes wird in [Service] festgelegt, welches Kommando zum Starten unseres Dienstes ausgeführt wird. Hier wird unser oben angelegtes Shell-Script verwendet. Außerdem müssen wir es als user „pi“ ausführen – zum einen dient das der Sicherheit, außerdem sind nicht alle verwendeten Python-Bibliotheken als root verwendbar.

Abschließend wird unter [Install] noch angegeben, in welcher „Startkonfiguration“ der Service gestartet wird. Hier ist die Standard-Konfiguration angegeben. Mehr dazu findet ihr im „Kofler“ [2] oder in den Linux-Manual-Pages.

Mit dem Befehl „service“ kann das script, z.B. zum testen, manuell gestartet werden:

pi@raspberrypi:~ $ service cariot start
==== AUTHENTICATING FOR org.freedesktop.systemd1.manage-units ===
Authentication is required to start 'cariot.service'.
Authenticating as: ,,, (pi)
Password: 
==== AUTHENTICATION COMPLETE ===

Analog kann mit „service cariot stop“ der service beendet werden. Um den Service bei booten automatisch zu starten, muss das folgende Kommando eingegeben werden:

pi@raspberrypi:~ $ sudo systemctl enable cariot

NTP für Read-Only-Raspbian vorbereiten

Nach dem Ausführen des „Read-Only-Raspbian“-Scripts snychronisiert ein NTP-Daemon in Standardkonfiguration die Systemzeit nicht mehr. Das Problem und die Lösung, die bei mir funktioniert hat, ist auf stackexchange.com beschrieben. Hier sind die wichtigsten Schritte zusammengefasst – für Erklärungen bitte in den Original-Beitrag schauen. Als erstes installiert ihr die benötigten Pakete, dann wird noch die config-Datei an die richtige Stelle kopiert und die Konfigurationsdatei des NTP-Service zum editieren geöffnet:

pi@raspberrypi:~ $ sudo apt-get install ntp ntpdate
pi@raspberrypi:~ $ cp /lib/systemd/system/ntp.service /etc/systemd/system/ntp.service
pi@raspberrypi:~ $ sudo nano /etc/systemd/system/ntp.service

In der NTP-Konfigurationsdatei muss das „PrivateTmp“-Feature deaktiviert werden, indem die Zeile „PrivateTmp=true“ auskommentiert wird. Die Datei sieht dann wir folgt aus:

[Unit]
Description=Network Time Service
Documentation=man:ntpd(8)
After=network.target
Conflicts=systemd-timesyncd.service

[Service]
Type=forking
# Debian uses a shell wrapper to process /etc/default/ntp
# and select DHCP-provided NTP servers if available
ExecStart=/usr/lib/ntp/ntp-systemd-wrapper
#PrivateTmp=true

[Install]
WantedBy=multi-user.target

Abschließend wird noch das Verzeichnis /var/lib/ntp als tmpfs gemounted. Dafür musst die Datei /etc/fstab geöffnet werden:

pi@raspberrypi:~ $ sudo nano /etc/fstab

Und der Eintrag „tmpfs /var/lib/ntp tmpfs nosuid,nodev 0 0“ hinzugefügt. Die Datei sieht dann wie folgt aus:

proc            /proc           proc    defaults          0       0
PARTUUID=4520e3fc-01  /boot           vfat    defaults,ro          0       2
PARTUUID=4520e3fc-02  /               ext4    defaults,noatime,ro  0       1
# a swapfile is not a swap partition, no line here
#   use  dphys-swapfile swap[on|off]  for that
tmpfs /var/log tmpfs nodev,nosuid 0 0
tmpfs /var/tmp tmpfs nodev,nosuid 0 0
tmpfs /tmp tmpfs nodev,nosuid 0 0
tmpfs /var/lib/ntp tmpfs nosuid,nodev 0 0

Vergeben einer fixen IP-Adresse für Ethernet

Für das Ethernet-Interface des Raspberry habe ich eine fixe IP-Adresse vergeben. Davor hatte ich Probleme mit dem Verbindungsaufbau zur AWS. Für das Debugging im Fahrzeug ist die fixe IP dann nützlich, da man z.B. einem Notebook eine passende IP geben und sich dann mit dem Raspberry PI verbinden kann. Ein DHCP-Server steht dort ja üblicherweise nicht zur Verfügung. Danach waren die Probleme mit der Verbindung zu AWS aber auch verschwunden. Vermutlich hatte NTP oder ein anderer Netzwerkdienst ein Problem mit dem Warten auf die Konfiguration des Interfaces per DHCP. Für die Vergabe einer statischen IP-Adresse müsst ihr die Datei /etc/dhcpcd.conf editieren. Da das Interface nur für die Verbindung mit einem Notebook genutzt werden soll, braucht nur die IP vergeben werden. Router und DNS-Server werden nicht gesetzt. Die Verbindung ins Internet wird ja über das im letzten Beitrag konfigurierte WiFi-Interface hergestellt. Ihr öffnet die Datei mit:

pi@raspberrypi:~ $ sudo nano /etc/dhcpcd.conf

Für das Interface eth0 kommentiert ihr alle vorhandenen Zeilen aus. Es müssen lediglich die beiden folgenden Zeilen vorhanden sein:

# Example static IP configuration:
interface eth0
static ip_address=192.168.188.200/24

Wenn ihr eine IP aus dem Bereich von eurem Router vergebt (die aber nicht schon an ein anderes Gerät vergeben sein darf), lässt sich der Raspberry PI auch zu Haus einfach ansprechen, indem ihr in per Kabel mit eurem Router verbindet. Im Fahrzeug muss das Notebook dann fix eine IP zugewiesen bekommen, die dem Segment der Raspberry PI entspricht, da ja kein DHCP-Server vorhanden ist. Es könnte hier z.B. die 192.168.188.201 verwendet werden.

Die SD-Karte als „read only“ einrichten

Abschließend wird das adafruit-Script ausgeführt, um die SD-Karte als read only einzurichten. Eine Beschreibung von dem Script findet ihr auf der Adafruit-Seite. Mit den folgenden zwei Befehlen könnt ihr das Script herunterladen und ausführen:

pi@raspberrypi:~ $ wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/read-only-fs.sh
pi@raspberrypi:~ $ sudo bash read-only-fs.sh

Auf die Frage, ob ihr das Filesystem auf read-only ändern wollt, müsst ihr natürlich mit yes („y“) antworten. Danach werden einige weitere Features abgefragt (beschrieben in dem Adafruit-Beitrag). Wir haben sie hier alle deaktiviert. Es sollte aber auch nicht schaden, wenn sie aktiviert sind.

pi@raspberrypi:~ $ sudo mount -o remount,rw /

Nach einem Neustart sollte der Raspberry PI die SD-Karte read-only mounten, eine Verbindung ins Internet über WiFi aufbauen, sich mit dem AWS IoT-Core verbinden und die im Python-Script aus dem letzten Beitrag ausgewählten NMEA0183 und OBD2-Botschaften übertragen. Das ganze ist noch recht unflexibel und noch nicht sehr robust gegenüber Störungen. Aber ein erster Prototyp steht und bevor wir diesen Teil verbessern, schauen wir uns erst mal die Daten bei AWS an.

Nachdem das Script gelaufen ist, sollte der Raspberry Pi es ohne Beschädigung des Dateisystems auf der SD-Karte überstehen, wenn er ohne herunterfahren ausgeschaltet wird. Der Nachteil ist, dass nicht mehr einfach Änderungen an Dateien vorgenommen oder Pakete installiert oder upgedated werden können. Indem ihr das Filesystem nach dem einloggen als read-write „remounted“, lässt sich – bis zum nächsten Neustart – aber alles wieder wie gewohnt durchführen. Dafür einfach das folgende Kommando eingeben:

[1] M. Kofler: Linux, 14. Auflage, Rheinwerk Verlag Bonn, 2016, S. 905 ff.

[2] M. Kofler, Linux, 14. Auflage, Rheinwerk Verlag Bonn, 2016, S. 920 ff.

1 Kommentar zu „OBD/AWS IoT-Script als Service – Quick and Dirty“

  1. Pingback: OBD2 mit Raspberry PI in AWS IoT - Vehicle-Hacks

Kommentarfunktion geschlossen.