Bluetooth

  • Post author:
  • Post category:其他


Bluetooth

本文翻译自Kolban’s book on ESP32

ESP32具有原生蓝牙支持(版本4.2)。 这意味着它可以与蓝牙设备(如键盘,鼠标和手机)进行交互。 让我们回顾一下蓝牙对我们意味着什么。 蓝牙是通过无线电信号提供数据传输的无线通信协议/技术。 让我们假设ESP32是连接的一端,任何其他蓝牙设备可以在另一端。 出于安全目的,任何蓝牙设备不能简单地“使用”没有一些明确的授权。 例如,如果我可以将蓝牙耳机放在手机附近,并开始聆听您的来电,那将是非常错误的。为了实现安全性,需要执行称为“配对”的过程。 这实现了两个蓝牙设备之间的信任级别,以便随后允许连接而不必重新配对。

Bluetooth specification

蓝牙是多个电子设备之间无线通信的规范。 目前有两个主要标准,分别是蓝牙(Classic)和蓝牙LE。 “LE”代表低能量,是希望由电池供电并且具有足够寿命的设备的规格。

在蓝牙故事中,我们有设备是“masters(主设备)”和设备“slaves(从设备)”。 slave只能与master建立连接并通信,而master可以与多个slave建立并发连接。 一个slave不能直接与另一个slave通信。 要通过master进行中继来实现slave和slave的通信。

最简单的通信是一个主机和一个从机,但是如果我们有多个从机连接到同一个主机,则产生的“network(网络)”被称为“piconet(微微网)”。

参与对话的每个设备都有一个唯一的地址,它是一个48位值,通常写为12个十六进制值(6个字节)。 这个地址被称为“蓝牙设备地址”,在其他文档中可能被缩写为“BD_ADDR”。

蓝牙地址的编码是前24位对负责分配剩余地址的组织进行编码,其余的24位是地址本身。 然而, 48位是设备的完整标识。

除了具有唯一的地址,每个设备都可以有一个符号名称来帮助我们有意义地识别它。这被称为 display name(显示名称)。 显示名称只是一个


page 181


到蓝牙地址的映射,它实际上是BD_ADDR地址,用于区分一个设备。 设备名称没有唯一性。 多个设备可以选择相同的设备名称。

让我们假设,最初,我们有两个设备,他们互不知道。 现在必须有一个发现过程。 其中一人将播出“inquiry(询问)”请求。 接收询问的设备可以通过发送他们自己的地址和可能的附加信息来回应以显示自己的存在。 查询的响应通常不包含响应设备的显示名称。 如果需要显示名称,则查询者现在可以发送请求,进一步获取响应设备的显示名称.

设备不一定必需响应查询请求。 它有一个属性设置,称为“inquiry scan(查询扫描)”,控制它是否对此作出响应。 如果该属性是打开的,那么它将响应查询请求,如果关闭,则不会响应。 将“inquiry scan(查询扫描)”这个短语当作设备选择它是否执行的动作“scanningfor inquires(扫描查询)”的操作。

一旦这两个设备知道彼此的地址,就可以通过称为“paging(分页)”的过程在它们之间形成连接。 同样,设备不必服务于接收到的连接请求。 它有一个属性设置,称为“pagingscan(分页扫描)”,控制这一点。 如果打开,则分页请求将导致形成新的连接。 如果关闭,则不会接受新的连接请求。

一旦在设备之间形成连接,该连接可以保持在各种状态,最常见的是活动的。 然而,其他状态可用,并且在没有预期数据的活动通信时,可以节省电力。

为了允许设备彼此通信,必须涉及安全元素。 我们通常不希望任意设备能够相互连接并共享任意信息。 为了达到这个目的,我们制定了一个叫做“bonding(邦定)”的过程。 结合是通过“pairing(配对)”的概念来实现的。 在配对中,设备交换他们的地址,姓名和其他数据,并生成彼此共享的密钥。 配对通常需要来自用户的明确交互以允许配对成功。 用户交互可以像“我批准这个设备”一样简单,只需点击一下按钮,或者通过输入密码来验证身份。

蓝牙协议为不同类别的电源提供支持。 这直接转化为收音机的信号强度。 请记住,广播的功率越大,电源的耗电就越重。 如果电源是电池,用于传输数据的电力越多,电池的寿命就越短。


Page 182


在较低级别,蓝牙负责在合作伙伴之间交换数据。 但是,蓝牙技术远不止简单的数据交换。 为了提供由多个制造商构建的设备之间的互操作性,已经定义了称为“profiles(配置文件,剖面)”的更高级协议。 这些配置文件定义了“what(什么)”通过蓝牙传输给定的设备功能。

我们会遇到的一些配置文件包括:


  • HSP

    – Head Set Profile (eg. a Bluetooth ear piece).

  • HFP

    – Hands Free Profile (eg. Bluetooth communication in a car).

  • HID

    – Human Interface Device (eg. a keyboard or mouse).

  • SPP

    – Serial Port Profile.

  • A2DP

    – Advanced Audio Distribution profile (eg. connection to Bluetooth speakers).

  • AVRCP

    – Audio Visual Remote Control Profile.

由于知道存在这些协议,两个蓝牙设备在彼此的范围内是不够的,它们也必须支持所期望使用的相同的配置文件。

在比连接更高的层面上是我们可以使用的传输协议。这些包括:


  • RFCOMM

    – Radio Frequency Communications Protocol. This protocol provides a reliable stream oriented transmission. Loosely, you can compare it to TCP.

  • L2CAP

    – Logical Link Control and Adaption Protocol. This is a packet oriented protocol. RFCOMM builds on L2CAP.

  • ACL

    – Asynchronous Connection oriented Logical protocol. L2CAP builds on ACL.

  • SCO

    – Voice quality audio protocol.

为了允许多个对话并行处理,引入了“端口”的概念。 这与TCP / IP端口号相似。 L2CAP端口可以是1到32767之间的奇数值。对于RFCOMM,端口号称为通道,介于1到30之间。在蓝牙文档中,端口被称为“Protocol Service Multiplexers(协议服务复用器)”或“PSMs”。


Page 183


L2CAP使用的某些端口号被指定为保留用于明确定义的功能。

也可以参阅:



Introduction to Bluetooth Low Energy

Bluetooth UUIDs

一个UUID是一个16字节的值(128位)。 它们通常以十六进制编写, 1个字节对应2个十六进制数字。 最常见的书面格式是4-2-2-2-6(字节)。

XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

当一个人收到一个UUID时,就是UUID的值,用来描述服务或数据的性质。 每种不同类型的服务都有自己独特的UUID。

因为16字节是描述服务标识的大量数据,所以我们有机会以16位或32位的简短形式描述UUID …但是这些保留给由蓝牙规范所有者构建的服务。 如果实现其中一种服务,那么我们不需要传输整个128位的UUID,但可以少一些。 一种特殊的UUID形式:

XXXXXXXX-0000-1000-8000-00805F9B34FB

在只需要前4个字节(32位)来标识服务类型的地方是可用的

0000XXXX-0000-1000-8000-00805F9B34FB

在只需要前2个字节(16位)来识别服务类型的地方是可用的。

如果需要,我们可以使用uuidgen命令或从网站www.uuidgenerator.net生成UUID。

Bluetooth GAP

通用访问协议(GAP)。 确定哪些设备可以是GAP相互作用。 在GAP故事中,主要有两类“things(事物)”。有中央设备和外围设备。




:payload(有效载荷):除去报文头和报文长度的实际数据,此处理解为数据包

我们可以通过Advertising Data payload(广播数据包)或Scan Response payload(扫描响应包)发送广播

外围设备将不断地发送最大为31个字节的advertising payload(广播包)。

外设在每个广播时间间隔内传输其广播数据。 广播数据包存在于所有BT设备中。 如果广播发送者需要扫描响应,它可以请求扫描响应,并且外围设备将发回扫描响应包。


Page 184


当外设正在传输广播数据时,我们可以把它看作是处于广播模式。 它正在传输其可能或不可能被相应的中心看到的数据。

BLE广播的概念不仅仅是寻找设备来形成后续连接。 广播数据包可以在其有效载荷中包含数据。 如果传输的数据不需要安全(例如,外部温度),那么我们就拥有一个有趣的解决方案。 外围设备可以简单地广播它的数据包,中央可以接收它们而不需要建立连接。 只要数据足够小,接收机就可以检查有效负载并接收数据。 在这个故事中,外围设备扮演着“broadcaster(广播员)”的角色,而中央则扮演着“observer(观察者)”的角色。 在这种模式下工作时,要意识到数据只能在一个方向上流动……从广播者到观察者。 如果您需要发送回应,则需要建立连接。

广告的速率可以设置为20毫秒至10.24秒之间的时间段,步长为0.625毫秒。 因此,我们可以频繁或缓慢地传送广告数据包。

也可以参阅:


A BLE Advertising Primer

Low level packet

在最低级别,广告数据包包含以下原始数据。 大多数情况下,我们不应该看到这些信

息。

原始蓝牙数据包

在最底层,广告数据包包含以下原始数据。 大多数情况下,我们不应该看到这些信息


header

指明了一个广告数据包类型,可以是下列之一:

  • ADV_IND
  • ADV_DIRECT_IND


Page 185


  • ADV_NONCONN_IND

  • ADV_SCAN_IND

  • SCAN_REQ – A request to receive additional scan data.
  • SCAN_RSP – The response to a previous SCAN_REQ.
  • CONNECT_REQ – A request to connect.

GAP Advertizing data

让我们来关注一个BLE外围广播信息的概念。 在高层次上,它会看起来像:

蓝牙广播

广播者以每20ms短或每10.28秒长的时间重复发送广播。 这个速度被称为advertising interval(广播时间间隔)。为了确保我们不会与两个或两个以上的广播者始终相互传输,最终会加入一个随机的欺诈因素.广播数据有效载荷的最大大小为31个字节。 每个有效载荷由一个或多个数据结构组成,其中每个结构的格式是:

Length Advertising Data Type Data
长度 广播数据类型 数据

length占用一个字节,指明了其后的数据有多少字节.有效负载中的data数量是可变的,但是当然,数据总量必须是31个字节或更少。 长度为0或可以使用可忽略的结构类型。

以下是我收到的真实有效负载的示例:

020105020A000319C1030302E0FF11094D4C452D3135202020202020202020

现在我们可以解析这个如下:

02 01 05 – advertising type 0x01; length 2 bytes
02 0a 00 – advertising type 0x0A; length 2 bytes
03 19 c1 03 – advertising type 0x19; length 3 bytes
03 02 e0 ff – advertising type 0x02; length 3 bytes
11 09 4d 4c 45 2d 31 35 20 20 20 20 20 20 20 20 20 – advertising type 09; length 17 bytes.


Page 186


这是一个简单的例程,将通过结构…

while (!finished)
{
  length = *payload;
  payload++;
  if (length != 0)
  {
    ad_type = *payload;
    payload += length;
    ESP_LOGD(tag, "Type: 0x%.2x, length: %d", ad_type, length);
  }
  sizeConsumed += 1 + length;
  if (sizeConsumed >= 31 || length == 0)
  {
    finished = 1;
  }
} // !finished

现在我们知道如何查看结构并访问它们,明显的问题是每个结构“意味着什么”。 对于大多数情况下,它们都是在BLE规范中构建的。 每个结构(在长度之后)具有单个字节,即“广告数据类型”。 这些单字节数字代码在这里描述:


https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile

在我们的ESP32中,对每个类型都有其定义:


  • ESP_BLE_AD_TYPE_FLAG

    (0x01) – The advert contains a byte of flags that are defined as following:

    • Bit 0 – LE Limited Discoverable Mode
    • Bit 1 – LE General Discoverable Mode
    • Bit 2 – BR/EDR is NOT supported.
    • Bit 3 – Indicates whether LE and BR/EDR Controller operates simultaneously
    • Bit 4 – Indicates whether LE and BR/EDR Host operates simultaneously
    • Bits 5-7 – Reserved.

  • ESP_BLE_AD_TYPE_16SRV_PART

    (0x02) – Incomplete list of 16bit service UUIDs.

  • ESP_BLE_AD_TYPE_16SRV_CMPL

    (0x03) – Complete list of 16 bit service UUIDs.


Page 187



  • ESP_BLE_AD_TYPE_32SRV_PART

    (0x04) – Incomplete list of 32bit service UUIDs.

  • ESP_BLE_AD_TYPE_32SRV_CMPL

    (0x05) – Complete list of 32bit service UUIDs.

  • ESP_BLE_AD_TYPE_128SRV_PART

    (0x06) – Incomplete list of 128bit service UUIDs.

  • ESP_BLE_AD_TYPE_128SRV_CMPL

    (x07) – Complete list of 128bit service UUIDs.

  • ESP_BLE_AD_TYPE_NAME_SHORT

    (0x08) – Shortened local name.

  • ESP_BLE_AD_TYPE_NAME_CMPL

    (0x09) – Complete local name.

  • ESP_BLE_AD_TYPE_TX_PWR

    (0x0A) – Transmit power level.

  • ESP_BLE_AD_TYPE_DEV_CLASS

    (0x0D)

  • ESP_BLE_AD_TYPE_SM_TK

    (0x10)

  • ESP_BLE_AD_TYPE_SM_OOB_FLAG

    (0x11)

  • ESP_BLE_AD_TYPE_INT_RANGE

    (0x12)

  • ESP_BLE_AD_TYPE_SOL_SRV_UUID

    (0x14)

  • ESP_BLE_AD_TYPE_128SOL_SRV_UUID

    (0x15)

  • ESP_BLE_AD_TYPE_SERVICE_DATA

    (0x16)

  • ESP_BLE_AD_TYPE_PUBLIC_TARGET

    (0x17)

  • ESP_BLE_AD_TYPE_RANDOM_TARGET

    (0x18)

  • ESP_BLE_AD_TYPE_APPEARANCE

    (0x19) – It is likely this conforms to the assigned numbers found here
02 01 05 – advertising type 0x01ESP_BLE_AD_TYPE_FLAG
02 0a 00 – advertising type 0x0AESP_BLE_AD_TYPE_TX_PWR
03 19 c1 03 – advertising type 0x19ESP_BLE_AD_TYPE_APPEARANCE
03 02 e0 ff – advertising type 0x02ESP_BLE_AD_TYPE_16SRV_PART


Page 188


11 09 4d 4c 45 2d 31 35 20 20 20 20 20 20 20 20 20 – advertising type 0x09ESP_BLE_AD_TYPE_NAME_CMPL

Advertisability – limited and general

//FIXME:此处翻译的不好

