6. mbed os GattClient 介绍

这里列出了与 GATT Client 相关的一些类,以及需要使用的方法。

左侧是 mbed os 提供了 BLE 相关工具,用于快速构建应用程序,这个留到最后再说。

中间是 GattClient 类的一些操作接口,主要是注册回调函数,以及从 Server 端发现 Service,Characteristic 和 Characteristic Descriptor 的接口。

右侧是与发现过程相关的,这里 DiscoveredService 类是指发现过程中找到的 Service,DiscoveredCharacteristic 类是指发现过程中找到的 Characteristic 。

image-20201220184348407

6.1. 本文目标

每一个类的 API 详细说明,请查看官方API手册,本文仅将这些 API 汇总出来,并简单分析其用途,最终学习 BLE_GattClient_CharacteristicWrite 这个例程是怎么使用这些 API 的。

6.2. GattClient 操作接口

GattClient 有很多方法,我上面罗列了一些常用方法,并分成了两组:

  • 与注册回调函数相关

  • 与发现服务过程相关

注册 Callback

接口如下:

void onDataRead(ble::ReadCallback_t callback);
void onDataWritten(ble::WriteCallback_t callback);
void onServiceDiscoveryTermination(ServiceDiscovery::TerminationCallback_t callback);
void onHVX(HVXCallback_t callback);
void onShutdown(const GattClientShutdownCallback_t &callback);
  • onDataRead:注册读 attribute 事件的回调函数。

  • onDataWritten:注册写 attribute 事件的回调函数。

  • onServiceDiscoveryTermination:注册服务发现结束事件的回调函数。

  • onHVX:注册 Server 端 Characteristic Value 通知(Notification)或指示(Indication)事件的回调函数。

  • onShutdown:注册连接关闭事件的回调函数。

注意:GattClient 里的读写操作的是某一个 attribute ,后面会看到 DiscoveredCharacteristic 里读写是操作 Characteristic Value 。

Discovery procedures

GATT Client 端可以发现 Service, Characteristic 和 Characteristic Descriptor 。GattClient 类与发现过程相关的 API 提供了这些功能。

// 根据给定 UUID 开启发现 Service 和 Characteristic 的过程。
ble_error_t launchServiceDiscovery (
    ble::connection_handle_t connectionHandle,
    ServiceDiscovery::ServiceCallback_t sc = nullptr,
    ServiceDiscovery::CharacteristicCallback_t cc = nullptr, 
    const UUID &matchingServiceUUID = UUID::ShortUUIDBytes_t(BLE_UUID_UNKNOWN), 
    const UUID &matchingCharacteristicUUIDIn = UUID::ShortUUIDBytes_t(BLE_UUID_UNKNOWN)
);

// 根据给定 UUID 开启发现 Service 的过程。
ble_error_t discoverServices (
    ble::connection_handle_t connectionHandle,
    ServiceDiscovery::ServiceCallback_t callback, 
    const UUID &matchingServiceUUID = UUID::ShortUUIDBytes_t(BLE_UUID_UNKNOWN)
);

// 根据给定 [startHandle, endHandle] 发现 Service 的过程。
ble_error_t discoverServices (
    ble::connection_handle_t connectionHandle, 
    ServiceDiscovery::ServiceCallback_t callback, 
    GattAttribute::Handle_t startHandle, 
    GattAttribute::Handle_t endHandle
);

// 根据给定 Characteristic 发现其 descriptor 的过程。
ble_error_t discoverCharacteristicDescriptors (
    const DiscoveredCharacteristic &characteristic, 
    const CharacteristicDescriptorDiscovery::DiscoveryCallback_t &discoveryCallback, 
    const CharacteristicDescriptorDiscovery::TerminationCallback_t &terminationCallback
);
  • launchServiceDiscovery

该函数用于发现 Service 和 Characteristic(只能由该 API 调用时发现),若在 Server 端发现了与 matchingServiceUUID 相匹配的 UUID ,那么就会回调函数 sc ;若在 Server 端发现了与 matchingCharacteristicUUIDIn 相匹配的 UUID ,那么就会回调函数 cc 。默认查找所有的 Service 和 Characteristic 。connectionHandle 是连接句柄,用于表示与 Server 端的连接 。

  • discoverServices

该函数用于单独发现 Service ,可以用 matchingServiceUUID 来匹配 Server 端的 Service 。也可以查找 Attribute Handle 范围为 [startHandle, endHandle] 里的 Service 。若找到,则回调 callback 函数。 connectionHandle 是连接句柄,用于表示与 Server 端的连接 。

  • discoverCharacteristicDescriptors

该函数用于发现 Characteristic Descriptors ,需要指明是哪一个 characteristic ,每发现一个 descriptor 就回调函数 discoveryCallback ,当所有的 descriptor 都查找完全后,回调函数 terminationCallback

6.3. discovery procedures 中的数据

该部分会介绍 DiscoveredService 和 DiscoveredCharacteristic 这两个类。

DiscoveredService

该类有 getter 和 setter 两类方法,Client 端主要用 getter 方法,因为相应的 Service 已经在 Server 端实现了。

可以通过 getUUID() 获取该 Service 的 UUID ,也可以通过 getStartHandle()getEndHandle() 获得该 Service 在 attribute 表里的 attribute handle 范围。

DiscoveredCharacteristic

与 DiscoveredService 相比较,DiscoveredCharacteristic 拥有的方法更多,这很好理解,因为我们需要读写 Characteristic 的值。

其拥有一个 Properties_t 类,里面存储了该 Characteristic 的属性(读/写/通知…),并提供了相应的读属性方法。

read() 用于读取其 Value 值(需要该 Characteristic 拥有 read 属性),Server 返回读取的数据。

write() 用于写 Value 值(需要该 Characteristic 拥有 write 属性),Server 返回写成功或失败。

writeWoResponse() 用于写 Value 值(需要该 Characteristic 拥有 writeWoResponse 属性),Server 不返回任何消息。

discoverDescriptors() 用于发现其 descroptor 。

6.4. 使用 mbed-os-ble-utils 工具

如何使用

该工具在 mbed-os-ble-utils 目录下,简单使用该工具,我们就可以抽象出一层应用层,在应用层只用于完成业务代码即可。该工具封装了广播、扫描、连接的过程,我们只需关注服务发现过程,以及对 Characteristic 的读写操作过程。

client 使用了 ble_process.h 和 gatt_client_process.h 这两个文件。

该工具的简单使用如下:

int main()
{
    BLE &ble = BLE::Instance();
    events::EventQueue event_queue;

    GattClientDemo demo;

    /* this process will handle basic ble setup and advertising for us */
    GattClientProcess ble_process(event_queue, ble);

    /* once it's done it will let us continue with our demo */
    ble_process.on_init(mbed::callback(&demo, &GattClientDemo::start));
    ble_process.on_connect(mbed::callback(&demo, &GattClientDemo::start_discovery));

    ble_process.start();

    return 0;
}

创建 GattClientProcess 类的一个实例,然后使用 on_init() 注册回调函数,当 mbed os BLE 协议栈初始化完成后调用,一般用于初始化所需内容。之后使用 on_connect() 注册回调函数,当 BLE 协议栈建立连接后调用,这里用于开始发现服务的过程。之后调用 start() 方法启动 BLE 协议栈。

内部架构简析

ble::Gap::EventHandler 这个类定义了许多事件的回调函数,这些函数都是虚函数,如下所示:

struct EventHandler {
        /**
         * Called when an advertising device receive a scan response.
         * @param event Scan request event.
         * @version: 5+.
         * @see AdvertisingParameters::setScanRequestNotification().
         */
        virtual void onScanRequestReceived(const ScanRequestEvent &event)
        {
        }
		......
}

当我们需要使用这些事件时,需要继承 ble::Gap::EventHandler 这个类,然后重写(override)相应的虚函数,所有的虚函数在文章最开始的图片里有。

BLEProcess 这个类用于处理设备位于广播态(Advertising)的相关内容。

GattClientProcess 这个类用于处理设备位于扫描态(Scanning)和发起态(Initiating)的相关内容。

而我们的应用程序则处理连接态(Connection)的相关内容。

有关 BLE 设备的广播态、扫描态、发起态和连接态可以参考爱洋葱的 BLE 系列博客

6.5. 例程分析+实验

以下简单分析 BLE_GattClient_CharacteristicWrite 这个官方例程。

首先看 main.c 函数

int main()
{
    BLE &ble = BLE::Instance();
    printf("\r\nGattClient demo of a writable characteristic\r\n");
    
    GattClientDemo demo;
    /* this process will handle basic setup and advertising for us */
    GattClientProcess ble_process(event_queue, ble);
    /* once it's done it will let us continue with our demo*/
    ble_process.on_init(callback(&demo, &GattClientDemo::start));
    ble_process.on_connect(callback(&demo, &GattClientDemo::start_discovery));
    ble_process.start();
    return 0;
}

这里借助了 ble 工具类 GattClientProcess ,注册了两个回调函数:

  • GattClientDemo::start() ,BLE 协议栈初始化完成后调用。

  • GattClientDemo::start_discovery(),BLE 协议栈建立连接后调用。

然后启动 BLE 协议栈。

BLE 协议栈初始化完成后会调用 GattClientDemo::start() 函数,在这里面注册了读 attribute 完成的回调函数 on_read() ,以及写 attribute 完成的回调函数 on_write()

/** Callback triggered when the ble initialization process has finished */
void start(BLE &ble, events::EventQueue &event_queue) {
    ble.gattClient().onDataRead(on_read);
    ble.gattClient().onDataWritten(on_write);
}

之后成功连接 Server ,GattClientDemo::start_discovery() 函数被调用,开始发现服务过程。

void start_discovery(BLE &ble, events::EventQueue &event_queue, const ble::ConnectionCompleteEvent &event) {
    printf("We are looking for a service with UUID 0xA000\r\n");
    printf("And a characteristic with UUID 0xA001\r\n");

    ble.gattClient().onServiceDiscoveryTermination(discovery_termination);
    ble.gattClient().launchServiceDiscovery(
        event.getConnectionHandle(),
        service_discovery,
        characteristic_discovery,
        EXAMPLE_SERVICE_UUID,
        WRITABLE_CHARACTERISTIC_UUID
    );
}

这里注册了发现服务过程结束的回调函数 discovery_termination() ,然后使用 launchServiceDiscovery 查找 Service (EXAMPLE_SERVICE_UUID – 0xA000) 和 Characteristic (WRITABLE_CHARACTERISTIC_UUID – 0xA001) 。当查找到 Service 时,回调函数 service_discovery() ;当查找到 Characteristic 时,回调函数 characteristic_discovery()

这三个回调函数很简单,就是简单打印信息,并且记录了找到的 Characteristic,因为待会要读写它的 Value 值。

static DiscoveredCharacteristic writable_characteristic;
static bool writable_characteristic_found = false;

void service_discovery(const DiscoveredService *service) {
    if (service->getUUID().shortOrLong() == UUID::UUID_TYPE_SHORT) {
        if (service->getUUID().getShortUUID() == EXAMPLE_SERVICE_UUID) {
            printf("We found the service we were looking for\r\n");
        }
    }
}
void characteristic_discovery(const DiscoveredCharacteristic *characteristic) {
    if (characteristic->getUUID().getShortUUID() == WRITABLE_CHARACTERISTIC_UUID) {
        printf("We found the characteristic we were looking for\r\n");
        writable_characteristic = *characteristic;
        writable_characteristic_found = true;
    }
}
void discovery_termination(ble::connection_handle_t connectionHandle) {
    if (writable_characteristic_found) {
        writable_characteristic_found = false;
        event_queue.call([]{ writable_characteristic.read(); });
    }
}

注意在发现服务过程结束的回调函数 discovery_termination()里 ,调用了这一行代码:

event_queue.call([]{ writable_characteristic.read(); });

因此下一步会执行事件队列里存储的 writable_characteristic.read(); 这个函数,读取数据完成后会触发 BLE 协议栈回调之前使用 onDataRead() 注册过的回调函数,即 on_read()

void on_read(const GattReadCallbackParams *response) {
    if (response->handle == writable_characteristic.getValueHandle()) {
        /* increment the value we just read */
        uint8_t value = response->data[0];
        value++;
        /* and write it back */
        writable_characteristic.write(1, &value);
        printf("Written new value of %x\r\n", value);
    }
}

这里简单将第一个字节的数据读取出来,并加 1,然后又使用 writable_characteristic.write() 将该数据写回 Server 端,Server 发生写完成响应包给 Client 后触发 BLE 协议栈回调之前使用 onDataWritten() 注册过的回调函数,即 on_write()

void on_write(const GattWriteCallbackParams *response) {
    if (response->handle == writable_characteristic.getValueHandle()) {
        event_queue.call_in(5000ms, []{ writable_characteristic.read(); });
    }
}

这里只是加入了一个事件:5 秒后再次使用 writable_characteristic.read() 读取数据,循环往复,永不停止。

实现现象如下:

GattClient demo of a writable characteristic
Ble process started.
Ble instance initialized
Started scanning for "GattServer"
We found "GattServer", connecting...
Connected to: 84:2e:14:31:b6:38
We are looking for a service with UUID 0xA000
And a characteristic with UUID 0xA001
We found the service we were looking for
We found the characteristic we were looking for
Written new value of 2
Written new value of 3
Written new value of 4
Written new value of 5
Written new value of 6