DIY Raspberry Car per Internet steuern (Teil 5)

tl;dr: Ein Web–Client sendet Befehle per Socket.IO (WebSockets) an das Auto. Ein separater Server stellt die Verbindung zwischen Client und Auto her.

...oder wie ich eine mobile Kamera baute (Teil 5)

In diesem Blog–Artikel dreht es sich ganz um die Software für die Steuerung von dem Auto aus dem letzten Blog–Artikel. Natürlich soll sich das Auto fahren und lenken lassen, aber es gibt ein paar zusätzliche Anforderungen. Die Steuerungssoftware soll folgendes leisten.

  • Das Auto lässt sich eine gewünschte Strecke fahren.
  • Jedes Gerät mit einem Web–Browser soll als Controller geeignet sein.
  • Wenn die Verbindung abreißt, dann soll das Auto stehen bleiben.
  • Das Auto kann sich in einem anderen Netzwerk befinden (LTE–Handy als Controller, Auto im WiFi).

Der erste Punkt sollte als Ziel klar sein und schränkt uns wenig ein. Die anderen Punkte haben konkrete technische Auswirkungen. Die Code–Beispiele in diesem Artikel sind auf ein Minimum beschränkte Prototypen: die technische Lösung ist klar erkennbar, aber viele Wünsche bleiben offen.

Web–Browser als Controller

Ein Internet–fähiges Gerät ist de facto immer zur Hand. Das Handy als Controller zu nehmen wäre praktisch — und es kann bereits per HTML, JavaScript und zBsp. HTTPs mit anderen Geräten kommunizieren. Daher ist die technische Grundlage das Web.

Fertige Lösungen für Verschlüsselung der Daten und ein Login gibt es außerdem. Ich fühle mich wohler, wenn nicht jeder mit Internet Zugriff auf meine mobile Kamera hat.

Stopp bei Verbindungsabriss

Bestimmt ist es für das Auto nicht so gut, wenn es zBsp. an der Wand steht und die Räder die ganze Zeit durchdrehen – für die Batterie ist es definitiv nicht gut. Deshalb soll es nur solange fahren, wie es auch gesteuert wird. Sonst soll es so schnell wie möglich stehen bleiben. Hier gibt es verschiedene Möglichkeiten.

Man kann Befehlen wie "vorwärts" eine maximale Gültigkeit geben: "vorwärts für eine Sekunde". Der nächste Befehl würde den vorherigen sozusagen ablösen. Dafür müssen allerdings verhältnismäßig viele Daten übertragen werden und das Timing ist genau abzustimmen. Wenn Befehle eine halbe Sekunde gelten und nur ein Befehl pro Sekunde empfangen oder gesendet werden kann, zuckelt das Auto so vor sich hin. Es könnte (zumindest theoretisch) passieren, das sich Befehle auf der Datenautobahn überholen.

Variante zwei: eine langlebige Verbindung mittels WebSockets wird aufgebaut, um Befehle wie "vorwärts" zu übertragen. Die Verbindung bleibt hier anders als bei HTTP offen und neue Befehle werden über die selbe Verbindung empfangen. Reißt die Verbindung ab, kann man reagieren und das Auto stoppen.

Es gibt wie immer noch viele andere Möglichkeiten. In diesem Artikel geht es mit WebSockets weiter.

LTE–Handy und WiFi–Auto

In diesem Setup gehe ich davon aus, dass das Handy das Auto nicht direkt erreichen kann. Anders gesagt: das Handy und das Auto befinden sich in verschiedenen lokalen Netzen. Nehmen wir an, das Auto befindet sich in einem WiFi–LAN mit Internet und es gibt mindestens eine Firewall am Router. Die IP–Adresse des Internetanschlusses ändert sich dann von Zeit zu Zeit. Der Browser auf dem Handy muss sich allerdings zu einem Web–Server verbinden, der "öffentlich" erreichbar ist. Dieser Web–Server kann folglich nicht auf dem Auto laufen, sondern muss ein "richtiger" Web–Server im Internet sein.

Das Auto muss sich ebenfalls als Client zu diesem Steuerungsserver verbinden und vom Server aus Befehle empfangen. Der Controller sendet also seine Steuerungsbefehle an den Server und der Server sendet sie an das Auto weiter.

RasperyPi Auto Per Wlan Steuern Diagramm

Dieser Aufbau hat noch eine Reihe weiterer Vorteile.

  • Der Server ist erreichbar, wenn das Auto abgeschaltet ist.
  • Man kann statt des echten Autos einen Emulator verbinden während der Programmierung.
  • (nicht im Beispiel–Code) Der Server könnte das Auto für einen Nutzer reservieren und als "in Benutzung" anzeigen.
  • (nicht im Beispiel–Code) Es könnten sich mehrere Autos anmelden, die gleichzeitig von verschiedenen Personen gesteuert werden könnten.

Der größte Nachteil ist ein meinen Augen das Setup des zusätzliche Web–Servers. Die Komplexität der Software erhöht sich (imho) nur minimal.

Socket.IO

Es ist bestimmt möglich direkt mit WebSockets arbeiten, aber ich habe eine schicke Library gefunden. Das Getting started Chat-Beispiel von Socket.IO ist fast genau was wir brauchen: ein Client (Controller) sendet einem anderen Client (Auto) Nachrichten (Befehle). Für einen Node.js–Server, einen JavaScript- oder Python–Client gibt es passende Libraries. Selbst eine Library für einen Python–Server wäre da.

Eine Erweiterung für Peer-to-Peer Verbindungen gibt es, die für die Übertragung des Kamerabilds später mal nützlich sein könnte. Einen Chat (Auto–Steuerung) mit mehreren Channels (verschiedene Autos) scheint auch möglich zu sein. Daher geht es mit Socket.IO weiter.

