Search code examples
uwpwindows-runtimebluetooth-lowenergyconsole-applicationc++-winrt

C++/WinRT GattCharacteristic ValueChanged Event Never Fires / Triggers


The GattCharacteristic.ValueChanged Event never seems to fire in a very bare bones C++ / WinRT console application.

For device with config

  • Local Name: TestDevice
  • Advertised Service: FFFF
  • Characteristic: EEEE

The following program will simply

  • search for a device TestDevice
  • find services of TestDevice with ShortID FFFF
  • find the characteristic of that service with shortID EEEE
  • Write the ClientCharacteristicConfigurationDescriptor GattClientCharacteristicConfigurationDescriptorValue to EEEE, which has property Read, Write and Notify
  • register a lambda cast to TypedEventHandler<GattCharacteristic,GattValueChangedEventArgs> to ValueChanged which should print New Value on value update

Updating the characteristic value does not trigger the ValueChanged event. However, the same device setup has been tested with othe bluetooth stack with success.

#include "pch.h"
#include <iostream>
#include <Windows.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Devices.Bluetooth.h>
#include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h>
#include <winrt/Windows.Devices.Bluetooth.Advertisement.h>

using winrt::Windows::Devices::Bluetooth::BluetoothConnectionStatus;
using winrt::Windows::Devices::Bluetooth::BluetoothLEDevice;
using winrt::Windows::Devices::Bluetooth::BluetoothUuidHelper;
using winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementReceivedEventArgs;
using winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementWatcher;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Devices::Bluetooth::GenericAttributeProfile;

using namespace winrt;


class WinBleCentral
{
public:
    WinBleCentral()
    {
        bleWatcher.Received(
            [this](BluetoothLEAdvertisementWatcher watcher, BluetoothLEAdvertisementReceivedEventArgs eventArgs)
            {
                hstring testHstring{ std::wstring_view(L"TestDevice") };
                if (testHstring == eventArgs.Advertisement().LocalName())
                {
                    this->bleWatcher.Stop();
                    std::cout << "Matched\n";
                    BluetoothLEDevice::FromBluetoothAddressAsync(eventArgs.BluetoothAddress()).Completed(
                        [this](IAsyncOperation<BluetoothLEDevice> sender, AsyncStatus status)
                        {
                            if (auto device = sender.GetResults(); device)
                            {
                                std::cout << "Connected\n";
                                winrt::guid serviceGUID = BluetoothUuidHelper::FromShortId(0xFFFF);

                                device.GetGattServicesForUuidAsync(serviceGUID).Completed(
                                    [this](IAsyncOperation<GattDeviceServicesResult> sender, AsyncStatus status)
                                    {
                                        GattDeviceServicesResult result = sender.get();
                                        if (result && status == winrt::Windows::Foundation::AsyncStatus::Completed)
                                        {
                                            winrt::guid charGUID = BluetoothUuidHelper::FromShortId(0xEEEE);
                                            std::cout << "Num Services: " << result.Services().Size() << '\n';
                                            for (auto&& service : result.Services())
                                            {
                                                service.GetCharacteristicsForUuidAsync(charGUID).Completed(
                                                    [this](IAsyncOperation<GattCharacteristicsResult>sender, AsyncStatus status)
                                                    {
                                                        std::cout << "Get Characteristics\n";
                                                        if (auto result = sender.GetResults(); result)
                                                        {
                                                            std::cout << "Num Characteristics: " << result.Characteristics().Size() << '\n';
                                                            for (auto character : result.Characteristics())
                                                            {
                                                                character.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Notify).Completed(
                                                                    [this, character](IAsyncOperation<GattCommunicationStatus>sender, AsyncStatus status)
                                                                    {
                                                                        character.ValueChanged([this](GattCharacteristic characteristic, GattValueChangedEventArgs const& args)
                                                                            {
                                                                                std::cout << "New Value!\n";
                                                                            });
                                                                    });
                                                            }
                                                        }
                                                    });
                                            }
                                        }
                                    }
                                );

                            }
                        });
                }
            });
        bleWatcher.Start();
    };

    BluetoothLEAdvertisementWatcher bleWatcher;
};