“advertisability”是一个真正的单词吗?我想不是……但它描述了我们想要涵盖的内容。当我们有一个蓝牙外设时,我们需要通过一个蓝牙中央设备来发现它,以便中央请求外设。我们实现这一目标的方式是通过广播其广播数据包来使外围设备可见。

当外设开始广播时,它可以将广播标记为limited-discoverable或general-discoverable。这里的概念是limiteddiscoverable的设备已经“最近”被启用开始广播,而“general-discoverable”的设备通常连续广播。

“那么你可能会问什么?那么,想一想寻找一个设备的用户。通过按下某个按钮开始广播以添加新设备以便找到并配对,这种情况并不少见。我们希望这个新设备在其他设备列表中显示得更高……这可以通过识别仅在有限时间段内广播的设备启用其“limited-discoverable”标志并提供提示来实现到运行在中央的软件,该设备可能将成为用户正在寻找的软件。

一般广播数据包被标记为ADV_IND。

Directed advertising

另一种形式的广播被称为定向。 在这种模式下,广播者将广播标记为“direct”,并且数据包包含做广播的外围设备的地址以及它希望连接的中心的地址。 收到这样一个数据包后,中央可以选择立即形成一个连接。


Page 189


定向广播数据包被标记为ADV_DIRECT_IND。

Non-connectable advertising

广播数据包可能被标记为不可连接。 这表明中央不应该试图形成联系。 这种模式通常由仅发布信息的广播者使用,以供观察者查找。

A non-connectable advertising packet is flagged as ADV_NONCONN_IND.

Filtering devices

当我们正在寻找一个设备时,我们可能会发现比我们想要的更多的设备。 我们可以选择过滤一组可用设备并隐藏我们认为不感兴趣的设备,而不是向用户展示所有设备。 例如,如果我在手机上运行心率监视器应用程序,我真的只对可以提供此类信息的设备感兴趣。 蓝牙耳机或外部温度不是我要找的。 我们想做的是将所有找到的设备进行过滤,只显示我们感兴趣的标准设备。 为了实现这一目标,每个设备不仅要通过广播证明其存在,还要提供可用于过滤的信息。

Performing a scan

如果没有人在扫描,那么生成广播数据的价值就不大。 在BLE中,收听广播数据的行为称

为“扫描”。 要在ESP32中执行扫描,我们执行以下任务:

  1. 注册一个callback 函数去处理接收到的数据
  2. 定义我们想要执行的扫描方式的参数
  3. 告诉ESP32开始扫描

    使用

    esp_ble_gap_register_callback()

    注册 GAP 事件回调函数,当事件到来时回调函数被调用.

    使用

    esp_ble_gap_set_scan_params()

    去设置我们想要执行的扫描方式

    使用

    esp_ble_gap_start_scanning()

    启动扫描

    如果我们需要在扫描完成之前中断扫描,我们可以调用

    esp_ble_gap_stop_scanning()

    .


    Page 190

我们调用esp_ble_gap_register_callback() 注册的处理函数是我们大多数逻辑发生的地方。 处理函数的定义如下:


void gap_event_handler(esp_gap_ble_cb_event_t event,esp_ble_gap_cb_param_t *param)


param

是结构体组成的联合体

Event Type Data Property
ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT adv_data_cmpl
ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT adv_data_raw_cmpl
ESP_GAP_BLE_ADV_START_COMPLETE_EVT adv_start_cmpl
ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT scan_param_cmpl
ESP_GAP_BLE_SCAN_RESULT_EVT scan_rst
ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT scan_rsp_data_raw_cmpl
ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT scan_rsp_data_cmpl
ESP_GAP_BLE_SCAN_START_COMPLETE_EVT scan_start_cmpl

//TODO:此处未翻译

