Bluetooth GATT Server

Questo argomento illustra come usare le API del server GATT (Bluetooth Generic Attribute) per le app UWP (Universal Windows Platform).

Importante

È necessario dichiarare la funzionalità "bluetooth" in Package.appxmanifest.

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

API importanti

Panoramica

Windows funziona in genere nel ruolo client. Tuttavia, si presentano molti scenari che richiedono a Windows di agire anche come server GATT Bluetooth LE. Quasi tutti gli scenari per i dispositivi IoT, insieme alla maggior parte delle comunicazioni BLE multipiattaforma richiederanno che Windows sia un server GATT. Inoltre, l'invio di notifiche ai dispositivi indossabili nelle vicinanze è diventato uno scenario popolare che richiede anche questa tecnologia.

Le operazioni del server ruotano attorno al provider di servizi e al GattLocalCharacteristic. Queste due classi forniranno le funzionalità necessarie per dichiarare, implementare ed esporre una gerarchia di dati a un dispositivo remoto.

Definire i servizi supportati

L'app può dichiarare uno o più servizi che verranno pubblicati da Windows. Ogni servizio viene identificato in modo univoco da un UUID.

Attributi e UUID

Ogni servizio, caratteristica e descrittore è definito da un UUID univoco a 128 bit.

Tutte le API di Windows usano il termine GUID, ma lo standard Bluetooth lo definisce come UUID. Ai fini dei nostri scopi, questi due termini sono intercambiabili, quindi continueremo a usare il termine UUID.

Se l'attributo è standard e definito dal SIG Bluetooth, avrà anche un ID breve a 16 bit corrispondente (ad esempio, l'UUID a livello di batteria è 00002A19-0000-1000-8000-00805F9B34FB e l'ID breve è 0x2A19). Questi UUID standard possono essere visualizzati in GattServiceUuids e GattCharacteristicUuids.

Se l'app implementa il proprio servizio personalizzato, sarà necessario generare un UUID personalizzato. Questa operazione viene eseguita facilmente in Visual Studio tramite Strumenti -> CreateGuid (usare l'opzione 5 per ottenerla nel formato "xxxxxxxx-xxxx-... xxxx"). Questo uuid può ora essere usato per dichiarare nuovi servizi locali, caratteristiche o descrittori.

Servizi con restrizioni

I servizi seguenti sono riservati dal sistema e non possono essere pubblicati in questo momento:

  1. Device Information Service (DIS)
  2. Generic Attribute Profile Service (GATT)
  3. Servizio profilo di accesso generico (GAP)
  4. Servizio di analisi dei parametri (SCP)

Se si tenta di creare un servizio bloccato, la chiamata a CreateAsync riporterà BluetoothError.DisabledByPolicy.

Attributi generati

I seguenti descrittori vengono generati automaticamente dal sistema, in base ai GattLocalCharacteristicParameters forniti durante la creazione della caratteristica:

  1. Configurazione caratteristica client (se la caratteristica è contrassegnata come indicabile o notificabile).
  2. Descrizione utente caratteristica (se la proprietà UserDescription è impostata). Per altre informazioni, si veda la proprietà GattLocalCharacteristicParameters.UserDescription
  3. Formato caratteristica (un descrittore per ogni formato di presentazione specificato). Per altre informazioni, si veda la proprietà GattLocalCharacteristicParameters.PresentationFormats.
  4. Formato aggregazione caratteristica (se è specificato più di un formato di presentazione). Per altre informazioni, si veda la proprietà GattLocalCharacteristicParameters.See PresentationFormats
  5. Proprietà estese caratteristiche (se la caratteristica è contrassegnata con il bit delle proprietà estese).

Il valore del descrittore delle Proprietà estese viene determinato tramite le proprietà delle caratteristiche ReliableWrites e WritableAuxiliaries.

Il tentativo di creare un descrittore riservato genererà un'eccezione.

Si noti che la trasmissione non è supportata in questo momento. Se si specifica la Trasmissione GattCharacteristicProperty, verrà generata un'eccezione.

Creare la gerarchia dei servizi e delle caratteristiche

GattServiceProvider viene usato per creare e annunciare la definizione del servizio primario radice. Ogni servizio richiede che sia un oggetto ServiceProvider ad accettare un GUID:

