Servidor Bluetooth GATT

Este tópico demonstra como usar as APIs de servidor 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

O Windows geralmente opera na função de cliente. No entanto, surgem muitos cenários que exigem que o Windows também atue como um servidor Bluetooth LE GATT. Quase todos os cenários para dispositivos IoT, juntamente com a maioria das comunicações BLE entre plataformas, exigirão que o Windows seja um servidor GATT. Além disso, o envio de notificações para dispositivos vestíveis próximos tornou-se um cenário popular que também requer essa tecnologia.

As operações do servidor girarão em torno do Provedor de Serviços e do GattLocalCharacteristic. Essas duas classes fornecerão a funcionalidade necessária para declarar, implementar e expor uma hierarquia de dados a um dispositivo remoto.

Definir os serviços suportados

Seu aplicativo pode declarar um ou mais serviços que serão publicados pelo Windows. Cada serviço é identificado exclusivamente por um UUID.

Atributos e UUIDs

Cada serviço, característica e descritor é definido por seu próprio UUID exclusivo de 128 bits.

Todas as APIs do Windows usam o termo GUID, mas o padrão Bluetooth as define como UUIDs. Para nossos propósitos, esses dois termos são intercambiáveis, portanto, continuaremos a usar o termo UUID.

Se o atributo for padrão e definido pelo Bluetooth SIG, ele também terá uma ID curta de 16 bits correspondente (por exemplo, o UUID do nível da bateria é 00002A19-0000-1000-8000-00805F9B34FB e a ID curta é 0x2A19). Esses UUIDs padrão podem ser vistos em GattServiceUuids e GattCharacteristicUuids.

Se o aplicativo estiver implementando seu próprio serviço personalizado, um UUID personalizado precisará ser gerado. Isso é feito facilmente no Visual Studio por meio de Ferramentas –> CreateGuid (use a opção 5 para obtê-lo no "xxxxxxxx-xxxx-... xxxx"). Esse uuid agora pode ser usado para declarar novos serviços, características ou descritores locais.

Serviços restritos

Os seguintes Serviços são reservados pelo sistema e não podem ser publicados no momento:

  1. Serviço de Informações do Dispositivo (DIS)
  2. Serviço de Perfil de Atributo Genérico (GATT)
  3. Serviço de Perfil de Acesso Genérico (GAP)
  4. Serviço de Parâmetros de Verificação (SCP)

A tentativa de criar um serviço bloqueado resultará no retorno de BluetoothError.DisabledByPolicy da chamada para CreateAsync.

Atributos gerados

Os seguintes descritores são gerados automaticamente pelo sistema, com base no GattLocalCharacteristicParameters fornecido durante a criação da característica:

  1. Configuração de característica do cliente (se a característica estiver marcada como indicável ou notificável).
  2. Descrição do usuário da característica (se a propriedade UserDescription estiver definida). Consulte a propriedade GattLocalCharacteristicParameters.UserDescription para obter mais informações.
  3. Formato de Característica (um descritor para cada formato de apresentação especificado). Consulte a propriedade GattLocalCharacteristicParameters.PresentationFormats para obter mais informações.
  4. Formato agregado de característica (se mais de um formato de apresentação for especificado). GattLocalCharacteristicParameters.Consulte a propriedade PresentationFormats para obter mais informações.
  5. Propriedades estendidas da característica (se a característica estiver marcada com o bit de propriedades estendidas).

O valor do descritor Extended Properties é determinado por meio das propriedades características ReliableWrites e WritableAuxiliaries.

A tentativa de criar um descritor reservado resultará em uma exceção.

Observe que a transmissão não é suportada no momento. Especificar o Broadcast GattCharacteristicProperty resultará em uma exceção.

Construa a hierarquia de serviços e características

O GattServiceProvider é usado para criar e anunciar a definição de serviço primário raiz. Cada serviço requer seu próprio objeto ServiceProvider que recebe um GUID:

GattServiceProviderResult result = await GattServiceProvider.CreateAsync(uuid);

if (result.Error == BluetoothError.Success)
{
    serviceProvider = result.ServiceProvider;
    // 
}

Os serviços primários são o nível superior da árvore GATT. Os serviços primários contêm características, bem como outros serviços (chamados de serviços "Incluídos" ou secundários).

Agora, preencha o serviço com as características e descritores necessários:

GattLocalCharacteristicResult characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid1, ReadParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
    // An error occurred.
    return;
}
_readCharacteristic = characteristicResult.Characteristic;
_readCharacteristic.ReadRequested += ReadCharacteristic_ReadRequested;

characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid2, WriteParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
    // An error occurred.
    return;
}
_writeCharacteristic = characteristicResult.Characteristic;
_writeCharacteristic.WriteRequested += WriteCharacteristic_WriteRequested;

characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid3, NotifyParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
    // An error occurred.
    return;
}
_notifyCharacteristic = characteristicResult.Characteristic;
_notifyCharacteristic.SubscribedClientsChanged += SubscribedClientsChanged;

Como mostrado acima, esse também é um bom lugar para declarar manipuladores de eventos para as operações às quais cada característica dá suporte. Para responder às solicitações corretamente, um aplicativo deve definir e definir um manipulador de eventos para cada tipo de solicitação compatível com o atributo. A falha ao registrar um manipulador resultará na conclusão imediata da solicitação com UnlikelyError pelo sistema.