event 定义了我们接收到的事件类型.其类型包括:


  • ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT

    – Raw advertising data operation complete.


  • ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT

    – Called when advertising data set is complete. Structure parameter is called

    scan_rsp_data_cmpl

    .


    • esp_bt_status_t status

      – The status of the event.

  • ESP_GAP_BLE_ADV_START_COMPLETE_EVT

    – Called when advertising scan startup is complete. The parameter is a property called scan_start_cmpl which contains:

    • esp_bt_status_t status – The status of the event.

  • ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT

    – Called when scan parameters set complete. Structure parameter is called scan_param_cmpl.

    • esp_bt_status_t status

  • ESP_GAP_BLE_SCAN_RESULT_EVT

    – The param is an instance of esp_ble_gap_cb_param_t. Called when one scan result is ready. The structure parameter is called scan_rst.

    • esp_gap_search_evt_t search_evt – Choices are:

    • ESP_GAP_SEARCH_INQ_RES_EVT

      – We have received a search result.


    Page 191



    • ESP_GAP_SEARCH_INQ_CMPL_EVT

      – The search is complete.

    • ESP_GAP_SEARCH_DISC_RES_EVT

    • ESP_GAP_SEARCH_DISC_BLE_RES_EVT

    • ESP_GAP_SEARCH_DISC_CMPL_EVT

    • ESP_GAP_SEARCH_DI_DISC_CMPL_EVT

    • ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT


    • esp_bd_addr_t bda

      – The address of the device. 6 bytes of data.


    • esp_bt_dev_type_t dev_type

      – One of:


    • ESP_BT_DEVICE_TYPE_BREDR

    • ESP_BT_DEVICE_TYPE_BLE

    • ESP_BT_DEVICE_TYPE_DUMO


    • esp_ble_addr_type_t ble_addr_type

      – One of


    • BLE_ADDR_TYPE_PUBLIC

    • BLE_ADDR_TYPE_RANDOM

    • BLE_ADDR_TYPE_RPA_PUBLIC

    • BLE_ADDR_TYPE_RPA_RANDOM


    • esp_ble_evt_type_t ble_evt_type

      – One of


    • ESP_BLE_EVT_CONN_ADV

    • ESP_BLE_EVT_CONN_DIR_ADV

    • ESP_BLE_EVT_DISC_ADV

    • ESP_BLE_EVT_NON_CONN_ADV

    • ESP_BLE_EVT_SCAN_RSP


    • int rssi

      – The signal strength.


    • uint8_t ble_adv[ESP_BLE_ADV_DATA_LEN_MAX]

      – The advertised data.


    • int flag

      – Flags

    • bit 0 – Limited Discoverable
    • bit 1 – General Discoverable
    • bit 2 – BR/EDR not supported
    • bit 3 – Simultaneous LE and BR/EDR (Controller)


    Page 192


    • bit 4 – Simultaneous LE and BR/EDR (Host)


    • int num_resps

      – The number of responses received. This is valid when the

      search_evt type is ESP_GAP_SEARCH_INQ_CMPL_EVT.


    • uint8_t adv_data_len


    • uint8_t scan_rsp_len


  • ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT

    – ???.


  • ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT

    – Called when the scan response data set is complete. Structure parameter is called:

    scan_rsp_data_cmpl

    .


    • esp_bt_status_t status

  • ESP_GAP_BLE_SCAN_START_COMPLETE_EVT

    – ???.

    A typical series of events received might be:


  • ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT


  • ESP_GAP_BLE_SCAN_START_COMPLETE_EVT


  • ESP_GAP_BLE_SCAN_RESULT_EVT

    – ESP_GAP_SEARCH_INQ_RES_EVT


  • ESP_GAP_BLE_SCAN_RESULT_EVT

    – ESP_GAP_SEARCH_INQ_CMPL_EVT

See also:

  • esp_ble_gap_register_callback
  • Error: Reference source not found
  • esp_ble_gap_start_scanning
  • esp_ble_gap_stop_scanning

Performing advertising

如果我们的ESP32将成为外设,那么它将广播它的存在。 ESP-IDF提供了一些API来

完成这项任务。 在高层次:

  1. Call esp_ble_gap_config_adv_data() to specify the content of our periodic

    advertisement.
  2. Call esp_ble_gap_start_advertising() to initiate the periodic advertisement.


Page 193


虽然表面简单,但我们需要考虑所有可用于我们的独特参数,并且有很多参数。


esp_ble_gap_config_adv_data()

. 这是指定广播有效载荷内容的地方

这是一个示例结构:

static esp_ble_adv_data_t test_adv_data;
test_adv_data.set_scan_rsp = false,
test_adv_data.include_name = true,
test_adv_data.include_txpower = true,
test_adv_data.min_interval = 0x20,
test_adv_data.max_interval = 0x40,
test_adv_data.appearance = 0x00,
test_adv_data.manufacturer_len = 0,
test_adv_data.p_manufacturer_data = NULL,
test_adv_data.service_data_len = 0,
test_adv_data.p_service_data = NULL,
test_adv_data.service_uuid_len = 32,
test_adv_data.p_service_uuid = test_service_uuid128,
test_adv_data.flag = 0x2,
};

一旦我们开始广播,我们可以使用以下方式检查广播的信息:

$ sudo hcitool lescan

See also:

– esp_ble_gap_config_adv_data

– esp_ble_gap_start_advertising

– esp_ble_gap_stop_advertising

Bluetooth GATT

通用属性协议(奇怪地称为GATT)提供了一种以标准格式传递数据的机制。 GATT总是存在于BLE中。 将GATT视为发送和接收GATT服务器(在运行时) “记录”数据的一种方式。 当GATT服务器发生有趣的事情时,客户端可以明确请求数据项的值以及接收推送的异步通知作为事件。

在这个属性的模型中,每个属性(attribute )由表中的一行组成。 属性的“handle”属性形成了它的关键。 请注意,句柄与行号不同。 句柄不需要连续。 但是,没有两个属性可以具有相同的句柄值。 服务器如何在内部管理属性不是规范的一部分,由BLE运行时设计人员决定。


Page 194


在协议的高层,有一个服务(service )的概念。 该服务是功能相关属性的分组。 每个服务都有自己UUID。 在服务中,特征是服务属性的特征。 每个特征类型都由其自己的UUID值标识。 另外一个特性包含一个值,特性,安全性,也可能包含描述符(descriptors)。