GattServiceProviderResult result = await GattServiceProvider.CreateAsync(uuid);

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

I servizi primari sono il primo livello dell'albero GATT. I servizi primari contengono caratteristiche e altri servizi (denominati "Inclusi" o servizi secondari).

A questo punto, compilare il servizio con le caratteristiche e i descrittori necessari:

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;

Come illustrato in precedenza, questo è anche un buon punto di riferimento per dichiarare i gestori eventi per le operazioni supportate da ogni caratteristica. Per rispondere correttamente alle richieste, un'app deve definire e impostare un gestore eventi per ogni tipo di richiesta supportato dall'attributo. Se non si registra un gestore, la richiesta verrà completata immediatamente con UnlikelyError dal sistema.

Caratteristiche costanti

In alcuni casi, esistono valori di caratteristica che non cambieranno durante il corso della durata dell'app. In tal caso, è consigliabile dichiarare una caratteristica costante per impedire l'attivazione di app non necessarie:

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;
}

Pubblicare il servizio

Dopo aver definito completamente il servizio, il passaggio successivo consiste nel pubblicare il supporto per il servizio. In questo modo il sistema operativo indica che il servizio deve essere restituito quando i dispositivi remoti eseguono una ricerca del servizio. È necessario impostare due proprietà: IsDiscoverable e IsConnectable:

GattServiceProviderAdvertisingParameters advParameters = new GattServiceProviderAdvertisingParameters
{
    IsDiscoverable = true,
    IsConnectable = true
};
serviceProvider.StartAdvertising(advParameters);
  • IsDiscoverable: annuncia il nome descrittivo ai dispositivi remoti nell'annuncio pubblicitario, rendendo il dispositivo individuabile.
  • IsConnectable: promuove un annuncio collegabile per l'uso in un ruolo periferico.

Quando un servizio è individuabile e collegabile, il sistema aggiungerà l'Uuid del servizio al pacchetto pubblicitario. Nel pacchetto Annuncio sono presenti solo 31 byte e un UUID a 128 bit ne occupa 16!

Si noti che quando un servizio viene pubblicato in primo piano, un'applicazione deve richiamare StopAdvertising quando l'applicazione viene sospesa.

Rispondere alle richieste di lettura e scrittura

Come abbiamo visto sopra durante la dichiarazione delle caratteristiche necessarie, GattLocalCharacteristics possiede 3 tipi di eventi- ReadRequested, WriteRequested e SubscribedClientsChanged.

Lettura

Quando un dispositivo remoto tenta di leggere un valore da una caratteristica (e non è un valore costante), viene richiamato l'evento ReadRequested. La caratteristica su cui è stata richiamata la lettura e gli argomenti (contenenti informazioni sul dispositivo remoto) vengono passate al delegato:

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();
}

Scrittura

Quando un dispositivo remoto tenta di scrivere un valore in una caratteristica, l'evento WriteRequested viene richiamato con i dettagli sul dispositivo remoto, sulla caratteristica da scrivere e sul valore stesso:

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();
}

Esistono due tipi di scritture, con e senza risposta. Utilizzare GattWriteOption (una proprietà sull'oggetto GattWriteRequest) per determinare quale tipo di scrittura sta eseguendo il dispositivo remoto.

Inviare notifiche ai client sottoscritti

Le notifiche più frequenti delle operazioni del server GATT eseguono la funzione critica di push dei dati nei dispositivi remoti. In alcuni casi, è consigliabile inviare una notifica a tutti i client sottoscritti, ma altre volte è possibile scegliere i dispositivi a cui inviare il nuovo valore:

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

Quando un nuovo dispositivo sottoscrive le notifiche, viene richiamato l'evento SubscribedClientsChanged:

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.  
}

Nota

L'applicazione può ottenere le dimensioni massime di notifica per un determinato client con la proprietà MaxNotificationSize . Tutti i dati maggiori della dimensione massima verranno troncati dal sistema.

Quando si gestisce l'evento GattLocalCharacteristic.SubscribedClientsChanged , è possibile utilizzare il processo descritto di seguito per determinare le informazioni complete sui dispositivi client attualmente sottoscritti: