11.4. Publicidade e pesquisa

Dois dispositivos BLE que nunca se encontraram antes têm primeiro de se descobrir mutuamente. A ligação em rede resolve isso atribuindo a cada dispositivo um endereço de um conjunto partilhado e permitindo que qualquer lado alcance o outro através de routers. O BLE não tem routers, nenhum conjunto partilhado e – entre a maioria dos pares de dispositivos – absolutamente nenhuma relação prévia. O Generic Access Profile (GAP) resolve a descoberta com um padrão de difusão-e-escuta. Um lado anuncia-se – transmite um pacote curto nos três canais de publicidade a intervalos regulares, descrevendo quem é. O outro lado pesquisa – varre os mesmos três canais à escuta desses pacotes.

O GAP define quatro funções em torno desse padrão, cada uma sendo uma combinação específica de publicidade e escuta.

11.4.1. As quatro funções GAP

A two-by-two matrix. Rows are labelled "advertises" and "does not advertise". Columns are labelled "accepts connections" and "does not accept connections". The four cells contain the role names: Peripheral, Broadcaster, Central, Observer.

As quatro funções GAP. O eixo vertical indica se o dispositivo anuncia; o eixo horizontal indica se aceita (ou inicia) ligações.

  • Um periférico anuncia pacotes que dizem «estou aqui e pode ligar-se a mim». Quando outro dispositivo abre uma ligação, o periférico para de anunciar e começa a responder a pedidos GATT. Cintas de frequência cardíaca, termómetros e a maioria das câmaras-como-sensores atuam como periféricos.

  • Um central pesquisa periféricos, escolhe um e inicia uma ligação. Após a ligação, fala GATT como cliente. Telemóveis, portáteis e câmaras que atuam como coletores de dados são centrais.

  • Um difusor anuncia mas nunca aceita ligações. A sua carga útil de publicidade é os dados – não há nada a que se ligar. Os iBeacons e a maioria dos beacons de presença em lojas são difusores.

  • Um observador pesquisa esses anúncios e lê a carga útil, também sem nunca ligar. Uma câmara que escuta beacons próximos e age com base no que ouve é um observador.

Um único dispositivo pode desempenhar mais de uma função ao mesmo tempo – uma câmara pode ser um periférico que publica o seu próprio estado e um central que se liga a um sensor próximo. O rádio multiplexa o trabalho.

11.4.2. O que um pacote de publicidade contém

Um pacote de publicidade é pequeno: 31 bytes de carga útil, ou 62 se o anunciante publicar também uma resposta de pesquisa que os pesquisadores podem solicitar em tempo real. A carga útil é uma lista de campos curtos com tipo:

  • Flags. Conectável ou não, detectável geral / limitado.

  • Nome local. Uma cadeia de caracteres curta e legível por humanos – o nome que o sistema operativo de um telemóvel ou portátil mostra no seu menu Bluetooth.

  • UUIDs de serviço. Uma lista de identificadores de serviço GATT que o dispositivo aloja, para que um pesquisador possa reconhecer periféricos capazes sem primeiro ligar. Uma cinta de frequência cardíaca anuncia 0x180D – o UUID padrão do serviço Heart-Rate – e uma aplicação de frequência cardíaca no telemóvel sabe apenas com isso que vale a pena ligar-se ao dispositivo.

  • Aparência. Um valor de 16 bits da lista de números atribuídos do Bluetooth (sensor, multimédia genérico, relógio genérico, …) – uma dica para o central sobre o que mostrar.

  • Dados específicos do fabricante. Bytes de formato livre prefixados com um ID de empresa. Os iBeacons usam este campo para transportar o seu UUID, major e minor; as aplicações personalizadas podem colocar aqui o que quiserem.

As cargas úteis de publicidade são limitadas. O limite de 31 bytes torna a escolha do que incluir uma decisão de conceção real – um nome longo legível por humanos pode rapidamente não deixar espaço para UUIDs de serviço. A API aioble.advertise() aceita cada um destes como argumento de palavra-chave e monta os bytes por si, transbordando automaticamente para a resposta de pesquisa se o pacote principal ficar cheio.

11.4.3. Pesquisa ativa e passiva

Um pesquisador pode funcionar em modo passivo, onde escuta os pacotes de publicidade e analisa o que chega, ou em modo ativo, onde também envia um pedido de pesquisa a cada anunciante e analisa a resposta de pesquisa que recebe.

A pesquisa passiva vê apenas o pacote de publicidade inicial (até 31 bytes). A pesquisa ativa duplica isso – a resposta de pesquisa é mais 31 bytes que o periférico pode usar para campos que não couberam. A pesquisa ativa também tem custos de energia em ambos os lados, uma vez que o pesquisador transmite e o anunciante transmite um pacote extra, pelo que é uma escolha e não uma predefinição.