Code–Samples

Die Code–Beispiele reichen aus, um ganz rudimentär das Auto zu steuern. Die Code–Beispiele sind schön kurz, aber man kann noch sehr viel verbessern.

Die nötigen Pakete müssen installiert sein. Eine Anleitung dafür findest du im JavaScript–Chat–Tutorial und hier der Python–Doku.

server.js

// Node.js server where controller and car meet const app = require('express')(); const http = require('http').createServer(app); const io = require('socket.io')(http); app.get('/', (req, res) => { res.sendFile(__dirname + '/controller.html'); }); io.on('connection', socket => { console.log('something connected'); // broadcast commands socket.on('command', cmd => io.emit('command', cmd)); // stop on disconnect socket.on('disconnect', () => io.emit('command', 'stop')); }); http.listen(3000, () => { console.log('listening on *:3000'); });

controller.html

<!-- controller running in the browser --> <html> <head> <title>Socket.IO Controller</title> </head> <body> <button onmousedown="command('left')" onmouseup="stop()">Left</button> <button onmousedown="command('foreward')" onmouseup="stop()">Foreward</button> <button onmousedown="command('right')" onmouseup="stop()">Right</button> <script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script> <script> var socket = io(); function command(cmd) { socket.emit('command', cmd); } function stop() { command('stop'); } </script> </body> </html>

emulated-car.py

# fake car for development import socketio import sys sio = socketio.Client() @sio.event def connect(): print('connected to server') @sio.event def command(data): print('command: ', data) @sio.event def disconnect(): print('disconnected from server') if __name__ == '__main__': try: sio.connect(sys.argv[1] if len(sys.argv) > 1 else 'http://localhost:3000') sio.wait() except KeyboardInterrupt: print('bye')

car.py

# script running on the car and receiving commands import socketio import RPi.GPIO as GPIO import time import sys print(""" Socket.io client for the car. Connects to server and receives commands and events. """) class Wheel: """ wheel(s) on one side of the car """ def __init__(self, enPin: int, inForewardPin: int, inBackwardPin: int): GPIO.setup(inForewardPin, GPIO.OUT) self.inForewardPin = inForewardPin GPIO.output(inForewardPin, False) self.inBackwardPin = inBackwardPin GPIO.setup(inBackwardPin, GPIO.OUT) GPIO.output(inBackwardPin, False) GPIO.setup(enPin, GPIO.OUT) self.pwm = GPIO.PWM(enPin, 100) self.pwm.start(0) self.pwm.ChangeDutyCycle(0) def setSpeed(self, speed: int): """ speed might be -3, -2, -1, 0, 1, 2, 3 """ force = min(3, abs(speed)) isForewards = speed > 0 if (isForewards): GPIO.output(self.inBackwardPin, False) GPIO.output(self.inForewardPin, True) else: GPIO.output(self.inForewardPin, False) GPIO.output(self.inBackwardPin, force > 0) if force > 0: self.pwm.ChangeDutyCycle(70 + (10 * force)) else: self.pwm.ChangeDutyCycle(0) if __name__ == '__main__': GPIO.setmode(GPIO.BOARD) RightWheels = Wheel(3, 5, 7) LeftWheels = Wheel(8, 10, 12) def driveForeward(): print('driving foreward') LeftWheels.setSpeed(3) RightWheels.setSpeed(3) def driveLeft(): print('driving left') LeftWheels.setSpeed(-3) RightWheels.setSpeed(3) def driveRight(): print('driving right') LeftWheels.setSpeed(3) RightWheels.setSpeed(-3) def driveStop(): print('full stop') LeftWheels.setSpeed(0) RightWheels.setSpeed(0) sio = socketio.Client() try: @sio.event def connect(): print('connected to server') # a LED would be handy driveStop() @sio.event def command(data): if data == 'left': driveLeft() elif data == 'right': driveRight() elif data == 'foreward': driveForeward() elif data == 'stop': driveStop() else: print("unknown command '%s'" % data) driveStop() @sio.event def disconnect(): print('disconnected from server') # a LED would be handy driveStop() sio.connect(sys.argv[1] if len(sys.argv) > 1 else 'http://localhost:3000') sio.wait() except KeyboardInterrupt: LeftWheels.setSpeed(0) RightWheels.setSpeed(0) GPIO.cleanup() print('bye')

Mit diesem Code konnte ich das Auto im Wohnzimmer fahren lassen — sehr zur Freude meiner Kinder. Die Steuerung ist allerdings noch sehr… rudimentär.

Was nun?

Wie geht es nun weiter? Das Projekt ist natürlich noch lange nicht fertig:

  • Die Controller UI kann noch sehr viel besser werden. Auf dem Handy sind die Buttons nicht bedienbar.
  • Die Geschwindigkeitsstufen für die Motoren bringen aktuell fast nichts. Vielleicht wird es mit einer höheren Motorspannung besser.
  • Die car.py sollte automatisch geladen werden, wenn man den Raspberry Pi startet.
  • Eine LED könnte anzeigen, ob das Auto mit dem Server verbunden (also "online") ist.
  • Ein Schalter zum Abschalten der Stromversorgung für die Räder wäre praktisch.
  • Ein Knopf in der UI, um den Raspberry Pi aus der Ferne abzuschalten wäre auch schön.
  • Am allermeisten fehlt mir jedoch das Kamerabild.

Für den ein oder anderen Punkt auf der List, schreibe ich sicherlich weitere Blog–Artikel. Vielen Dank für's Lesen, viel Spaß beim Werkeln und du kannst gerne Feedback geben.