Kamera-Schwarm

Raspberry Pi Kamera-Netzwerk

In diesem Beitrag stelle ich eine Erweiterung meines CameraCtrl-Projekts vor.
Im Bild sieht man mich in Stereo und hochkonzentriert bei der Arbeit. Zu meiner Verteidigung: ich musste den Knopf zum Aktualisieren der Vorschaubilder drücken.

Aufgrund des schlechten WLAN-Empfangs in meinem Keller war die Steuerung der Kamera und die Übertragung der Bilder oft lästig. Einen WLAN-Repeater wollte ich nicht einsetzen, um nicht extra dafür ein eigenes Gerät im Keller betreiben zu müssen. Daher habe ich mir einen externen WLAN-Adapter besorgt.

Zudem war (nachdem ich etwa ein Jahr darauf gewartet habe) endlich der Raspberry Pi Zero 2 wieder erhältlich, sodass ich mir in meinem Überschwang gleich mehrere besorgt habe. Und auch noch neue Kameramodule.

Dabei hatte ich die Idee CameraCtrl um IoT-Schwarm-Funktionalität zu erweitern. Bei einer einzelnen Kamera ist, je nachdem was bzw. wo ich gerade arbeite die Perspektive oft ungünstig, aber ich möchte auch nicht während des Werkens immer wieder die Kamera umpositionieren müssen. Ideal scheint mir eine zweite fixe Perspektive von der gegenüberliegenden Wand (und vielleicht zukünftig noch eine beweglichere Kamera für Detailaufnahmen).

Es wird also zunächst nur ein Schwärmchen aus zwei bis drei Geräten. Ziel ist, den Zugriff auf die Geräte über eine einzige Schnittstelle (API + Webinterface) möglichst bequem und robust zu ermöglichen.

Übrigens habe ich diesmal kein Gehäuse gebaut. Für das Gerät mit dem WLAN-Adapter habe ich das offizielle Pi Zero 2 Gehäuse verwendet, allerdings ist die Verkleidung für das neue und größere Camera Module 3 nicht passend. Ich habe das Loch erweitert, sodass nun neben der Linse der ganze silberne Kasten aus dem Gehäuse ragt. Die Öffnung ließ sich problemlos mit dem Skalpell vergrößern.
Das zweite Gerät, das ich zu Testzwecken im Einsatz hatte, hat noch gar kein Gehäuse. Im echten Einsatz werde ich erstmal nur das bereits im Keller befindliche CameraCtrl-Gerät mit dem Schwarm verbinden.

Netzwerk

Der RPi mit dem WLAN-Adapter fungiert als zentrale Anlaufstelle von/nach außen.

Externen WLAN-Adapter einrichten

Zunächst habe ich den WLAN-Adapter zum Laufen gebracht. Dazu musste ich den Source-Code des Treibers herunterladen und auf dem RPi compilieren. Der von mir verwendete TPLink-Adapter basiert auf einem Realtek-Chip für den ich alles nötige einschließlich Anleitung im Internet gefunden habe.

Zum Glück hat alles auf Anhieb geklappt und ich konnte mich über den TPLink mit dem Pi verbinden.

Access Point

Der interne Adapter war nun also ungenutzt, sodass ich ihn als Access Point für weitere Raspberries einrichten konnte.

Quellen:

Ich habe festgestellt, dass es zum Einrichten eines Access Point unter Linux unzählige Möglichkeiten gibt. Und ich kann nicht behaupten, da den Durchblick zu haben. Für mich hat der im folgenden beschriebene Weg funktioniert. Wenn du etwas Vergleichbares vor hast, rate ich dringend dazu, weitere Quellen zu Rate zu ziehen.

hostapd einrichten

Ich verwende hostapd, um den Access Point einzurichten.

Installation: sudo apt-get install hostapd

Dann erstmal den hostapd Service anhalten: sudo service hostapd stop

Neuere Versionen von hostapd laufen maskiert und müssen daher demaskiert werden, damit die Konfiguration möglich wird:

sudo systemctl unmask hostapd.service  
sudo systemctl enable hostapd.service 

Config erstellen: /etc/hostapd/hostapd.conf:

interface=wlan0
driver=nl80211
hw_mode=g
channel=6
wmm_enabled=0
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0
wpa=2
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
ssid=YOUR-SSID
wpa_passphrase=YOUR-PASSPHRASE-PASSWORD

Achtung: Die Zeilen dürfen keine Leerzeichen am Ende haben!

Netzwerkname (SSID) und Passwort (wpa_passphrase) habe ich natürlich angepasst.

/etc/default/hostapd bearbeiten:
Kommentar vor Eintrag DAEMON_CONF entfernen und Config angeben: DAEMON_CONF="/etc/hostapd/hostapd.conf"

/etc/dhcpcd.conf bearbeiten. Am Ende folgendes einfügen:

interface wlan0
static ip_address=192.168.50.1/24
nohook wpa_supplicant

Hostapd Service starten: sudo service hostapd start

Und um ganz sicher zu gehen: Neustart.

DNS

Zur Adressvergabe im Netzwerk des Access Point verwende ich dnsmasq.

Quellen:

interface=wlan0
dhcp-range=192.168.50.50,192.168.50.100,255.255.255.0,infinite

sudo service dnsmasq start

Bisher hat aber auch der zweite RPi bei der Anmeldung immer die gleiche IP erhalten (obwohl die Vergabe nach dieser Konfiguration dynamisch erfolgt). Falls es hier zukünftig Probleme gibt, ließe sich das vermutlich am einfachsten durch die Vergabe fester IPs lösen.

Internet Routing für Peers erlauben

Um auch zukünftig Software auf den über den Access Point verbundenen Geräten installieren bzw. aktualisieren zu können, habe ich den Internetzugang über Einträge in iptables durchgeschleust.

Meinen ersten Ansatz, eine Netzwerkbrücke einzurichten, musste ich verwerfen, da das nur möglich ist, wenn einer der Adapter in der Brücke kabelgebunden, also kein WLAN-Adapter ist. Eine Brücke zwischen zwei WLAN-Adaptern ist nur möglich, wenn beide im 4-Adressen-Modus (AP = Access Point) betrieben werden können. Das ließe sich unter Linux, soweit ich das verstehe, auch so einrichten, aber dann würde es vermutlich an der Verbindung zu meinem Router scheitern.

IPv4-Paketweiterleitung erlauben in /etc/sysctl.conf:

  • Kommentar vor Eintrag net.ipv4.ip_forward=1 entfernen.
  • Neustart.

Über die Kommandozeile habe ich folgende iptables-Einträge vorgenommen:

sudo iptables -t nat -A POSTROUTING -o wlan1 -j MASQUERADE
sudo iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A FORWARD -i wlan0 -o wlan1 -j ACCEPT

wlan0 ist der Onboard-Adapter über den sich die Peers verbinden. wlan1 ist der externe Adapter über den der RPi sich mit dem WLAN-Router verbindet.

Regeln speichern: iptables-save > /etc/iptables/rules.v4

Regeln automatisch bei Neustart wiederherstellen: sudo apt-get install iptables-persistent

Peer verbinden

Auf dem zweiten Raspberry Pi konnte ich nun die Verbindung zum Access Point einrichten.

Netzwerk zu wpa_supplicant.conf hinzufügen:

sudo -i
wpa_passphrase "WLAN-NAME" "WLAN-PASSWORT" >> /etc/wpa_supplicant/wpa_supplicant.conf
exit

Dadurch wird das Passwort verschlüsselt eingetragen.

Dann /etc/wpa_supplicant/wpa_supplicant.conf bearbeiten:

  • Auskommentiertes Klartext-Passwort entfernen
  • Priorität festlegen

Jedem Eintrag kann eine Prioriät gegeben werden. Default ist 0, höhere Werte werden bevorzugt.
Beispiel: priority=1
So kann die direkte Verbindung zum WLAN-Router noch als Fallback dienen.

Zum Testen haben sich folgende Befehle als praktisch erwiesen:

  • Info: wpa_cli -i wlan0 status
  • Verbindung trennen: wpa_cli -i wlan0 disconnect
  • Verbindung wiederherstellen: wpa_cli -i wlan0 reconnect

Erweiterung von CameraCtrl um Schwarm

Repository: CameraCtrl Repository

Nun ging es an die Erweiterung der Software.

Geräte automatisch finden

Mir stellte sich die Frage, wie ich auf RPi Nr. 1 automatisiert herausfinden könnte, welche Geräte über den Access Point zu ihm verbunden sind.

Ich habe mich für sudo arp -n -i wlan0 entschieden (nur Unix).

Das sieht dann z.B. so aus (hier mit nur einem Gerät in der Tabelle):

Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.50.57            ether   d8:3a:dd:82:36:ba   C                     wlan0

Da sich IPs ändern können, identifiziere ich die Geräte über ihre MAC-Adresse.

Über einen regulären Ausdruck werte ich die Tabelle in Python aus:

import re
from subprocess import getoutput

device_list = dict()
arp_output = getoutput("sudo arp -n -i wlan0")  
ip_exp = r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"  
hex_exp = r"[0-9a-fA-F]{2}"  
macaddr_exp = f"({hex_exp}:{hex_exp}:{hex_exp}:{hex_exp}:{hex_exp}:{hex_exp})"  
arp_list = re.findall(f"{ip_exp}.*{macaddr_exp}", arp_output)  
for ip, mac in arp_list:  
    device = {"ip": ip}  
    device_list[mac] = device

Nun wollte ich noch sicherstellen, dass das gefundene Gerät auch antwortet. Das habe ich über ping umgesetzt:

from subprocess import call

for mac_address, device in device_list.items():  
    packet_count_param = '-n' if platform.lower() == 'win32' else '-c'  
    ip = device['ip']  
    available = call(['ping', packet_count_param, '1', ip]) == 0

Im Nachhinein betrachtet hätte ich dafür vielleicht auch direkt über die CameraCtrl-API gehen und eine Status-Anfrage zum Testen der Erreichbarkeit stellen können. Dann hätte ich gleich sichergestellt, dass CameraCtrl auf dem Gerät läuft und der Zugriff korrekt eingerichtet ist. Der Vorteil von ping ist hingegen, dass es so niedrigschwellig ist. Wenn das schon nicht klappt, brauche ich gar nicht erst auf höherer Ebene nach dem Fehler suchen, sondern weiß dass die grundlegende Netzwerkkommunikation nicht passt.
Eine Status-Anfrage habe ich übrigens später aus anderen Gründen noch in die API eingebaut.

Entwicklungsumgebung mit Localhost

Nachdem das obige funktionierte und ich die Informationen auf einer Seite im Browser anzeigen konnte, reichte mir das erstmal als Beweis, dass meine Idee grundsätztlich umsetzbar sein würde. Daher wechselte ich für den größten Teil der Software-Erweiterung auf meine bequemere Windows-Entwicklungsumgebung. Da ich dort natürlich keinen Access Point mit zusätzlichen Geräten einrichten oder simulieren wollte und auch der arp-Befehl nicht zur Verfügung steht, habe ich mir mit Dummy-Geräten beholfen:

device_list['aa:aa:aa:aa:aa:aa'] = {"ip": "127.0.0.1"}  # localhost
device_list['00:00:00:00:00:00'] = {"ip": "999.999.999.999"}  # nicht erreichbar

Erst als ein Großteil der Entwicklung abgeschlossen war, habe ich wieder zur echten Hardware gewechselt. Da gab es dann nur noch ein paar Probleme zu lösen, die in meiner Windows-Umgebung so nicht aufgetreten sind und ein paar Ergänzungen an die ich nicht gedacht hatte.

Self

Damit auch die Camera, über die der Schwarm verwaltet wird über die Liste gesteuert werden kann, habe ich ein Gerät namens self aufgenommen. Für den Zugriff habe ich dann jeweils eine Sonderbehandlung eingebaut, da die Funktionalität ja direkt intern zur Verfügung steht und ich die Extrarunde über localhost in diesem Fall vermeiden wollte (das wäre wohl nicht wirklich nötig gewesen, aber war auch nicht so viel Aufwand).

