5

I am using the Windows api Gatt Client BLE for C++, my goal is to connect two devices (but in this case I will try just one) and keep reading and writing data constantly without closing the device at any time. All my devices have one specific service that contains a read characteristic and a write one.

HOW TO TEST:

Use Visual studio 2017 (v141) with Windows SDK Version: 10.0.18362.0, create a new console (.exe) solution, change the Platform in Project -> Properties to Win32 and go to Project -> Properties -> C/C++ -> Command Line and add these options:

/std:c++17 /await 

Then copy the following code in a file (you can copy all in the same .cpp file):

#pragma once
#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <iostream>
#include <queue>
#include <map>
#include <mutex>
#include <condition_variable>
#include <string>

#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

#include "winrt/Windows.Devices.Bluetooth.h"
#include "winrt/Windows.Devices.Bluetooth.GenericAttributeProfile.h"
#include "winrt/Windows.Devices.Enumeration.h"

#include "winrt/Windows.Storage.Streams.h"

#pragma comment(lib, "windowsapp")


using namespace std;

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Web::Syndication;

using namespace Windows::Devices::Bluetooth;
using namespace Windows::Devices::Bluetooth::GenericAttributeProfile;
using namespace Windows::Devices::Enumeration;

using namespace Windows::Storage::Streams;

#pragma region STRUCS AND ENUMS

#define LOG_ERROR(e) cout << e << endl;

union to_guid
{
    uint8_t buf[16];
    guid guid;
};

const uint8_t BYTE_ORDER[] = { 3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15 };

guid make_guid(const wchar_t* value)
{
    to_guid to_guid;
    memset(&to_guid, 0, sizeof(to_guid));
    int offset = 0;
    for (unsigned int i = 0; i < wcslen(value); i++) {
        if (value[i] >= '0' && value[i] <= '9')
        {
            uint8_t digit = value[i] - '0';
            to_guid.buf[BYTE_ORDER[offset / 2]] += offset % 2 == 0 ? digit << 4 : digit;
            offset++;
        }
        else if (value[i] >= 'A' && value[i] <= 'F')
        {
            uint8_t digit = 10 + value[i] - 'A';
            to_guid.buf[BYTE_ORDER[offset / 2]] += offset % 2 == 0 ? digit << 4 : digit;
            offset++;
        }
        else if (value[i] >= 'a' && value[i] <= 'f')
        {
            uint8_t digit = 10 + value[i] - 'a';
            to_guid.buf[BYTE_ORDER[offset / 2]] += offset % 2 == 0 ? digit << 4 : digit;
            offset++;
        }
        else
        {
            // skip char
        }
    }

    return to_guid.guid;
}


mutex subscribeLock;
condition_variable subscribeSignal;

mutex _mutexWrite;
condition_variable signalWrite;

struct DeviceCacheEntry {
    BluetoothLEDevice device = nullptr;
    GattDeviceService service = nullptr;
    GattCharacteristic characteristic = nullptr;
};
map<wstring, DeviceCacheEntry> cache;

struct Subscription {
    GattCharacteristic::ValueChanged_revoker revoker;
};

struct BLEDeviceData {
    wstring id;
    wstring name;
    bool isConnectable = false;
    Subscription* subscription = NULL;
};
vector<BLEDeviceData> deviceList{};

mutex deviceListLock;
condition_variable deviceListSignal;

#pragma endregion

#pragma region CACHE FUNCTIONS

//Call this function to get a device from cache or async if it wasn't found
IAsyncOperation<BluetoothLEDevice> getDevice(wchar_t* deviceId) {
    if (cache.count(wstring(deviceId)) && cache[wstring(deviceId)].device)
        co_return cache[wstring(deviceId)].device;
    BluetoothLEDevice result = co_await BluetoothLEDevice::FromIdAsync(deviceId);
    if (result == nullptr) {
        LOG_ERROR("Failed to connect to device.")
            co_return nullptr;
    }
    else {
        DeviceCacheEntry d;
        d.device = result;
        if (!cache.count(wstring(deviceId))) {
            cache.insert({ wstring(deviceId), d });
        }
        else {
            cache[wstring(deviceId)] = d;
        }
        co_return cache[wstring(deviceId)].device;
    }
}