GATT-Profile

在GATT的规范中,我们有两个角色的概念……即客户端(client )和服务器(server)的角色。 GATT服务器通常是被动的实体,它等待GATT客户的请求并在它们到达时提供服务。

当GATT客户端启动时,它几乎没有涉及GATT服务器,并首先对服务器执行查询以确定其服务和特征。

每个BLE设备都必须具备成为GATT服务器的能力。在我们更多地讨论GATT之前,让我们先来谈谈ATT。 ATT是属性协议,是GATT的基础。想想一个维护一组属性的逻辑服务器。 我们将一个属性定义为:

  • handle – A 16 bit number representing the key/identity of the attribute.
  • type – 128 bit UUID describing the type of the attribute.
  • permissions – Permissions on the attribute. The permissions can be:

    • None
    • Read


    Page 195


  • Write
  • Read/Write

    • value – The value of the attribute. Can be up to 512 bytes.

我们可以将这些属性看作是一种表格形式:

handle1 type1 permissions1 value1
handle2 type2 permissions2 value2
handle3 type3 permissions3 value3
handle… type… permissions… value…

GATT 服务器管理可以由GATT客户端读取或写入的属性。 GATT服务器在内部如何存储这些属性未在规范中定义,并由实施者选择。

这个原始属性的故事在GATT协议中由服务和特征的概念进一步规定。

想想服务是一种合乎逻辑的描述,即GATT服务器准备好做一些明确定义的事情.如果它声称提供服务,那么它必须遵守该服务描述的合同。该服务合同由一组特征和组成该服务的一组特征定义。

当我们定义一个新的服务时,一个条目被添加到表中,如下所示:

handle UUID~primaryService~ (0x2800) permissions UUID of service
handle UUID~characteristic~ (0x2803) permissions … data …

GATT Characteristic

研究这个概念,即在底层故事中,一切都是属性,让我们用属性来解释特征的概念。

每个特征(characteristic )都被声明为:


Page 196


UUID (0x2803) Read only • properties – 1 byte• value handle – 2 bytes• characteristic UUID – 2, 4 or 16 bytes

属性是一个位域,包括:

  • broadcast – Characteristic value can be in advertising packets.
  • read – Client may read the characteristic value.
  • write – Client may write the characteristic value and send a response.
  • write without response – Client may write the characteristic value with no response.
  • notify – Value notification available.
  • indicate – Value indication available.
  • signed write command – ???

Being a GATT client

从ESP32的角度来看, GATT客户端需要做:

  1. 通过esp_ble_gattc_register_callback()注册回调去接受GATT事件.
  2. 通过esp_ble_gattc_app_register() 注册这个应用.
  3. 通过 esp_ble_gattc_open()打开一个GATT服务连接.
  4. 连接完成,通过 esp_ble_gattc_search_service()扫描服务.

当我们调用 esp_ble_gattc_open() 时,我们正在请求打开与特定设备的GATT连接。 这个请求是非阻塞的,将在后台执行。 最终,我们将收到一份GATT事件说明其结果。 事件类型将是 ESP_GATTC_OPEN_EVT.


Page 197


GATT1

作为来自ESP_GATTC_OPEN_EVT的响应数据的一部分,我们将收到一个连接标识符(conn_id)。 这个conn_id可以被宽松地认为是合作伙伴设备的套接字。

一旦我们与合作伙伴设备建立了连接,我们就可以询问它提供的服务。 我们通过调用

esp_ble_gattc_search_service()

函数来完成此操作。 像其他BLE机制一样,这是一个异步操作,会导致生成一系列GATT事件。 设备返回的每一项服务都会触发

ESP_GATTC_SEARCH_RES_EVT

事件,最后,当我们看到所有的服务时,我们得到一个

ESP_GATTC_SEARCH_CMPL_EVT

。 如下图所示:

GATT1

对于我们返回的每个服务,我们都可以开始在设备上调用这些服务。

See also:


Page 198


  • esp_ble_gattc_register_callback
  • esp_ble_gattc_app_register
  • esp_ble_gattc_open
  • esp_ble_gattc_search_service
  • GATT Services

Being a GATT Server

作为GATT服务器的高级别是:

esp_bt_controller_init()
esp_bt_controller_enable()
esp_bluedroid_init()
esp_bluedroid_enable()
esp_ble_gatts_register_callback()
esp_ble_gap_register_callback()
esp_ble_gatts_app_register()

调用esp_ble_gatts_app_register()注册我们的应用程序。这将控制权交还给BLE子系统,准备就绪后,将使用事件类型ESP_GATTS_REG_EVT回调GATT服务器事件处理程序。当我们收到这些信息时,我们会进行下一部分设置:

esp_ble_gap_set_device_name()
esp_ble_gap_config_adv_data()
esp_ble_gatts_create_service()

我们可以从Linux系统运行hcitool 来测试其是否正常工作:

$ sudo hcitool lescan

