Попытка создать клиентское приложение Gatt для Windows C++, которое не завершается ошибкой при установлении соединения

Я использую Windows API Gatt Client BLE для C++ моя цель состоит в том, чтобы соединить два устройства (но в этом случае я попробую только одно) и постоянно читать и записывать данные, не закрывая устройство в любое время. У всех моих устройств есть одна конкретная служба, которая содержит характеристику чтения и запись.

КАК ПРОВЕРИТЬ:

Используйте Visual Studio 2017 (v141) с Windows SDK версии: 10.0.18362.0, создайте новое консольное решение (.exe), измените платформу в Project -> Properties на Win32 и перейдите в Project -> Properties -> C/C++ -> Командная строка и добавьте следующие параметры:

/std:c++17 /await 

Затем скопируйте следующий код в файл (вы можете скопировать все в тот же файл .cpp):

#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

Наконец, скопируйте эту основную функцию (ее можно скопировать в конец того же файла):

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

Вам понадобится устройство BLE с сервисом, который содержит характеристику чтения и записи, установите соответствующие значения в deviceMac, serviceUUID, readUUID и writeUUID, вы также можете изменить байты, которые будут записываться в data и dataSize, а также время между записями в время ожидания. Переменная otherMac должна быть Mac-устройством USB-ключа BLE, но я рекомендую проверить его, получив идентификаторы устройств из deviceList внутри цикла for.

Когда вы запускаете этот код в некоторых редких случаях, вы получите сообщение об ошибке Не удалось получить услуги. Статус: с результатом 1 (недоступен) или 3 (отказано в доступе), а в остальных случаях будет правильно считывать данные устройства и после в то время как он выдаст ошибку Ошибка SendDataAsync: объект был удален, и оттуда он продолжит выдавать SubscribeCharacteristicAsync error: объект был удален, поэтому в какой-то момент он перестанет быть возможность чтения данных устройства. Что может быть причиной?

EDIT 1: Это довольно странно, потому что с этим кодом данные никогда не записываются правильно (сообщение Данные успешно записаны не отображается), но в моем законченном коде я всегда был может записать данные, возможно, проблема все та же и связана с характеристикой, хранящейся в кэше map ‹wstring, DeviceCacheEntry›, поскольку, возможно, она хранится как копия и при попытке доступа он в какой-то момент удаляется Windows (поскольку это копия оригинала, который хранится в кеше) и выдает ошибку, как описано в ответе на это сообщение в пункте под названием ОБНОВЛЕНИЕ 2 - НЕКОТОРЫЕ СТРАНЫ


person María Román    schedule 11.06.2021    source источник
comment
@TedLyngmo Я добавляю полную и минимальную версию кода, которую вы можете выполнить, чтобы проверить проблему, а также еще один вопрос, похожий (но не такой же), который может помочь найти правильный ответ.   -  person María Román    schedule 16.06.2021
comment
Очень хороший! Я уверен, что это облегчит его тестирование любому, у кого есть подходящее устройство BLE.   -  person Ted Lyngmo    schedule 16.06.2021
comment
из того, что я получил из вопроса, код работает, но вы не можете поддерживать соединение, я прав?   -  person Pouria Ansari    schedule 18.06.2021
comment
@PouriaAnsari точно, я предположил, что, возможно, это связано с тем, что характеристика удалена (объект имеет исключенное исключение, которое отображается, когда он пытается записать или подписаться на использование этой характеристики), но я также стараюсь не использовать кеш (всегда получаю характерную асинхронность ) но не получается   -  person María Román    schedule 21.06.2021


Ответы (1)


Как поясняется в приведенной ниже ссылке, устройства BLE не устанавливают долгосрочное сопряжение. Они подключаются достаточно долго для обмена данными, затем отключаются для прослушивания трансляций и объявлений.

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

Поэтому все, что вам нужно сделать, это снова подключить код к устройству без потери процесса.

person Pouria Ansari    schedule 21.06.2021
comment
также см. эту stackoverflow.com/questions/64208349/ - person Pouria Ansari; 21.06.2021
comment
Когда я пытался выполнить сопряжение, это правда, что он не может установить связь с устройством, но случай, который я выявил, заключается в установлении соединения без предварительного сопряжения, он работает, но время от времени он не может правильно читать, потому что характеристика расположена, так просто я перечисляю устройства и когда нахожу готов слушать анонсы и уведомления, так что следую тому что там указано. - person María Román; 21.06.2021
comment
Что касается второго, это имеет смысл в том случае, если вы пытаетесь подключить два устройства, что может привести к отключению одного из них, но не предлагается никакого решения, позволяющего поддерживать работоспособность двух устройств одновременно, подключенных к одному клиенту Gatt. - person María Román; 21.06.2021
comment
@MaríaRomán Я думаю, ваша проблема в том, что вы не пытаетесь правильно отключить, а затем подключиться к устройству. Так что пересмотрите свой подход. - person Pouria Ansari; 22.06.2021
comment
Это связано с отключением, кажется, немного сломано. Выход из приложения не прерывает соединения. Таким образом, убедитесь, что вы используете обратный вызов App.xml.cs OnSuspended для завершения ваших соединений в ответе этот пост? Где эта функция OnSuspended в C++ Windows Gatt API? - person María Román; 22.06.2021
comment
@MaríaRomán Это именно то, что я имел в виду, вы должны попытаться отключиться после тайм-аута и попытаться снова подключиться внутри блока кода. - person Pouria Ansari; 23.06.2021
comment
Да, но проблема в том, что я хочу избежать тайм-аутов, я хочу установить соединение, которое никогда не разорвется между серверным устройством Gatt и клиентом Windows Gatt, за исключением случаев, когда клиент отключается. Я также отключаю устройство, когда оно выходит из строя, и оно может снова успешно подключиться, но я хочу избежать этого отключения по тайм-ауту, потому что мой приоритет - иметь стабильное соединение. - person María Román; 23.06.2021
comment
@MaríaRomán Как я уже сказал, устройства BLE не устанавливают долгосрочные соединения. Они подключаются достаточно долго для обмена данными, а затем отключаются. Стабилен ли ваш поток данных? Можете ли вы быть уверены, что вам нужно стабильное соединение? - person Pouria Ansari; 23.06.2021
comment
Мой поток данных стабилен, и мне нужно стабильное соединение, эта программа отлично работает на Android, и я не знаю, почему в Windows все по-другому, я могу установить стабильное соединение на Android, но не могу на Windows, делает ли это смысл? Я также читал некоторые другие комментарии о людях, которые заставляют свои проекты BLE работать на нескольких платформах, но имеют проблемы с Windows. - person María Román; 01.07.2021
comment
@MariaRomán Это именно та проблема, которая описана в msdn. Как я уже говорил, вы должны изменить свой код, чтобы он работал в Windows. - person Pouria Ansari; 02.07.2021