Cliente GATT do Bluetooth

Este artigo demonstra como usar as APIs de cliente do GATT (Atributo Genérico Bluetooth) para aplicativos UWP (Plataforma Universal do Windows).

Importante

Você deve declarar a funcionalidade "bluetooth" em Package.appxmanifest.

<Capabilities> <DeviceCapability Name="bluetooth" /> </Capabilities>

APIs importantes

Visão geral

Os desenvolvedores podem usar as APIs no namespace Windows.Devices.Bluetooth.GenericAttributeProfile para acessar dispositivos Bluetooth LE. Os dispositivos Bluetooth LE expõem sua funcionalidade por meio de uma coleção de:

  • Serviços
  • Características
  • Descritores

Os serviços definem o contrato funcional do dispositivo LE e contêm uma coleção de características que definem o serviço. Essas características, por sua vez, contêm descritores que descrevem as características. Esses 3 termos são genericamente conhecidos como atributos de um dispositivo.

As APIs Bluetooth LE GATT expõem objetos e funções, em vez de acesso ao transporte bruto. As APIs do GATT também permitem que os desenvolvedores trabalhem com dispositivos Bluetooth LE com a capacidade de executar as seguintes tarefas:

  • Executar descoberta de atributo
  • Valores de atributo de leitura e gravação
  • Registrar um retorno de chamada para o evento Characteristic ValueChanged

Para criar uma implementação útil, um desenvolvedor deve ter conhecimento prévio dos serviços e características do GATT que o aplicativo pretende consumir e processar os valores de características específicos, de modo que os dados binários fornecidos pela API sejam transformados em dados úteis antes de serem apresentados ao usuário. As APIs Bluetooth GATT expõem apenas os primitivos básicos necessários para se comunicar com um dispositivo Bluetooth LE. Para interpretar os dados, um perfil de aplicativo deve ser definido, seja por um perfil padrão Bluetooth SIG ou por um perfil personalizado implementado por um fornecedor de dispositivo. Um perfil cria um contrato de vinculação entre o aplicativo e o dispositivo, sobre o que os dados trocados representam e como interpretá-los.

Por conveniência, o Bluetooth SIG mantém uma lista de perfis públicos disponíveis.

Exemplos

Para obter um exemplo completo, consulte Exemplo de Bluetooth Low Energy.

Consultar dispositivos próximos

Há dois métodos principais para consultar dispositivos próximos:

O 2º método é discutido longamente na documentação do anúncio, portanto, não será muito discutido aqui, mas a ideia básica é encontrar o endereço Bluetooth de dispositivos próximos que satisfaçam o filtro de anúncio específico. Depois de obter o endereço, você pode chamar BluetoothLEDevice.FromBluetoothAddressAsync para obter uma referência ao dispositivo.

Agora, de volta ao método DeviceWatcher. Um dispositivo Bluetooth LE é como qualquer outro dispositivo no Windows e pode ser consultado usando as APIs de enumeração. Use a classe DeviceWatcher e passe uma cadeia de caracteres de consulta especificando os dispositivos a serem procurados:

// Query for extra properties you want returned
string[] requestedProperties = { "System.Devices.Aep.DeviceAddress", "System.Devices.Aep.IsConnected" };

DeviceWatcher deviceWatcher =
            DeviceInformation.CreateWatcher(
                    BluetoothLEDevice.GetDeviceSelectorFromPairingState(false),
                    requestedProperties,
                    DeviceInformationKind.AssociationEndpoint);

// Register event handlers before starting the watcher.
// Added, Updated and Removed are required to get all nearby devices
deviceWatcher.Added += DeviceWatcher_Added;
deviceWatcher.Updated += DeviceWatcher_Updated;
deviceWatcher.Removed += DeviceWatcher_Removed;

// EnumerationCompleted and Stopped are optional to implement.
deviceWatcher.EnumerationCompleted += DeviceWatcher_EnumerationCompleted;
deviceWatcher.Stopped += DeviceWatcher_Stopped;

// Start the watcher.
deviceWatcher.Start();

Depois de iniciar o DeviceWatcher, você receberá DeviceInformation para cada dispositivo que atenda à consulta no manipulador para o evento Added para os dispositivos em questão. Para uma visão mais detalhada do DeviceWatcher, consulte o exemplo completo no Github.

Conectando-se ao dispositivo

Depois que um dispositivo desejado for descoberto, use o DeviceInformation.Id para obter o objeto Bluetooth LE Device para o dispositivo em questão:

async void ConnectDevice(DeviceInformation deviceInfo)
{
    // Note: BluetoothLEDevice.FromIdAsync must be called from a UI thread because it may prompt for consent.
    BluetoothLEDevice bluetoothLeDevice = await BluetoothLEDevice.FromIdAsync(deviceInfo.Id);
    // ...
}

Por outro lado, descartar todas as referências a um objeto BluetoothLEDevice para um dispositivo (e se nenhum outro aplicativo no sistema tiver uma referência ao dispositivo) disparará uma desconexão automática após um pequeno período de tempo limite.

bluetoothLeDevice.Dispose();

Se o aplicativo precisar acessar o dispositivo novamente, basta recriar o objeto do dispositivo e acessar uma característica (discutida na próxima seção) para acionar o sistema operacional para se reconectar quando necessário. Se o dispositivo estiver próximo, você terá acesso ao dispositivo, caso contrário, ele retornará com um erro DeviceUnreachable.

Observação

Criar um objeto BluetoothLEDevice chamando esse método sozinho não inicia (necessariamente) uma conexão. Para iniciar uma conexão, defina GattSession.MaintainConnection como true, ou chame um método de descoberta de serviço não armazenado em cache no BluetoothLEDevice ou execute uma operação de leitura/gravação no dispositivo.

  • Se GattSession.MaintainConnection estiver definido como true, o sistema aguardará indefinidamente por uma conexão e se conectará quando o dispositivo estiver disponível. Não há nada para seu aplicativo esperar, já que GattSession.MaintainConnection é uma propriedade.
  • Para descoberta de serviço e operações de leitura/gravação no GATT, o sistema aguarda um tempo finito, mas variável. Qualquer coisa, desde instantânea até questão de minutos. Os fatores incluem o tráfego na pilha e o quão enfileirada está a solicitação. Se não houver nenhuma outra solicitação pendente e o dispositivo remoto estiver inacessível, o sistema aguardará sete (7) segundos antes de atingir o tempo limite. Se houver outras solicitações pendentes, cada uma das solicitações na fila poderá levar sete (7) segundos para ser processada, portanto, quanto mais a sua estiver no final da fila, mais tempo você esperará.

Atualmente, não é possível cancelar o processo de conexão.

Enumerando serviços e características com suporte

Agora que você tem um objeto BluetoothLEDevice, a próxima etapa é descobrir quais dados o dispositivo expõe. A primeira etapa para fazer isso é consultar os serviços:

GattDeviceServicesResult result = await bluetoothLeDevice.GetGattServicesAsync();

if (result.Status == GattCommunicationStatus.Success)
{
    var services = result.Services;
    // ...
}

Uma vez identificado o serviço de interesse, a próxima etapa é consultar as características.

GattCharacteristicsResult result = await service.GetCharacteristicsAsync();

if (result.Status == GattCommunicationStatus.Success)
{
    var characteristics = result.Characteristics;
    // ...
}

O sistema operacional retorna uma lista ReadOnly de objetos GattCharacteristic nos quais você pode executar operações.

Executar operações de leitura/gravação em uma característica

A característica é a unidade fundamental da comunicação baseada no GATT. Ele contém um valor que representa uma parte distinta dos dados no dispositivo. Por exemplo, a característica de nível de bateria tem um valor que representa o nível de bateria do dispositivo.

Leia as propriedades da característica para determinar quais operações são suportadas:

GattCharacteristicProperties properties = characteristic.CharacteristicProperties

if(properties.HasFlag(GattCharacteristicProperties.Read))
{
    // This characteristic supports reading from it.
}
if(properties.HasFlag(GattCharacteristicProperties.Write))
{
    // This characteristic supports writing to it.
}
if(properties.HasFlag(GattCharacteristicProperties.Notify))
{
    // This characteristic supports subscribing to notifications.
}

Se houver suporte para leitura, você poderá ler o valor:

GattReadResult result = await selectedCharacteristic.ReadValueAsync();
if (result.Status == GattCommunicationStatus.Success)
{
    var reader = DataReader.FromBuffer(result.Value);
    byte[] input = new byte[reader.UnconsumedBufferLength];
    reader.ReadBytes(input);
    // Utilize the data as needed
}

A escrita em uma característica segue um padrão semelhante:

var writer = new DataWriter();
// WriteByte used for simplicity. Other common functions - WriteInt16 and WriteSingle
writer.WriteByte(0x01);

GattCommunicationStatus result = await selectedCharacteristic.WriteValueAsync(writer.DetachBuffer());
if (result == GattCommunicationStatus.Success)
{
    // Successfully wrote to device
}

Dica

DataReader e DataWriter são indispensáveis ao trabalhar com os buffers brutos que você obtém de muitas das APIs Bluetooth.

Inscrever-se para receber notificações

Verifique se a característica dá suporte a Indicar ou Notificar (verifique as propriedades da característica para ter certeza).

Indicar é considerado mais confiável porque cada evento de alteração de valor é acoplado a uma confirmação do dispositivo cliente. A notificação é mais prevalente porque a maioria das transações do GATT prefere economizar energia em vez de ser extremamente confiável. De qualquer forma, tudo isso é tratado na camada do controlador para que o aplicativo não se envolva. Vamos nos referir coletivamente a eles simplesmente como "notificações".

Há duas coisas a serem cuidadas antes de receber notificações:

  • Gravar no CCCD (Descritor de Configuração de Característica do Cliente)
  • Manipular o evento Characteristic.ValueChanged

Gravar no CCCD informa ao dispositivo Server que esse cliente deseja saber cada vez que esse valor de característica específico for alterado. Para fazer isso:

GattCommunicationStatus status = await selectedCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(
                        GattClientCharacteristicConfigurationDescriptorValue.Notify);
if(status == GattCommunicationStatus.Success)
{
    // Server has been informed of clients interest.
}

Agora, o evento ValueChanged do GattCharacteristic será chamado sempre que o valor for alterado no dispositivo remoto. Tudo o que resta é implementar o manipulador:

characteristic.ValueChanged += Characteristic_ValueChanged;

...

void Characteristic_ValueChanged(GattCharacteristic sender,
                                    GattValueChangedEventArgs args)
{
    // An Indicate or Notify reported that the value has changed.
    var reader = DataReader.FromBuffer(args.CharacteristicValue)
    // Parse the data however required.
}