//Call this function to get a service from cache or async if it wasn't found
IAsyncOperation<GattDeviceService> getService(wchar_t* deviceId, wchar_t* serviceId) {
    if (cache.count(wstring(deviceId)) && cache[wstring(deviceId)].service)
        co_return cache[wstring(deviceId)].service;
    auto device = co_await getDevice(deviceId);
    if (device == nullptr)
        co_return nullptr;
    GattDeviceServicesResult result = co_await device.GetGattServicesForUuidAsync(make_guid(serviceId), BluetoothCacheMode::Cached);
    if (result.Status() != GattCommunicationStatus::Success) {
        LOG_ERROR("Failed getting services. Status: " << (int)result.Status())
            co_return nullptr;
    }
    else if (result.Services().Size() == 0) {
        LOG_ERROR("No service found with uuid")
            co_return nullptr;
    }
    else {
        if (cache.count(wstring(deviceId))) {
            cache[wstring(deviceId)].service = result.Services().GetAt(0);
        }
        co_return cache[wstring(deviceId)].service;
    }
}

//Call this function to get a characteristic from cache or async if it wasn't found
IAsyncOperation<GattCharacteristic> getCharacteristic(wchar_t* deviceId, wchar_t* serviceId, wchar_t* characteristicId) {
    try {
        if (cache.count(wstring(deviceId)) && cache[wstring(deviceId)].characteristic)
            co_return cache[wstring(deviceId)].characteristic;
        auto service = co_await getService(deviceId, serviceId);
        if (service == nullptr)
            co_return nullptr;
        GattCharacteristicsResult result = co_await service.GetCharacteristicsForUuidAsync(make_guid(characteristicId), BluetoothCacheMode::Cached);
        if (result.Status() != GattCommunicationStatus::Success) {
            LOG_ERROR("Error scanning characteristics from service. Status: " << (int)result.Status())
                co_return nullptr;
        }
        else if (result.Characteristics().Size() == 0) {
            LOG_ERROR("No characteristic found with uuid")
                co_return nullptr;
        }
        else {
            if (cache.count(wstring(deviceId))) {
                cache[wstring(deviceId)].characteristic = result.Characteristics().GetAt(0);
            }
            co_return cache[wstring(deviceId)].characteristic;
        }
    }
    catch (...) {
        LOG_ERROR("Exception while trying to get characteristic")
    }
}

#pragma endregion

#pragma region SCAN DEVICES FUNCTIONS

DeviceWatcher deviceWatcher{ nullptr };
mutex deviceWatcherLock;
DeviceWatcher::Added_revoker deviceWatcherAddedRevoker;
DeviceWatcher::Updated_revoker deviceWatcherUpdatedRevoker;
DeviceWatcher::Removed_revoker deviceWatcherRemovedRevoker;
DeviceWatcher::EnumerationCompleted_revoker deviceWatcherCompletedRevoker;

struct TestBLE {
    static void ScanDevices();
    static void StopDeviceScan();
};

//This function would be called when a new BLE device is detected
void DeviceWatcher_Added(DeviceWatcher sender, DeviceInformation deviceInfo) {
    BLEDeviceData deviceData;
    deviceData.id = wstring(deviceInfo.Id().c_str());
    deviceData.name = wstring(deviceInfo.Name().c_str());
    if (deviceInfo.Properties().HasKey(L"System.Devices.Aep.Bluetooth.Le.IsConnectable")) {
        deviceData.isConnectable = unbox_value<bool>(deviceInfo.Properties().Lookup(L"System.Devices.Aep.Bluetooth.Le.IsConnectable"));
    }
    deviceList.push_back(deviceData);
}

//This function would be called when an existing BLE device is updated
void DeviceWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate deviceInfoUpdate) {
    wstring deviceData = wstring(deviceInfoUpdate.Id().c_str());
    for (int i = 0; i < deviceList.size(); i++) {
        if (deviceList[i].id == deviceData) {
            if (deviceInfoUpdate.Properties().HasKey(L"System.Devices.Aep.Bluetooth.Le.IsConnectable")) {
                deviceList[i].isConnectable = unbox_value<bool>(deviceInfoUpdate.Properties().Lookup(L"System.Devices.Aep.Bluetooth.Le.IsConnectable"));
            }
            break;
        }
    }
}

void DeviceWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate deviceInfoUpdate) {
    
}

void DeviceWatcher_EnumerationCompleted(DeviceWatcher sender, IInspectable const&) {
    TestBLE::StopDeviceScan();
    TestBLE::ScanDevices();
}

