10.13. Subir fotogramas activados a la nube

Cuando el movimiento se dispara, la cámara ahora enciende el panel de control. Eso es suficiente para el uso en vivo, pero el propietario también quiere un archivo permanente de cada fotograma activado, almacenado en algún lugar fuera de la cámara. Eso es una llamada HTTP saliente: la cámara actuando como cliente.

10.13.1. La cámara como cliente

El módulo requests es el cliente HTTP saliente de la cámara. Su superficie es una copia deliberada del requests de CPython: las mismas funciones de módulo con nombre de verbo, los mismos argumentos por palabra clave files=, json=, headers= y auth=. Si has hecho llamadas HTTP desde CPython, ya conoces la 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() abre una conexión TCP, envía la solicitud y devuelve un Response con status_code, reason, headers, content, json() y el resto de las propiedades familiares.

files={...} construye un cuerpo multipart/form-data. El valor es una tupla (filename, file-like); requests.post() lee el objeto de tipo archivo por fragmentos, de modo que no haga falta volver a almacenar en búfer todo el JPEG en una cadena primero. io.BytesIO envuelve los bytes JPEG ya cargados en memoria para que expongan una interfaz de lectura como archivo.

headers={...} es un diccionario directo que se envía como cabeceras de la solicitud; aquí, un token de portador (bearer) en la posición estándar Authorization. El proveedor del archivo documenta qué formato de token desea; el ejemplo es la forma más común.

10.13.2. Conectarlo al detector de movimiento

La corrutina del detector de movimiento presentada antes ya se ejecuta en cada nuevo fotograma y se dispara cuando change > state['threshold']. Añade la subida ahí, pero dispárala como tarea en segundo plano para que el detector no deje de vigilar mientras la subida está en curso:

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() programa la corrutina de subida y retorna de inmediato. El detector sigue capturando fotogramas; la subida se ejecuta junto a él; la cámara nunca se detiene.

10.13.3. Modos de fallo

El código de red falla. La cámara puede estar sin conexión, el archivo puede estar caído, el token de portador puede haber caducado. Las categorías que vale la pena capturar:

  • OSError – no se pudo abrir la conexión TCP o se cerró a mitad de la transferencia. Fallo de DNS, sin ruta, conexión reiniciada. requests lanza exactamente esta excepción.

  • status_code >= 400 – el servidor recibió la solicitud y la rechazó. 401 por un token caducado, 403 por uno revocado, 413 por un cuerpo demasiado grande, 5xx por que el archivo no está en buen estado.

  • Tiempo de espera silencioso – requests usa un tiempo de espera de socket predeterminado (unos pocos segundos); pasado ese límite lanza OSError con errno.ETIMEDOUT.

Para un archivo que realmente importa, encolarías los fotogramas rechazados en /sdcard/pending/ y reintentarías en un bucle más lento: eso son unas pocas líneas más por caso, además de lo que se muestra.

10.13.4. Lo que requests no hace

El port a MicroPython es deliberadamente pequeño. Algunas cosas que el requests de CPython hace y que este no:

  • Agrupación de conexiones (connection pooling). Cada llamada abre una nueva conexión TCP.

  • Reintentos automáticos ante errores transitorios. Envuelve la llamada tú mismo.

  • Respuestas en streaming. r.content se lee por completo en la RAM; no hay un equivalente a stream=True.

  • Descompresión automática de respuestas comprimidas con gzip. Establece la cabecera Accept-Encoding explícitamente solo si el servidor está configurado para ello.

Consulta requests — Cliente HTTP para ver la lista completa de métodos y qué entra y qué queda fuera de alcance.

HTTPS funciona sin más configuración: el esquema de la URL lo controla, y el contexto SSL predeterminado se crea sobre la marcha. Para verificar el certificado del archivo contra un paquete de CA que hayas cargado en la cámara, consulta la sección como cliente de Verificación de un servidor público (la cámara como cliente).

La aplicación está completamente entregada: vista previa en vivo, detección de movimiento, panel de control con inicio de sesión, HTTPS, CORS/CSRF, archivo en la nube.