10.13. Carregar fotogramas despoletados para a nuvem

Quando é detetado movimento, a câmara ilumina agora o painel de controlo. Isso é suficiente para utilização em tempo real, mas o proprietário também quer um arquivo permanente de cada fotograma despoletado, armazenado fora da câmara. Trata-se de uma chamada HTTP de saída – a câmara a atuar como cliente.

10.13.1. A câmara como cliente

O módulo requests é o cliente HTTP de saída da câmara. A sua superfície é uma cópia deliberada do requests do CPython – as mesmas funções do módulo com nomes de verbos HTTP, os mesmos argumentos de palavra-chave files=, json=, headers=, auth=. Se 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 ligação TCP, envia o pedido e devolve um Response com status_code, reason, headers, content, json(), e as restantes propriedades familiares.

files={...} constrói um corpo multipart/form-data. O valor é um tuplo (filename, file-like); requests.post() lê o objeto file-like em partes para que o JPEG completo não tenha de ser re-armazenado numa string primeiro. io.BytesIO envolve os bytes JPEG já em memória para que estes exponham uma interface de leitura como ficheiro.

headers={...} é um dicionário simples que é enviado como cabeçalhos do pedido – aqui, um token bearer na posição Authorization padrão. O fornecedor de arquivo documenta o formato de token que pretende; o exemplo é a forma mais comum.

10.13.2. Integração no detetor de movimento

A corrotina do detetor de movimento introduzida anteriormente já é executada em cada novo fotograma e é ativada quando change > state['threshold']. Adicione o carregamento aí, mas ative-o como uma tarefa em background para que o detetor não pare de vigiar enquanto o carregamento está em 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() agenda a corrotina de carregamento e retorna imediatamente. O detetor continua a capturar fotogramas; o carregamento é executado em paralelo; a câmara nunca para.

10.13.3. Modos de falha

O código de rede falha. A câmara pode estar offline, o arquivo pode estar indisponível, o token bearer pode ter expirado. As categorias que vale a pena tratar:

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

  • status_code >= 400 – o servidor recebeu o pedido e rejeitou-o. 401 para um token expirado, 403 para um token revogado, 413 para um corpo demasiado grande, 5xx para o arquivo em mau estado.

  • Timeout silencioso – requests utiliza um timeout de socket predefinido (alguns segundos); ultrapassado esse limite, lança OSError com errno.ETIMEDOUT.

Para um arquivo verdadeiramente importante, colocaria os fotogramas rejeitados em fila em /sdcard/pending/ e tentaria novamente num ciclo mais lento – são mais algumas linhas por caso, para além do que é mostrado.

10.13.4. O que o requests não faz

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

  • Pooling de ligações. Cada chamada abre uma nova ligação TCP.

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

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

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

Consulte requests — cliente HTTP para a lista completa de métodos e o que está dentro e fora do âmbito.

HTTPS funciona de imediato – o esquema do URL controla isso, e o contexto SSL predefinido é criado dinamicamente. Para verificar o certificado do arquivo em relação a um pacote de AC carregado na câmara, consulte a secção como cliente de Verificar um servidor público (câmara como cliente).

A aplicação está completamente pronta: pré-visualização em tempo real, deteção de movimento, painel de controlo com autenticação, HTTPS, CORS/CSRF, arquivo na nuvem.