//Call this function to scan async all BLE devices
void TestBLE::ScanDevices() {
    try {
        lock_guard lock(deviceWatcherLock);
        IVector<hstring> requestedProperties = single_threaded_vector<hstring>({ L"System.Devices.Aep.DeviceAddress", L"System.Devices.Aep.IsConnected", L"System.Devices.Aep.Bluetooth.Le.IsConnectable" });
        hstring aqsFilter = L"(System.Devices.Aep.ProtocolId:=\"{bb7bb05e-5972-42b5-94fc-76eaa7084d49}\")"; // list Bluetooth LE devices
        deviceWatcher = DeviceInformation::CreateWatcher(aqsFilter, requestedProperties, DeviceInformationKind::AssociationEndpoint);
        deviceWatcherAddedRevoker = deviceWatcher.Added(auto_revoke, &DeviceWatcher_Added);
        deviceWatcherUpdatedRevoker = deviceWatcher.Updated(auto_revoke, &DeviceWatcher_Updated);
        deviceWatcherRemovedRevoker = deviceWatcher.Removed(auto_revoke, &DeviceWatcher_Removed);
        deviceWatcherCompletedRevoker = deviceWatcher.EnumerationCompleted(auto_revoke, &DeviceWatcher_EnumerationCompleted);
        deviceWatcher.Start();
    }
    catch (exception e) {
        LOG_ERROR(e.what())
    }
}

void TestBLE::StopDeviceScan() {
    scoped_lock lock(deviceListLock, deviceWatcherLock);
    if (deviceWatcher != nullptr) {
        deviceWatcherAddedRevoker.revoke();
        deviceWatcherUpdatedRevoker.revoke();
        deviceWatcherRemovedRevoker.revoke();
        deviceWatcherCompletedRevoker.revoke();
        deviceWatcher.Stop();
        deviceWatcher = nullptr;
    }
    deviceListSignal.notify_one();
}

#pragma endregion

#pragma region SUBSCRIBE/READ FUNCTIONS

//On this function you can read all data from the specified characteristic
void Characteristic_ValueChanged(GattCharacteristic const& characteristic, GattValueChangedEventArgs args)
{
    LOG_ERROR("Read data from device: " << to_string(characteristic.Service().Device().DeviceId()) << ", data size: " << args.CharacteristicValue().Length())
}

//Function used to subscribe async to the specific device
fire_and_forget SubscribeCharacteristicAsync(wstring deviceId, wstring serviceId, wstring characteristicId, bool* result) {
    try {
        auto characteristic = co_await getCharacteristic(&deviceId[0], &serviceId[0], &characteristicId[0]);
        if (characteristic != nullptr) {
            auto status = co_await characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue::Notify);
            if (status != GattCommunicationStatus::Success) {
                LOG_ERROR("Error subscribing to characteristic. Status: " << (int)status)
            }
            else {
                for (int i = 0; i < deviceList.size(); i++) {
                    if (deviceList[i].id == deviceId) {
                        deviceList[i].subscription = new Subscription();
                        deviceList[i].subscription->revoker = characteristic.ValueChanged(auto_revoke, &Characteristic_ValueChanged);
                        break;
                    }
                }
                if (result != 0)
                    *result = true;
            }
        }
    }
    catch (hresult_error& ex)
    {
        LOG_ERROR("SubscribeCharacteristicAsync error: " << to_string(ex.message().c_str()))
        for (int i = 0; i < deviceList.size(); i++) {
            if (deviceList[i].id == deviceId && deviceList[i].subscription) {
                delete deviceList[i].subscription;
                deviceList[i].subscription = NULL;
                break;
            }
        }
    }
    subscribeSignal.notify_one();
}

//Call this function to subscribe to the specific device so you can read data from it
bool SubscribeCharacteristic(wstring deviceId, wstring serviceId, wstring characteristicId) {
    unique_lock<mutex> lock(subscribeLock);
    bool result = false;
    SubscribeCharacteristicAsync(deviceId, serviceId, characteristicId, &result);
    subscribeSignal.wait(lock);
    return result;
}

#pragma endregion

#pragma region WRITE FUNCTIONS

//Function used to send data async to the specific device
fire_and_forget SendDataAsync(wchar_t* deviceId, wchar_t* serviceId, wchar_t* characteristicId, uint8_t * data, uint16_t size, bool* result) {
    try {
        auto characteristic = co_await getCharacteristic(deviceId, serviceId, characteristicId);
        if (characteristic != nullptr) {
            DataWriter writer;
            writer.WriteBytes(array_view<uint8_t const>(data, data + size));
            IBuffer buffer = writer.DetachBuffer();
            auto status = co_await characteristic.WriteValueAsync(buffer, GattWriteOption::WriteWithoutResponse);
            if (status != GattCommunicationStatus::Success) {
                LOG_ERROR("Error writing value to characteristic. Status: " << (int)status)
            }
            else if (result != 0) {
                LOG_ERROR("Data written succesfully")
                *result = true;
            }
        }
    }
    catch (hresult_error& ex)
    {
        LOG_ERROR("SendDataAsync error: " << to_string(ex.message().c_str()))
        for (int i = 0; i < deviceList.size(); i++) {
            if (deviceList[i].id == deviceId && deviceList[i].subscription) {
                delete deviceList[i].subscription;
                deviceList[i].subscription = NULL;
                break;
            }
        }
    }
    signalWrite.notify_one();
}

//Call this function to write data on the device
bool SendData(wchar_t* deviceId, wchar_t* serviceId, wchar_t* characteristicId, uint8_t * data, uint16_t size) {
    bool result = false;
    unique_lock<mutex> lock(_mutexWrite);
    // copy data to stack so that caller can free its memory in non-blocking mode
    SendDataAsync(deviceId, serviceId, characteristicId, data, size, &result);

    signalWrite.wait(lock);

    return result;
}

#pragma endregion

Finally copy this main function (it can be copied at the end of the same file):

int main() {
    //The mac of the device that will be tested
    wstring deviceMac = L"00:11:22:33:44:55";
    //These are the serviceUUID, readCharacteristicUUID and writeCharacteristicUUID as I said previously
    wstring serviceUUID = L"{47918888-5555-2222-1111-000000000000}";
    wstring readUUID = L"{31a28888-5555-2222-1111-00000000cede}";
    wstring writeUUID = L"{f55a8888-5555-222-1111-00000000957a}";

    //I think it is the mac of the BLE USB Dongle because it is in all device id when they are enumerated
    wstring otherMac = L"24:4b:fe:3a:1a:ba";
    //The device Id that we are looking for
    wstring deviceId = L"BluetoothLE#BluetoothLE" + otherMac;
    deviceId += L"-";
    deviceId += deviceMac;

    //To start scanning just call this function
    TestBLE::ScanDevices();

    //Data to be written all the time
    const uint16_t dataSize = 3;
    uint8_t data [dataSize]= { 0x0, 0xff, 0xff };

    //Wait time in miliseconds between each write
    chrono::milliseconds waitTime = 100ms;

    //It will be executed always
    while (true) {
        //Then every device and their info updated would be in this vector
        for (int i = 0; i < deviceList.size(); i++) {
            //If the device is connectable we will try to connect if we aren't subscribed yet or send information
            if (deviceList[i].isConnectable) {
                //We can do here the following code to know the structure of the device id (if otherMac variable is the BLE USB dongle mac or not)
                //cout << to_string(deviceList[i].id) << endl;
                if (!deviceList[i].subscription && deviceList[i].id == deviceId) {
                    SubscribeCharacteristic(deviceList[i].id, serviceUUID, readUUID);
                }
                else if (deviceList[i].subscription) {
                    SendData(&deviceId[0], &serviceUUID[0], &writeUUID[0], data, dataSize);
                }
            }
        }
        this_thread::sleep_for(waitTime);
    }
}

You will need a BLE device with a service that contains a reading and a writing characteristic, set the corresponding values ​​in the deviceMac, serviceUUID, readUUID and writeUUID variables, you can also modify the bytes that are going to be written in data and dataSize, and the time between writes in waitTime. The otherMac variable should be the mac of the BLE USB dongle device but I recommend that you check it by getting the id of the devices from deviceList inside the for loop.

When you run this code on some rare times you will get the error "Failed getting services. Status:" with result 1 (unreachable) or 3 (access denied) and in the rest of the cases it will be reading the device data correctly and after a while it will give the error "SendDataAsync error: Object has been disposed" and from there it will continue giving "SubscribeCharacteristicAsync error: Object has been disposed", so at some point it will stop being able to read data of the device. What could be the reason?

EDIT 1: It is quite strange because with this code the data is never written correctly (the "Data written succesfully" message is not displayed) but in my completed code I have always been able to write the data, maybe the problem is still the same and it is related to the characteristic stored in the "map <wstring, DeviceCacheEntry> cache" since perhaps it is stored as a copy and when trying to access it at some point it is disposed by Windows (since it is a copy of the original that is stored in the cache) and gives the error as described in the answer to this post in the point named "UPDATE 2 - SOME WEIRDNESS"

  • 1
    @TedLyngmo I add a complete and minimal code version that you can execute to check the problem and also another question similar (but not same) that can help to reach to the correct answer – María Román Jun 16 '21 at 10:46
  • 1
    Very good! I'm sure that makes it easier for anyone having a proper BLE device to test it. – Ted Lyngmo Jun 16 '21 at 10:52
  • 1
    from what I've got from the question the code works but you cannot maintain the connection am I right? – Pvria Ansari Jun 18 '21 at 13:18
  • 1
    @PouriaAnsari exactly, I supposed that maybe is because the characteristic is deleted ("object has beed disposed" exception that is shown when it tries to write or subscribe to using this characteristic), but I also try to not using cache (get always a characteristic async) but it fails – María Román Jun 21 '21 at 06:16
  • As it is explained [on MSDN](https://social.msdn.microsoft.com/Forums/vstudio/en-US/5fdff026-3732-4bd2-b57e-fbeb5ab721c8/bluetooth-le-winrt-c-code-works-if-device-not-paired-fails-with-unreachable-if-device-is-paired?forum=wdk): *"BLE devices don't establish long-term pairing. They connect long enough to exchange data, then they disconnect to listen for broadcasts and announcements."* So all the thing you should do is make the code connect to device again without losing the process. – Pvria Ansari Jun 21 '21 at 08:50
  • also see this https://stackoverflow.com/questions/64208349/winrt-c-issue-with-concurrent-midi-and-ble-communication – Pvria Ansari Jun 21 '21 at 08:59
  • When I tried to pair, it is true that it cannot establish communication with the device, but the case that I have exposed is making a connection without pairing previously, it works but from time to time it cannot read correctly because the characteristic is disposed, so simply I list the devices and when I find it I am ready to listen to the announcements and notifications, so I follow what is specified there. – María Román Jun 21 '21 at 09:39
  • Regarding the second, it makes sense in the case that you try to connect two devices that may cause a disconnection of one of them, but no solution is proposed to keep two devices alive at the same time connected to a single Gatt client. – María Román Jun 21 '21 at 09:39
  • @MaríaRomán I think your problem is that you do not try to properly disconnect and then connect to the device So revise your approach. – Pvria Ansari Jun 22 '21 at 07:57
  • Is this related to "Disconnecting seems to be a bit broken. Quitting an app does not terminate connections. As such make sure you use the App.xml.cs OnSuspended callback to terminate your connections" in the answer of [this post](https://stackoverflow.com/questions/35420940/windows-uwp-connect-to-ble-device-after-discovery)? Where is that "OnSuspended" function in C++ Windows Gatt API? – María Román Jun 22 '21 at 10:10
  • @MaríaRomán It is exactly What I meant you should try to disconnect after a timeout and try to reconnect inside the code block. – Pvria Ansari Jun 23 '21 at 07:46
  • Yes but the problem is that I want to avoid "timeouts", what I want is to establish a connection that will never stop between the Gatt server device and the Windows Gatt Client except if the client disconnects. I also disconnect device when it fails and it can connect again succesfully but I want to avoid that timeout disconnection because my priority is to have a stable connection. – María Román Jun 23 '21 at 07:55
  • @MaríaRomán As I said " BLE devices don't establish long-term connections They connect long enough to exchange data, then they disconnect " Is your data stream stable? Can you be sure you need a stable connection? – Pvria Ansari Jun 23 '21 at 08:00
  • My data stream is stable and I need a stable connection, this program works perfectly on Android and I don't know why in Windows is so different, I could stablish a stable connection on Android but I can't on Windows, does that make sense? I also read some other comments about people getting their BLE projects working in several platform but having problems on Windows. – María Román Jul 01 '21 at 06:05
  • @MaríaRomán That is exactly the problem which is explained in msdn . As I said before you must change your code to work in windows. – Pvria Ansari Jul 02 '21 at 13:26

0 Answers0