int main()
{
    WinBleCentral bleCentral;
    while (getchar() != '\n');
}

Solution

  • This appears to be down to the GattCharacteristic object falling out of scope. By simply keeping a reference of tha variable the above works as expected.

    Below is the same program as above with the Async side of things stripped out to improve readability. I've tested and the exact same behaviour is displayed in both cases.

    #include "pch.h"
    #include <iostream>
    
    #include <Windows.h>
    #include <winrt/Windows.Foundation.h>
    #include <winrt/Windows.Devices.Bluetooth.h>
    #include <winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h>
    #include <winrt/Windows.Devices.Bluetooth.Advertisement.h>
    
    using winrt::Windows::Devices::Bluetooth::BluetoothConnectionStatus;
    using winrt::Windows::Devices::Bluetooth::BluetoothLEDevice;
    using winrt::Windows::Devices::Bluetooth::BluetoothUuidHelper;
    using winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementReceivedEventArgs;
    using winrt::Windows::Devices::Bluetooth::Advertisement::BluetoothLEAdvertisementWatcher;
    using namespace winrt::Windows::Foundation;
    using namespace winrt::Windows::Foundation::Collections;
    using namespace winrt::Windows::Devices::Bluetooth::GenericAttributeProfile;
    
    using namespace winrt;
    
    class WinBleCentral
    {
    public:
     
        WinBleCentral()
        {
            std::cout << "Start\n";
            bleWatcher.Received(
                [this](BluetoothLEAdvertisementWatcher watcher, BluetoothLEAdvertisementReceivedEventArgs eventArgs)
                {
                    hstring testHstring{ std::wstring_view(L"TestDevice") };
                    if (testHstring == eventArgs.Advertisement().LocalName())
                    {
                        this->bleWatcher.Stop();
                        std::cout << "Matched\n";
                        auto device = BluetoothLEDevice::FromBluetoothAddressAsync(eventArgs.BluetoothAddress()).get();
                        std::cout << "Device\n";
                        winrt::guid serviceGUID = BluetoothUuidHelper::FromShortId(0xFFFF);
                        auto serviceResults = device.GetGattServicesForUuidAsync(serviceGUID).get();
                        std::cout << "Service Results\n";
                        winrt::guid charGUID = BluetoothUuidHelper::FromShortId(0xEEEE);
                        auto service = serviceResults.Services().GetAt(0);
                        std::cout << "Services\n";
                        auto characteristicResults = service.GetCharacteristicsForUuidAsync(charGUID).get();
                        std::cout << "Characteristic Results\n";
                        auto characteristic = characteristicResults.Characteristics().GetAt(0);
                        characteristics.push_back(characteristic);
                        std::cout << "Characterstic\n";
                        auto gattCommunicationStatus = characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Notify).get();
                        std::cout << "WriteClientCharacteristicConfiguration\n";
                        characteristic.ValueChanged([this](GattCharacteristic const& notifyingCharacteristic, GattValueChangedEventArgs args)
                            {
                                std::cout << "New Value!\n";
                            });
                    }
                });
            bleWatcher.Start();
        }
    
        std::vector<GattCharacteristic> characteristics;
        BluetoothLEAdvertisementWatcher bleWatcher;
    };
    
    
    int main()
    {
        WinBleCentral bleCentral;
        while (getchar() != '\n');
    }
    
    

    even though std::vector<GattCharacteristic> characteristics is never used, the simple act of characteristics.push_back(characteristic) is enough to keep a reference alive. This behaviour was definitely not expected.

    To keep with the whole UWP style, I initially used the IVector and the Append method, but the entire thing collapses at check_hresult(WINRT_IMPL_SHIM(winrt::Windows::Foundation::Collections::IVector<T>)->Append(impl::bind_in(value))); for what appears to be a null pointer.

    For the above, all error checking has been eschewed for readability. Actual implementation should have healthy checking of all Async operations to make sure the data is actually there.