10.13. Ausgelöste Einzelbilder in die Cloud hochladen

Wenn Bewegung erkannt wird, leuchtet das Dashboard der Kamera jetzt auf. Für den Live-Einsatz reicht das aus, aber der Besitzer möchte zusätzlich ein dauerhaftes Archiv jedes ausgelösten Einzelbildes, das irgendwo außerhalb der Kamera gespeichert wird. Das ist ein ausgehender HTTP-Aufruf – die Kamera tritt als Client auf.

10.13.1. Die Kamera als Client

Das Modul requests ist der ausgehende HTTP-Client der Kamera. Seine Schnittstelle ist eine bewusste Nachbildung von CPythons requests – dieselben nach HTTP-Verben benannten Modulfunktionen, dieselben Schlüsselwortargumente files=, json=, headers=, auth=. Wenn Sie schon einmal HTTP-Aufrufe aus CPython gemacht haben, kennen Sie die API bereits:

import requests
import io

ARCHIVE_URL = 'https://api.backyard-cloud.com/frames'
ARCHIVE_TOKEN = load_archive_token()

async def archive_frame(jpeg, ts):
    try:
        r = requests.post(
            ARCHIVE_URL,
            files={'image': (
                'frame-{}.jpg'.format(ts),
                io.BytesIO(jpeg),
            )},
            headers={'Authorization': 'Bearer ' + ARCHIVE_TOKEN},
        )
    except OSError as e:
        print('upload failed:', e)
        return False
    if r.status_code >= 400:
        print('archive rejected:', r.status_code, r.reason)
        return False
    return True

requests.post() öffnet eine TCP-Verbindung, sendet die Anfrage und gibt ein Response mit status_code, reason, headers, content, json() und den übrigen vertrauten Eigenschaften zurück.

files={...} erstellt einen multipart/form-data-Body. Der Wert ist ein (filename, file-like)-Tupel; requests.post() liest das dateiähnliche Objekt in Blöcken, sodass das gesamte JPEG nicht erst wieder in einen String gepuffert werden muss. io.BytesIO umhüllt die bereits im Speicher befindlichen JPEG-Bytes, sodass sie eine als-Datei-lesbare Schnittstelle bereitstellen.

headers={...} ist ein einfaches Dict, das als Anfrage-Header gesendet wird – hier ein Bearer-Token an der üblichen Authorization-Position. Der Archivanbieter dokumentiert, welches Token-Format er erwartet; das Beispiel zeigt die häufigste Form.

10.13.2. Einbindung in den Bewegungsmelder

Die zuvor eingeführte Koroutine des Bewegungsmelders läuft bereits bei jedem neuen Einzelbild und löst aus, wenn change > state['threshold']. Fügen Sie den Upload dort hinzu, starten Sie ihn aber als Hintergrund-Task, damit der Melder während des laufenden Uploads nicht aufhört zu beobachten:

async def motion_detector():
    global last_motion
    prev = None
    while True:
        await new_frame.wait()
        change = compute_change(prev, latest_jpeg)
        if change > state['threshold']:
            state['trigger_count'] += 1
            ts = int(time.time())
            last_motion = {'ts': ts,
                           'count': state['trigger_count'],
                           'change': change}
            motion_event.set()
            asyncio.create_task(archive_frame(latest_jpeg, ts))
        prev = latest_jpeg
        await asyncio.sleep_ms(50)

asyncio.create_task() plant die Upload-Koroutine ein und kehrt sofort zurück. Der Melder erfasst weiterhin Einzelbilder; der Upload läuft daneben; die Kamera bleibt nie hängen.

10.13.3. Fehlerfälle

Netzwerkcode schlägt fehl. Die Kamera könnte offline sein, das Archiv könnte ausgefallen sein, das Bearer-Token könnte abgelaufen sein. Die abzufangenden Kategorien:

  • OSError – die TCP-Verbindung konnte nicht geöffnet werden oder wurde mitten in der Übertragung geschlossen. DNS-Fehler, keine Route, Verbindungsabbruch. requests löst genau diese Ausnahme aus.

  • status_code >= 400 – der Server hat die Anfrage empfangen und abgelehnt. 401 für ein abgelaufenes Token, 403 für ein widerrufenes, 413 für einen zu großen Body, 5xx, wenn das Archiv nicht gesund ist.

  • Stille Zeitüberschreitung – requests verwendet eine standardmäßige Socket-Zeitüberschreitung (einige Sekunden); darüber hinaus löst es OSError mit errno.ETIMEDOUT aus.

Für ein Archiv, auf das es wirklich ankommt, würden Sie abgelehnte Einzelbilder in /sdcard/pending/ einreihen und in einer langsameren Schleife erneut versuchen – das sind je nach Fall ein paar Zeilen mehr, zusätzlich zum Gezeigten.

10.13.4. Was requests nicht tut

Der MicroPython-Port ist bewusst klein gehalten. Einige Dinge, die CPythons requests tut, dieser hier aber nicht:

  • Connection-Pooling. Jeder Aufruf öffnet eine neue TCP-Verbindung.

  • Automatische Wiederholungen bei vorübergehenden Fehlern. Umhüllen Sie den Aufruf selbst.

  • Streaming von Antworten. r.content wird vollständig in den RAM gelesen; es gibt kein Äquivalent zu stream=True.

  • Automatische Dekomprimierung von gzip-komprimierten Antworten. Setzen Sie den Accept-Encoding-Header nur dann explizit, wenn der Server entsprechend konfiguriert ist.

Die vollständige Liste der Methoden und was innerhalb / außerhalb des Geltungsbereichs liegt, finden Sie unter requests — HTTP-Client.

HTTPS funktioniert ohne weiteres Zutun – das URL-Schema steuert es, und der standardmäßige SSL-Kontext wird spontan erstellt. Um das Zertifikat des Archivs gegen ein CA-Bundle zu überprüfen, das Sie auf die Kamera geladen haben, lesen Sie den Abschnitt als Client von Einen öffentlichen Server verifizieren (Kamera als Client).

Die App ist vollständig ausgeliefert: Live-Vorschau, Bewegungserkennung, Dashboard mit Login, HTTPS, CORS/CSRF, Cloud-Archiv.