24:0A:C4:00:00:96 MYDEVICE

See also:

  • esp_ble_gatts_register_callback
  • esp_ble_gap_register_callback
  • esp_ble_gap_set_device_name
  • esp_ble_gap_config_adv_data
  • esp_ble_gatts_app_register
  • esp_ble_gatts_create_service

Notifications and indications

我们可能不希望BLE客户端持续轮询BLE服务器以读取特征值以确定它是否及何时发生变化。 这有两个主要缺点。 首先,如果我们假设大多数情况下没有变化,我们正在浪费资源,唤醒服务器并被告知它没有改变。 相反,我们想要的是一个中断机制。 这是notifications 和 indications 发挥作用的地方。


Page 199


当客户端连接到服务器时,服务器可以将已更改值的通知推送给客户端。通过这种方式,客户不需要做任何工作,除非知道该值发生了变化。直到值发生变化,才会发生传输。

这使我们陷入了轮询的第二个不利方面。 如果我们在进行轮询,我们大概会在两次轮询之间等待。 在这种情况下,我们在值发生变化和通知对等点之间存在延迟。 这个延迟在通知中减少了。

通知是一个“火灾和遗忘”的故事。 服务器发出通知(notification)但不知道对方是否收到通知。 与通知类似的是指示(indication)的概念。 就像通知一样,只有将值标记为已更改,才会将任何内容传送给对等…但是,与通知不同,指示会确认对方收到更新。

与轮询和通知相关联的是具有UUID 0x2902的客户端特征描述符。 这包含了几个标志。一个用于通知,另一个用于指示。 其能够生成通知或指示对于给定的特征服务器应该具有与其关联描述符。 在实际执行通知或指示之前,服务器应在执行操作之前检查标志中的位。 如果客户希望执行通知或指示,那么应该更新这些位。 这是为了防止,服务器消耗了电量产生通知,而客户端对接收该项通知不感兴趣。

GATT XML descriptions

GATT服务,特征,描述符和声明描述(declaration descriptions)作为XML文档提供。

See also:

– GATT XML

Service Discovery Protocol

当客户端应用程序希望请求建立连接时,客户端需要知道服务器正在监听的端口号。在TCP/IP中,这是通过让客户端和服务器使用约定好的端口来实现的。在蓝牙中,则提供了具体的功能。

具体来说,有一种可用的服务称为服务发现协议或“SDP”。在服务器上,当提供服务时,该服务的端口号被注册到本地SDP。当客户端现在希望使用目标服务时,它首先从运行在服务器上的SDP请求端点(endpoint)信息。

SDP返回端点信息,并且客户端现在拥与目标服务创建连接需要的所有信息。


Page 200


由SDP服务器管理的信息单位称为“服务记录(service record)”或“SDP记录(SDP record)”.

一个名为sdptool的命令行工具可用于检查蓝牙设备的SDP数据。 一个简单的命令使用示例为:

$ sdptool browse <Bluetooth Address>

这将返回一系列表单的记录:

– Service Name

– Service Description

– Service Provider

– Service RecHandler

– Service Class ID List

– Protocol Descriptor List

– Profile Descriptor List

– Language Base Attr List

See also:

– man(1) – sdptool

ESP32 and Bluetooth

逻辑实现:

esp_bt_controller_init()
esp_bt_controller_enable(ESP_BT_MODE_BTDM)
esp_bluedroid_init()
esp_bluedroid_enable()
esp_ble_gap_register_callback()
esp_ble_gattc_register_callback()
esp_ble_gattc_app_register()
esp_ble_gap_set_scan_params()
esp_ble_gap_start_scanning(20)


Page 201


GATT3

GATT Server – Read request

当客户端请求读取特征时,会收到一个ESP_GATTS_READ_EVT。这应该使用esp_ble_gatts_send_response()函数发送响应。我们可以用gatttool来测试

$ sudo gatttool --device=<deviceAddr> --interactive
??> connect
??> char-read-uuid <charUUID>
handle: 0x???? value: ??


Page 202


See also:

– ESP_GATTS_READ_EVT

– esp_ble_gatts_send_response

Debugging ESP32 Bluetooth

ESP32蓝牙的实现建立在名为Bluedroid的环境上。为了全面了解ESP32蓝牙环境,我们还需要了解Bluedroid环境。 例如,获取底层调试信息需要我们了解Bluedroid API。

For example:

#include <gatt_api.h> // bluedroid include
…
GATT_SetTraceLevel(6);

这将调整GATT 追踪级别.

Bluetooth C Programming in Linux

在C / Linux环境中,我们实现了一个名为“BlueZ”的API堆栈。 BlueZ实施支持RFCOMM, L2CAP, SCO和HCI。搭建蓝牙编程的环境,我们必须安装名为“libbluetooth-dev”的软件包:

$ sudo apt-get install libbluetooth-dev

编译时,我们需要链接libbluetooth。

See also:

– An Introduction to Bluetooth Programming

hci_get_route

int hci_get_route(bdaddr_t *bdaddr)

