10.13. Enviando quadros disparados para a nuvem

Quando o movimento dispara, a câmera agora acende o painel. Isso é suficiente para uso ao vivo, mas o proprietário também quer um arquivo permanente de cada quadro disparado, armazenado em algum lugar fora da câmera. Isso é uma chamada HTTP de saída – a câmera atuando como um cliente.

10.13.1. A câmera como cliente

O módulo requests é o cliente HTTP de saída da câmera. Sua interface é uma cópia proposital do requests do CPython – as mesmas funções de módulo nomeadas por verbos, os mesmos argumentos nomeados files=, json=, headers=, auth=. Se você já fez chamadas HTTP a partir do CPython, já conhece a 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 uma conexão TCP, envia a requisição e retorna uma Response com status_code, reason, headers, content, json() e o restante das propriedades familiares.

files={...} constrói um corpo multipart/form-data. O valor é uma tupla (filename, file-like); requests.post() lê o objeto file-like em blocos para que todo o JPEG não tenha que ser rebufferizado em uma string primeiro. io.BytesIO envolve os bytes JPEG já existentes na memória para que exponham uma interface de leitura como arquivo.

headers={...} é um dict direto que é enviado como cabeçalhos da requisição – aqui, um token bearer na posição Authorization padrão. O provedor de arquivamento documenta o formato de token que deseja; o exemplo mostra a forma mais comum.

10.13.2. Conectando ao detector de movimento

A corrotina de detecção de movimento apresentada anteriormente já roda a cada novo quadro e dispara quando change > state['threshold']. Adicione o envio ali, mas dispare-o como uma tarefa em segundo plano para que o detector não pare de observar enquanto o envio está em andamento:

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() agenda a corrotina de envio e retorna imediatamente. O detector continua capturando quadros; o envio roda em paralelo a ele; a câmera nunca trava.

10.13.3. Modos de falha

Código de rede falha. A câmera pode estar offline, o arquivo pode estar fora do ar, o token bearer pode ter expirado. As categorias que vale a pena capturar:

  • OSError – a conexão TCP não pôde ser aberta ou foi fechada no meio da transferência. Falha de DNS, sem rota, conexão resetada. requests lança exatamente essa exceção.

  • status_code >= 400 – o servidor recebeu a requisição e a rejeitou. 401 para um token expirado, 403 para um revogado, 413 para um corpo grande demais, 5xx para o arquivo estar com problemas.

  • Timeout silencioso – requests usa um timeout de socket padrão (alguns segundos); passado isso, ele lança OSError com errno.ETIMEDOUT.

Para um arquivo que realmente importa, você enfileiraria os quadros rejeitados em /sdcard/pending/ e os tentaria novamente em um loop mais lento – isso são mais algumas linhas por caso, além do que está mostrado.

10.13.4. O que o requests não faz

A versão para MicroPython é propositalmente pequena. Algumas coisas que o requests do CPython faz e que esta versão não faz:

  • Pool de conexões. Cada chamada abre uma nova conexão TCP.

  • Novas tentativas automáticas em erros transitórios. Envolva a chamada você mesmo.

  • Respostas em streaming. r.content é lido inteiramente na RAM; não há equivalente a stream=True.

  • Descompressão automática de respostas com gzip. Defina o cabeçalho Accept-Encoding explicitamente apenas se o servidor estiver configurado para isso.

Veja requests — Cliente HTTP para a lista completa de métodos e o que está dentro ou fora do escopo.

HTTPS funciona imediatamente – o esquema da URL o aciona, e o contexto SSL padrão é criado dinamicamente. Para verificar o certificado do arquivo contra um pacote de CA que você carregou na câmera, veja a seção como cliente de Verificando um servidor público (câmera como cliente).

O aplicativo está totalmente concluído: pré-visualização ao vivo, detecção de movimento, painel com login, HTTPS, CORS/CSRF, arquivamento na nuvem.