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:
- Step-by-Step-Tutorial mit wenig Erläuterung
- Etwas mehr Infos zu hostapd
- Bessere Anleitung, Info zum permanenten Anlegen von
iptables
-Regeln - Mehr zum permanenten Anlegen von
iptables
-Regeln
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:
-
sudo apt-get install dnsmasq
-
sudo service dnsmasq stop
-
sudo vim /etc/dnsmasq.conf
am Anfang oder Ende einfügen:
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.