hci_open_dev

Open the specified device and get a handle to that device. The returned value is a socket.


Page 203



int hci_open_dev(int dev_id)

If the return is -1 then an error was encountered and the details of the error can be found in errno.

hci_inquiry

int hci_inquiry(
int dev_id,
int len,
int max_rsp,
const uint8_t *lap,
inquiry_info **ii,
long flags)

where:

– dev_id – the device id of the adapter as retruned by hci_get_route.

– len – The duration of the scan * 1.28 seconds.

– max_rsp – The maximum number of responses we are willing to accept.

– lap – may be NULL

– ii – Pointer to an array of inquiry_info structures to be populated. The storage must exist and be at least of size max_rsp * sizeof(inquiry_info).

– flags

– 0 – Cached results allowed

– IREQ_CACHE_FLUSH – Any cached values are discarded and only new responses will be used.

The inquiry_info is a struct containing:

– bdaddr_t bdaddr

– uint8_t pscan_rep_mode

– uint8_t pscan_period_mode

– uint8_t pscan_mode

– uint8_t dev_class[3] – The device class is encoded in the assigned numbers –baseband.

– uint16_t clock_offset


Page 204


See also:

– Assigned numbers – baseband

hci_read_remote_name

Retrieve the display name of a specified device.

int hci_read_remote_name(
int hci_sock,
const baddr_t *addr,
int len,
char *name,
int timeout
)
  • len – the size of the name buffer to hold the display name.
  • name – a buffer to hold the display name.
  • timeout – Maximum number of milliseconds to wait before giving up.

    On return, a value of 0 indicates success.

str2ba

Convert a string representation of a Bluetooth address into an address.


str2ba(const char *str, bdaddr_t *ba)

Where

str

is the string representation of the Bluetooth address and

ba

is a pointer to a

bdaddr_t

structure to hold the resulting address.

ba2str

Convert a Bluetooth address to a string. The string buffer must be at least 18 bytes long.


ba2str(const bdaddr_t *ba, char *str)

The ba is a pointer to a

bdaddr_t

structure while

str

is the buffer to be populated with the string representation.

sdp_connect()
sdp_service_search_attr_req()
sdp_record_register()


Page 205


Bluetooth programming in Node.js

Node.js提供了编写运行在服务器上的JavaScript应用程序的环境。这些服务器包括Windows, Linux和Mac。默认情况下, Node提供了运行JavaScript的核心功能,并扩展了网和文件系统等。从本质上讲, Node没有提供蓝牙编程的相关功能。但是, Node可以通过名为npm的优秀包管理系统进行扩展。其中一个可用软件包称为“Noble”,它在Node上提供BLE客户端(中央)。另外还有提供BLE服务器的“Bleno”。

See also:

– npm: noble

– npm: bleno

– Github: sandeepmistry/noble

– Github: sandeepmistry/bleno

Using Noble

To use noble, we would issue a require of the package “noble” package after installing it

using npm. For example:

var noble = require("noble");

To determine what BLE servers (peripherals) are out there, we would next perform a scan to locate them. We can do this with:

noble.startScanning()

Scanning continues until we explicitly ask to stop with a call to:

noble.stopScanning()

Events are generated through scanning and we can register handlers for them using:

– noble.on(“scanStart”, callback) – Be informed when scanning starts.

– noble.on(“scanStop”, callback) – Be informed when scanning ends.

– noble.on(“discover”, callback(peripheral)) – Be informed when a new BLE peripheral is found.

When a peripheral is discovered, we are passed an object that represents it which contains:

{
  id: BD address
  uuid: BD address
  address: BD address
  addressType: "public" or ...
  connectable: true or false
  Page 206advertisement: {
    localName: Name of device.
    txPowerLevel:
    manufacturerData:
    serviceData:
    serviceUuids:
    solicitationServiceUuids:
    serviceSolicitationUuids:
  }
  rssi:
  services:
  state: "connected" or "disconnected" or ...
}

The peripheral object contains methods including:

– connect() – Connect to the BLE server.

– disconnect() – Disconnect from the BLE server.

– discoverServices() – Discover the services offered by the server.

Once we have discovered a peripheral, we can select it and perform work against it.The connect() method allows us to connect to it. This method takes a callback function that is invoked when the connection has been completed.

For example:

function onConnect(error) {
// Perform work here after being connected.
} …
peripheral.connect(onConnect);

Once we are connected, we can now interrogate the BLE peripheral and ask it about the services it offers. We do this through the discoverServices() function. This takes a callback function that is passed an array of found services. For any given service, it is uniquely identified by a serviceUuid. We can examine the uuids and work with the ones we want. The discoverServices() function can filter our services so that only the ones of interest to us are returned. Remember that a BLE service is a container of characteristics so next we will ask the service for the set of characteristics for which it has management. We do this by invoking the discoverCharacteristics() method on our service object. This results in the characteristics we can read and write. From a characteristic object, we can invoke read() to read a value and write() to write a value.

Tech reference:

noble object


Page 207


未完待续