Umbau und Wiederverwendung

Als neuer Baustein vom CameraCtrl kam die Klasse DeviceList hinzu. Dort landete auch das weiter oben beschriebene Finden und Anpingen der Geräte. Durch eine kleine Anpassung der Config-Klasse konnte ich die DeviceList von dieser erben lassen und hatte somit ziemlich schnell die JSON-Persistenz der Geräte-Liste geregelt.

Im Wesentlichen geht es beim CameraCtrl-Schwarm ja darum, dass CameraCtrl selbst als Client für andere (untergeordnete) CameraCtrl-Server fungiert (und das dann selbst wieder nach außen anbietet).
Und die bestehende CameraCtrlClient-Klasse machte bereits genau das. Sie war aber doch etwas spezialisiert auf die Verwendung auf dem Endgerät (meinem Windows-Rechner) auf dem die Bilder am Ende landen sollen. Daher entschied ich mich, allgemein verwendbare Funktionalität in eine Klasse CameraAccess herauszuziehen von der CameraCtrlClient dann erbte.

In der DeviceList wird bei einer Anfrage an ein Gerät dann ad hoc ein Wegwerf-CameraAccess angelegt. Da das Anlegen nicht besonders kostspielig ist, konnte ich mir so das Verwalten von CameraAccess-Objekten sparen.

Die Geräte werden über ihre Mac-Adresse eindeutig identifiziert. Für manche Zwecke (insbesondere das Ablegen von gerätespezifischen Dateien) habe ich zudem eine device_id eingeführt, die jedoch der Mac-Adresse entspricht, nur dass : durch _ ersetzt ist (: ist in Datei- und Ordnernamen nicht erlaubt). Die API-Zugriffen habe ich so gestaltet, dass immer beides zulässig ist.

Schwarm-Übersicht

Kern der Erweiterung ist die Schwarm-Übersichtsseite /swarm für den Browser auf der die Geräte verwaltet werden können.

Für die Seite habe ich wieder ein Template (swarm.html) angelegt und auch sonst folgt mein Vorgehen wieder weitgehend dem was ich bereits für die Index-Seite (/) des einzelnen Geräts gemacht habe (siehe CameraCtrl oder direkt im Repository). Für das Styling habe ich CSS-Klassen definiert und das globale style.css erweitert.

Als globale Optionen habe ich vier Buttons eingebaut:

  • Aktualisieren: Suche nach neuen Geräten und erneute Verbindungsversuche bei nicht erreichbaren
  • Pausieren aller Kamera-Timer
  • Fortsetzen aller Kamera-Timer
  • Alle Geräte herunterfahren

In der Geräte-Liste hat jedes Gerät seinen eigenen Kasten mit folgenden Optionen:

  • Es kann ein Gerätename vergeben werden, um das Identifizieren der Kamera zu erleichtern
  • Ein kleines Vorschau-Bild kann über einen Button erneuert werden. Ein Klick auf das Bild führt zur großen Vorschau.
  • Ein Button öffnet die Geräte-Einstellungen, sodass Port und Bearer-Token für den Zugriff auf das Gerät eingegeben werden können
  • Der Button Kamera-Einstellungen führt zur altbekannten Übersichtsseite für das jeweilige Gerät
  • Pausieren/Fortsetzen/Statusabfrage: je nach Status der Kamera steht der entsprechende Button zur Verfügung (letzterer falls der Status unbekannt ist)
  • Gerät ignorieren, es wird dann bei (fast) allen Aktionen übersprungen und landet auf der Ignore-List

In der Ignore-List gibt es folgende Optionen:

  • Nicht mehr ignorieren
  • Gerät entfernen, was allerdings aktuell dazu führen würde, dass es bei einer erneuten Suche nach neuen Geräten wieder auftaucht. Das stört mich gerade nicht wirklich. Lösbar wäre das z.B. durch eine interne Liste der entfernten Geräte, die dann manuell über die Kommandozeile oder irgendwo versteckt bearbeitet werden könnte.