Características constantes

Às vezes, há valores característicos que não serão alterados durante o ciclo de vida do aplicativo. Nesse caso, é aconselhável declarar uma característica constante para evitar a ativação desnecessária do aplicativo:

byte[] value = new byte[] {0x21};
var constantParameters = new GattLocalCharacteristicParameters
{
    CharacteristicProperties = (GattCharacteristicProperties.Read),
    StaticValue = value.AsBuffer(),
    ReadProtectionLevel = GattProtectionLevel.Plain,
};

var characteristicResult = await serviceProvider.Service.CreateCharacteristicAsync(uuid4, constantParameters);
if (characteristicResult.Error != BluetoothError.Success)
{
    // An error occurred.
    return;
}

Publicar o serviço

Depois que o serviço for totalmente definido, a próxima etapa é publicar o suporte para o serviço. Isso informa ao sistema operacional que o serviço deve ser retornado quando dispositivos remotos executam uma descoberta de serviço. Você terá que definir duas propriedades - IsDiscoverable e IsConnectable:

GattServiceProviderAdvertisingParameters advParameters = new GattServiceProviderAdvertisingParameters
{
    IsDiscoverable = true,
    IsConnectable = true
};
serviceProvider.StartAdvertising(advParameters);
  • IsDiscoverable: anuncia o nome amigável para dispositivos remotos no anúncio, tornando o dispositivo detectável.
  • IsConnectable: anuncia um anúncio conectável para uso na função periférica.

Quando um serviço é detectável e conectável, o sistema adicionará o uuid de serviço ao pacote de anúncio. Existem apenas 31 bytes no pacote de anúncio e um UUID de 128 bits ocupa 16 deles!

Observe que, quando um serviço é publicado em primeiro plano, um aplicativo deve chamar StopAdvertising quando o aplicativo é suspenso.

Responder a solicitações de leitura e gravação

Como vimos acima ao declarar as características necessárias, GattLocalCharacteristics tem 3 tipos de eventos - ReadRequested, WriteRequested e SubscribedClientsChanged.

Ler

Quando um dispositivo remoto tenta ler um valor de uma característica (e não é um valor constante), o evento ReadRequested é chamado. A característica que a leitura foi chamada, bem como args (contendo informações sobre o dispositivo remoto) é passada para o delegado:

characteristic.ReadRequested += Characteristic_ReadRequested;
// ... 

async void ReadCharacteristic_ReadRequested(GattLocalCharacteristic sender, GattReadRequestedEventArgs args)
{
    var deferral = args.GetDeferral();
    
    // Our familiar friend - DataWriter.
    var writer = new DataWriter();
    // populate writer w/ some data. 
    // ... 

    var request = await args.GetRequestAsync();
    request.RespondWithValue(writer.DetachBuffer());
    
    deferral.Complete();
}

Gravar

Quando um dispositivo remoto tenta gravar um valor em uma característica, o evento WriteRequested é chamado com detalhes sobre o dispositivo remoto, em qual característica gravar e o próprio valor:

characteristic.ReadRequested += Characteristic_ReadRequested;
// ...

async void WriteCharacteristic_WriteRequested(GattLocalCharacteristic sender, GattWriteRequestedEventArgs args)
{
    var deferral = args.GetDeferral();
    
    var request = await args.GetRequestAsync();
    var reader = DataReader.FromBuffer(request.Value);
    // Parse data as necessary. 

    if (request.Option == GattWriteOption.WriteWithResponse)
    {
        request.Respond();
    }
    
    deferral.Complete();
}

Existem 2 tipos de Gravações - com e sem resposta. Use GattWriteOption (uma propriedade no objeto GattWriteRequest) para descobrir qual tipo de gravação o dispositivo remoto está executando.

Enviar notificações para clientes inscritos

A mais frequente das operações do GATT Server, as notificações desempenham a função crítica de enviar dados para os dispositivos remotos. Às vezes, você deseja notificar todos os clientes inscritos, mas outras vezes você pode escolher para quais dispositivos enviar o novo valor:

async void NotifyValue()
{
    var writer = new DataWriter();
    // Populate writer with data
    // ...
    
    await notifyCharacteristic.NotifyValueAsync(writer.DetachBuffer());
}

Quando um novo dispositivo se inscreve para notificações, o evento SubscribedClientsChanged é chamado:

characteristic.SubscribedClientsChanged += SubscribedClientsChanged;
// ...

void _notifyCharacteristic_SubscribedClientsChanged(GattLocalCharacteristic sender, object args)
{
    List<GattSubscribedClient> clients = sender.SubscribedClients;
    // Diff the new list of clients from a previously saved one 
    // to get which device has subscribed for notifications. 

    // You can also just validate that the list of clients is expected for this app.  
}

Observação

Seu aplicativo pode obter o tamanho máximo de notificação para um cliente específico com a propriedade MaxNotificationSize . Todos os dados maiores que o tamanho máximo serão truncados pelo sistema.

Ao manipular o evento GattLocalCharacteristic.SubscribedClientsChanged , você pode usar o processo descrito abaixo para determinar informações completas sobre os dispositivos cliente inscritos no momento: