10.13. Caricamento dei frame attivati sul cloud

Quando viene rilevato del movimento, la camera ora illumina la dashboard. Per l’uso dal vivo questo basta, ma il proprietario desidera anche un archivio permanente di ogni frame attivato, conservato da qualche parte all’esterno della camera. Si tratta di una chiamata HTTP in uscita, con la camera che agisce da client.

10.13.1. La camera come client

Il modulo requests è il client HTTP in uscita della camera. La sua interfaccia è una copia deliberata di requests di CPython: le stesse funzioni del modulo che prendono il nome dai verbi, gli stessi argomenti keyword files=, json=, headers=, auth=. Se hai già effettuato chiamate HTTP da CPython, conosci già l’API:

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() apre una connessione TCP, invia la richiesta e restituisce un oggetto Response con status_code, reason, headers, content, json() e le altre proprietà familiari.

files={...} costruisce un corpo multipart/form-data. Il valore è una tupla (filename, file-like); requests.post() legge l’oggetto file-like a blocchi, in modo che l’intero JPEG non debba essere prima ribufferizzato in una stringa. io.BytesIO avvolge i byte JPEG già presenti in memoria, così da esporli con un’interfaccia di lettura come file.

headers={...} è un semplice dict che viene inviato come header della richiesta: qui, un bearer token nella posizione standard Authorization. Il fornitore dell’archivio documenta quale formato di token si aspetta; l’esempio mostra la forma più comune.

10.13.2. Integrazione nel rilevatore di movimento

La coroutine del rilevatore di movimento introdotta in precedenza viene già eseguita a ogni nuovo frame e si attiva quando change > state['threshold']. Aggiungi qui il caricamento, ma avvialo come task in background in modo che il rilevatore non smetta di osservare mentre il caricamento è in corso:

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() pianifica la coroutine di caricamento e restituisce immediatamente. Il rilevatore continua ad acquisire frame; il caricamento procede in parallelo; la camera non si blocca mai.

10.13.3. Modalità di errore

Il codice di rete può fallire. La camera potrebbe essere offline, l’archivio potrebbe non essere disponibile, il bearer token potrebbe essere scaduto. Le categorie che vale la pena intercettare:

  • OSError – la connessione TCP non ha potuto essere aperta oppure è stata chiusa durante il trasferimento. Errore DNS, nessuna route, connessione resettata. requests solleva esattamente questa eccezione.

  • status_code >= 400 – il server ha ricevuto la richiesta e l’ha rifiutata. 401 per un token scaduto, 403 per uno revocato, 413 per un corpo troppo grande, 5xx per l’archivio non integro.

  • Timeout silenzioso – requests utilizza un timeout di socket predefinito (pochi secondi); superato questo, solleva OSError con errno.ETIMEDOUT.

Per un archivio che conta davvero, accoderesti i frame rifiutati in /sdcard/pending/ e riproveresti con un ciclo più lento: si tratta di poche righe in più per ogni caso, oltre a quanto mostrato.

10.13.4. Cosa requests non fa

Il port per MicroPython è volutamente ridotto. Alcune cose che requests di CPython fa e che questo non fa:

  • Pooling delle connessioni. Ogni chiamata apre una nuova connessione TCP.

  • Tentativi automatici in caso di errori transitori. Devi avvolgere tu stesso la chiamata.

  • Risposte in streaming. r.content viene letto interamente in RAM; non esiste un equivalente di stream=True.

  • Decompressione automatica delle risposte compresse con gzip. Imposta l’header Accept-Encoding esplicitamente solo se il server è configurato per gestirla.

Consulta requests — Client HTTP per l’elenco completo dei metodi e per ciò che rientra o meno nell’ambito supportato.

Lo HTTPS funziona subito senza configurazione: è lo schema dell’URL a determinarlo e il contesto SSL predefinito viene creato al volo. Per verificare il certificato dell’archivio rispetto a un bundle di CA che hai caricato sulla camera, consulta la sezione as a client di Verificare un server pubblico (camera come client).

L’applicazione è ora completa: anteprima dal vivo, rilevamento del movimento, dashboard con login, HTTPS, CORS/CSRF, archivio sul cloud.