Na API aioble, active=True em aioble.scan() muda o modo, e cada ScanResult expõe os adv_data combinados mais resp_data bem como auxiliares como result.name() e result.services() que ocultam a análise ao nível de bytes.

Nota

Os atributos adv_data e resp_data são as cargas úteis brutas de publicidade e resposta de pesquisa (bytes). Os auxiliares – name(), services(), manufacturer() – cobrem os campos padrão comuns e são a escolha certa em 99% dos casos. Recorra aos bytes brutos apenas quando precisar de um campo de fornecedor que os auxiliares não analisam (URLs Eddystone, UUID/major/minor de iBeacon, tipos de publicidade personalizados). O esquema de bytes é o TLV padrão: cada campo é length, type, value....

11.4.4. O intervalo de publicidade

A frequência com que o periférico transmite é uma compensação entre consumo/latência de descoberta. Anúncios enviados a cada 20 ms são detetados quase de imediato por um pesquisador, mas mantêm o rádio ocupado e esgotam a bateria; anúncios a cada segundo usam quase nenhuma energia, mas tornam a varredura do pesquisador mais lenta para detetar o dispositivo.

interval_us em aioble.advertise() define o intervalo em microssegundos:

  • 20 000 a 100 000 us (20 ms - 100 ms) – emparelhamento rápido, a aplicação espera uma resposta rápida, dispositivo ligado à corrente.

  • 250 000 a 1 000 000 us (250 ms - 1 s) – uma predefinição razoável para um periférico alimentado por bateria que quer ser detectável sem gastar carga.

  • Acima de 1 000 000 us – difusão de fundo lenta, beacons que enviam uma atualização de posição a cada poucos segundos.

O lado do pesquisador tem os seus próprios controlos – aioble.scan() aceita interval_us e window_us (com que frequência o pesquisador acorda o seu rádio e quanto tempo escuta de cada vez). As predefinições são adequadas; a única alteração comum é definir ambas como iguais para uma pesquisa contínua quando a bateria não é uma preocupação.

11.4.5. Padrões sem ligação – difusor e observador

As páginas em Agir como periférico e Agir como central percorrem a forma conectável da API – onde um periférico aceita uma ligação e os dois lados trocam dados através do GATT. A outra forma é sem ligação: um difusor transmite a carga útil como anúncio, e qualquer observador ao alcance pode lê-la sem nunca ligar. Beacons, sensores de presença e telemetria unidirecional vivem aqui.

Um difusor é aioble.advertise() com connectable=False. Os dados específicos do fabricante transportam a carga útil:

import aioble
import asyncio
import struct

_COMPANY_ID = const(0xFFFF)                # 0xFFFF is "no specific vendor"

async def beacon():
    seq = 0
    while True:
        seq = (seq + 1) & 0xFFFF
        payload = struct.pack("<H", seq)
        await aioble.advertise(
            interval_us=500000,
            connectable=False,
            name="openmv-beacon",
            manufacturer=(_COMPANY_ID, payload),
            timeout_ms=1000,                # one cycle, then loop
        )

asyncio.run(beacon())

A palavra-chave timeout_ms termina a chamada de anuncio após um segundo; o ciclo externo reemite-a com o número de sequência seguinte para que os ouvintes vejam dados atualizados. O flag connectable=False é o que torna o anúncio do estilo difusor – a câmara não responderá a um pedido de ligação mesmo que chegue um.

Um observador é o scanner somente de leitura correspondente. Executa aioble.scan() indefinidamente, analisa os anúncios recebidos e nunca chama connect()

import aioble
import asyncio

_COMPANY_ID = const(0xFFFF)

async def watch():
    async with aioble.scan(duration_ms=0, active=False) as scanner:
        async for result in scanner:
            for company, data in result.manufacturer(filter=_COMPANY_ID):
                print(result.device.addr_hex(),
                      "rssi", result.rssi, "data", data)

asyncio.run(watch())

duration_ms=0 pesquisa até o gestor de contexto sair; active=False mantém o rádio do próprio observador silencioso (sem pedidos de resposta de pesquisa) para o menor consumo de energia. O argumento filter= em manufacturer() descarta todos os anúncios que não correspondem ao ID da empresa, pelo que o ciclo só é acionado pelo tráfego do difusor.

11.4.6. Da descoberta a uma ligação

Quando um central escolhe um periférico com que falar, para de escutar, envia um pedido de ligação no canal de publicidade que o periférico usou por último, e ambos os lados entram nos canais de dados de salto da camada de ligação. O periférico normalmente para de anunciar neste ponto. O que acontece a seguir – parâmetros de ligação, descoberta GATT, o tempo de vida da ligação – está em Ligações.