How to register for BLE notifcations from a WPF app running on Windows 10 Creators Update?

1

I am attempting to write a C# app that uses the WinRT Bluetooth LE APIs (the Windows.Devices.Bluetooth namespace). The app is a Windows Classic Desktop application (WPF, not UWP). When running a version of Windows 10 prior to the Creators Update, these APIs function as expected. However, when running the Creators Update, the APIs that should send data to a Bluetooth device do not function. Specifically, the following methods return Success status codes but do not transmit any data over the Bluetooth radio (as verified using a Bluetooth traffic sniffer):

  • GattCharacteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync
  • GattCharacteristic.WriteValueAsync

As a result, any attempt to register a ValueChanged handler for a characteristic do not function. Since the registration is never sent to the Bluetooth LE device, no notifications are received by the application.

I understand that not all UWP APIs can be used from a non-UWP app, but it is my hope that someone has successfully developed a BLE app in such a configuration (or could at least confirm it is impossible now). I am able to connect and read data from and write data to a BLE device prior to the Creators Update, and it is only in this latest version of Windows 10 the problem mentioned above manifests. (Note: The Async APIs used in the sample code were added in the Creators Update. The prior version of our application used the older BLE APIs, but they also do not function when running the Creators Update.)

Specifically, my question is: given the following project reference list and sample code, is there anything I can try to get working Bluetooth LE connectivity on Windows 10 running the Creators Update from a non-UWP application? Note that the obvious answer of "convert the application to a UWP app" does not work for us, because we interact with other hardware and files in a way that is not possible inside the UWP sandbox.

The project was configured with the following references:

  • System.Runtime.WindowsRuntime, found in: C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework.NETCore\v4.5\System.Runtime.WindowsRuntime.dll
  • Windows, found in: C:\Program Files (x86)\Windows Kits\10\UnionMetadata\Facade\Windows.WinMD
  • Windows.Foundation.FoundationContract, found in: C:\Program Files (x86)\Windows Kits\10\References\10.0.15063.0\Windows.Foundation.FoundationContract\3.0.0.0\Windows.Foundation.FoundationContract.winmd
  • Windows.Foundation.UniversalApiContract, found in: C:\Program Files (x86)\Windows Kits\10\References\10.0.15063.0\Windows.Foundation.UniversalApiContract\4.0.0.0\Windows.Foundation.UniversalApiContract.winmd

Following is a very stripped-down version of the Bluetooth code from my application. Note that a lot of error handling and such has been removed for clarity, but it should give a general idea of what I'm trying to do:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Devices.Enumeration;
using Windows.Foundation;
using Windows.Storage.Streams;
using System.Threading;

namespace BLEMinimumApp
{
    class Program
    {
        private List<string> foundDevices = new List<string>(5);

        static void Main(string[] args)
        {
            new Program().Execute();
        }

        private void Execute()
        {
            Console.WriteLine("Starting device watcher...");

            string[] requestedProperties = { "System.Devices.Aep.IsConnected" };
            String query = "";
            //query for Bluetooth LE devices
            query += "(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\")";
            //query for devices with controllers' name
            query += " AND (System.ItemNameDisplay:=\"GPLeft\" OR System.ItemNameDisplay:=\"GPRight\")";

            var deviceWatcher = DeviceInformation.CreateWatcher(query, requestedProperties, DeviceInformationKind.AssociationEndpoint);
            deviceWatcher.Added += DeviceWatcher_OnAdded;
            deviceWatcher.Start();

            Console.ReadLine();
        }

        private async void DeviceWatcher_OnAdded(DeviceWatcher sender, DeviceInformation deviceInfo)
        {
            lock (foundDevices)
            {
                if (foundDevices.Contains(deviceInfo.Name))
                {
                    return;
                }
                foundDevices.Add(deviceInfo.Name);
            }

            Console.WriteLine($"[{deviceInfo.Name}] DeviceWatcher_OnAdded...");
            await ConnectTo(deviceInfo);
        }

        private async Task ConnectTo(DeviceInformation deviceInfo)
        {
            try
            {
                // get the device
                BluetoothLEDevice device = await BluetoothLEDevice.FromIdAsync(deviceInfo.Id);
                Console.WriteLine($"[{device.Name}] Device found: connectionStatus={device?.ConnectionStatus}");

                // get the GATT service
                Thread.Sleep(150);
                Console.WriteLine($"[{device.Name}] Get GATT Services");
                var gattServicesResult = await device.GetGattServicesForUuidAsync(new Guid("<GUID REMOVED FOR SO POST"));
                Console.WriteLine($"[{device.Name}] GATT services result: status={gattServicesResult?.Status}, count={gattServicesResult?.Services?.Count}, cx={device.ConnectionStatus}");

                if (gattServicesResult == null 
                    || gattServicesResult.Status != GattCommunicationStatus.Success 
                    || gattServicesResult.Services == null 
                    || gattServicesResult.Services?.Count < 1)
                {
                    Console.WriteLine($"[{device.Name}] Failed to find GATT service.");
                    return;
                }

                var service = gattServicesResult.Services[0];
                Console.WriteLine($"[{device?.Name}] GATT service found: gattDeviceService={service.Uuid}");

                // get the GATT characteristic
                Thread.Sleep(150);
                Console.WriteLine($"[{device.Name}] Get GATT characteristics");
                var gattCharacteristicsResult = await service.GetCharacteristicsForUuidAsync(new Guid("<GUID REMOVED FOR SO POST>"));
                Console.WriteLine($"[{device.Name}] GATT Characteristics result: status={gattCharacteristicsResult?.Status}, count={gattCharacteristicsResult?.Characteristics?.Count}, cx={device.ConnectionStatus}");

                if (gattCharacteristicsResult == null
                    || gattCharacteristicsResult.Status != GattCommunicationStatus.Success
                    || gattCharacteristicsResult.Characteristics == null
                    || gattCharacteristicsResult.Characteristics?.Count < 1)
                {
                    Console.WriteLine($"[{device.Name}] Failed to find GATT characteristic.");
                    return;
                }

                var characteristic = gattCharacteristicsResult.Characteristics[0];

                // register for notifications
                Thread.Sleep(150);

                characteristic.ValueChanged += (sender, args) =>
                {
                    Console.WriteLine($"[{device.Name}] Received notification containing {args.CharacteristicValue.Length} bytes");
                };
                Console.WriteLine($"[{device.Name}] Writing CCCD...");
                GattWriteResult result =
                    await characteristic.WriteClientCharacteristicConfigurationDescriptorWithResultAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
                Console.WriteLine($"[{device?.Name}] Characteristics write result: status={result.Status}, protocolError={result.ProtocolError}");

                // send configuration to device 
                await SendConfiguration(device, characteristic);
            }
            catch (Exception ex) when((uint) ex.HResult == 0x800710df)
            {
                Console.WriteLine("bluetooth error 1");
                // ERROR_DEVICE_NOT_AVAILABLE because the Bluetooth radio is not on.
            }
        }

        private async Task SendConfiguration(BluetoothLEDevice device, GattCharacteristic characteristic)
        {
            if (characteristic != null)
            {
                var writer = new DataWriter();
                // CONFIGURATION REMOVED, but this code writes device-specific bytes to the DataWriter

                await SendMessage(device, characteristic, writer.DetachBuffer());
            }
        }

        private async Task SendMessage(BluetoothLEDevice device, GattCharacteristic characteristic, IBuffer message)
        {
            if (characteristic != null && device.ConnectionStatus.Equals(BluetoothConnectionStatus.Connected) && message != null)
            {
                Console.WriteLine($"[{device.Name}] Sending message...");
                GattCommunicationStatus result = await characteristic.WriteValueAsync(message);
                Console.WriteLine($"[{device.Name}] Result: {result}");
            }
        }
    }
}
c#
windows
bluetooth
bluetooth-lowenergy
creators-update
asked on Stack Overflow Aug 29, 2017 by Sean Kleinjung

2 Answers

1

COM Security might be preventing your app from receiving notifications. Please refer following thread for a resolution. I suggest to use registry hack.

https://social.msdn.microsoft.com/Forums/en-US/58da3fdb-a0e1-4161-8af3-778b6839f4e1/bluetooth-bluetoothledevicefromidasync-does-not-complete-on-10015063?forum=wdk

I had a similar issue and with above registry change my app receives few notifications and stops with no apparent reason whatsoever. Wasted lot of time on this issue and waiting for a patch from Microsoft.

answered on Stack Overflow Aug 30, 2017 by vt08505
1

The latest update corrects this. You can also workaround the issue using the instruction from Matt Beaver on the link referenced by the other poster.

Basically either:

  1. Specify an AppId for your application.
  2. Call CoInitializeSecurity in your application to allow us to call back
answered on Stack Overflow Aug 31, 2017 by Frank Gorgenyi

User contributions licensed under CC BY-SA 3.0