Neue Schwarm-Routen

/swarm/shots liefert (vergleichbar zu /shots) Listen mit den verfügbaren Bildern aller Geräte, jeweils unter der entsprechenden Mac-Adresse.

Folgende Routen funktionieren als Weiterleitungen auf ein spezifisches Gerät:

  • /swarm/<device_id>/shots
  • /swarm/<device_id>/shots/<file_name> (Herunterladen, Löschen)

Neu hinzugekommen:

  • /swarm/<device_id>/status: Statusinformationen zum jeweiligen Gerät (enthält aktuell nur die Info, ob die Kamera läuft oder pausiert ist)
  • /swarm/<device_id>/toggle_pause: Erlaubt das Umschalten des Kamera-Timers von pausiert zu laufend bzw. umgekehrt.

Index

Über /swarm/<device_id>/ ist die jeweilige Kamera-Übersichts- bzw. Einstellungs-Seite erreichbar. Dabei musste ich im HTML das Vorschaubild patchen, da es sonst auf das Vorschaubild der Schwarm-Kamera zeigen würde:

preview_path = self.device_list.preview_path(mac_address)
preview_path = preview_path.relative_to(Path(__file__).parent / 'static')  
preview_path = preview_path.as_posix()  
preview_regexp = r'(<img [^>]* src="/)(.*preview\.jpg)(".*/>)'  
text = re.sub(preview_regexp, f'\\1{preview_path}\\3', device_response.text, flags=re.MULTILINE)

Und natürlich muss im Falle eines Preview-Update das Bild auf dem Schwarm-Gerät aktualisiert werden:

if 'update' in request.form:  
    access.update_preview()  
    return redirect(f'/swarm/{device_id}/')

Preview-Größe

Die /preview-Route (und somit auch /swarm/<device_id>/preview) unterstützt nun das optionale Parameter width über den die Bildbreite in Pixeln angegeben werden kann. Das Preview wird dann vor dem Versenden auf die entsprechende Größe herunterskaliert. So geht das Anzeigen der Vorschaubilder in der Schwarm-Übersicht schneller (da hier dann nur kleine Bildchen vom jeweiligen Gerät übertragen werden).

from PIL import Image

def downsized_image(file_path: Path, max_width: int) -> Image:  
    image = Image.open(file_path)  
    if image.width > max_width:  
        height = math.floor(image.height * max_width / image.width)  
        image = image.resize((max_width, height))  
  
    return image

Client

Im Client musste gar nicht mehr so viel angepasst werden. Beim Synchronisieren der Bilder musste ich nun nur jeweils die Mac-Adresse mit berücksichtigen. Zudem habe ich der UI einen ToggleButton verpasst, der erlaubt von allen Geräten zu Synchronisieren oder auch nur von der Schwarm-Kamera selbst. Die Bilder werden jetzt nach device_id in Unterordnern des Zielordners abgelegt, damit es nicht zu Namenskonflikten kommt.

Für die Zukunft wäre es noch schön, statt des einen großen Preview-Bildes in der Client-UI ebenfalls die Schwarm-Previews anzeigen zu können.

Außerdem wäre ein Timelapse-Editor schön, bei dem die Bilder der jeweiligen Geräte ausgewählt werden können. Aber das wäre wohl ein etwas größeres Unterfangen, mit dem sich die Sache schon so langsam einer Videoschnitt-Software annähern würde. Ob es sinnvoll ist soetwas nachzuprogrammieren sei mal dahingestellt, aber ein bisschen juckt es mich schon in den Fingern.

Hier verstecken sich die Spione:

Man sieht am WLAN-Adapter, dass alles höchst professionell verkabelt wurde. Immerhin geht das Fenster noch ohne Kollision auf und ich habe Empfang. Die neue Kamera lebt aktuell hinter dem Schrank, aber sowohl Halterung als auch Position möchte ich noch anpassen.