10.13. Încărcarea cadrelor declanșate în cloud

Când mișcarea declanșează, camera aprinde acum tabloul de bord. Acest lucru este suficient pentru utilizarea în direct, dar proprietarul dorește și o arhivă permanentă a fiecărui cadru declanșat, stocată undeva în afara camerei. Acesta este un apel HTTP de ieșire – camera acționează ca un client.

10.13.1. Camera ca client

Modulul requests este clientul HTTP de ieșire al camerei. Suprafața sa este o copie deliberată a modulului requests din CPython – aceleași funcții de modul denumite după verbe, aceleași argumente cu cuvânt-cheie files=, json=, headers=, auth=. Dacă ai făcut apeluri HTTP din CPython, cunoști deja API-ul:

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() deschide o conexiune TCP, trimite cererea și returnează un Response cu status_code, reason, headers, content, json() și restul proprietăților familiare.

files={...} construiește un corp multipart/form-data. Valoarea este un tuplu (filename, file-like); requests.post() citește obiectul de tip fișier în bucăți, astfel încât întregul JPEG să nu trebuiască să fie re-tamponat mai întâi într-un șir de caractere. io.BytesIO încapsulează octeții JPEG deja prezenți în memorie, astfel încât aceștia să expună o interfață de citire ca fișier.

headers={...} este un dicționar simplu care este trimis ca anteturi de cerere – aici, un token bearer în poziția standard Authorization. Furnizorul arhivei documentează formatul de token pe care îl dorește; exemplul este forma cea mai comună.

10.13.2. Conectarea în detectorul de mișcare

Coroutina detectorului de mișcare introdusă mai devreme rulează deja la fiecare cadru nou și se declanșează când change > state['threshold']. Adaugă încărcarea acolo, dar declanșeaz-o ca sarcină de fundal, astfel încât detectorul să nu se oprească din supraveghere în timp ce încărcarea este în desfășurare:

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() programează coroutina de încărcare și revine imediat. Detectorul continuă să captureze cadre; încărcarea rulează în paralel cu el; camera nu se blochează niciodată.

10.13.3. Moduri de eșec

Codul de rețea eșuează. Camera ar putea fi offline, arhiva ar putea fi indisponibilă, tokenul bearer ar putea să fi expirat. Categoriile care merită prinse:

  • OSError – conexiunea TCP nu a putut fi deschisă sau a fost închisă în timpul transferului. Eșec DNS, lipsă de rută, conexiune resetată. requests ridică exact această excepție.

  • status_code >= 400 – serverul a primit cererea și a respins-o. 401 pentru un token expirat, 403 pentru unul revocat, 413 pentru un corp prea mare, 5xx pentru o arhivă nesănătoasă.

  • Expirare silențioasă – requests folosește o expirare implicită a socketului (câteva secunde); după aceea ridică OSError cu errno.ETIMEDOUT.

Pentru o arhivă care contează cu adevărat, ai pune în coadă cadrele respinse în /sdcard/pending/ și ai reîncerca într-o buclă mai lentă – asta înseamnă câteva linii în plus pentru fiecare caz, pe lângă ceea ce este prezentat.

10.13.4. Ce nu face requests

Portarea pentru MicroPython este în mod deliberat mică. Câteva lucruri pe care requests din CPython le face, dar acesta nu:

  • Gruparea conexiunilor (connection pooling). Fiecare apel deschide o nouă conexiune TCP.

  • Reîncercări automate la erori tranzitorii. Învelește apelul tu însuți.

  • Răspunsuri în flux (streaming). r.content este citit integral în RAM; nu există un echivalent stream=True.

  • Decompresia automată a răspunsurilor comprimate cu gzip. Setează anteturul Accept-Encoding explicit doar dacă serverul este configurat pentru asta.

Vezi requests — Client HTTP pentru lista completă de metode și ce intră sau nu în domeniul de aplicare.

HTTPS funcționează imediat – schema URL îl determină, iar contextul SSL implicit este creat din mers. Pentru verificarea certificatului arhivei față de un pachet CA pe care l-ai încărcat pe cameră, vezi secțiunea camera ca client din Verificarea unui server public (camera ca client).

Aplicația este complet livrată: previzualizare în direct, detectare a mișcării, tablou de bord cu autentificare, HTTPS, CORS/CSRF, arhivă în cloud.