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.